diff --git a/.editorconfig b/.editorconfig index 2995eb3fa6a8fb..315f2be3c4d455 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,3 +45,11 @@ indent_size = 4 [*.py] indent_style = space indent_size = 4 + +[json-schema/*.json] +indent_style = space +indent_size = 4 + +[plugins/obs-services/json/**/*.json] +indent_style = space +indent_size = 4 diff --git a/.github/actions/obs-services-json-parser-validator/action.yaml b/.github/actions/obs-services-json-parser-validator/action.yaml new file mode 100644 index 00000000000000..9d990a407f0e50 --- /dev/null +++ b/.github/actions/obs-services-json-parser-validator/action.yaml @@ -0,0 +1,51 @@ +name: obs-services JSON Parser Validator +description: Runs quicktype againts obs-services JSON schemas and checks for any changes introduced by it +inputs: + failCondition: + description: Controls whether failed checks also fail the workflow run + required: false + default: never + workingDirectory: + description: Working directory for checks + required: false + default: ${{ github.workspace }} +runs: + using: composite + steps: + - name: Check Runner Operating System 🏃‍♂️ + if: runner.os == 'Windows' + shell: bash + run: | + : Check Runner Operating System 🏃‍♂️ + echo "::notice::obs-services-json-parser-validator action requires a macOS-based or Linux-based runner." + exit 2 + + - name: Install Dependencies 🛍️ + if: runner.os == 'Linux' + shell: bash + run: | + : Install Dependencies 🛍️ + echo ::group::Install Dependencies + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH + brew install --quiet zsh + echo ::endgroup:: + + - name: Validate obs-services JSON Parser 🎛️ + id: result + shell: zsh --no-rcs --errexit --pipefail {0} + working-directory: ${{ github.workspace }} + env: + GITHUB_EVENT_FORCED: ${{ github.event.forced }} + GITHUB_REF_BEFORE: ${{ github.event.before }} + run: | + : Validate obs-services JSON Parser 🎛️ + if (( ${+RUNNER_DEBUG} )) setopt XTRACE + + print ::group::Install quicktype + npm install -g quicktype@23.0.49 + print ::endgroup:: + + print ::group::Run Validation + ./build-aux/regen-obs-services-json-parser.zsh --fail-${{ inputs.failCondition }} --check + print ::endgroup:: diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index 7f7453b0e9d25a..aedd8e24ce5c1f 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -61,3 +61,15 @@ jobs: uses: ./.github/actions/qt-xml-validator with: failCondition: error + + obs-services-json-parser-validator: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Validate obs-services JSON Parser + id: obs-services-json-parser-check + uses: ./.github/actions/obs-services-json-parser-validator + with: + failCondition: error diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 10de47680421f5..4b971940743a8f 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -24,6 +24,10 @@ if(NOT TARGET OBS::json11) add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" "${CMAKE_BINARY_DIR}/deps/json11") endif() +if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") +endif() + add_executable(obs-studio) add_executable(OBS::studio ALIAS obs-studio) @@ -36,7 +40,8 @@ target_link_libraries( OBS::libobs OBS::frontend-api OBS::libff-util - OBS::json11) + OBS::json11 + OBS::obf) include(cmake/ui-qt.cmake) include(cmake/ui-elements.cmake) @@ -44,15 +49,6 @@ include(cmake/ui-windows.cmake) include(cmake/feature-importers.cmake) include(cmake/feature-browserpanels.cmake) -if(NOT OAUTH_BASE_URL) - # cmake-format: off - set(OAUTH_BASE_URL "https://auth.obsproject.com/" CACHE STRING "Default OAuth base URL") - mark_as_advanced(OAUTH_BASE_URL) - # cmake-format: on -endif() -include(cmake/feature-twitch.cmake) -include(cmake/feature-restream.cmake) -include(cmake/feature-youtube.cmake) include(cmake/feature-sparkle.cmake) include(cmake/feature-whatsnew.cmake) @@ -64,17 +60,11 @@ target_sources( obs-studio PRIVATE # cmake-format: sortable api-interface.cpp - auth-base.cpp - auth-base.hpp - auth-listener.cpp - auth-listener.hpp - auth-oauth.cpp - auth-oauth.hpp + broadcast-flow.cpp + broadcast-flow.hpp display-helpers.hpp multiview.cpp multiview.hpp - obf.c - obf.h obs-app.cpp obs-app.hpp obs-proxy-style.cpp diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index 8fb3bea3a3816d..06540d4d3969df 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -6,6 +6,10 @@ #include +#ifdef ENABLE_WAYLAND +#include +#endif + using namespace std; Q_DECLARE_METATYPE(OBSScene); @@ -445,6 +449,59 @@ struct OBSStudioAPI : obs_frontend_callbacks { return true; } + bool obs_frontend_is_browser_available(void) override + { +#ifdef BROWSER_AVAILABLE +#ifdef ENABLE_WAYLAND + return (obs_get_nix_platform() != OBS_NIX_PLATFORM_WAYLAND); +#else + return true; +#endif +#else + return false; +#endif + } + + void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, + size_t size) override + { +#ifdef BROWSER_AVAILABLE +#ifdef ENABLE_WAYLAND + if (!obs_frontend_is_browser_available()) + return nullptr; +#endif + struct obs_frontend_browser_params data = {0}; + if (size > sizeof(data)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_get_browser_widget with size " + "%llu which is more than OBS Studio currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(data)); + return nullptr; + } + + memcpy(&data, params, size); + + return (void *)main->GetBrowserWidget(data); +#else + UNUSED_PARAMETER(params); + UNUSED_PARAMETER(size); + return nullptr; +#endif + } + + void obs_frontend_delete_browser_cookie(const char *url) override + { + if (!url) + return; + + std::string urlStr = url; + if (!urlStr.empty()) + main->DeleteCookie(urlStr); + } + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override { @@ -747,6 +804,33 @@ struct OBSStudioAPI : obs_frontend_callbacks { undo_data, redo_data, repeatable); } + void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, + size_t size) override + { + struct obs_frontend_broadcast_flow data = {0}; + if (size > sizeof(data)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_broadcast_flow with size " + "%llu which is more than OBS Studio currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(data)); + return; + } + + memcpy(&data, flow, size); + + main->AddBroadcastFlow(service, data); + } + + void obs_frontend_remove_broadcast_flow( + const obs_service_t *service) override + { + main->RemoveBroadcastFlow(service); + } + void on_load(obs_data_t *settings) override { for (size_t i = saveCallbacks.size(); i > 0; i--) { diff --git a/UI/auth-base.cpp b/UI/auth-base.cpp deleted file mode 100644 index b1a4315fd29dae..00000000000000 --- a/UI/auth-base.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "auth-base.hpp" -#include "window-basic-main.hpp" - -#include -#include - -struct AuthInfo { - Auth::Def def; - Auth::create_cb create; -}; - -static std::vector authDefs; - -void Auth::RegisterAuth(const Def &d, create_cb create) -{ - AuthInfo info = {d, create}; - authDefs.push_back(info); -} - -std::shared_ptr Auth::Create(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.create(); - } - } - - return nullptr; -} - -Auth::Type Auth::AuthType(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.def.type; - } - } - - return Type::None; -} - -bool Auth::External(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.def.externalOAuth; - } - } - - return false; -} - -void Auth::Load() -{ - OBSBasic *main = OBSBasic::Get(); - const char *typeStr = config_get_string(main->Config(), "Auth", "Type"); - if (!typeStr) - typeStr = ""; - - main->auth = Create(typeStr); - if (main->auth) { - if (main->auth->LoadInternal()) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled( - main->auth->broadcastFlow()); - } - } else { - main->SetBroadcastFlowEnabled(false); - } -} - -void Auth::Save() -{ - OBSBasic *main = OBSBasic::Get(); - Auth *auth = main->auth.get(); - if (!auth) { - if (config_has_user_value(main->Config(), "Auth", "Type")) { - config_remove_value(main->Config(), "Auth", "Type"); - config_save_safe(main->Config(), "tmp", nullptr); - } - return; - } - - config_set_string(main->Config(), "Auth", "Type", auth->service()); - auth->SaveInternal(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/UI/auth-base.hpp b/UI/auth-base.hpp deleted file mode 100644 index 8d727f6cf7e6cc..00000000000000 --- a/UI/auth-base.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include - -class Auth : public QObject { - Q_OBJECT - -protected: - virtual void SaveInternal() = 0; - virtual bool LoadInternal() = 0; - - bool firstLoad = true; - - struct ErrorInfo { - std::string message; - std::string error; - - ErrorInfo(std::string message_, std::string error_) - : message(message_), - error(error_) - { - } - }; - -public: - enum class Type { - None, - OAuth_StreamKey, - OAuth_LinkedAccount, - }; - - struct Def { - std::string service; - Type type; - bool externalOAuth; - bool usesBroadcastFlow; - }; - - typedef std::function()> create_cb; - - inline Auth(const Def &d) : def(d) {} - virtual ~Auth() {} - - inline Type type() const { return def.type; } - inline const char *service() const { return def.service.c_str(); } - inline bool external() const { return def.externalOAuth; } - inline bool broadcastFlow() const { return def.usesBroadcastFlow; } - - virtual void LoadUI() {} - - virtual void OnStreamConfig() {} - - static std::shared_ptr Create(const std::string &service); - static Type AuthType(const std::string &service); - static bool External(const std::string &service); - static void Load(); - static void Save(); - -protected: - static void RegisterAuth(const Def &d, create_cb create); - -private: - Def def; -}; diff --git a/UI/auth-listener.cpp b/UI/auth-listener.cpp deleted file mode 100644 index 12a7d72fef4118..00000000000000 --- a/UI/auth-listener.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "qt-wrappers.hpp" - -#define LOGO_URL "https://obsproject.com/assets/images/new_icon_small-r.png" - -static const QString serverResponseHeader = - QStringLiteral("HTTP/1.0 200 OK\n" - "Connection: close\n" - "Content-Type: text/html; charset=UTF-8\n" - "Server: OBS Studio\n" - "\n" - "OBS Studio" - ""); - -static const QString responseTemplate = - "
" - "\"OBS\"" - "
" - "

%1

"; - -AuthListener::AuthListener(QObject *parent) : QObject(parent) -{ - server = new QTcpServer(this); - connect(server, &QTcpServer::newConnection, this, - &AuthListener::NewConnection); - if (!server->listen(QHostAddress::LocalHost, 0)) { - blog(LOG_DEBUG, "Server could not start"); - emit fail(); - } else { - blog(LOG_DEBUG, "Server started at port %d", - server->serverPort()); - } -} - -quint16 AuthListener::GetPort() -{ - return server ? server->serverPort() : 0; -} - -void AuthListener::SetState(QString state) -{ - this->state = state; -} - -void AuthListener::NewConnection() -{ - QTcpSocket *socket = server->nextPendingConnection(); - if (socket) { - connect(socket, &QTcpSocket::disconnected, socket, - &QTcpSocket::deleteLater); - connect(socket, &QTcpSocket::readyRead, socket, [&, socket]() { - QByteArray buffer; - while (socket->bytesAvailable() > 0) { - buffer.append(socket->readAll()); - } - socket->write(QT_TO_UTF8(serverResponseHeader)); - QString redirect = QString::fromLatin1(buffer); - blog(LOG_DEBUG, "redirect: %s", QT_TO_UTF8(redirect)); - - QRegularExpression re_state( - "(&|\\?)state=(?[^&]+)"); - QRegularExpression re_code( - "(&|\\?)code=(?[^&]+)"); - - QRegularExpressionMatch match = - re_state.match(redirect); - - QString code; - - if (match.hasMatch()) { - if (state == match.captured("state")) { - match = re_code.match(redirect); - if (!match.hasMatch()) - blog(LOG_DEBUG, "no 'code' " - "in server " - "redirect"); - - code = match.captured("code"); - } else { - blog(LOG_WARNING, "state mismatch " - "while handling " - "redirect"); - } - } else { - blog(LOG_DEBUG, "no 'state' in " - "server redirect"); - } - - if (code.isEmpty()) { - auto data = responseTemplate.arg( - QTStr("YouTube.Auth.NoCode")); - socket->write(QT_TO_UTF8(data)); - emit fail(); - } else { - auto data = responseTemplate.arg( - QTStr("YouTube.Auth.Ok")); - socket->write(QT_TO_UTF8(data)); - emit ok(code); - } - socket->flush(); - socket->close(); - }); - } else { - emit fail(); - } -} diff --git a/UI/auth-listener.hpp b/UI/auth-listener.hpp deleted file mode 100644 index fa1db2deafd97a..00000000000000 --- a/UI/auth-listener.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include - -class AuthListener : public QObject { - Q_OBJECT - - QTcpServer *server; - QString state; - -signals: - void ok(const QString &code); - void fail(); - -protected: - void NewConnection(); - -public: - explicit AuthListener(QObject *parent = 0); - quint16 GetPort(); - void SetState(QString state); -}; diff --git a/UI/auth-oauth.cpp b/UI/auth-oauth.cpp deleted file mode 100644 index bb2fd714ca8501..00000000000000 --- a/UI/auth-oauth.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include "auth-oauth.hpp" - -#include -#include -#include - -#include -#include - -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include - -#include - -#include "ui-config.h" - -using namespace json11; - -#ifdef BROWSER_AVAILABLE -#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; -#endif - -/* ------------------------------------------------------------------------- */ - -OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) - : QDialog(parent), - get_token(token) -{ -#ifdef BROWSER_AVAILABLE - if (!cef) { - return; - } - - setWindowTitle("Auth"); - setMinimumSize(400, 400); - resize(700, 700); - - Qt::WindowFlags flags = windowFlags(); - Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; - setWindowFlags(flags & (~helpFlag)); - - OBSBasic::InitBrowserPanelSafeBlock(); - - cefWidget = cef->create_widget(nullptr, url, panel_cookies); - if (!cefWidget) { - fail = true; - return; - } - - connect(cefWidget, SIGNAL(titleChanged(const QString &)), this, - SLOT(setWindowTitle(const QString &))); - connect(cefWidget, SIGNAL(urlChanged(const QString &)), this, - SLOT(urlChanged(const QString &))); - - QPushButton *close = new QPushButton(QTStr("Cancel")); - connect(close, &QAbstractButton::clicked, this, &QDialog::reject); - - QHBoxLayout *bottomLayout = new QHBoxLayout(); - bottomLayout->addStretch(); - bottomLayout->addWidget(close); - bottomLayout->addStretch(); - - QVBoxLayout *topLayout = new QVBoxLayout(this); - topLayout->addWidget(cefWidget); - topLayout->addLayout(bottomLayout); -#else - UNUSED_PARAMETER(url); -#endif -} - -OAuthLogin::~OAuthLogin() {} - -int OAuthLogin::exec() -{ -#ifdef BROWSER_AVAILABLE - if (cefWidget) { - return QDialog::exec(); - } -#endif - return QDialog::Rejected; -} - -void OAuthLogin::reject() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::reject(); -} - -void OAuthLogin::accept() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::accept(); -} - -void OAuthLogin::urlChanged(const QString &url) -{ - std::string uri = get_token ? "access_token=" : "code="; - int code_idx = url.indexOf(uri.c_str()); - if (code_idx == -1) - return; - - if (!url.startsWith(OAUTH_BASE_URL)) - return; - - code_idx += (int)uri.size(); - - int next_idx = url.indexOf("&", code_idx); - if (next_idx != -1) - code = url.mid(code_idx, next_idx - code_idx); - else - code = url.right(url.size() - code_idx); - - accept(); -} - -/* ------------------------------------------------------------------------- */ - -struct OAuthInfo { - Auth::Def def; - OAuth::login_cb login; - OAuth::delete_cookies_cb delete_cookies; -}; - -static std::vector loginCBs; - -void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, - delete_cookies_cb delete_cookies) -{ - OAuthInfo info = {d, login, delete_cookies}; - loginCBs.push_back(info); - RegisterAuth(d, create); -} - -std::shared_ptr OAuth::Login(QWidget *parent, const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - return a.login(parent, service); - } - } - - return nullptr; -} - -void OAuth::DeleteCookies(const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - a.delete_cookies(); - } - } -} - -void OAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "RefreshToken", - refresh_token.c_str()); - config_set_string(main->Config(), service(), "Token", token.c_str()); - config_set_uint(main->Config(), service(), "ExpireTime", expire_time); - config_set_int(main->Config(), service(), "ScopeVer", currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool OAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - refresh_token = get_config_str(main, service(), "RefreshToken"); - token = get_config_str(main, service(), "Token"); - expire_time = config_get_uint(main->Config(), service(), "ExpireTime"); - currentScopeVer = - (int)config_get_int(main->Config(), service(), "ScopeVer"); - return implicit ? !token.empty() : !refresh_token.empty(); -} - -bool OAuth::TokenExpired() -{ - if (token.empty()) - return true; - if ((uint64_t)time(nullptr) > expire_time - 5) - return true; - return false; -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, - const std::string &secret, const std::string &redirect_uri, - int scope_ver, const std::string &auth_code, bool retry) -{ - return GetTokenInternal(url, client_id, secret, redirect_uri, scope_ver, - auth_code, retry); -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, - int scope_ver, const std::string &auth_code, bool retry) -{ - return GetTokenInternal(url, client_id, {}, {}, scope_ver, auth_code, - retry); -} - -bool OAuth::GetTokenInternal(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry) -try { - std::string output; - std::string error; - std::string desc; - - if (currentScopeVer > 0 && currentScopeVer < scope_ver) { - if (RetryLogin()) { - return true; - } else { - QString title = QTStr("Auth.InvalidScope.Title"); - QString text = - QTStr("Auth.InvalidScope.Text").arg(service()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - } - - if (auth_code.empty() && !TokenExpired()) { - return true; - } - - std::string post_data; - post_data += "action=redirect&client_id="; - post_data += client_id; - if (!secret.empty()) { - post_data += "&client_secret="; - post_data += secret; - } - if (!redirect_uri.empty()) { - post_data += "&redirect_uri="; - post_data += redirect_uri; - } - - if (!auth_code.empty()) { - post_data += "&grant_type=authorization_code&code="; - post_data += auth_code; - } else { - post_data += "&grant_type=refresh_token&refresh_token="; - post_data += refresh_token; - } - - bool success = false; - - auto func = [&]() { - success = GetRemoteFile(url, output, error, nullptr, - "application/x-www-form-urlencoded", "", - post_data.c_str(), - std::vector(), nullptr, 5); - }; - - ExecThreadedWithoutBlocking(func, QTStr("Auth.Authing.Title"), - QTStr("Auth.Authing.Text").arg(service())); - if (!success || output.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - Json json = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - /* -------------------------- */ - /* error handling */ - - error = json["error"].string_value(); - if (!retry && error == "invalid_grant") { - if (RetryLogin()) { - return true; - } - } - if (!error.empty()) - throw ErrorInfo(error, - json["error_description"].string_value()); - - /* -------------------------- */ - /* success! */ - - expire_time = (uint64_t)time(nullptr) + json["expires_in"].int_value(); - token = json["access_token"].string_value(); - if (token.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - if (!auth_code.empty()) { - refresh_token = json["refresh_token"].string_value(); - if (refresh_token.empty()) - throw ErrorInfo("Failed to get refresh token from " - "remote", - error); - - currentScopeVer = scope_ver; - } - - return true; - -} catch (ErrorInfo &info) { - if (!retry) { - QString title = QTStr("Auth.AuthFailure.Title"); - QString text = QTStr("Auth.AuthFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void OAuthStreamKey::OnStreamConfig() -{ - if (key_.empty()) - return; - - OBSBasic *main = OBSBasic::Get(); - obs_service_t *service = main->GetService(); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - - bool bwtest = obs_data_get_bool(settings, "bwtest"); - - if (bwtest && strcmp(this->service(), "Twitch") == 0) - obs_data_set_string(settings, "key", - (key_ + "?bandwidthtest=true").c_str()); - else - obs_data_set_string(settings, "key", key_.c_str()); - - obs_service_update(service, settings); -} diff --git a/UI/auth-oauth.hpp b/UI/auth-oauth.hpp deleted file mode 100644 index 364dccdc4442ab..00000000000000 --- a/UI/auth-oauth.hpp +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "auth-base.hpp" - -class QCefWidget; - -class OAuthLogin : public QDialog { - Q_OBJECT - - QCefWidget *cefWidget = nullptr; - QString code; - bool get_token = false; - bool fail = false; - -public: - OAuthLogin(QWidget *parent, const std::string &url, bool token); - ~OAuthLogin(); - - inline QString GetCode() const { return code; } - inline bool LoadFail() const { return fail; } - - virtual int exec() override; - virtual void reject() override; - virtual void accept() override; - -public slots: - void urlChanged(const QString &url); -}; - -class OAuth : public Auth { - Q_OBJECT - -public: - inline OAuth(const Def &d) : Auth(d) {} - - typedef std::function( - QWidget *, const std::string &service_name)> - login_cb; - typedef std::function delete_cookies_cb; - - static std::shared_ptr Login(QWidget *parent, - const std::string &service); - static void DeleteCookies(const std::string &service); - - static void RegisterOAuth(const Def &d, create_cb create, - login_cb login, - delete_cookies_cb delete_cookies); - -protected: - std::string refresh_token; - std::string token; - bool implicit = false; - uint64_t expire_time = 0; - int currentScopeVer = 0; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - virtual bool RetryLogin() = 0; - bool TokenExpired(); - bool GetToken(const char *url, const std::string &client_id, - int scope_ver, - const std::string &auth_code = std::string(), - bool retry = false); - bool GetToken(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry); - -private: - bool GetTokenInternal(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry); -}; - -class OAuthStreamKey : public OAuth { - Q_OBJECT - -protected: - std::string key_; - -public: - inline OAuthStreamKey(const Def &d) : OAuth(d) {} - - inline const std::string &key() const { return key_; } - - virtual void OnStreamConfig() override; -}; diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp deleted file mode 100644 index 9188da118c9684..00000000000000 --- a/UI/auth-restream.cpp +++ /dev/null @@ -1,292 +0,0 @@ -#include "auth-restream.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" - -using namespace json11; - -/* ------------------------------------------------------------------------- */ - -#define RESTREAM_AUTH_URL OAUTH_BASE_URL "v1/restream/redirect" -#define RESTREAM_TOKEN_URL OAUTH_BASE_URL "v1/restream/token" -#define RESTREAM_STREAMKEY_URL "https://api.restream.io/v2/user/streamKey" -#define RESTREAM_SCOPE_VERSION 1 - -#define RESTREAM_CHAT_DOCK_NAME "restreamChat" -#define RESTREAM_INFO_DOCK_NAME "restreamInfo" -#define RESTREAM_CHANNELS_DOCK_NAME "restreamChannel" - -static Auth::Def restreamDef = {"Restream", Auth::Type::OAuth_StreamKey}; - -/* ------------------------------------------------------------------------- */ - -RestreamAuth::RestreamAuth(const Def &d) : OAuthStreamKey(d) {} - -RestreamAuth::~RestreamAuth() -{ - if (!uiLoaded) - return; - - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(RESTREAM_CHAT_DOCK_NAME); - main->RemoveDockWidget(RESTREAM_INFO_DOCK_NAME); - main->RemoveDockWidget(RESTREAM_CHANNELS_DOCK_NAME); -} - -bool RestreamAuth::GetChannelInfo() -try { - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION)) - return false; - if (token.empty()) - return false; - if (!key_.empty()) - return true; - - std::string auth; - auth += "Authorization: Bearer "; - auth += token; - - std::vector headers; - headers.push_back(std::string("Client-ID: ") + client_id); - headers.push_back(std::move(auth)); - - std::string output; - std::string error; - Json json; - bool success; - - auto func = [&]() { - success = GetRemoteFile(RESTREAM_STREAMKEY_URL, output, error, - nullptr, "application/json", "", - nullptr, headers, nullptr, 5); - }; - - ExecThreadedWithoutBlocking( - func, QTStr("Auth.LoadingChannel.Title"), - QTStr("Auth.LoadingChannel.Text").arg(service())); - if (!success || output.empty()) - throw ErrorInfo("Failed to get stream key from remote", error); - - json = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - error = json["error"].string_value(); - if (!error.empty()) - throw ErrorInfo(error, - json["error_description"].string_value()); - - key_ = json["streamKey"].string_value(); - - return true; -} catch (ErrorInfo info) { - QString title = QTStr("Auth.ChannelFailure.Title"); - QString text = QTStr("Auth.ChannelFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void RestreamAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); - OAuthStreamKey::SaveInternal(); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool RestreamAuth::LoadInternal() -{ - firstLoad = false; - return OAuthStreamKey::LoadInternal(); -} - -void RestreamAuth::LoadUI() -{ - if (uiLoaded) - return; - if (!GetChannelInfo()) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - /* ----------------------------------- */ - - url = "https://restream.io/chat-application"; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - BrowserDock *chat = new BrowserDock(); - chat->setObjectName(RESTREAM_CHAT_DOCK_NAME); - chat->resize(420, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, url, panel_cookies); - chat->SetWidget(browser); - - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://restream.io/titles/embed"; - - BrowserDock *info = new BrowserDock(); - info->setObjectName(RESTREAM_INFO_DOCK_NAME); - info->resize(410, 600); - info->setMinimumSize(200, 150); - info->setWindowTitle(QTStr("Auth.StreamInfo")); - info->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(info, url, panel_cookies); - info->SetWidget(browser); - - main->AddDockWidget(info, Qt::LeftDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://restream.io/channel/embed"; - - BrowserDock *channels = new BrowserDock(); - channels->setObjectName(RESTREAM_CHANNELS_DOCK_NAME); - channels->resize(410, 600); - channels->setMinimumSize(410, 300); - channels->setWindowTitle(QTStr("RestreamAuth.Channels")); - channels->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(channels, url, panel_cookies); - channels->SetWidget(browser); - - main->AddDockWidget(channels, Qt::LeftDockWidgetArea); - - /* ----------------------------------- */ - - chat->setFloating(true); - info->setFloating(true); - channels->setFloating(true); - - chat->move(pos.x() + size.width() - chat->width() - 30, pos.y() + 60); - info->move(pos.x() + 20, pos.y() + 60); - channels->move(pos.x() + 20 + info->width() + 10, pos.y() + 60); - - if (firstLoad) { - chat->setVisible(true); - info->setVisible(true); - channels->setVisible(true); - } else { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); - } - - uiLoaded = true; -} - -bool RestreamAuth::RetryLogin() -{ - OAuthLogin login(OBSBasic::Get(), RESTREAM_AUTH_URL, false); - cef->add_popup_whitelist_url("about:blank", &login); - if (login.exec() == QDialog::Rejected) { - return false; - } - - std::shared_ptr auth = - std::make_shared(restreamDef); - - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - return GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()), true); -} - -std::shared_ptr RestreamAuth::Login(QWidget *parent, const std::string &) -{ - OAuthLogin login(parent, RESTREAM_AUTH_URL, false); - cef->add_popup_whitelist_url("about:blank", &login); - - if (login.exec() == QDialog::Rejected) { - return nullptr; - } - - std::shared_ptr auth = - std::make_shared(restreamDef); - - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - if (!auth->GetToken(RESTREAM_TOKEN_URL, client_id, - RESTREAM_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()))) { - return nullptr; - } - - std::string error; - if (auth->GetChannelInfo()) { - return auth; - } - - return nullptr; -} - -static std::shared_ptr CreateRestreamAuth() -{ - return std::make_shared(restreamDef); -} - -static void DeleteCookies() -{ - if (panel_cookies) { - panel_cookies->DeleteCookies("restream.io", std::string()); - } -} - -void RegisterRestreamAuth() -{ -#if !defined(__APPLE__) && !defined(_WIN32) - if (QApplication::platformName().contains("wayland")) - return; -#endif - - OAuth::RegisterOAuth(restreamDef, CreateRestreamAuth, - RestreamAuth::Login, DeleteCookies); -} diff --git a/UI/auth-restream.hpp b/UI/auth-restream.hpp deleted file mode 100644 index 334906c42f7a5e..00000000000000 --- a/UI/auth-restream.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "auth-oauth.hpp" - -class BrowserDock; - -class RestreamAuth : public OAuthStreamKey { - Q_OBJECT - - bool uiLoaded = false; - - virtual bool RetryLogin() override; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - bool GetChannelInfo(); - - virtual void LoadUI() override; - -public: - RestreamAuth(const Def &d); - ~RestreamAuth(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service_name); -}; diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp deleted file mode 100644 index 386a0e9275cc9a..00000000000000 --- a/UI/auth-twitch.cpp +++ /dev/null @@ -1,522 +0,0 @@ -#include "auth-twitch.hpp" - -#include -#include -#include -#include -#include - -#include -#include - -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include - -#include "ui-config.h" -#include "obf.h" - -using namespace json11; - -/* ------------------------------------------------------------------------- */ - -#define TWITCH_AUTH_URL OAUTH_BASE_URL "v1/twitch/redirect" -#define TWITCH_TOKEN_URL OAUTH_BASE_URL "v1/twitch/token" - -#define TWITCH_SCOPE_VERSION 1 - -#define TWITCH_CHAT_DOCK_NAME "twitchChat" -#define TWITCH_INFO_DOCK_NAME "twitchInfo" -#define TWITCH_STATS_DOCK_NAME "twitchStats" -#define TWITCH_FEED_DOCK_NAME "twitchFeed" - -static Auth::Def twitchDef = {"Twitch", Auth::Type::OAuth_StreamKey}; - -/* ------------------------------------------------------------------------- */ - -TwitchAuth::TwitchAuth(const Def &d) : OAuthStreamKey(d) -{ - if (!cef) - return; - - cef->add_popup_whitelist_url( - "https://twitch.tv/popout/frankerfacez/chat?ffz-settings", - this); - - /* enables javascript-based popups. basically bttv popups */ - cef->add_popup_whitelist_url("about:blank#blocked", this); - - uiLoadTimer.setSingleShot(true); - uiLoadTimer.setInterval(500); - connect(&uiLoadTimer, &QTimer::timeout, this, - &TwitchAuth::TryLoadSecondaryUIPanes); -} - -TwitchAuth::~TwitchAuth() -{ - if (!uiLoaded) - return; - - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(TWITCH_CHAT_DOCK_NAME); - main->RemoveDockWidget(TWITCH_INFO_DOCK_NAME); - main->RemoveDockWidget(TWITCH_STATS_DOCK_NAME); - main->RemoveDockWidget(TWITCH_FEED_DOCK_NAME); -} - -bool TwitchAuth::MakeApiRequest(const char *path, Json &json_out) -{ - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - std::string url = "https://api.twitch.tv/helix/"; - url += std::string(path); - - std::vector headers; - headers.push_back(std::string("Client-ID: ") + client_id); - headers.push_back(std::string("Authorization: Bearer ") + token); - - std::string output; - std::string error; - long error_code = 0; - - bool success = false; - - auto func = [&]() { - success = GetRemoteFile(url.c_str(), output, error, &error_code, - "application/json", "", nullptr, - headers, nullptr, 5); - }; - - ExecThreadedWithoutBlocking( - func, QTStr("Auth.LoadingChannel.Title"), - QTStr("Auth.LoadingChannel.Text").arg(service())); - if (error_code == 403) { - OBSMessageBox::warning(OBSBasic::Get(), - Str("TwitchAuth.TwoFactorFail.Title"), - Str("TwitchAuth.TwoFactorFail.Text"), - true); - blog(LOG_WARNING, "%s: %s", __FUNCTION__, - "Got 403 from Twitch, user probably does not " - "have two-factor authentication enabled on " - "their account"); - return false; - } - - if (!success || output.empty()) - throw ErrorInfo("Failed to get text from remote", error); - - json_out = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - error = json_out["error"].string_value(); - if (!error.empty()) - throw ErrorInfo(error, json_out["message"].string_value()); - - return true; -} - -bool TwitchAuth::GetChannelInfo() -try { - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - if (!GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION)) - return false; - if (token.empty()) - return false; - if (!key_.empty()) - return true; - - Json json; - bool success = MakeApiRequest("users", json); - - if (!success) - return false; - - name = json["data"][0]["login"].string_value(); - - std::string path = "streams/key?broadcaster_id=" + - json["data"][0]["id"].string_value(); - success = MakeApiRequest(path.c_str(), json); - if (!success) - return false; - - key_ = json["data"][0]["stream_key"].string_value(); - - return true; -} catch (ErrorInfo info) { - QString title = QTStr("Auth.ChannelFailure.Title"); - QString text = QTStr("Auth.ChannelFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void TwitchAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "Name", name.c_str()); - config_set_string(main->Config(), service(), "UUID", uuid.c_str()); - - if (uiLoaded) { - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); - } - OAuthStreamKey::SaveInternal(); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool TwitchAuth::LoadInternal() -{ - if (!cef) - return false; - - OBSBasic *main = OBSBasic::Get(); - name = get_config_str(main, service(), "Name"); - uuid = get_config_str(main, service(), "UUID"); - - firstLoad = false; - return OAuthStreamKey::LoadInternal(); -} - -static const char *ffz_script = "\ -var ffz = document.createElement('script');\ -ffz.setAttribute('src','https://cdn.frankerfacez.com/script/script.min.js');\ -document.head.appendChild(ffz);"; - -static const char *bttv_script = "\ -localStorage.setItem('bttv_clickTwitchEmotes', true);\ -localStorage.setItem('bttv_darkenedMode', true);\ -localStorage.setItem('bttv_bttvGIFEmotes', true);\ -var bttv = document.createElement('script');\ -bttv.setAttribute('src','https://cdn.betterttv.net/betterttv.js');\ -document.head.appendChild(bttv);"; - -static const char *referrer_script1 = "\ -Object.defineProperty(document, 'referrer', {get : function() { return '"; -static const char *referrer_script2 = "'; }});"; - -void TwitchAuth::LoadUI() -{ - if (!cef) - return; - if (uiLoaded) - return; - if (!GetChannelInfo()) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - /* Twitch panels require a UUID, it does not actually need to be unique, - * and is generated client-side. - * It is only for preferences stored in the browser's local store. */ - if (uuid.empty()) { - QString qtUuid = QUuid::createUuid().toString(); - qtUuid.replace(QRegularExpression("[{}-]"), ""); - uuid = qtUuid.toStdString(); - } - - std::string moderation_tools_url; - moderation_tools_url = "https://www.twitch.tv/"; - moderation_tools_url += name; - moderation_tools_url += "/dashboard/settings/moderation?no-reload=true"; - - /* ----------------------------------- */ - - url = "https://www.twitch.tv/popout/"; - url += name; - url += "/chat"; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - BrowserDock *chat = new BrowserDock(); - chat->setObjectName(TWITCH_CHAT_DOCK_NAME); - chat->resize(300, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, url, panel_cookies); - chat->SetWidget(browser); - cef->add_force_popup_url(moderation_tools_url, chat); - - if (App()->IsThemeDark()) { - script = "localStorage.setItem('twilight.theme', 1);"; - } else { - script = "localStorage.setItem('twilight.theme', 0);"; - } - - const int twAddonChoice = - config_get_int(main->Config(), service(), "AddonChoice"); - if (twAddonChoice) { - if (twAddonChoice & 0x1) - script += bttv_script; - if (twAddonChoice & 0x2) - script += ffz_script; - } - - browser->setStartupScript(script); - - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - chat->setFloating(true); - chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); - - if (firstLoad) { - chat->setVisible(true); - } else { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); - } - - TryLoadSecondaryUIPanes(); - - uiLoaded = true; -} - -void TwitchAuth::LoadSecondaryUIPanes() -{ - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - if (App()->IsThemeDark()) { - script = "localStorage.setItem('twilight.theme', 1);"; - } else { - script = "localStorage.setItem('twilight.theme', 0);"; - } - script += referrer_script1; - script += "https://www.twitch.tv/"; - script += name; - script += "/dashboard/live"; - script += referrer_script2; - - const int twAddonChoice = - config_get_int(main->Config(), service(), "AddonChoice"); - if (twAddonChoice) { - if (twAddonChoice & 0x1) - script += bttv_script; - if (twAddonChoice & 0x2) - script += ffz_script; - } - - /* ----------------------------------- */ - - url = "https://dashboard.twitch.tv/popout/u/"; - url += name; - url += "/stream-manager/edit-stream-info"; - - BrowserDock *info = new BrowserDock(); - info->setObjectName(TWITCH_INFO_DOCK_NAME); - info->resize(300, 650); - info->setMinimumSize(200, 300); - info->setWindowTitle(QTStr("Auth.StreamInfo")); - info->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(info, url, panel_cookies); - info->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(info, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://www.twitch.tv/popout/"; - url += name; - url += "/dashboard/live/stats"; - - BrowserDock *stats = new BrowserDock(); - stats->setObjectName(TWITCH_STATS_DOCK_NAME); - stats->resize(200, 250); - stats->setMinimumSize(200, 150); - stats->setWindowTitle(QTStr("TwitchAuth.Stats")); - stats->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(stats, url, panel_cookies); - stats->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(stats, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://dashboard.twitch.tv/popout/u/"; - url += name; - url += "/stream-manager/activity-feed"; - url += "?uuid=" + uuid; - - BrowserDock *feed = new BrowserDock(); - feed->setObjectName(TWITCH_FEED_DOCK_NAME); - feed->resize(300, 650); - feed->setMinimumSize(200, 300); - feed->setWindowTitle(QTStr("TwitchAuth.Feed")); - feed->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(feed, url, panel_cookies); - feed->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(feed, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - info->setFloating(true); - stats->setFloating(true); - feed->setFloating(true); - - QSize statSize = stats->frameSize(); - - info->move(pos.x() + 50, pos.y() + 50); - stats->move(pos.x() + size.width() / 2 - statSize.width() / 2, - pos.y() + size.height() / 2 - statSize.height() / 2); - feed->move(pos.x() + 100, pos.y() + 100); - - if (firstLoad) { - info->setVisible(true); - stats->setVisible(false); - feed->setVisible(false); - } else { - uint32_t lastVersion = config_get_int(App()->GlobalConfig(), - "General", "LastVersion"); - - if (lastVersion <= MAKE_SEMANTIC_VERSION(23, 0, 2)) { - feed->setVisible(false); - } - - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); - } -} - -/* Twitch.tv has an OAuth for itself. If we try to load multiple panel pages - * at once before it's OAuth'ed itself, they will all try to perform the auth - * process at the same time, get their own request codes, and only the last - * code will be valid -- so one or more panels are guaranteed to fail. - * - * To solve this, we want to load just one panel first (the chat), and then all - * subsequent panels should only be loaded once we know that Twitch has auth'ed - * itself (if the cookie "auth-token" exists for twitch.tv). - * - * This is annoying to deal with. */ -void TwitchAuth::TryLoadSecondaryUIPanes() -{ - QPointer this_ = this; - - auto cb = [this_](bool found) { - if (!this_) { - return; - } - - if (!found) { - QMetaObject::invokeMethod(&this_->uiLoadTimer, "start"); - } else { - QMetaObject::invokeMethod(this_, - "LoadSecondaryUIPanes"); - } - }; - - panel_cookies->CheckForCookie("https://www.twitch.tv", "auth-token", - cb); -} - -bool TwitchAuth::RetryLogin() -{ - OAuthLogin login(OBSBasic::Get(), TWITCH_AUTH_URL, false); - if (login.exec() == QDialog::Rejected) { - return false; - } - - std::shared_ptr auth = - std::make_shared(twitchDef); - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - return GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()), true); -} - -std::shared_ptr TwitchAuth::Login(QWidget *parent, const std::string &) -{ - OAuthLogin login(parent, TWITCH_AUTH_URL, false); - if (login.exec() == QDialog::Rejected) { - return nullptr; - } - - std::shared_ptr auth = - std::make_shared(twitchDef); - - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - if (!auth->GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()))) { - return nullptr; - } - - if (auth->GetChannelInfo()) { - return auth; - } - - return nullptr; -} - -static std::shared_ptr CreateTwitchAuth() -{ - return std::make_shared(twitchDef); -} - -static void DeleteCookies() -{ - if (panel_cookies) - panel_cookies->DeleteCookies("twitch.tv", std::string()); -} - -void RegisterTwitchAuth() -{ -#if !defined(__APPLE__) && !defined(_WIN32) - if (QApplication::platformName().contains("wayland")) - return; -#endif - - OAuth::RegisterOAuth(twitchDef, CreateTwitchAuth, TwitchAuth::Login, - DeleteCookies); -} diff --git a/UI/auth-twitch.hpp b/UI/auth-twitch.hpp deleted file mode 100644 index 152b2a95730fd7..00000000000000 --- a/UI/auth-twitch.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include "auth-oauth.hpp" - -class BrowserDock; - -class TwitchAuth : public OAuthStreamKey { - Q_OBJECT - - friend class TwitchLogin; - - bool uiLoaded = false; - - std::string name; - std::string uuid; - - virtual bool RetryLogin() override; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - bool MakeApiRequest(const char *path, json11::Json &json_out); - bool GetChannelInfo(); - - virtual void LoadUI() override; - -public: - TwitchAuth(const Def &d); - ~TwitchAuth(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service_name); - - QTimer uiLoadTimer; - -public slots: - void TryLoadSecondaryUIPanes(); - void LoadSecondaryUIPanes(); -}; diff --git a/UI/auth-youtube.cpp b/UI/auth-youtube.cpp deleted file mode 100644 index 743c12ed577526..00000000000000 --- a/UI/auth-youtube.cpp +++ /dev/null @@ -1,412 +0,0 @@ -#include "auth-youtube.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include - -#pragma comment(lib, "shell32") -#endif - -#include "auth-listener.hpp" -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "ui-config.h" -#include "youtube-api-wrappers.hpp" -#include "window-basic-main.hpp" -#include "obf.h" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#endif - -using namespace json11; - -/* ------------------------------------------------------------------------- */ -#define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" -#define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" -#define YOUTUBE_SCOPE_VERSION 1 -#define YOUTUBE_API_STATE_LENGTH 32 -#define SECTION_NAME "YouTube" - -#define YOUTUBE_CHAT_PLACEHOLDER_URL \ - "https://obsproject.com/placeholders/youtube-chat" -#define YOUTUBE_CHAT_POPOUT_URL \ - "https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v=%1" - -#define YOUTUBE_CHAT_DOCK_NAME "ytChat" - -static const char allowedChars[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -static const int allowedCount = static_cast(sizeof(allowedChars) - 1); -/* ------------------------------------------------------------------------- */ - -static inline void OpenBrowser(const QString auth_uri) -{ - QUrl url(auth_uri, QUrl::StrictMode); - QDesktopServices::openUrl(url); -} - -void RegisterYoutubeAuth() -{ - for (auto &service : youtubeServices) { - OAuth::RegisterOAuth( - service, - [service]() { - return std::make_shared( - service); - }, - YoutubeAuth::Login, []() { return; }); - } -} - -YoutubeAuth::YoutubeAuth(const Def &d) - : OAuthStreamKey(d), - section(SECTION_NAME) -{ -} - -YoutubeAuth::~YoutubeAuth() -{ - if (!uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(YOUTUBE_CHAT_DOCK_NAME); - chat = nullptr; -#endif -} - -bool YoutubeAuth::RetryLogin() -{ - return true; -} - -void YoutubeAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); - - const char *section_name = section.c_str(); - config_set_string(main->Config(), section_name, "RefreshToken", - refresh_token.c_str()); - config_set_string(main->Config(), section_name, "Token", token.c_str()); - config_set_uint(main->Config(), section_name, "ExpireTime", - expire_time); - config_set_int(main->Config(), section_name, "ScopeVer", - currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool YoutubeAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - - const char *section_name = section.c_str(); - refresh_token = get_config_str(main, section_name, "RefreshToken"); - token = get_config_str(main, section_name, "Token"); - expire_time = - config_get_uint(main->Config(), section_name, "ExpireTime"); - currentScopeVer = - (int)config_get_int(main->Config(), section_name, "ScopeVer"); - firstLoad = false; - return implicit ? !token.empty() : !refresh_token.empty(); -} - -#ifdef BROWSER_AVAILABLE -static const char *ytchat_script = "\ -const obsCSS = document.createElement('style');\ -obsCSS.innerHTML = \"#panel-pages.yt-live-chat-renderer {display: none;}\ -yt-live-chat-viewer-engagement-message-renderer {display: none;}\";\ -document.querySelector('head').appendChild(obsCSS);"; -#endif - -void YoutubeAuth::LoadUI() -{ - if (uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - if (!cef) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - chat = new YoutubeChatDock(); - chat->setObjectName(YOUTUBE_CHAT_DOCK_NAME); - chat->resize(300, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, YOUTUBE_CHAT_PLACEHOLDER_URL, - panel_cookies); - browser->setStartupScript(ytchat_script); - - chat->SetWidget(browser); - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - chat->setFloating(true); - chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); - - if (firstLoad) { - chat->setVisible(true); - } else { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); - } -#endif - - uiLoaded = true; -} - -void YoutubeAuth::SetChatId(const QString &chat_id, - const std::string &api_chat_id) -{ -#ifdef BROWSER_AVAILABLE - QString chat_url = QString(YOUTUBE_CHAT_POPOUT_URL).arg(chat_id); - - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(chat_url.toStdString()); - chat->SetApiChatId(api_chat_id); - } -#else - UNUSED_PARAMETER(chat_id); - UNUSED_PARAMETER(api_chat_id); -#endif -} - -void YoutubeAuth::ResetChat() -{ -#ifdef BROWSER_AVAILABLE - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(YOUTUBE_CHAT_PLACEHOLDER_URL); - } -#endif -} - -QString YoutubeAuth::GenerateState() -{ - char state[YOUTUBE_API_STATE_LENGTH + 1]; - QRandomGenerator *rng = QRandomGenerator::system(); - int i; - - for (i = 0; i < YOUTUBE_API_STATE_LENGTH; i++) - state[i] = allowedChars[rng->bounded(0, allowedCount)]; - state[i] = 0; - - return state; -} - -// Static. -std::shared_ptr YoutubeAuth::Login(QWidget *owner, - const std::string &service) -{ - QString auth_code; - AuthListener server; - - auto it = std::find_if(youtubeServices.begin(), youtubeServices.end(), - [service](auto &item) { - return service == item.service; - }); - if (it == youtubeServices.end()) { - return nullptr; - } - const auto auth = std::make_shared(*it); - - QString redirect_uri = - QString("http://127.0.0.1:%1").arg(server.GetPort()); - - QMessageBox dlg(owner); - dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); - dlg.setWindowTitle(QTStr("YouTube.Auth.WaitingAuth.Title")); - - std::string clientid = YOUTUBE_CLIENTID; - std::string secret = YOUTUBE_SECRET; - deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); - deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); - - QString state; - state = auth->GenerateState(); - server.SetState(state); - - QString url_template; - url_template += "%1"; - url_template += "?response_type=code"; - url_template += "&client_id=%2"; - url_template += "&redirect_uri=%3"; - url_template += "&state=%4"; - url_template += "&scope=https://www.googleapis.com/auth/youtube"; - QString url = url_template.arg(YOUTUBE_AUTH_URL, clientid.c_str(), - redirect_uri, state); - - QString text = QTStr("YouTube.Auth.WaitingAuth.Text"); - text = text.arg( - QString("Google OAuth Service").arg(url)); - - dlg.setText(text); - dlg.setTextFormat(Qt::RichText); - dlg.setStandardButtons(QMessageBox::StandardButton::Cancel); - - connect(&dlg, &QMessageBox::buttonClicked, &dlg, - [&](QAbstractButton *) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Action Cancelled."); -#endif - // TODO: Stop server. - dlg.reject(); - }); - - // Async Login. - connect(&server, &AuthListener::ok, &dlg, - [&dlg, &auth_code](QString code) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Got youtube redirected answer: %s", - QT_TO_UTF8(code)); -#endif - auth_code = code; - dlg.accept(); - }); - connect(&server, &AuthListener::fail, &dlg, [&dlg]() { -#ifdef _DEBUG - blog(LOG_DEBUG, "No access granted"); -#endif - dlg.reject(); - }); - - auto open_external_browser = [url]() { - OpenBrowser(url); - }; - QScopedPointer thread(CreateQThread(open_external_browser)); - thread->start(); - - dlg.exec(); - if (dlg.result() == QMessageBox::Cancel || - dlg.result() == QDialog::Rejected) - return nullptr; - - if (!auth->GetToken(YOUTUBE_TOKEN_URL, clientid, secret, - QT_TO_UTF8(redirect_uri), YOUTUBE_SCOPE_VERSION, - QT_TO_UTF8(auth_code), true)) { - return nullptr; - } - - config_t *config = OBSBasic::Get()->Config(); - config_remove_value(config, "YouTube", "ChannelName"); - - ChannelDescription cd; - if (auth->GetChannelDescription(cd)) - config_set_string(config, "YouTube", "ChannelName", - QT_TO_UTF8(cd.title)); - - config_save_safe(config, "tmp", nullptr); - return auth; -} - -#ifdef BROWSER_AVAILABLE -void YoutubeChatDock::SetWidget(QCefWidget *widget_) -{ - lineEdit = new LineEditAutoResize(); - lineEdit->setVisible(false); - lineEdit->setMaxLength(200); - lineEdit->setPlaceholderText(QTStr("YouTube.Chat.Input.Placeholder")); - sendButton = new QPushButton(QTStr("YouTube.Chat.Input.Send")); - sendButton->setVisible(false); - - chatLayout = new QHBoxLayout(); - chatLayout->setContentsMargins(0, 0, 0, 0); - chatLayout->addWidget(lineEdit, 1); - chatLayout->addWidget(sendButton); - - QVBoxLayout *layout = new QVBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(widget_, 1); - layout->addLayout(chatLayout); - - QWidget *widget = new QWidget(); - widget->setLayout(layout); - setWidget(widget); - - QWidget::connect(lineEdit, SIGNAL(returnPressed()), this, - SLOT(SendChatMessage())); - QWidget::connect(sendButton, SIGNAL(pressed()), this, - SLOT(SendChatMessage())); - - cefWidget.reset(widget_); -} - -void YoutubeChatDock::SetApiChatId(const std::string &id) -{ - this->apiChatId = id; - QMetaObject::invokeMethod(this, "EnableChatInput", - Qt::QueuedConnection); -} - -void YoutubeChatDock::SendChatMessage() -{ - const QString message = lineEdit->text(); - if (message == "") - return; - - OBSBasic *main = OBSBasic::Get(); - YoutubeApiWrappers *apiYouTube( - dynamic_cast(main->GetAuth())); - - ExecuteFuncSafeBlock([&]() { - lineEdit->setText(""); - lineEdit->setPlaceholderText( - QTStr("YouTube.Chat.Input.Sending")); - if (apiYouTube->SendChatMessage(apiChatId, message)) { - os_sleep_ms(3000); - } else { - QString error = apiYouTube->GetLastError(); - apiYouTube->GetTranslatedError(error); - QMetaObject::invokeMethod( - this, "ShowErrorMessage", Qt::QueuedConnection, - Q_ARG(const QString &, error)); - } - lineEdit->setPlaceholderText( - QTStr("YouTube.Chat.Input.Placeholder")); - }); -} - -void YoutubeChatDock::ShowErrorMessage(const QString &error) -{ - QMessageBox::warning(this, QTStr("YouTube.Chat.Error.Title"), - QTStr("YouTube.Chat.Error.Text").arg(error)); -} - -void YoutubeChatDock::EnableChatInput() -{ - lineEdit->setVisible(true); - sendButton->setVisible(true); -} -#endif diff --git a/UI/auth-youtube.hpp b/UI/auth-youtube.hpp deleted file mode 100644 index ffe35c25c39c09..00000000000000 --- a/UI/auth-youtube.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "auth-oauth.hpp" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#include "lineedit-autoresize.hpp" -#include -class YoutubeChatDock : public BrowserDock { - Q_OBJECT - -private: - std::string apiChatId; - LineEditAutoResize *lineEdit; - QPushButton *sendButton; - QHBoxLayout *chatLayout; - -public: - void SetWidget(QCefWidget *widget_); - void SetApiChatId(const std::string &id); - -private slots: - void SendChatMessage(); - void ShowErrorMessage(const QString &error); - void EnableChatInput(); -}; -#endif - -inline const std::vector youtubeServices = { - {"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; - -class YoutubeAuth : public OAuthStreamKey { - Q_OBJECT - - bool uiLoaded = false; - std::string section; - -#ifdef BROWSER_AVAILABLE - YoutubeChatDock *chat; -#endif - - virtual bool RetryLogin() override; - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - virtual void LoadUI() override; - - QString GenerateState(); - -public: - YoutubeAuth(const Def &d); - ~YoutubeAuth(); - - void SetChatId(const QString &chat_id, const std::string &api_chat_id); - void ResetChat(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service); -}; diff --git a/UI/broadcast-flow.cpp b/UI/broadcast-flow.cpp new file mode 100644 index 00000000000000..8af6441337d176 --- /dev/null +++ b/UI/broadcast-flow.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "broadcast-flow.hpp" + +void OBSBroadcastFlow::ManageBroadcast(bool streamingActive) +{ + if (flow.manage_broadcast2) { + flow.manage_broadcast2(flow.priv, streamingActive); + } else if (!streamingActive) { + flow.manage_broadcast(flow.priv); + } + + if (streamingActive) + return; + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); +} + +bool OBSBroadcastFlow::AllowManagingWhileStreaming() +{ + return (flow.flags & OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) != + 0; +} + +obs_broadcast_state OBSBroadcastFlow::BroadcastState() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) == + 0 && + broadcastState == OBS_BROADCAST_INACTIVE) { + return OBS_BROADCAST_NONE; + } + + return broadcastState; +} + +obs_broadcast_start OBSBroadcastFlow::BroadcastStartType() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) == + 0 && + broadcastStartType == OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_START_WITH_STREAM; + } + + return broadcastStartType; +} + +obs_broadcast_stop OBSBroadcastFlow::BroadcastStopType() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) == + 0 && + broadcastStopType == OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_STOP_NEVER; + } + + return broadcastStopType; +} +bool OBSBroadcastFlow::DifferedStartBroadcast() +{ + if (BroadcastStartType() != OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return false; + } + + flow.differed_start_broadcast(flow.priv); + broadcastState = flow.get_broadcast_state(flow.priv); + + /* If not active after start, failure */ + return broadcastState == OBS_BROADCAST_ACTIVE; +} + +obs_broadcast_stream_state OBSBroadcastFlow::IsBroadcastStreamActive() +{ + if (BroadcastStartType() != OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_STREAM_FAILURE; + } + + return flow.is_broadcast_stream_active(flow.priv); +} + +bool OBSBroadcastFlow::DifferedStopBroadcast() +{ + if (BroadcastStopType() != OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) + return false; + + bool ret = flow.differed_stop_broadcast(flow.priv); + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); + + return ret; +} + +void OBSBroadcastFlow::StopStreaming() +{ + flow.stopped_streaming(flow.priv); + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); +} + +std::string OBSBroadcastFlow::GetLastError() +{ + if (!flow.get_last_error) + return ""; + + const char *error = flow.get_last_error(flow.priv); + if (!error) + return ""; + + return error; +} diff --git a/UI/broadcast-flow.hpp b/UI/broadcast-flow.hpp new file mode 100644 index 00000000000000..8df5ff6750439e --- /dev/null +++ b/UI/broadcast-flow.hpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class OBSBroadcastFlow { + const obs_service_t *boundedService = nullptr; + + obs_frontend_broadcast_flow flow = {0}; + + obs_broadcast_state broadcastState; + obs_broadcast_start broadcastStartType; + obs_broadcast_stop broadcastStopType; + +public: + inline OBSBroadcastFlow(const obs_service_t *service, + const obs_frontend_broadcast_flow &flow) + : boundedService(service), + flow(flow), + broadcastState(flow.get_broadcast_state(flow.priv)), + broadcastStartType(flow.get_broadcast_start_type(flow.priv)), + broadcastStopType(flow.get_broadcast_stop_type(flow.priv)){}; + inline ~OBSBroadcastFlow() {} + + const obs_service_t *GetBondedService() { return boundedService; } + + void ManageBroadcast(bool streamingActive); + + bool AllowManagingWhileStreaming(); + + obs_broadcast_state BroadcastState(); + obs_broadcast_start BroadcastStartType(); + obs_broadcast_stop BroadcastStopType(); + + bool DifferedStartBroadcast(); + obs_broadcast_stream_state IsBroadcastStreamActive(); + bool DifferedStopBroadcast(); + + void StopStreaming(); + + std::string GetLastError(); +}; diff --git a/UI/cmake/feature-restream.cmake b/UI/cmake/feature-restream.cmake deleted file mode 100644 index 813d45971c005b..00000000000000 --- a/UI/cmake/feature-restream.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(RESTREAM_CLIENTID - AND RESTREAM_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-restream.cpp auth-restream.hpp) - target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED) -else() - target_disable_feature(obs-studio "Restream API connection") - set(RESTREAM_CLIENTID "") - set(RESTREAM_HASH "0") -endif() diff --git a/UI/cmake/feature-twitch.cmake b/UI/cmake/feature-twitch.cmake deleted file mode 100644 index 099f4c8e07d131..00000000000000 --- a/UI/cmake/feature-twitch.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(TWITCH_CLIENTID - AND TWITCH_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-twitch.cpp auth-twitch.hpp) - target_enable_feature(obs-studio "Twitch API connection" TWITCH_ENABLED) -else() - target_disable_feature(obs-studio "Twitch API connection") - set(TWITCH_CLIENTID "") - set(TWITCH_HASH "0") -endif() diff --git a/UI/cmake/feature-youtube.cmake b/UI/cmake/feature-youtube.cmake deleted file mode 100644 index 23d460b70c01c3..00000000000000 --- a/UI/cmake/feature-youtube.cmake +++ /dev/null @@ -1,13 +0,0 @@ -if(YOUTUBE_CLIENTID - AND YOUTUBE_SECRET - AND YOUTUBE_CLIENTID_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND YOUTUBE_SECRET_HASH MATCHES "(0|[a-fA-F0-9]+)") - target_sources(obs-studio PRIVATE auth-youtube.cpp auth-youtube.hpp window-youtube-actions.cpp - window-youtube-actions.hpp youtube-api-wrappers.cpp youtube-api-wrappers.hpp) - - target_enable_feature(obs-studio "YouTube API connection" YOUTUBE_ENABLED) -else() - target_disable_feature(obs-studio "YouTube API connection") - set(YOUTUBE_SECRET_HASH 0) - set(YOUTUBE_CLIENTID_HASH 0) -endif() diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 0650d3d3a98654..18351dd56b638f 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -18,51 +18,6 @@ if(TARGET obs-browser target_include_directories(obs-browser-panels INTERFACE ${CMAKE_SOURCE_DIR}/plugins/obs-browser/panel) endif() -set(OAUTH_BASE_URL - "https://auth.obsproject.com/" - CACHE STRING "Default OAuth base URL") - -mark_as_advanced(OAUTH_BASE_URL) - -if(NOT DEFINED TWITCH_CLIENTID - OR "${TWITCH_CLIENTID}" STREQUAL "" - OR NOT DEFINED TWITCH_HASH - OR "${TWITCH_HASH}" STREQUAL "" - OR NOT TARGET OBS::browser-panels) - set(TWITCH_ENABLED OFF) - set(TWITCH_CLIENTID "") - set(TWITCH_HASH "0") -else() - set(TWITCH_ENABLED ON) -endif() - -if(NOT DEFINED RESTREAM_CLIENTID - OR "${RESTREAM_CLIENTID}" STREQUAL "" - OR NOT DEFINED RESTREAM_HASH - OR "${RESTREAM_HASH}" STREQUAL "" - OR NOT TARGET OBS::browser-panels) - set(RESTREAM_ENABLED OFF) - set(RESTREAM_CLIENTID "") - set(RESTREAM_HASH "0") -else() - set(RESTREAM_ENABLED ON) -endif() - -if(NOT DEFINED YOUTUBE_CLIENTID - OR "${YOUTUBE_CLIENTID}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET - OR "${YOUTUBE_SECRET}" STREQUAL "" - OR NOT DEFINED YOUTUBE_CLIENTID_HASH - OR "${YOUTUBE_CLIENTID_HASH}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET_HASH - OR "${YOUTUBE_SECRET_HASH}" STREQUAL "") - set(YOUTUBE_SECRET_HASH "0") - set(YOUTUBE_CLIENTID_HASH "0") - set(YOUTUBE_ENABLED OFF) -else() - set(YOUTUBE_ENABLED ON) -endif() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in ${CMAKE_CURRENT_BINARY_DIR}/ui-config.h) find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) @@ -115,7 +70,6 @@ target_sources( forms/OBSMissingFiles.ui forms/OBSRemux.ui forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui forms/source-toolbar/browser-source-toolbar.ui forms/source-toolbar/color-source-toolbar.ui forms/source-toolbar/device-select-toolbar.ui @@ -126,19 +80,13 @@ target_sources( target_sources( obs - PRIVATE auth-oauth.cpp - auth-oauth.hpp - auth-listener.cpp - auth-listener.hpp - obf.c - obf.h - obs-app.cpp + PRIVATE obs-app.cpp obs-app.hpp obs-proxy-style.cpp obs-proxy-style.hpp api-interface.cpp - auth-base.cpp - auth-base.hpp + broadcast-flow.cpp + broadcast-flow.hpp display-helpers.hpp platform.hpp qt-display.cpp @@ -201,6 +149,8 @@ target_sources( scene-tree.cpp scene-tree.hpp screenshot-obj.hpp + service-sort-filter.cpp + service-sort-filter.hpp slider-absoluteset-style.cpp slider-absoluteset-style.hpp slider-ignorewheel.cpp @@ -247,6 +197,7 @@ target_sources( window-basic-main-profiles.cpp window-basic-main-scene-collections.cpp window-basic-main-screenshot.cpp + window-basic-main-service.cpp window-basic-main-transitions.cpp window-basic-preview.cpp window-basic-properties.cpp @@ -290,8 +241,19 @@ target_compile_features(obs PRIVATE cxx_std_17) target_include_directories(obs PRIVATE ${CMAKE_SOURCE_DIR}/deps/json11 ${CMAKE_SOURCE_DIR}/deps/libff) -target_link_libraries(obs PRIVATE CURL::libcurl FFmpeg::avcodec FFmpeg::avutil FFmpeg::avformat OBS::libobs - OBS::frontend-api) +if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") +endif() + +target_link_libraries( + obs + PRIVATE CURL::libcurl + FFmpeg::avcodec + FFmpeg::avutil + FFmpeg::avformat + OBS::libobs + OBS::frontend-api + OBS::obf) set_target_properties(obs PROPERTIES FOLDER "frontend") @@ -304,16 +266,6 @@ if(TARGET OBS::browser-panels) target_sources(obs PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp) - if(TWITCH_ENABLED) - target_compile_definitions(obs PRIVATE TWITCH_ENABLED) - target_sources(obs PRIVATE auth-twitch.cpp auth-twitch.hpp) - endif() - - if(RESTREAM_ENABLED) - target_compile_definitions(obs PRIVATE RESTREAM_ENABLED) - target_sources(obs PRIVATE auth-restream.cpp auth-restream.hpp) - endif() - if(OS_WINDOWS OR OS_MACOS) set(ENABLE_WHATSNEW ON @@ -327,12 +279,6 @@ if(TARGET OBS::browser-panels) endif() endif() -if(YOUTUBE_ENABLED) - target_compile_definitions(obs PRIVATE YOUTUBE_ENABLED) - target_sources(obs PRIVATE auth-youtube.cpp auth-youtube.hpp youtube-api-wrappers.cpp youtube-api-wrappers.hpp - window-youtube-actions.cpp window-youtube-actions.hpp) -endif() - if(OS_WINDOWS) set_target_properties(obs PROPERTIES WIN32_EXECUTABLE ON OUTPUT_NAME "obs${_ARCH_SUFFIX}") diff --git a/UI/cmake/ui-elements.cmake b/UI/cmake/ui-elements.cmake index a24cd9b592d77d..c19d6e97d5a4b1 100644 --- a/UI/cmake/ui-elements.cmake +++ b/UI/cmake/ui-elements.cmake @@ -45,6 +45,8 @@ target_sources( scene-tree.cpp scene-tree.hpp screenshot-obj.hpp + service-sort-filter.cpp + service-sort-filter.hpp slider-absoluteset-style.cpp slider-absoluteset-style.hpp slider-ignorewheel.cpp diff --git a/UI/cmake/ui-qt.cmake b/UI/cmake/ui-qt.cmake index 3107f2aaea3fd2..9018e8aa622fca 100644 --- a/UI/cmake/ui-qt.cmake +++ b/UI/cmake/ui-qt.cmake @@ -40,7 +40,6 @@ set(_qt_sources forms/OBSMissingFiles.ui forms/OBSRemux.ui forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui forms/source-toolbar/browser-source-toolbar.ui forms/source-toolbar/color-source-toolbar.ui forms/source-toolbar/device-select-toolbar.ui diff --git a/UI/cmake/ui-windows.cmake b/UI/cmake/ui-windows.cmake index 1c08ba5ee4fec2..a5e98f5c976cdb 100644 --- a/UI/cmake/ui-windows.cmake +++ b/UI/cmake/ui-windows.cmake @@ -20,6 +20,7 @@ target_sources( window-basic-main-profiles.cpp window-basic-main-scene-collections.cpp window-basic-main-screenshot.cpp + window-basic-main-service.cpp window-basic-main-transitions.cpp window-basic-main.cpp window-basic-main.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 4b2f4de90dbd0f..c5f5432fa19b93 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -140,26 +140,6 @@ ExtraBrowsers="Custom Browser Docks" ExtraBrowsers.Info="Add docks by giving them a name and URL, then click Apply or Close to open the docks. You can add or remove docks at any time." ExtraBrowsers.DockName="Dock Name" -# Auth -Auth.Authing.Title="Authenticating..." -Auth.Authing.Text="Authenticating with %1, please wait..." -Auth.AuthFailure.Title="Authentication Failure" -Auth.AuthFailure.Text="Failed to authenticate with %1:\n\n%2: %3" -Auth.InvalidScope.Title="Authentication Required" -Auth.InvalidScope.Text="The authentication requirements for %1 have changed. Some features may not be available." -Auth.LoadingChannel.Title="Loading channel information..." -Auth.LoadingChannel.Text="Loading channel information for %1, please wait..." -Auth.LoadingChannel.Error="Couldn't get channel information." -Auth.ChannelFailure.Title="Failed to load channel" -Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3" -Auth.Chat="Chat" -Auth.StreamInfo="Stream Information" -TwitchAuth.Stats="Twitch Stats" -TwitchAuth.Feed="Twitch Activity Feed" -TwitchAuth.TwoFactorFail.Title="Could not query stream key" -TwitchAuth.TwoFactorFail.Text="OBS was unable to connect to your Twitch account. Please make sure two-factor authentication is set up in your Twitch security settings as this is required to stream." -RestreamAuth.Channels="Restream Channels" - # copy filters Copy.Filters="Copy Filters" Paste.Filters="Paste Filters" @@ -168,13 +148,6 @@ Paste.Filters="Paste Filters" BrowserPanelInit.Title="Initializing Browser..." BrowserPanelInit.Text="Initializing browser, please wait..." -# bandwidth test -BandwidthTest.Region="Region" -BandwidthTest.Region.US="United States" -BandwidthTest.Region.EU="Europe" -BandwidthTest.Region.Asia="Asia" -BandwidthTest.Region.Other="Other" - # auto config wizard Basic.AutoConfig="Auto-Configuration Wizard" Basic.AutoConfig.ApplySettings="Apply Settings" @@ -193,23 +166,14 @@ Basic.AutoConfig.VideoPage.FPS.PreferHighRes="Either 60 or 30, but prefer high r Basic.AutoConfig.VideoPage.CanvasExplanation="Note: The canvas (base) resolution is not necessarily the same as the resolution you will stream or record with. Your actual stream/recording resolution may be scaled down from the canvas resolution to reduce resource usage or bitrate requirements." Basic.AutoConfig.StreamPage="Stream Information" Basic.AutoConfig.StreamPage.SubTitle="Please enter your stream information" -Basic.AutoConfig.StreamPage.ConnectAccount="Connect Account (recommended)" -Basic.AutoConfig.StreamPage.DisconnectAccount="Disconnect Account" -Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title="Disconnect Account?" -Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text="This change will apply immediately. Are you sure you want to disconnect your account?" Basic.AutoConfig.StreamPage.GetStreamKey="Get Stream Key" Basic.AutoConfig.StreamPage.MoreInfo="More Info" -Basic.AutoConfig.StreamPage.UseStreamKey="Use Stream Key" -Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced="Use Stream Key (advanced)" Basic.AutoConfig.StreamPage.Service="Service" Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..." Basic.AutoConfig.StreamPage.Service.Custom="Custom..." Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Stream Key" -Basic.AutoConfig.StreamPage.StreamKey.ToolTip="RIST: enter the encryption passphrase.\nRTMP: enter the key provided by the service.\nSRT: enter the streamid if the service uses one." Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" -Basic.AutoConfig.StreamPage.BearerToken="Bearer Token" -Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardware Encoding eliminates most CPU usage, but may require more bitrate to obtain the same level of quality." @@ -431,6 +395,7 @@ Output.NoBroadcast.Title="No Broadcast Configured" Output.NoBroadcast.Text="You need to set up a broadcast before you can start streaming." Output.BroadcastStartFailed="Failed to start broadcast" Output.BroadcastStopFailed="Failed to stop broadcast" +Output.BroadcastUnknownError="An error has occured" # log upload dialog text and messages LogReturnDialog="Log Upload Successful" @@ -726,6 +691,11 @@ Basic.Main.SetupBroadcast="Manage Broadcast" Basic.Main.StopStreaming="Stop Streaming" Basic.Main.StopBroadcast="End Broadcast" Basic.Main.AutoStopEnabled="(Auto Stop)" +Basic.Main.BroadcastManualStartWarning.Title="Manual start required" +Basic.Main.BroadcastManualStartWarning="Auto-start is disabled for this event, click \"Go Live\" to start your broadcast." +Basic.Main.BroadcastEndWarning="You will not be able to reconnect.
Your stream will stop and you will no longer be live." +Basic.Main.ManageBroadcastBWTest.Title="Bandwidth Test Enabled" +Basic.Main.ManageBroadcastBWTest.Text="You have OBS configured in bandwidth test mode. Managing broadcast is not allowed under this mode, you will need to disable it in order manage your broadcast." Basic.Main.StoppingStreaming="Stopping Stream..." Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" Basic.Main.ShowContextBar="Show Source Toolbar" @@ -911,24 +881,18 @@ Basic.Settings.General.ChannelDescription.beta="Potentially unstable pre-release # basic mode 'stream' settings Basic.Settings.Stream="Stream" -Basic.Settings.Stream.Custom.UseAuthentication="Use authentication" -Basic.Settings.Stream.Custom.Username="Username" -Basic.Settings.Stream.Custom.Password="Password" -Basic.Settings.Stream.Custom.Username.ToolTip="RIST: enter the srp_username.\nRTMP: enter the username.\nSRT: not used." -Basic.Settings.Stream.Custom.Password.ToolTip="RIST: enter the srp_password.\nRTMP: enter the password.\nSRT: enter the encryption passphrase." +Basic.Settings.Stream.DeprecatedType="%1 (Deprecated)" +Basic.Settings.Stream.ServiceSettings="Service Settings" +Basic.Settings.Stream.NoDefaultProtocol.Title="No Default Protocol" +Basic.Settings.Stream.NoDefaultProtocol.Text="The service \"%1\" does not have a default protocol.\n\nThe service change will be reverted." Basic.Settings.Stream.BandwidthTestMode="Enable Bandwidth Test Mode" -Basic.Settings.Stream.TTVAddon="Twitch Chat Add-Ons" -Basic.Settings.Stream.TTVAddon.None="None" -Basic.Settings.Stream.TTVAddon.BTTV="BetterTTV" -Basic.Settings.Stream.TTVAddon.FFZ="FrankerFaceZ" -Basic.Settings.Stream.TTVAddon.Both="BetterTTV and FrankerFaceZ" Basic.Settings.Stream.MissingSettingAlert="Missing Stream Setup" Basic.Settings.Stream.StreamSettingsWarning="Open Settings" Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOpen settings to enter the URL and Stream Key in the 'stream' tab." Basic.Settings.Stream.MissingUrl="Stream URL is missing.\n\nOpen settings to enter the URL in the 'Stream' tab." Basic.Settings.Stream.MissingStreamKey="Stream key is missing.\n\nOpen settings to enter the stream key in the 'Stream' tab." -Basic.Settings.Stream.IgnoreRecommended="Ignore streaming service setting recommendations" -Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Recommended Settings" +Basic.Settings.Stream.IgnoreRecommended="Ignore service setting recommendations and maximums" +Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Service Settings" Basic.Settings.Stream.IgnoreRecommended.Warn.Text="Warning: Ignoring the service's limitations may result in degraded stream quality or prevent you from streaming.\n\nContinue?" Basic.Settings.Stream.Recommended.MaxVideoBitrate="Maximum Video Bitrate: %1 kbps" Basic.Settings.Stream.Recommended.MaxAudioBitrate="Maximum Audio Bitrate: %1 kbps" @@ -1414,101 +1378,3 @@ ContextBar.MediaControls.RestartMedia="Restart Media" ContextBar.MediaControls.PlaylistNext="Next in Playlist" ContextBar.MediaControls.PlaylistPrevious="Previous in Playlist" ContextBar.MediaControls.BlindSeek="Media Seek Widget" - -# YouTube Actions and Auth -YouTube.Auth.Ok="Authorization completed successfully.\nYou can now close this page." -YouTube.Auth.NoCode="The authorization process was not completed." -YouTube.Auth.NoChannels="No channel(s) available on selected account" -YouTube.Auth.WaitingAuth.Title="YouTube User Authorization" -YouTube.Auth.WaitingAuth.Text="Please complete the authorization in your external browser.
If the external browser does not open, follow this link and complete the authorization:
%1" -YouTube.AuthError.Text="Failed to get channel information: %1." - -YouTube.Actions.WindowTitle="YouTube Broadcast Setup - Channel: %1" -YouTube.Actions.CreateNewEvent="Create New Broadcast" -YouTube.Actions.ChooseEvent="Select Existing Broadcast" -YouTube.Actions.Title="Title*" -YouTube.Actions.MyBroadcast="My Broadcast" -YouTube.Actions.Description="Description" -YouTube.Actions.Privacy="Privacy*" -YouTube.Actions.Privacy.Private="Private" -YouTube.Actions.Privacy.Public="Public" -YouTube.Actions.Privacy.Unlisted="Unlisted" -YouTube.Actions.Category="Category" - -YouTube.Actions.Thumbnail="Thumbnail" -YouTube.Actions.Thumbnail.SelectFile="Select file..." -YouTube.Actions.Thumbnail.NoFileSelected="No file selected" -YouTube.Actions.Thumbnail.ClearFile="Clear" - -YouTube.Actions.MadeForKids="Is this video made for kids?*" -YouTube.Actions.MadeForKids.Yes="Yes, it's made for kids" -YouTube.Actions.MadeForKids.No="No, it's not made for kids" -YouTube.Actions.MadeForKids.Help="(?)" -YouTube.Actions.AdditionalSettings="Additional settings" -YouTube.Actions.Latency="Latency" -YouTube.Actions.Latency.Normal="Normal" -YouTube.Actions.Latency.Low="Low" -YouTube.Actions.Latency.UltraLow="Ultra low" -YouTube.Actions.EnableAutoStart="Enable Auto-start" -YouTube.Actions.EnableAutoStop="Enable Auto-stop" -YouTube.Actions.AutoStartStop.TT="Indicates whether this scheduled broadcast should start automatically" -YouTube.Actions.EnableDVR="Enable DVR" -YouTube.Actions.360Video="360 video" -YouTube.Actions.360Video.Help="(?)" -YouTube.Actions.ScheduleForLater="Schedule for later" -YouTube.Actions.RememberSettings="Remember these settings" - -YouTube.Actions.Create_Ready="Create broadcast" -YouTube.Actions.Create_GoLive="Create broadcast and start streaming" -YouTube.Actions.Choose_Ready="Select broadcast" -YouTube.Actions.Choose_GoLive="Select broadcast and start streaming" -YouTube.Actions.Create_Schedule="Schedule broadcast" -YouTube.Actions.Create_Schedule_Ready="Schedule and select broadcast" -YouTube.Actions.Dashboard="Open YouTube Studio" - -YouTube.Actions.Error.Title="Live broadcast creation error" -YouTube.Actions.Error.Text="YouTube access error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" -YouTube.Actions.Error.General="YouTube access error. Please check your network connection or your YouTube server access." -YouTube.Actions.Error.NoBroadcastCreated="Broadcast creation error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" -YouTube.Actions.Error.NoStreamCreated="No stream created. Please relink your account." -YouTube.Actions.Error.YouTubeApi="YouTube API Error. Please see the log file for more information." -YouTube.Actions.Error.BroadcastNotFound="The selected broadcast was not found." -YouTube.Actions.Error.FileMissing="Selected file does not exist." -YouTube.Actions.Error.FileOpeningFailed="Failed opening selected file." -YouTube.Actions.Error.FileTooLarge="Selected file is too large (Limit: 2 MiB)." -YouTube.Actions.Error.BroadcastTransitionFailed="Transitioning the broadcast failed: %1

If this error persists open the broadcast in YouTube Studio and try manually." -YouTube.Actions.Error.BroadcastTestStarting="Broadcast is transitioning to the test stage, this can take some time. Please try again in 10-30 seconds." - -YouTube.Actions.EventsLoading="Loading list of events..." -YouTube.Actions.EventCreated.Title="Event Created" -YouTube.Actions.EventCreated.Text="Event created successfully." - -YouTube.Actions.Stream="Stream" -YouTube.Actions.Stream.ScheduledFor="Scheduled for %1" -YouTube.Actions.Stream.Resume="Resume interrupted stream" -YouTube.Actions.Stream.YTStudio="Automatically created by YouTube Studio" - -YouTube.Actions.Notify.Title="YouTube" -YouTube.Actions.Notify.CreatingBroadcast="Creating a new Live Broadcast, please wait..." - -YouTube.Actions.AutoStartStreamingWarning.Title="Manual start required" -YouTube.Actions.AutoStartStreamingWarning="Auto-start is disabled for this event, click \"Go Live\" to start your broadcast." -YouTube.Actions.AutoStopStreamingWarning="You will not be able to reconnect.
Your stream will stop and you will no longer be live." - -YouTube.Chat.Input.Send="Send" -YouTube.Chat.Input.Placeholder="Enter message here..." -YouTube.Chat.Input.Sending="Sending..." -YouTube.Chat.Error.Title="Error while sending message" -YouTube.Chat.Error.Text="The message couldn't be sent: %1" - -# YouTube API errors in format "YouTube.Errors." -YouTube.Errors.liveStreamingNotEnabled="Live streaming is not enabled on the selected YouTube channel.

See youtube.com/features for more information." -YouTube.Errors.livePermissionBlocked="Live streaming is unavailable on the selected YouTube Channel.
Please note that it may take up to 24 hours for live streaming to become available after enabling it in your channel settings.

See youtube.com/features for details." -YouTube.Errors.errorExecutingTransition="Transition failed due to a backend error. Please try again in a few seconds." -YouTube.Errors.errorStreamInactive="YouTube is not receiving data for your stream. Please check your configuration and try again." -YouTube.Errors.invalidTransition="The attempted transition was invalid. This may be due to the stream not having finished a previous transition. Please wait a few seconds and try again." -# Chat errors -YouTube.Errors.liveChatDisabled="Live chat is disabled on this stream." -YouTube.Errors.liveChatEnded="Live stream has ended." -YouTube.Errors.messageTextInvalid="The message text is not valid." -YouTube.Errors.rateLimitExceeded="You are sending messages too quickly." diff --git a/UI/data/services-conversion-matrix.json b/UI/data/services-conversion-matrix.json new file mode 100644 index 00000000000000..64d0de788dcde1 --- /dev/null +++ b/UI/data/services-conversion-matrix.json @@ -0,0 +1,104 @@ +{ + "Twitch": "twitch", + "YouTube - HLS": "youtube", + "YouTube - RTMPS": "youtube", + "YouTube / YouTube Gaming": "youtube", + "YouTube - RTMP": "youtube", + "YouTube - RTMPS (Beta)": "youtube", + "Loola.tv": "loola", + "Lovecast": "lovecast", + "Luzento.com - RTMP": "luzento", + "VIMM": "vimm", + "Web.TV": "web_tv", + "GoodGame.ru": "goodgame", + "YouStreamer": "youstreamer", + "Vaughn Live / iNSTAGIB": "vaughn_live", + "Breakers.TV": "breakers_tv", + "Facebook Live": "facebook", + "Restream.io": "restream", + "Restream.io - RTMP": "restream", + "Restream.io - FTL": "restream", + "Castr.io": "castr", + "Boomstream": "boomstream", + "Meridix Live Sports Platform": "meridix", + "AfreecaTV": "afreecatv", + "아프리카TV": "afreecatv", + "Afreeca.TV": "afreecatv", + "CAM4": "cam4", + "ePlay": "eplay", + "Picarto": "picarto", + "Livestream": "livestream", + "Uscreen": "uscreen", + "Stripchat": "stripchat", + "CamSoda": "camsoda", + "Chaturbate": "chartubate", + "Twitter": "twitter", + "Twitter / Periscope": "twitter", + "Switchboard Live": "switchboard_live", + "Switchboard Live (Joicaster)": "switcherboard_live", + "Looch": "looch", + "Eventials": "eventials", + "EventLive.pro": "eventlive", + "Lahzenegar - StreamG | لحظه‌نگار - استریمجی": "lahzenegar", + "MyLive": "mylive", + "Trovo": "trovo", + "Madcat": "trovo", + "Mixcloud": "mixcloud", + "SermonAudio Cloud": "sermonaudio", + "SermonAudio.com": "sermonaudio", + "Vimeo": "vimeo", + "Aparat": "aparat", + "KakaoTV": "kakaotv", + "Piczel.tv": "piczel_tv", + "STAGE TEN": "stage_ten", + "DLive": "dlive", + "Lightcast.com": "lightcast_com", + "Bongacams": "bongacams", + "Chathostess": "chathostess", + "OnlyFans.com": "onlyfans", + "YouNow": "younow", + "Steam": "steam", + "Konduit.live": "konduit", + "LOCO": "loco", + "niconico, premium member (ニコニコ生放送 プレミアム会員)": "niconico-premium", + "niconico, free member (ニコニコ生放送 一般会員)": "niconico-free", + "WASD.TV": "wasd_tv", + "Nimo TV": "nimo_tv", + "XLoveCam.com": "xlovecam", + "AngelThump": "angelthump", + "api.video": "api_video", + "SHOWROOM": "showroom", + "Mux": "mux", + "Viloud": "viloud", + "MyFreeCams": "myfreecams", + "PolyStreamer.com": "polystreamer", + "OPENREC.tv - Premium member (プレミアム会員)": "openrec_tv", + "nanoStream Cloud / bintu": "nanostream", + "Dacast": "dacast", + "Bilibili Live - RTMP | 哔哩哔哩直播 - RTMP": "bilibili", + "Bilibili Live": "bilibili", + "Volume.com": "volume_com", + "BoxCast": "boxcast", + "Disciple Media": "disciple", + "Jio Games": "jio_games", + "Kuaishou Live": "kuaishou", + "Utreon": "ultreon", + "Autistici.org Live": "autistici_org", + "PhoneLiveStreaming": "phonelivestreaming", + "ManyVids": "manyvids", + "Fantasy.Club": "fantasy_club", + "WpStream": "wpstream", + "Sympla": "sympla", + "Mildom": "mildom", + "Nonolive": "nonolive", + "StreamVi": "streamvi", + "Livepush": "livepush", + "Vindral": "vindral", + "Whowatch (ふわっち)": "whowhatch", + "IRLToolkit": "irltoolkit", + "Bitmovin": "bitmovin", + "Live Streamer Cafe": "live_streamer_cafe", + "Enchant.events": "enchant_events", + "Joystick.TV": "joystick_tv", + "Livepeer Studio": "livepeer_studio" +} diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index 3a47002d1827db..4fd32e1f0cab2f 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Grey.qss b/UI/data/themes/Grey.qss index 93733dd07c6797..7eea4b0d83bb86 100644 --- a/UI/data/themes/Grey.qss +++ b/UI/data/themes/Grey.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Light.qss b/UI/data/themes/Light.qss index 3f8f81781f6e3e..f9316a10de507e 100644 --- a/UI/data/themes/Light.qss +++ b/UI/data/themes/Light.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 351e9bef9a24be..1c9c5b48dae0eb 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -271,7 +271,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: rgb(59,65,71); } diff --git a/UI/data/themes/Yami.qss b/UI/data/themes/Yami.qss index 54b3c2e6a225cf..453fa44afcab37 100644 --- a/UI/data/themes/Yami.qss +++ b/UI/data/themes/Yami.qss @@ -273,7 +273,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index a6b858082c2aaa..55e545956f7ce6 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -74,32 +74,7 @@ - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Basic.AutoConfig.StreamPage.MoreInfo - - - - - + @@ -118,386 +93,103 @@ - - - 1 + + + + 0 + 0 + + + + Basic.Settings.Stream.ServiceSettings + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - QFormLayout::ExpandingFieldsGrow + + + + Basic.Settings.Output.VideoBitrate - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + bitrate - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 87 - 17 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 87 - 17 - - - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKey - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - QFormLayout::ExpandingFieldsGrow + + + + + + - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 500 - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Basic.Settings.Output.VideoBitrate - - - bitrate - - - - - - - - - - 500 - - - 51000 - - - 2500 - - - - - - - Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip - - - Basic.AutoConfig.StreamPage.PreferHardwareEncoding - - - true - - - - - - - Basic.AutoConfig.StreamPage.PerformBandwidthTest - - - true - - - - - - - BandwidthTest.Region - - - - - - BandwidthTest.Region.Asia - - - - - - - BandwidthTest.Region.US - - - - - - - BandwidthTest.Region.EU - - - - - - - BandwidthTest.Region.Other - - - - - - - - - - PointingHandCursor - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Vertical - - - - 6 - 6 - - - - - - - - 7 - - - 7 - - - - - - true - - - - Auth.LoadingChannel.Title - - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced - - - - - - + + 51000 + + + 2500 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip + + + Basic.AutoConfig.StreamPage.PreferHardwareEncoding + + + true + + + + + + + Basic.AutoConfig.StreamPage.PerformBandwidthTest + + + true + + + + + + + Qt::Vertical + + + + 6 + 6 + + + + + @@ -509,22 +201,5 @@ - - - connectAccount2 - clicked() - connectAccount - click() - - - 382 - 279 - - - 114 - 82 - - - - + diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 30d3dd69f82b59..716da7ad586af2 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -1516,11 +1516,14 @@ - Basic.Main.StartBroadcast + Basic.Main.SetupBroadcast true + + false + diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index bcd7c365701a2e..b50d2d013a4c4a 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -907,7 +907,7 @@ - + 0 0 @@ -928,7 +928,7 @@ 0 - + @@ -942,497 +942,102 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - service + + + + + + 20 - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 20 - - - - - - - - 0 - 0 - - - - Basic.AutoConfig.StreamPage.MoreInfo - - - - + + + Basic.Settings.Stream.IgnoreRecommended + - + - + 0 0 - - 0 + + Basic.Settings.Stream.ServiceSettings - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 170 - 19 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 10 - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 170 - 19 - - - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKey - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - - 0 - 0 - - - - 1 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - -4 - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Qt::Horizontal - - - - 170 - 8 - - - - - - - - 8 - - - 7 - - - 7 - - - - - font-weight: bold - - - Auth.LoadingChannel.Title - - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount - - - - - + + + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + Qt::Horizontal - 40 - 20 + 170 + 8 - - - - - - Basic.Settings.Stream.BandwidthTestMode - - - - - - - Basic.Settings.Stream.Custom.UseAuthentication - - - - - - - Basic.Settings.Stream.Custom.Username - - - authUsername - - - - - - - - - - Basic.Settings.Stream.Custom.Password - - - authPw - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - - - - Basic.Settings.Stream.TTVAddon - - - twitchAddonDropdown - - - - - - - Basic.Settings.Stream.IgnoreRecommended - - - - - - - - - - Qt::RichText - - - true - - - - - - - + + - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced + - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - - - PointingHandCursor + + Qt::RichText - - Basic.AutoConfig.StreamPage.ConnectAccount + + true - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + @@ -7753,11 +7358,6 @@ - - UrlPushButton - QPushButton -
url-push-button.hpp
-
OBSHotkeyEdit QLineEdit @@ -7806,22 +7406,6 @@ multiviewDrawAreas multiviewLayout service - moreInfoButton - connectAccount - useStreamKey - server - customServer - key - show - getStreamKeyButton - connectAccount2 - disconnectAccount - bandwidthTestEnable - useAuth - twitchAddonDropdown - authUsername - authPw - authPwShow outputMode simpleOutputVBitrate simpleOutputABitrate @@ -7939,8 +7523,6 @@ enableLowLatencyMode browserHWAccel hotkeyFocusType - ignoreRecommended - useStreamKeyAdv hotkeyFilterSearch hotkeyFilterInput hotkeyFilterReset @@ -8254,22 +7836,6 @@ - - connectAccount2 - clicked() - connectAccount - click() - - - 484 - 142 - - - 454 - 87 - - - advOutSplitFile toggled(bool) diff --git a/UI/obf.h b/UI/obf.h deleted file mode 100644 index e264e19c43b22b..00000000000000 --- a/UI/obf.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -extern void deobfuscate_str(char *str, uint64_t val); - -#ifdef __cplusplus -} -#endif diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp index df753551b10e05..a4d5d23025d076 100644 --- a/UI/obs-frontend-api/obs-frontend-api.cpp +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -350,6 +350,47 @@ bool obs_frontend_add_custom_qdock(const char *id, void *dock) : false; } +bool obs_frontend_is_browser_available(void) +{ + return !!callbacks_valid() ? c->obs_frontend_is_browser_available() + : false; +} + +void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, size_t size) +{ + if (!callbacks_valid()) + return nullptr; + + if ((offsetof(struct obs_frontend_browser_params, url) + + sizeof(params->url) > + size) || + !params->url) { + blog(LOG_ERROR, "Required value 'url' not found." + " obs_frontend_get_browser_widget failed."); + return nullptr; + } + + struct obs_frontend_browser_params zeroed = {0}; + if (size > sizeof(zeroed)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_get_browser_widget with size " + "%llu which is more than obs-frontend-api currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(zeroed)); + return nullptr; + } + + return c->obs_frontend_get_browser_widget_s(params, size); +} + +void obs_frontend_delete_browser_cookie(const char *url) +{ + if (callbacks_valid()) + c->obs_frontend_delete_browser_cookie(url); +} + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) { @@ -631,3 +672,86 @@ void obs_frontend_add_undo_redo_action(const char *name, c->obs_frontend_add_undo_redo_action( name, undo, redo, undo_data, redo_data, repeatable); } + +void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, size_t size) +{ + if (!callbacks_valid()) + return; + + if (obs_frontend_streaming_active()) { + blog(LOG_ERROR, "obs_frontend_broadcast_flow can not be added " + "while streaming is active"); + return; + } + +#define CHECK_REQUIRED_VAL(val) \ + do { \ + if ((offsetof(struct obs_frontend_broadcast_flow, val) + \ + sizeof(flow->val) > \ + size) || \ + !flow->val) { \ + blog(LOG_ERROR, \ + "Required value '" #val "' not found." \ + " obs_frontend_add_broadcast_flow failed."); \ + return; \ + } \ + } while (false) + + CHECK_REQUIRED_VAL(priv); + CHECK_REQUIRED_VAL(get_broadcast_state); + CHECK_REQUIRED_VAL(get_broadcast_start_type); + CHECK_REQUIRED_VAL(get_broadcast_stop_type); + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) != + 0) { + CHECK_REQUIRED_VAL(manage_broadcast2); + } else { + CHECK_REQUIRED_VAL(manage_broadcast); + } + + CHECK_REQUIRED_VAL(stopped_streaming); + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) != + 0) { + CHECK_REQUIRED_VAL(differed_start_broadcast); + CHECK_REQUIRED_VAL(is_broadcast_stream_active); + CHECK_REQUIRED_VAL(get_last_error); + } + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) != + 0) { + CHECK_REQUIRED_VAL(differed_stop_broadcast); + CHECK_REQUIRED_VAL(get_last_error); + } +#undef CHECK_REQUIRED_VAL + + struct obs_frontend_broadcast_flow zeroed = {0}; + if (size > sizeof(zeroed)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_broadcast_flow with size " + "%llu which is more than obs-frontend-api currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(zeroed)); + return; + } + + c->obs_frontend_add_broadcast_flow_s(service, flow, size); +} + +void obs_frontend_remove_broadcast_flow(const obs_service_t *service) +{ + if (!callbacks_valid()) + return; + + if (obs_frontend_streaming_active()) { + blog(LOG_ERROR, + "obs_frontend_broadcast_flow can not be removed " + "while streaming is active"); + return; + } + + c->obs_frontend_remove_broadcast_flow(service); +} diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h index a0913e683359f1..382d0709a10b78 100644 --- a/UI/obs-frontend-api/obs-frontend-api.h +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -81,6 +82,66 @@ obs_frontend_source_list_free(struct obs_frontend_source_list *source_list) da_free(source_list->sources); } +struct obs_frontend_browser_params { + const char *url; + const char *startup_script; + const char **force_popup_urls; + const char **popup_whitelist_urls; + bool enable_cookie; +}; + +enum obs_broadcast_flow_flag { + OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING = (1 << 0), + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START = (1 << 1), + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP = (1 << 2), +}; + +enum obs_broadcast_state { + OBS_BROADCAST_NONE = 0, + OBS_BROADCAST_ACTIVE, + OBS_BROADCAST_INACTIVE, +}; + +enum obs_broadcast_start { + OBS_BROADCAST_START_WITH_STREAM = 0, + OBS_BROADCAST_START_WITH_STREAM_NOW, + OBS_BROADCAST_START_DIFFER_FROM_STREAM, +}; + +enum obs_broadcast_stop { + OBS_BROADCAST_STOP_NEVER = 0, + OBS_BROADCAST_STOP_WITH_STREAM, + OBS_BROADCAST_STOP_DIFFER_FROM_STREAM, +}; + +enum obs_broadcast_stream_state { + OBS_BROADCAST_STREAM_FAILURE = 0, + OBS_BROADCAST_STREAM_INACTIVE, + OBS_BROADCAST_STREAM_ACTIVE, +}; + +struct obs_frontend_broadcast_flow { + void *priv; + + uint32_t flags; + + enum obs_broadcast_state (*get_broadcast_state)(void *priv); + enum obs_broadcast_start (*get_broadcast_start_type)(void *priv); + enum obs_broadcast_stop (*get_broadcast_stop_type)(void *priv); + + void (*manage_broadcast)(void *priv); + void (*manage_broadcast2)(void *priv, bool streaming_active); + + void (*stopped_streaming)(void *priv); + + void (*differed_start_broadcast)(void *priv); + enum obs_broadcast_stream_state (*is_broadcast_stream_active)( + void *priv); + + bool (*differed_stop_broadcast)(void *priv); + + const char *(*get_last_error)(void *priv); +}; #endif //!SWIG /* ------------------------------------------------------------------------- */ @@ -150,6 +211,17 @@ EXPORT void obs_frontend_remove_dock(const char *id); /* takes QDockWidget for dock */ EXPORT bool obs_frontend_add_custom_qdock(const char *id, void *dock); +EXPORT bool obs_frontend_is_browser_available(void); + +/* returns QAction */ +EXPORT void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, size_t size); +#define obs_frontend_get_browser_widget(params) \ + obs_frontend_get_browser_widget_s( \ + params, sizeof(struct obs_frontend_browser_params)) + +EXPORT void obs_frontend_delete_browser_cookie(const char *url); + typedef void (*obs_frontend_event_cb)(enum obs_frontend_event event, void *private_data); @@ -178,6 +250,14 @@ EXPORT void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate); EXPORT void obs_frontend_pop_ui_translation(void); +EXPORT void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, size_t size); +#define obs_frontend_add_broadcast_flow(service, flow) \ + obs_frontend_add_broadcast_flow_s( \ + service, flow, sizeof(struct obs_frontend_broadcast_flow)) +EXPORT void obs_frontend_remove_broadcast_flow(const obs_service_t *service); + #endif //!SWIG EXPORT void obs_frontend_streaming_start(void); diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp index 90a0e06ea9dd5b..186cf305ba8893 100644 --- a/UI/obs-frontend-api/obs-frontend-internal.hpp +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -73,6 +73,14 @@ struct obs_frontend_callbacks { virtual bool obs_frontend_add_custom_qdock(const char *id, void *dock) = 0; + virtual bool obs_frontend_is_browser_available(void) = 0; + + virtual void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, + size_t size) = 0; + + virtual void obs_frontend_delete_browser_cookie(const char *url) = 0; + virtual void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) = 0; @@ -167,6 +175,13 @@ struct obs_frontend_callbacks { const char *undo_data, const char *redo_data, bool repeatable) = 0; + + virtual void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, + size_t size) = 0; + virtual void + obs_frontend_remove_broadcast_flow(const obs_service_t *service) = 0; }; EXPORT void diff --git a/UI/service-sort-filter.cpp b/UI/service-sort-filter.cpp new file mode 100644 index 00000000000000..b1c77fc739b1bf --- /dev/null +++ b/UI/service-sort-filter.cpp @@ -0,0 +1,39 @@ +#include "service-sort-filter.hpp" + +#include + +#include "obs-app.hpp" + +#define SERVICE_SHOW_ALL QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll") +constexpr const char *CUSTOM_SERVICE_ID = "custom_service"; + +ServiceSortFilterProxyModel::ServiceSortFilterProxyModel(QComboBox *parent) + : QSortFilterProxyModel(parent) +{ + setSortCaseSensitivity(Qt::CaseInsensitive); + + // Store text/data combo to be able to find the id while sorting + for (int i = 0; i < parent->count(); i++) + items[parent->itemText(i)] = parent->itemData(i); +} + +bool ServiceSortFilterProxyModel::lessThan(const QModelIndex &left, + const QModelIndex &right) const +{ + QVariant leftData = sourceModel()->data(left); + QVariant rightData = sourceModel()->data(right); + + // Make Show All last + if (leftData.toString() == SERVICE_SHOW_ALL) + return false; + if (rightData.toString() == SERVICE_SHOW_ALL) + return true; + + // Make Custom… first + if (items.value(leftData.toString()) == CUSTOM_SERVICE_ID) + return true; + if (items.value(rightData.toString()) == CUSTOM_SERVICE_ID) + return false; + + return QSortFilterProxyModel::lessThan(left, right); +} diff --git a/UI/service-sort-filter.hpp b/UI/service-sort-filter.hpp new file mode 100644 index 00000000000000..07d523188007c1 --- /dev/null +++ b/UI/service-sort-filter.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +class ServiceSortFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT + QHash items; + +public: + ServiceSortFilterProxyModel(QComboBox *parent); + +protected: + bool lessThan(const QModelIndex &left, + const QModelIndex &right) const override; +}; diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 44daae4566973e..cccc19ff938f7c 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -117,32 +117,6 @@ void AutoConfigTestPage::StartRecordingEncoderStage() testThread = std::thread([this]() { TestRecordingEncoderThread(); }); } -void AutoConfigTestPage::GetServers(std::vector &servers) -{ - OBSDataAutoRelease settings = obs_data_create(); - obs_data_set_string(settings, "service", wiz->serviceName.c_str()); - - obs_properties_t *ppts = obs_get_service_properties("rtmp_common"); - obs_property_t *p = obs_properties_get(ppts, "service"); - obs_property_modified(p, settings); - - p = obs_properties_get(ppts, "server"); - size_t count = obs_property_list_item_count(p); - servers.reserve(count); - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *server = obs_property_list_item_string(p, i); - - if (wiz->CanTestServer(name)) { - ServerInfo info(name, server); - servers.push_back(info); - } - } - - obs_properties_destroy(ppts); -} - static inline void string_depad_key(string &key) { while (!key.empty()) { @@ -195,48 +169,24 @@ void AutoConfigTestPage::TestBandwidthThread() /* -----------------------------------*/ /* create obs objects */ - const char *serverType = wiz->customServer ? "rtmp_custom" - : "rtmp_common"; - OBSEncoderAutoRelease vencoder = obs_video_encoder_create( "obs_x264", "test_x264", nullptr, nullptr); OBSEncoderAutoRelease aencoder = obs_audio_encoder_create( "ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSServiceAutoRelease service = obs_service_create( - serverType, "test_service", nullptr, nullptr); /* -----------------------------------*/ /* configure settings */ - // service: "service", "server", "key" // vencoder: "bitrate", "rate_control", // obs_service_apply_encoder_settings // aencoder: "bitrate" // output: "bind_ip" via main config -> "Output", "BindIP" // obs_output_set_service - OBSDataAutoRelease service_settings = obs_data_create(); OBSDataAutoRelease vencoder_settings = obs_data_create(); OBSDataAutoRelease aencoder_settings = obs_data_create(); OBSDataAutoRelease output_settings = obs_data_create(); - std::string key = wiz->key; - if (wiz->service == AutoConfig::Service::Twitch) { - string_depad_key(key); - key += "?bandwidthtest"; - } else if (wiz->serviceName == "Restream.io" || - wiz->serviceName == "Restream.io - RTMP") { - string_depad_key(key); - key += "?test=true"; - } else if (wiz->serviceName == "Restream.io - FTL") { - string_depad_key(key); - key += "?test"; - } - - obs_data_set_string(service_settings, "service", - wiz->serviceName.c_str()); - obs_data_set_string(service_settings, "key", key.c_str()); - obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate); obs_data_set_string(vencoder_settings, "rate_control", "CBR"); obs_data_set_string(vencoder_settings, "preset", "veryfast"); @@ -253,46 +203,24 @@ void AutoConfigTestPage::TestBandwidthThread() /* determine which servers to test */ std::vector servers; - if (wiz->customServer) - servers.emplace_back(wiz->server.c_str(), wiz->server.c_str()); - else - GetServers(servers); - - /* just use the first server if it only has one alternate server, - * or if using Restream or Nimo TV due to their "auto" servers */ - if (servers.size() < 3 || - wiz->serviceName.substr(0, 11) == "Restream.io" || - wiz->serviceName == "Nimo TV") { - servers.resize(1); - - } else if (wiz->service == AutoConfig::Service::Twitch && - wiz->twitchAuto) { - /* if using Twitch and "Auto" is available, test 3 closest - * server */ - servers.erase(servers.begin() + 1); - servers.resize(3); - } else if (wiz->service == AutoConfig::Service::YouTube) { - /* Only test first set of primary + backup servers */ - servers.resize(2); - } - - /* -----------------------------------*/ - /* apply service settings */ + servers.emplace_back(obs_service_get_connect_info( + wiz->service, OBS_SERVICE_CONNECT_INFO_SERVER_URL)); - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, - aencoder_settings); + /* Since we no longer hack arround obs_properties_t, we can only test + * the server set in the service properties. + * + * TODO: Create API of proc handler to restore this feature */ /* -----------------------------------*/ /* create output */ /* Check if the service has a preferred output type */ const char *output_type = - obs_service_get_preferred_output_type(service); + obs_service_get_preferred_output_type(wiz->service); if (!output_type || (obs_get_output_flags(output_type) & OBS_OUTPUT_SERVICE) == 0) { /* Otherwise, prefer first-party output types */ - const char *protocol = obs_service_get_protocol(service); + const char *protocol = obs_service_get_protocol(wiz->service); if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { output_type = "rtmp_output"; @@ -330,6 +258,14 @@ void AutoConfigTestPage::TestBandwidthThread() 0, nullptr); } + /* -----------------------------------*/ + /* apply service settings */ + + obs_service_apply_encoder_settings2( + wiz->service, obs_encoder_get_id(vencoder), vencoder_settings); + obs_service_apply_encoder_settings2( + wiz->service, obs_encoder_get_id(aencoder), aencoder_settings); + /* -----------------------------------*/ /* connect encoders/services/outputs */ @@ -342,7 +278,7 @@ void AutoConfigTestPage::TestBandwidthThread() obs_output_set_audio_encoder(output, aencoder, 0); obs_output_set_reconnect_settings(output, 0, 0); - obs_output_set_service(output, service); + obs_output_set_service(output, wiz->service); /* -----------------------------------*/ /* connect signals */ @@ -385,6 +321,10 @@ void AutoConfigTestPage::TestBandwidthThread() bool success = false; + bool canBandwidthTest = obs_service_can_bandwidth_test(wiz->service); + if (canBandwidthTest) + obs_service_enable_bandwidth_test(wiz->service, true); + for (size_t i = 0; i < servers.size(); i++) { auto &server = servers[i]; @@ -398,10 +338,6 @@ void AutoConfigTestPage::TestBandwidthThread() Q_ARG(QString, QTStr(TEST_BW_CONNECTING) .arg(server.name.c_str()))); - obs_data_set_string(service_settings, "server", - server.address.c_str()); - obs_service_update(service, service_settings); - if (!obs_output_start(output)) continue; @@ -473,6 +409,9 @@ void AutoConfigTestPage::TestBandwidthThread() success = true; } + if (canBandwidthTest) + obs_service_enable_bandwidth_test(wiz->service, false); + if (!success) { QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, @@ -1057,36 +996,45 @@ void AutoConfigTestPage::FinalizeResults() }; if (wiz->type == AutoConfig::Type::Streaming) { - const char *serverType = wiz->customServer ? "rtmp_custom" - : "rtmp_common"; - - OBSServiceAutoRelease service = obs_service_create( - serverType, "temp_service", nullptr, nullptr); - OBSDataAutoRelease service_settings = obs_data_create(); OBSDataAutoRelease vencoder_settings = obs_data_create(); obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); - obs_data_set_string(service_settings, "service", - wiz->serviceName.c_str()); - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, - nullptr); + obs_service_apply_encoder_settings2(wiz->service, "obs_x264", + vencoder_settings); BPtr res_list; - size_t res_count; - int maxFPS; - obs_service_get_supported_resolutions(service, &res_list, - &res_count); - obs_service_get_max_fps(service, &maxFPS); + size_t res_count = 0; + bool res_with_fps = false; + int maxFPS = 0; + obs_service_get_supported_resolutions2( + wiz->service, &res_list, &res_count, &res_with_fps); + if (!res_with_fps) + obs_service_get_max_fps(wiz->service, &maxFPS); if (res_list) { set_closest_res(wiz->idealResolutionCX, wiz->idealResolutionCY, res_list, res_count); } + + if (res_count && res_with_fps) { + for (size_t i = 0; i < res_count; i++) { + { + if (res_list[i].cx != + wiz->idealResolutionCX || + res_list[i].cy != + wiz->idealResolutionCY) + continue; + + if (res_list[i].fps > maxFPS) + maxFPS = res_list[i].fps; + } + } + } + if (maxFPS) { double idealFPS = (double)wiz->idealFPSNum / (double)wiz->idealFPSDen; @@ -1099,11 +1047,9 @@ void AutoConfigTestPage::FinalizeResults() wiz->idealBitrate = (int)obs_data_get_int(vencoder_settings, "bitrate"); - if (!wiz->customServer) - form->addRow( - newLabel("Basic.AutoConfig.StreamPage.Service"), - new QLabel(wiz->serviceName.c_str(), - ui->finishPage)); + form->addRow(newLabel("Basic.AutoConfig.StreamPage.Service"), + new QLabel(wiz->serviceName.c_str(), + ui->finishPage)); form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), new QLabel(wiz->serverName.c_str(), ui->finishPage)); diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 5a183f04956a6f..d136e4103bb6b7 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -9,63 +10,18 @@ #include "obs-app.hpp" #include "url-push-button.hpp" +#include "service-sort-filter.hpp" + #include "ui_AutoConfigStartPage.h" #include "ui_AutoConfigVideoPage.h" #include "ui_AutoConfigStreamPage.h" -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" #include "ui-config.h" -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; #define wiz reinterpret_cast(wizard()) /* ------------------------------------------------------------------------- */ -#define SERVICE_PATH "service.json" - -static OBSData OpenServiceSettings(std::string &type) -{ - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return OBSData(); - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - - return settings.Get(); -} - -static void GetServiceInfo(std::string &type, std::string &service, - std::string &server, std::string &key) -{ - OBSData settings = OpenServiceSettings(type); - - service = obs_data_get_string(settings, "service"); - server = obs_data_get_string(settings, "server"); - key = obs_data_get_string(settings, "key"); -} - -/* ------------------------------------------------------------------------- */ - AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage) @@ -251,18 +207,16 @@ enum class ListOpt : int { Custom, }; -AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) +constexpr const char *TEMP_SERVICE_NAME = "temp_service"; + +AutoConfigStreamPage::AutoConfigStreamPage(obs_service_t *service, + QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) { ui->setupUi(this); ui->bitrateLabel->setVisible(false); ui->bitrate->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); int vertSpacing = ui->topLayout->verticalSpacing(); @@ -270,9 +224,9 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) m.setBottom(vertSpacing / 2); ui->topLayout->setContentsMargins(m); - m = ui->loginPageLayout->contentsMargins(); + m = ui->serviceProps->contentsMargins(); m.setTop(vertSpacing / 2); - ui->loginPageLayout->setContentsMargins(m); + ui->serviceProps->setContentsMargins(m); m = ui->streamkeyPageLayout->contentsMargins(); m.setTop(vertSpacing / 2); @@ -283,38 +237,40 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) LoadServices(false); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(ServiceChanged())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(ServiceChanged())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateKeyLink())); - connect(ui->customServer, SIGNAL(editingFinished()), this, - SLOT(UpdateKeyLink())); + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *id = obs_service_get_id(service); + uint32_t flags = obs_service_get_flags(service); + + tempService = + obs_service_create_private(id, TEMP_SERVICE_NAME, nullptr); + + /* Avoid sharing the same obs_data_t pointer, + * between the service ,the temp service and the properties view */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); + obs_service_update(tempService, settings); + + /* Reset to index 0 if internal or deprecated */ + if ((flags & OBS_SERVICE_INTERNAL) == 0 && + (flags & OBS_SERVICE_DEPRECATED) == 0) { + if ((flags & OBS_SERVICE_UNCOMMON) != 0) + LoadServices(true); + + int idx = ui->service->findData(QT_UTF8(id)); + + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex(idx); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(settings); + ui->serviceLayout->addWidget(streamServiceProps); + + ServiceChanged(); + } else { + ui->service->currentIndexChanged(0); + } + connect(ui->doBandwidthTest, SIGNAL(toggled(bool)), this, SLOT(ServiceChanged())); - - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateServerList())); - - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateKeyLink())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateMoreInfoLink())); - - connect(ui->useStreamKeyAdv, &QPushButton::clicked, this, - [&]() { ui->streamKeyWidget->setVisible(true); }); - - connect(ui->key, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateCompleted())); - connect(ui->regionUS, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionEU, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionAsia, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionOther, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); } AutoConfigStreamPage::~AutoConfigStreamPage() {} @@ -329,87 +285,34 @@ int AutoConfigStreamPage::nextId() const return AutoConfig::TestPage; } -inline bool AutoConfigStreamPage::IsCustomService() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::Custom; -} - bool AutoConfigStreamPage::validatePage() { - OBSDataAutoRelease service_settings = obs_data_create(); - - wiz->customServer = IsCustomService(); - - const char *serverType = wiz->customServer ? "rtmp_custom" - : "rtmp_common"; - - if (!wiz->customServer) { - obs_data_set_string(service_settings, "service", - QT_TO_UTF8(ui->service->currentText())); - } - - OBSServiceAutoRelease service = obs_service_create( - serverType, "temp_service", service_settings, nullptr); - int bitrate; - if (!ui->doBandwidthTest->isChecked()) { + if (!(ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled())) { bitrate = ui->bitrate->value(); wiz->idealBitrate = bitrate; } else { /* Default test target is 10 Mbps */ bitrate = 10000; -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(wiz->serviceName)) { - /* Adjust upper bound to YouTube limits - * for resolutions above 1080p */ - if (wiz->baseResolutionCY > 1440) - bitrate = 51000; - else if (wiz->baseResolutionCY > 1080) - bitrate = 18000; - } -#endif } OBSDataAutoRelease settings = obs_data_create(); obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(service, settings, nullptr); + obs_service_apply_encoder_settings2(tempService, "obs_x264", settings); - if (wiz->customServer) { - QString server = ui->customServer->text().trimmed(); - wiz->server = wiz->serverName = QT_TO_UTF8(server); - } else { - wiz->serverName = QT_TO_UTF8(ui->server->currentText()); - wiz->server = QT_TO_UTF8(ui->server->currentData().toString()); - } + wiz->service = obs_service_get_ref(tempService); - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->bandwidthTest = ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled(); wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); wiz->idealBitrate = wiz->startingBitrate; - wiz->regionUS = ui->regionUS->isChecked(); - wiz->regionEU = ui->regionEU->isChecked(); - wiz->regionAsia = ui->regionAsia->isChecked(); - wiz->regionOther = ui->regionOther->isChecked(); wiz->serviceName = QT_TO_UTF8(ui->service->currentText()); if (ui->preferHardware) wiz->preferHardware = ui->preferHardware->isChecked(); - wiz->key = QT_TO_UTF8(ui->key->text()); - - if (!wiz->customServer) { - if (wiz->serviceName == "Twitch") - wiz->service = AutoConfig::Service::Twitch; -#ifdef YOUTUBE_ENABLED - else if (IsYouTubeService(wiz->serviceName)) - wiz->service = AutoConfig::Service::YouTube; -#endif - else - wiz->service = AutoConfig::Service::Other; - } else { - wiz->service = AutoConfig::Service::Other; - } - if (wiz->service != AutoConfig::Service::Twitch && - wiz->service != AutoConfig::Service::YouTube && - wiz->bandwidthTest) { + if (wiz->bandwidthTest && + !obs_service_can_bandwidth_test(tempService)) { QMessageBox::StandardButton button; #define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) button = OBSMessageBox::question(this, WARNING_TEXT("Title"), @@ -423,433 +326,197 @@ bool AutoConfigStreamPage::validatePage() return true; } -void AutoConfigStreamPage::on_show_clicked() -{ - if (ui->key->echoMode() == QLineEdit::Password) { - ui->key->setEchoMode(QLineEdit::Normal); - ui->show->setText(QTStr("Hide")); - } else { - ui->key->setEchoMode(QLineEdit::Password); - ui->show->setText(QTStr("Show")); - } -} - -void AutoConfigStreamPage::OnOAuthStreamKeyConnected() -{ - OAuthStreamKey *a = reinterpret_cast(auth.get()); - - if (a) { - bool validKey = !a->key().empty(); - - if (validKey) - ui->key->setText(QT_UTF8(a->key().c_str())); - - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(a->service())) { - ui->key->clear(); - - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - - ui->connectedAccountText->setText( - QTStr("Auth.LoadingChannel.Title")); - - YoutubeApiWrappers *ytAuth = - reinterpret_cast(a); - ChannelDescription cd; - if (ytAuth->GetChannelDescription(cd)) { - ui->connectedAccountText->setText(cd.title); - - /* Create throwaway stream key for bandwidth test */ - if (ui->doBandwidthTest->isChecked()) { - StreamDescription stream = { - "", "", - "OBS Studio Test Stream"}; - if (ytAuth->InsertStream(stream)) { - ui->key->setText(stream.name); - } - } - } - } -#endif - } - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -void AutoConfigStreamPage::OnAuthConnected() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - Auth::Type type = Auth::AuthType(service); - - if (type == Auth::Type::OAuth_StreamKey || - type == Auth::Type::OAuth_LinkedAccount) { - OnOAuthStreamKeyConnected(); - } -} - -void AutoConfigStreamPage::on_connectAccount_clicked() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - - OAuth::DeleteCookies(service); - - auth = OAuthStreamKey::Login(this, service); - if (!!auth) { - OnAuthConnected(); - - ui->useStreamKeyAdv->setVisible(false); - } -} - -#define DISCONNECT_COMFIRM_TITLE \ - "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" -#define DISCONNECT_COMFIRM_TEXT \ - "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" - -void AutoConfigStreamPage::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), - QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { - return; - } - - OBSBasic *main = OBSBasic::Get(); - - main->auth.reset(); - auth.reset(); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif - - reset_service_ui_fields(service); - - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - /* Restore key link when disconnecting account */ - UpdateKeyLink(); -} - -void AutoConfigStreamPage::on_useStreamKey_clicked() -{ - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - UpdateCompleted(); -} - -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} - -static inline bool is_external_oauth(const std::string &service) -{ - return Auth::External(service); -} - -void AutoConfigStreamPage::reset_service_ui_fields(std::string &service) -{ -#ifdef YOUTUBE_ENABLED - // when account is already connected: - OAuthStreamKey *a = reinterpret_cast(auth.get()); - if (a && service == a->service() && IsYouTubeService(a->service())) { - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - return; - } -#endif - - bool external_oauth = is_external_oauth(service); - if (external_oauth) { - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(true); - ui->useStreamKeyAdv->setVisible(true); - - ui->stackedWidget->setCurrentIndex((int)Section::StreamKey); - - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && key.isEmpty() ? (int)Section::Connect - : (int)Section::StreamKey; - - ui->stackedWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - ui->useStreamKeyAdv->setVisible(false); - } else { - ui->connectAccount2->setVisible(false); - ui->useStreamKeyAdv->setVisible(false); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} - void AutoConfigStreamPage::ServiceChanged() { - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; - if (showMore) - return; - - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool regionBased = service == "Twitch"; - bool testBandwidth = ui->doBandwidthTest->isChecked(); - bool custom = IsCustomService(); + obs_service_update(tempService, streamServiceProps->GetSettings()); - reset_service_ui_fields(service); + /* Enable bandwidth test if available */ + bool canBandwidthTest = obs_service_can_bandwidth_test(tempService); + if (canBandwidthTest) + obs_service_enable_bandwidth_test(tempService, true); - /* Test three closest servers if "Auto" is available for Twitch */ - if (service == "Twitch" && wiz->twitchAuto) - regionBased = false; + /* Check if the service can connect to allow to do a bandwidth test */ + bool canTryToConnect = obs_service_can_try_to_connect(tempService); - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); - ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); + /* Disable bandwidth test */ + if (canBandwidthTest) + obs_service_enable_bandwidth_test(tempService, false); - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, - ui->serverStackedWidget); - - ui->region->setVisible(false); - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - } else { - if (!testBandwidth) - ui->streamkeyPageLayout->insertRow( - 2, ui->serverLabel, ui->serverStackedWidget); - - ui->region->setVisible(regionBased && testBandwidth); - ui->serverStackedWidget->setCurrentIndex(0); - ui->serverStackedWidget->setHidden(testBandwidth); - ui->serverLabel->setHidden(testBandwidth); - } + /* Make wizard bandwidth test available if the service can connect. + * Otherwise, disable it */ + ui->doBandwidthTest->setEnabled(canTryToConnect); - wiz->testRegions = regionBased && testBandwidth; + bool testBandwidth = ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled(); ui->bitrateLabel->setHidden(testBandwidth); ui->bitrate->setHidden(testBandwidth); - OBSBasic *main = OBSBasic::Get(); - - if (main->auth) { - auto system_auth_service = main->auth->service(); - bool service_check = service.find(system_auth_service) != - std::string::npos; -#ifdef YOUTUBE_ENABLED - service_check = - service_check ? service_check - : IsYouTubeService(system_auth_service) && - IsYouTubeService(service); -#endif - if (service_check) { - auth.reset(); - auth = main->auth; - OnAuthConnected(); - } - } - UpdateCompleted(); } -void AutoConfigStreamPage::UpdateMoreInfoLink() -{ - if (IsCustomService()) { - ui->moreInfoButton->hide(); - return; - } - - QString serviceName = ui->service->currentText(); - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - const char *more_info_link = - obs_data_get_string(settings, "more_info_link"); +constexpr int SHOW_ALL = 1; - if (!more_info_link || (*more_info_link == '\0')) { - ui->moreInfoButton->hide(); - } else { - ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); - ui->moreInfoButton->show(); - } - obs_properties_destroy(props); -} - -void AutoConfigStreamPage::UpdateKeyLink() +/* Note: Identical to OBSBasicSettings function except it does not show deprecated services */ +void AutoConfigStreamPage::LoadServices(bool showAll) { - QString serviceName = ui->service->currentText(); - QString customServer = ui->customServer->text().trimmed(); - QString streamKeyLink; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); + const char *id; + size_t idx = 0; + bool needShowAllOption = false; - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); + QSignalBlocker sb(ui->service); - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + ui->service->clear(); + ui->service->setModel(new QStandardItemModel(0, 1, ui->service)); - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = - "https://www.facebook.com/live/producer?ref=OBS"; - } + while (obs_enum_service_types(idx++, &id)) { + uint32_t flags = obs_get_service_flags(id); - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - } else { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - } + if ((flags & OBS_SERVICE_INTERNAL) != 0 || + (flags & OBS_SERVICE_DEPRECATED) != 0) + continue; - if (QString(streamKeyLink).isNull() || - QString(streamKeyLink).isEmpty()) { - ui->streamKeyButton->hide(); - } else { - ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->streamKeyButton->show(); - } - obs_properties_destroy(props); -} + QStringList protocols = + QT_UTF8(obs_get_service_supported_protocols(id)) + .split(";"); -void AutoConfigStreamPage::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); + if (protocols.empty()) { + blog(LOG_WARNING, "No protocol found for service '%s'", + id); + continue; + } - OBSDataAutoRelease settings = obs_data_create(); + bool protocolRegistered = false; + for (uint32_t i = 0; i < protocols.size(); i++) { + protocolRegistered |= obs_is_output_protocol_registered( + QT_TO_UTF8(protocols[i])); + } - obs_data_set_bool(settings, "show_all", showAll); + if (!protocolRegistered) { + blog(LOG_WARNING, + "No registered protocol compatible with service '%s'", + id); + continue; + } - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); + bool isUncommon = (flags & OBS_SERVICE_UNCOMMON) != 0; - ui->service->blockSignals(true); - ui->service->clear(); + QString name(obs_service_get_display_name(id)); - QStringList names; + if (showAll || !isUncommon) + ui->service->addItem(name, QT_UTF8(id)); - obs_property_t *services = obs_properties_get(props, "service"); - size_t services_count = obs_property_list_item_count(services); - for (size_t i = 0; i < services_count; i++) { - const char *name = obs_property_list_item_string(services, i); - names.push_back(name); + if (isUncommon && !showAll) + needShowAllOption = true; } - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { + if (needShowAllOption) { ui->service->addItem( QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); + QVariant(SHOW_ALL)); } - ui->service->insertItem( - 0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), - QVariant((int)ListOpt::Custom)); - - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); - } + QSortFilterProxyModel *model = + new ServiceSortFilterProxyModel(ui->service); + model->setSourceModel(ui->service->model()); + // Combo's current model must be reparented, + // Otherwise QComboBox::setModel() will delete it + ui->service->model()->setParent(model); - obs_properties_destroy(props); + ui->service->setModel(model); - ui->service->blockSignals(false); + ui->service->model()->sort(0); } -void AutoConfigStreamPage::UpdateServerList() +void AutoConfigStreamPage::UpdateCompleted() { - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; + /* Assume ready if an URL is present, we can't add a specific behavior + * for each service. */ + const char *streamUrl = obs_service_get_connect_info( + tempService, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + + ready = (streamUrl != NULL && streamUrl[0] != '\0'); + + emit completeChanged(); +} - if (showMore) { +void AutoConfigStreamPage::on_service_currentIndexChanged(int) +{ + if (ui->service->currentData().toInt() == SHOW_ALL) { LoadServices(true); ui->service->showPopup(); return; - } else { - lastService = serviceName; } - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - obs_property_t *servers = obs_properties_get(props, "server"); - - ui->server->clear(); + OBSServiceAutoRelease oldService = + obs_service_get_ref(tempService.Get()); + + QString service = ui->service->currentData().toString(); + OBSDataAutoRelease newSettings = + obs_service_defaults(QT_TO_UTF8(service)); + tempService = obs_service_create_private( + QT_TO_UTF8(service), TEMP_SERVICE_NAME, newSettings); + + bool cancelChange = false; + if (!obs_service_get_protocol(tempService)) { + /* + * Cancel the change if the service happen to be without default protocol. + * + * This is better than generating dozens of obs_service_t to check + * if there is a default protocol while filling the combo box. + */ + OBSMessageBox::warning( + this, + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Title"), + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Text") + .arg(ui->service->currentText())); + cancelChange = true; + } + + if (cancelChange) { + tempService = obs_service_get_ref(oldService); + const char *id = obs_service_get_id(tempService); + uint32_t flags = obs_get_service_flags(id); + if ((flags & OBS_SERVICE_INTERNAL) != 0) { + QString name(obs_service_get_display_name(id)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); + + ui->service->setPlaceholderText(name); + } + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex( + ui->service->findData(QT_UTF8(id))); - size_t servers_count = obs_property_list_item_count(servers); - for (size_t i = 0; i < servers_count; i++) { - const char *name = obs_property_list_item_name(servers, i); - const char *server = obs_property_list_item_string(servers, i); - ui->server->addItem(name, server); + return; } - obs_properties_destroy(props); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(nullptr); + ui->serviceLayout->addWidget(streamServiceProps); + + ServiceChanged(); } -void AutoConfigStreamPage::UpdateCompleted() +OBSPropertiesView * +AutoConfigStreamPage::CreateTempServicePropertyView(obs_data_t *settings) { - if (ui->stackedWidget->currentIndex() == (int)Section::Connect || - (ui->key->text().isEmpty() && !auth)) { - ready = false; - } else { - bool custom = IsCustomService(); - if (custom) { - ready = !ui->customServer->text().isEmpty(); - } else { - ready = !wiz->testRegions || - ui->regionUS->isChecked() || - ui->regionEU->isChecked() || - ui->regionAsia->isChecked() || - ui->regionOther->isChecked(); - } - } - emit completeChanged(); + OBSDataAutoRelease defaultSettings = + obs_service_defaults(obs_service_get_id(tempService)); + OBSPropertiesView *view; + + view = new OBSPropertiesView( + settings ? settings : defaultSettings.Get(), tempService, + (PropertiesReloadCallback)obs_service_properties, nullptr, + nullptr, 170); + ; + view->setFrameShape(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + QObject::connect(view, &OBSPropertiesView::Changed, this, + &AutoConfigStreamPage::ServiceChanged); + + return view; } /* ------------------------------------------------------------------------- */ @@ -861,21 +528,16 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) calldata_t cd = {0}; calldata_set_int(&cd, "seconds", 5); - proc_handler_t *ph = obs_get_proc_handler(); - proc_handler_call(ph, "twitch_ingests_refresh", &cd); - calldata_free(&cd); - OBSBasic *main = reinterpret_cast(parent); main->EnableOutputs(false); installEventFilter(CreateShortcutFilter()); - std::string serviceType; - GetServiceInfo(serviceType, serviceName, server, key); #if defined(_WIN32) || defined(__APPLE__) setWizardStyle(QWizard::ModernStyle); #endif - streamPage = new AutoConfigStreamPage(); + obs_service_t *service = main->GetService(); + streamPage = new AutoConfigStreamPage(service); setPage(StartPage, new AutoConfigStartPage()); setPage(VideoPage, new AutoConfigVideoPage()); @@ -890,79 +552,9 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) baseResolutionCX = ovi.base_width; baseResolutionCY = ovi.base_height; - /* ----------------------------------------- */ - /* check to see if Twitch's "auto" available */ - - OBSDataAutoRelease twitchSettings = obs_data_create(); - - obs_data_set_string(twitchSettings, "service", "Twitch"); - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_properties_apply_settings(props, twitchSettings); - - obs_property_t *p = obs_properties_get(props, "server"); - const char *first = obs_property_list_item_string(p, 0); - twitchAuto = strcmp(first, "auto") == 0; - - obs_properties_destroy(props); - - /* ----------------------------------------- */ - /* load service/servers */ - - customServer = serviceType == "rtmp_custom"; - - QComboBox *serviceList = streamPage->ui->service; - - if (!serviceName.empty()) { - serviceList->blockSignals(true); - - int count = serviceList->count(); - bool found = false; - - for (int i = 0; i < count; i++) { - QString name = serviceList->itemText(i); - - if (name == serviceName.c_str()) { - serviceList->setCurrentIndex(i); - found = true; - break; - } - } - - if (!found) { - serviceList->insertItem(0, serviceName.c_str()); - serviceList->setCurrentIndex(0); - } - - serviceList->blockSignals(false); - } - - streamPage->UpdateServerList(); - streamPage->UpdateKeyLink(); - streamPage->UpdateMoreInfoLink(); - streamPage->lastService.clear(); - - if (!customServer) { - QComboBox *serverList = streamPage->ui->server; - int idx = serverList->findData(QString(server.c_str())); - if (idx == -1) - idx = 0; - - serverList->setCurrentIndex(idx); - } else { - streamPage->ui->customServer->setText(server.c_str()); - int idx = streamPage->ui->service->findData( - QVariant((int)ListOpt::Custom)); - streamPage->ui->service->setCurrentIndex(idx); - } - - if (!key.empty()) - streamPage->ui->key->setText(key.c_str()); - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); streamPage->ui->bitrate->setValue(bitrate); - streamPage->ServiceChanged(); TestHardwareEncoding(); if (!hardwareEncodingAvailable) { @@ -1018,30 +610,6 @@ void AutoConfig::TestHardwareEncoding() } } -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (service == Service::Twitch) { - if (astrcmp_n(server, "US West:", 8) == 0 || - astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionUS; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAsia; - } else if (regionOther) { - return true; - } - } else { - return true; - } - - return false; -} - void AutoConfig::done(int result) { QWizard::done(result); @@ -1076,38 +644,19 @@ void AutoConfig::SaveStreamSettings() /* ---------------------------------- */ /* save service */ - const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); - - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!IsYouTubeService(serviceName)) - obs_data_set_string(settings, "key", key.c_str()); -#else - obs_data_set_string(settings, "key", key.c_str()); -#endif - - OBSServiceAutoRelease newService = obs_service_create( - service_id, "default_service", settings, hotkeyData); + OBSServiceAutoRelease newService = + obs_service_create(obs_service_get_id(service), + "default_service", settings, nullptr); if (!newService) return; main->SetService(newService); main->SaveService(); - main->auth = streamPage->auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } /* ---------------------------------- */ /* save stream settings */ diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index 5d966c7956e272..bdcaee4bb680b7 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -12,6 +12,7 @@ #include #include #include +#include class Ui_AutoConfigStartPage; class Ui_AutoConfigVideoPage; @@ -19,7 +20,7 @@ class Ui_AutoConfigStreamPage; class Ui_AutoConfigTestPage; class AutoConfigStreamPage; -class Auth; +class OBSPropertiesView; class AutoConfig : public QWizard { Q_OBJECT @@ -36,12 +37,6 @@ class AutoConfig : public QWizard { VirtualCam, }; - enum class Service { - Twitch, - YouTube, - Other, - }; - enum class Encoder { x264, NVENC, @@ -68,7 +63,6 @@ class AutoConfig : public QWizard { AutoConfigStreamPage *streamPage = nullptr; - Service service = Service::Other; Quality recordingQuality = Quality::Stream; Encoder recordingEncoder = Encoder::Stream; Encoder streamingEncoder = Encoder::x264; @@ -84,7 +78,7 @@ class AutoConfig : public QWizard { std::string serviceName; std::string serverName; std::string server; - std::string key; + OBSServiceAutoRelease service = nullptr; bool hardwareEncodingAvailable = false; bool nvencAvailable = false; @@ -93,21 +87,13 @@ class AutoConfig : public QWizard { bool appleAvailable = false; int startingBitrate = 2500; - bool customServer = false; bool bandwidthTest = false; - bool testRegions = true; - bool twitchAuto = false; - bool regionUS = true; - bool regionEU = true; - bool regionAsia = true; - bool regionOther = true; bool preferHighFPS = false; bool preferHardware = false; int specificFPSNum = 0; int specificFPSDen = 0; void TestHardwareEncoding(); - bool CanTestServer(const char *server); virtual void done(int result) override; @@ -165,43 +151,31 @@ class AutoConfigStreamPage : public QWizardPage { friend class AutoConfig; - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - std::unique_ptr ui; QString lastService; bool ready = false; + OBSServiceAutoRelease tempService; + OBSPropertiesView *streamServiceProps = nullptr; + void LoadServices(bool showAll); - inline bool IsCustomService() const; + + OBSPropertiesView *CreateTempServicePropertyView(obs_data_t *settings); + +private slots: + void on_service_currentIndexChanged(int idx); public: - AutoConfigStreamPage(QWidget *parent = nullptr); + AutoConfigStreamPage(obs_service_t *service, QWidget *parent = nullptr); ~AutoConfigStreamPage(); virtual bool isComplete() const override; virtual int nextId() const override; virtual bool validatePage() override; - void OnAuthConnected(); - void OnOAuthStreamKeyConnected(); - public slots: - void on_show_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); }; class AutoConfigTestPage : public QWizardPage { @@ -255,9 +229,13 @@ class AutoConfigTestPage : public QWizardPage { address(address_) { } - }; - void GetServers(std::vector &servers); + inline ServerInfo(const char *address_) + : name(address_), + address(address_) + { + } + }; public: AutoConfigTestPage(QWidget *parent = nullptr); diff --git a/UI/window-basic-main-browser.cpp b/UI/window-basic-main-browser.cpp index ec99f141e4987b..9673064528e91d 100644 --- a/UI/window-basic-main-browser.cpp +++ b/UI/window-basic-main-browser.cpp @@ -162,3 +162,56 @@ void OBSBasic::InitBrowserPanelSafeBlock() InitPanelCookieManager(); #endif } + +QWidget *OBSBasic::GetBrowserWidget(const obs_frontend_browser_params ¶ms) +{ +#ifdef BROWSER_AVAILABLE + if (!cef) + return nullptr; + + if (params.enable_cookie) + OBSBasic::InitBrowserPanelSafeBlock(); + + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + QCefWidget *browser = cef->create_widget( + nullptr, params.url, + params.enable_cookie ? panel_cookies : nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + if (params.startup_script) + browser->setStartupScript(params.startup_script); + + if (params.force_popup_urls) { + const char **urls = params.force_popup_urls; + while (*urls) { + cef->add_force_popup_url(*urls, browser); + urls++; + } + } + + if (params.popup_whitelist_urls) { + const char **urls = params.popup_whitelist_urls; + while (*urls) { + cef->add_popup_whitelist_url(*urls, browser); + urls++; + } + } + + return browser; +#else + return nullptr; +#endif +} + +void OBSBasic::DeleteCookie(const std::string &url) +{ +#ifdef BROWSER_AVAILABLE + if (panel_cookies) + panel_cookies->DeleteCookies(url, std::string()); +#endif +} diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index f7e62edc20575e..dcf85836558002 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -818,8 +818,12 @@ void SimpleOutput::Update() obs_data_set_string(audioSettings, "rate_control", "CBR"); obs_data_set_int(audioSettings, "bitrate", audioBitrate); - obs_service_apply_encoder_settings(main->GetService(), videoSettings, - audioSettings); + obs_service_apply_encoder_settings2(main->GetService(), + obs_encoder_get_id(videoStreaming), + videoSettings); + obs_service_apply_encoder_settings2(main->GetService(), + obs_encoder_get_id(audioStreaming), + audioSettings); if (!enforceBitrate) { obs_data_set_int(videoSettings, "bitrate", videoBitrate); @@ -1091,10 +1095,6 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) if (!Active()) SetupOutputs(); - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - /* --------------------- */ const char *type = GetStreamOutputType(service); @@ -1141,8 +1141,6 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) return true; } -static inline bool ServiceSupportsVodTrack(const char *service); - static void clear_archive_encoder(obs_output_t *output, const char *expected_name) { @@ -1170,13 +1168,13 @@ void SimpleOutput::SetupVodTrack(obs_service_t *service) GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - const char *id = obs_service_get_id(service); if (strcmp(id, "rtmp_custom") == 0) enable = enableForCustomServer ? enable : false; else - enable = advanced && enable && ServiceSupportsVodTrack(name); + enable = advanced && enable && + (obs_service_get_audio_track_cap(service) == + OBS_SERVICE_AUDIO_ARCHIVE_TRACK); if (enable) obs_output_set_audio_encoder(streamOutput, audioArchive, 1); @@ -1709,8 +1707,9 @@ void AdvancedOutput::UpdateStreamSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings, "bitrate"); int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + obs_service_apply_encoder_settings2( + main->GetService(), obs_encoder_get_id(videoStreaming), + settings); if (!enforceBitrate) obs_data_set_int(settings, "bitrate", bitrate); @@ -1754,18 +1753,6 @@ void AdvancedOutput::Update() UpdateAudioSettings(); } -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - inline void AdvancedOutput::SetupStreaming() { bool rescale = config_get_bool(main->Config(), "AdvOut", "Rescale"); @@ -1787,8 +1774,9 @@ inline void AdvancedOutput::SetupStreaming() const char *id = obs_service_get_id(main->GetService()); if (strcmp(id, "rtmp_custom") == 0) { OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + obs_service_apply_encoder_settings2( + main->GetService(), obs_encoder_get_id(videoStreaming), + settings); obs_encoder_update(videoStreaming, settings); } } @@ -2001,8 +1989,9 @@ inline void AdvancedOutput::UpdateAudioSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings( - main->GetService(), nullptr, + obs_service_apply_encoder_settings2( + main->GetService(), + obs_encoder_get_id(streamAudioEnc), settings[i]); if (!enforceBitrate) @@ -2062,9 +2051,8 @@ inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) + if (obs_service_get_audio_track_cap(service) != + OBS_SERVICE_AUDIO_ARCHIVE_TRACK) vodTrackEnabled = false; } @@ -2086,10 +2074,6 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) if (!Active()) SetupOutputs(); - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - /* --------------------- */ const char *type = GetStreamOutputType(service); diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 4fcb8ef7dc0186..e4348c1f682c26 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -313,14 +313,23 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir.c_str()); - Auth::Save(); if (create_new) { - auth.reset(); DestroyPanelCookieManager(); } else if (!rename) { DuplicateCurrentCookieProfile(config); } + if (!rename) { + /* Save dock state before saving */ + config_set_string(basicConfig, "BasicWindow", "DockState", + saveState().toBase64().constData()); + + /* Save dock state on the new profile if duplication */ + if (!create_new) + config_set_string(config, "BasicWindow", "DockState", + saveState().toBase64().constData()); + } + config_set_string(config, "General", "Name", newName.c_str()); basicConfig.SaveSafe("tmp"); config.SaveSafe("tmp"); @@ -340,8 +349,6 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - // Run auto configuration setup wizard when a new profile is made to assist // setting up blank settings if (create_new && showWizardChecked) { @@ -351,10 +358,25 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, wizard.exec(); } - if (api && !rename) { + if (rename) + return true; + + if (api) { api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED); } + + if (create_new) { + on_resetDocks_triggered(true); + } else { + /* Restore dock state post duplication + * Plugins might have removed and added back docks */ + const char *dockStateStr = config_get_string( + basicConfig, "BasicWindow", "DockState"); + + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + RestoreState(dockState); + } return true; } @@ -600,8 +622,6 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) bool needsRestart = ProfileNeedsRestart(config, settingsRequiringRestart); - Auth::Save(); - auth.reset(); DeleteCookies(); DestroyPanelCookieManager(); @@ -620,8 +640,6 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - if (api) { api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED); @@ -638,6 +656,20 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) close(); } } + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) + on_resetDocks_triggered(true); + else { + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + RestoreState(dockState); + } } void OBSBasic::on_actionImportProfile_triggered() @@ -755,6 +787,11 @@ void OBSBasic::ChangeProfile() return; } + /* Save dock state before the changing event is emitted */ + config_set_string(basicConfig, "BasicWindow", "DockState", + saveState().toBase64().constData()); + basicConfig.SaveSafe("tmp"); + size_t path_len = path.size(); path += "/basic.ini"; @@ -779,8 +816,6 @@ void OBSBasic::ChangeProfile() config_set_string(App()->GlobalConfig(), "Basic", "Profile", newName); config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir); - Auth::Save(); - auth.reset(); DestroyPanelCookieManager(); config.Swap(basicConfig); @@ -792,8 +827,6 @@ void OBSBasic::ChangeProfile() UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - CheckForSimpleModeX264Fallback(); blog(LOG_INFO, "Switched to profile '%s' (%s)", newName, newDir); @@ -813,6 +846,20 @@ void OBSBasic::ChangeProfile() close(); } } + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) + on_resetDocks_triggered(true); + else { + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + RestoreState(dockState); + } } void OBSBasic::CheckForSimpleModeX264Fallback() diff --git a/UI/window-basic-main-service.cpp b/UI/window-basic-main-service.cpp new file mode 100644 index 00000000000000..7c4f94b99bf394 --- /dev/null +++ b/UI/window-basic-main-service.cpp @@ -0,0 +1,636 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "window-basic-main.hpp" + +#include + +#include +#include + +#include + +#include "platform.hpp" + +#define SERVICE_PATH "streamService.json" + +void OBSBasic::SaveService() +{ + if (!service) + return; + + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), + SERVICE_PATH); + if (ret <= 0) + return; + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak")) + blog(LOG_WARNING, "Failed to save service"); +} + +static bool service_available(const char *service) +{ + const char *val; + size_t i = 0; + + while (obs_enum_service_types(i++, &val)) + if (strcmp(val, service) == 0) + return true; + + return false; +} + +static bool migrate_twitch_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-twitch.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-twitch plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + OBSDataAutoRelease settingsData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "Twitch", "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "Twitch", "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "Twitch", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "Twitch", "ScopeVer")); + + obs_data_set_string(settingsData, "feed_uuid", + config_get_string(basicConfig, "Twitch", "UUID")); + + obs_data_set_int(settings, "chat_addon", + config_get_int(basicConfig, "Twitch", "AddonChoice")); + obs_data_set_bool(settings, "enforce_bwtest", + obs_data_get_bool(settings, "bwtest")); + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + obs_data_set_obj(serviceData, "settings", settingsData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-twitch data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "Twitch", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_restream_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-restream.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-restream plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "Restream", + "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "Restream", + "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "Restream", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "Restream", "ScopeVer")); + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-restream data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "Restream", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_youtube_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-youtube.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-youtube plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + OBSDataAutoRelease settingsData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "YouTube", "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "YouTube", "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "YouTube", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "YouTube", "ScopeVer")); + + bool rememberSettings = + config_get_bool(basicConfig, "YouTube", "RememberSettings"); + obs_data_set_bool(settingsData, "remember", rememberSettings); + + if (rememberSettings) { + obs_data_set_string(settingsData, "title", + config_get_string(basicConfig, "YouTube", + "Title")); + + obs_data_set_string(settingsData, "description", + config_get_string(basicConfig, "YouTube", + "Description")); + + std::string priv = + config_get_string(basicConfig, "YouTube", "Privacy"); + int privInt = 0 /* "private" */; + if (priv == "public") + privInt = 1; + else if (priv == "unlisted") + privInt = 2; + obs_data_set_int(settingsData, "privacy", privInt); + + obs_data_set_string(settingsData, "category_id", + config_get_string(basicConfig, "YouTube", + "CategoryID")); + + std::string latency = + config_get_string(basicConfig, "YouTube", "Latency"); + int latencyInt = 0 /* "normal" */; + if (latency == "low") + latencyInt = 1; + else if (latency == "ultraLow") + latencyInt = 2; + obs_data_set_int(settingsData, "latency", latencyInt); + + obs_data_set_bool(settingsData, "dvr", + config_get_bool(basicConfig, "YouTube", + "DVR")); + + obs_data_set_bool(settingsData, "made_for_kids", + config_get_bool(basicConfig, "YouTube", + "MadeForKids")); + + obs_data_set_bool(settingsData, "schedule_for_later", + config_get_bool(basicConfig, "YouTube", + "ScheduleForLater")); + + obs_data_set_bool(settingsData, "auto_start", + config_get_bool(basicConfig, "YouTube", + "AutoStart")); + + obs_data_set_bool(settingsData, "auto_stop", + config_get_bool(basicConfig, "YouTube", + "AutoStop")); + + std::string projection = + config_get_string(basicConfig, "YouTube", "Projection"); + obs_data_set_int(settingsData, "projection", + projection == "360" ? 1 : 0); + + obs_data_set_string(settingsData, "thumbnail_file", + config_get_string(basicConfig, "YouTube", + "ThumbnailFile")); + } + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + obs_data_set_obj(serviceData, "settings", settingsData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-youtube data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "YouTube", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_rtmp_common(config_t *basicConfig, obs_data_t *data) +{ + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + std::string matrixFilePath; + if (!GetDataFilePath("services-conversion-matrix.json", + matrixFilePath)) { + blog(LOG_ERROR, "Failed to find service conversion matrix"); + return false; + } + + std::ifstream matrixFile(matrixFilePath); + if (!matrixFile.is_open()) { + blog(LOG_ERROR, "Failed to open service conversion matrix"); + return false; + } + + std::string error; + std::stringstream ss; + ss << matrixFile.rdbuf(); + json11::Json matrix = json11::Json::parse(ss.str(), error); + if (matrix.is_null()) { + blog(LOG_ERROR, "Failed to parse service conversion matrix"); + return false; + } + + std::string service = obs_data_get_string(settings, "service"); + std::string type = matrix[service].string_value(); + if (!service_available(type.c_str())) { + blog(LOG_ERROR, "Service '%s' not found, migration failed", + type.c_str()); + return false; + } + + blog(LOG_INFO, "Migrating 'rtmp_common' service to '%s'", type.c_str()); + obs_data_set_string(data, "type", type.c_str()); + + bool integration = false; + if (config_has_user_value(basicConfig, "Auth", "Type")) { + std::string authType = + config_get_string(basicConfig, "Auth", "Type"); + /* If authType is the same as service name, integration is enabled */ + integration = (service == authType); + } + + if (integration) { + /* Migrate integrations */ + if (type == "twitch") { + if (!migrate_twitch_integration(basicConfig, settings)) + return false; + } else if (type == "restream") { + if (!migrate_restream_integration(basicConfig, + settings)) + return false; + } else if (type == "youtube") { + if (!migrate_youtube_integration(basicConfig, settings)) + return false; + } + } else if (type == "twitch" || type == "restream" || + type == "youtube") { + /* Those service use "stream_key" */ + obs_data_set_string(settings, "stream_key", + obs_data_get_string(data, "key")); + obs_data_erase(settings, "key"); + } else if (type != "dacast" && type != "nimo_tv" && + type != "showroom" && type != "younow") { + /* Services from obs-services use "stream_id" + * other services than integration kept "key" */ + obs_data_set_string(settings, "stream_id", + obs_data_get_string(data, "key")); + obs_data_erase(settings, "key"); + } + obs_data_erase(settings, "bwtest"); + + if (service == "YouNow") { + obs_data_set_string(settings, "protocol", "FTL"); + } else if (service == "YouTube - HLS") { + obs_data_set_string(settings, "protocol", "HLS"); + } else if (type == "twitch" || type == "dacast" || type == "nimo_tv" || + type == "showroom") { + obs_data_set_string(settings, "protocol", "RTMP"); + } + + if (!obs_data_has_user_value(settings, "protocol")) { + std::string url = obs_data_get_string(settings, "server"); + std::string prefix = url.substr(0, url.find("://") + 3); + obs_data_set_string(settings, "protocol", + prefix == "rtmps://" ? "RTMPS" : "RTMP"); + } + + obs_data_set_obj(data, "settings", settings); + + return true; +} + +static bool migrate_rtmp_custom(obs_data_t *data) +{ + if (!service_available("custom_service")) { + blog(LOG_ERROR, + "Service 'custom_service' not found, migration failed"); + return false; + } + + blog(LOG_INFO, "Migrating 'rtmp_custom' service to 'custom_service'"); + + obs_data_set_string(data, "type", "custom_service"); + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + obs_data_erase(settings, "use_auth"); + obs_data_erase(settings, "bwtest"); + + std::string url = obs_data_get_string(settings, "server"); + + std::string protocol; + std::string prefix = url.substr(0, url.find("://") + 3); + + if (prefix == "rtmp://") + protocol = "RTMP"; + else if (prefix == "rtmps://") + protocol = "RTMPS"; + else if (prefix == "ftl://") + protocol = "FTL"; + else if (prefix == "srt://") + protocol = "SRT"; + else if (prefix == "rist://") + protocol = "RIST"; + + obs_data_set_string(settings, "protocol", protocol.c_str()); + + /* Migrate encryption passphrase */ + std::string passphrase; + if (protocol == "SRT") { + passphrase = obs_data_get_string(settings, "password"); + obs_data_erase(settings, "username"); + obs_data_erase(settings, "password"); + } else if (protocol == "RIST") { + passphrase = obs_data_get_string(settings, "key"); + + obs_data_erase(settings, "key"); + } + + if (!passphrase.empty()) + obs_data_set_string(settings, "encrypt_passphrase", + passphrase.c_str()); + + /* Migrate stream ID/key */ + if (protocol != "RIST") { + std::string stream_id = obs_data_get_string(settings, "key"); + if (!stream_id.empty()) + obs_data_set_string(settings, "stream_id", + stream_id.c_str()); + + obs_data_erase(settings, "key"); + } + + obs_data_set_obj(data, "settings", settings); + + return true; +} + +bool OBSBasic::LoadService() +{ + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), + SERVICE_PATH); + if (ret <= 0) + return false; + + OBSDataAutoRelease data = + obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); + + if (!data) { + char oldServiceJsonPath[512]; + + if (GetProfilePath(oldServiceJsonPath, + sizeof(oldServiceJsonPath), + "service.json") <= 0) + return false; + + data = obs_data_create_from_json_file_safe(oldServiceJsonPath, + "bak"); + + if (!data) + return false; + + /* Migration if old rtmp-services and network settings */ + std::string type = obs_data_get_string(data, "type"); + /* rtmp_common was set as a default value so type can be empty */ + if (type == "rtmp_common" || type.empty()) { + if (!migrate_rtmp_common(basicConfig, data)) + return false; + + } else if (type == "rtmp_custom") { + if (!migrate_rtmp_custom(data)) + return false; + } + + if (!obs_data_save_json(data, serviceJsonPath)) + blog(LOG_WARNING, "Failed to save migrated service"); + } + + std::string type = obs_data_get_string(data, "type"); + + if (!service_available(type.c_str())) { + blog(LOG_ERROR, + "Service '%s' not found, fallback to empty custom service", + type.c_str()); + return false; + } + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + service = obs_service_create(type.c_str(), "default_service", settings, + nullptr); + obs_service_release(service); + + if (!service) + return false; + + /* Check service returns a protocol */ + const char *protocol = obs_service_get_protocol(service); + if (!protocol) { + blog(LOG_ERROR, + "Service '%s' did not returned a protocol, fallback to empty custom service", + type.c_str()); + return false; + } + + /* Check if the protocol protocol is registered */ + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_ERROR, + "Protocol %s is not supported, fallback to empty custom service", + protocol); + return false; + } + + /* Enforce Opus on FTL if needed */ + if (strcmp(protocol, "FTL") == 0) { + const char *option = config_get_string( + basicConfig, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(basicConfig, "SimpleOutput", + "StreamAudioEncoder", "opus"); + + option = config_get_string(basicConfig, "AdvOut", + "AudioEncoder"); + if (strcmp(obs_get_encoder_codec(option), "opus") != 0) + config_set_string(basicConfig, "AdvOut", "AudioEncoder", + "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, + nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = + obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (!newService) + return; + + ResetServiceBroadcastFlow(); + + service = newService; + + LoadServiceBroadcastFlow(); +} + +void OBSBasic::ResetServiceBroadcastFlow() +{ + if (!serviceBroadcastFlow) + return; + + serviceBroadcastFlow = nullptr; + + ui->broadcastButton->setVisible(false); + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); +} + +void OBSBasic::LoadServiceBroadcastFlow() +{ + if (serviceBroadcastFlow) + return; + + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service) + continue; + + serviceBroadcastFlow = &broadcastFlows[i]; + ui->broadcastButton->setVisible(true); + + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != + OBS_BROADCAST_NONE); + + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::ManageBroadcastButtonClicked); + break; + } +} + +bool OBSBasic::AddBroadcastFlow(const obs_service_t *service_, + const obs_frontend_broadcast_flow &flow) +{ + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service_) + continue; + + blog(LOG_WARNING, + "This service already has an broadcast flow added"); + return false; + } + + broadcastFlows.append(OBSBroadcastFlow(service_, flow)); + + LoadServiceBroadcastFlow(); + + return true; +} + +void OBSBasic::RemoveBroadcastFlow(const obs_service_t *service_) +{ + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service_) + continue; + + broadcastFlows.removeAt(i); + + if (GetService() == service_) + ResetServiceBroadcastFlow(); + + return; + } + + blog(LOG_WARNING, "This service has no broadcast flow added to it"); +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 9807cfd83378ec..bca98b94bd5b7a 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -60,11 +60,6 @@ #endif #include "window-projector.hpp" #include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif #include "qt-wrappers.hpp" #include "context-bar-controls.hpp" #include "obs-proxy-style.hpp" @@ -274,12 +269,6 @@ void setupDockAction(QDockWidget *dock) action->connect(action, &QAction::enabledChanged, neverDisable); } -extern void RegisterTwitchAuth(); -extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif - OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), @@ -287,16 +276,6 @@ OBSBasic::OBSBasic(QWidget *parent) { setAttribute(Qt::WA_NativeWindow); -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif - setAcceptDrops(true); setContextMenuPolicy(Qt::CustomContextMenu); @@ -308,7 +287,6 @@ OBSBasic::OBSBasic(QWidget *parent) QStyle *contextBarStyle = new OBSContextBarProxyStyle(); contextBarStyle->setParent(ui->contextContainer); ui->contextContainer->setStyle(contextBarStyle); - ui->broadcastButton->setVisible(false); startingDockLayout = saveState(); @@ -504,9 +482,6 @@ OBSBasic::OBSBasic(QWidget *parent) connect(ui->scenes, SIGNAL(scenesReordered()), this, SLOT(ScenesReordered())); - connect(ui->broadcastButton, &QPushButton::clicked, this, - &OBSBasic::BroadcastButtonClicked); - connect(App(), &OBSApp::StyleChanged, this, &OBSBasic::ThemeChanged); QActionGroup *actionGroup = new QActionGroup(this); @@ -1302,93 +1277,6 @@ void OBSBasic::LoadData(obs_data_t *data, const char *file) } } -#define SERVICE_PATH "service.json" - -void OBSBasic::SaveService() -{ - if (!service) - return; - - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return; - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak")) - blog(LOG_WARNING, "Failed to save service"); -} - -bool OBSBasic::LoadService() -{ - const char *type; - - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return false; - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); - - if (!data) - return false; - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, - hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on FTL if needed */ - if (strcmp(obs_service_get_protocol(service), "FTL") == 0 || - strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string( - basicConfig, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(basicConfig, "SimpleOutput", - "StreamAudioEncoder", "opus"); - - option = config_get_string(basicConfig, "AdvOut", - "AudioEncoder"); - if (strcmp(obs_get_encoder_codec(option), "opus") != 0) - config_set_string(basicConfig, "AdvOut", "AudioEncoder", - "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, - nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; @@ -2138,46 +2026,6 @@ void OBSBasic::OBSInit() } #endif - const char *dockStateStr = config_get_string( - App()->GlobalConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General", - "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool( - App()->GlobalConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GlobalConfig(), "General", - "ResetDockLock23", true); - config_remove_value(App()->GlobalConfig(), - "BasicWindow", "DocksLocked"); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow", - "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GlobalConfig(), "BasicWindow", - "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - SystemTray(true); TaskbarOverlayInit(); @@ -2316,14 +2164,54 @@ void OBSBasic::OnFirstLoad() introCheckThread->start(); } #endif - - Auth::Load(); - bool showLogViewerOnStartup = config_get_bool( App()->GlobalConfig(), "LogViewer", "ShowLogStartup"); if (showLogViewerOnStartup) on_actionViewCurrentLog_triggered(); + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = + QByteArray::fromBase64(QByteArray(dockStateStr)); + RestoreState(dockState); + } + + bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General", + "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool( + App()->GlobalConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GlobalConfig(), "General", + "ResetDockLock23", true); + config_remove_value(App()->GlobalConfig(), + "BasicWindow", "DocksLocked"); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow", + "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GlobalConfig(), "BasicWindow", + "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); } #if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0 @@ -4523,23 +4411,6 @@ void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) /* Main class functions */ -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = - obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - int OBSBasic::GetTransitionDuration() { return ui->transitionDuration->value(); @@ -4993,14 +4864,12 @@ void OBSBasic::closeEvent(QCloseEvent *event) return; } -#ifdef YOUTUBE_ENABLED - /* Also don't close the window if the youtube stream check is active */ - if (youtubeStreamCheckThread) { + /* Also don't close the window if the broadcast stream check is active */ + if (broadcastStreamCheckThread) { QTimer::singleShot(1000, this, SLOT(close())); event->ignore(); return; } -#endif if (isVisible()) config_set_string(App()->GlobalConfig(), "BasicWindow", @@ -5059,9 +4928,7 @@ void OBSBasic::closeEvent(QCloseEvent *event) signalHandlers.clear(); - Auth::Save(); SaveProjectNow(); - auth.reset(); delete extraBrowsers; @@ -5069,8 +4936,9 @@ void OBSBasic::closeEvent(QCloseEvent *event) if (program) program->DestroyDisplay(); - config_set_string(App()->GlobalConfig(), "BasicWindow", "DockState", + config_set_string(basicConfig, "BasicWindow", "DockState", saveState().toBase64().constData()); + config_save_safe(basicConfig, "tmp", nullptr); #ifdef BROWSER_AVAILABLE if (cef) @@ -5142,6 +5010,20 @@ void OBSBasic::changeEvent(QEvent *event) if (previewEnabled) EnablePreviewDisplay(true); } + + if (!dockStateToRestore.isEmpty() && + (isVisible() || !isMaximized())) { + if (!restoreState(dockStateToRestore)) + on_resetDocks_triggered(true); + + dockStateToRestore.clear(); + } + } else if ((event->type() == QEvent::Show) && + !dockStateToRestore.isEmpty()) { + if (!restoreState(dockStateToRestore)) + on_resetDocks_triggered(true); + + dockStateToRestore.clear(); } } @@ -6749,39 +6631,13 @@ void OBSBasic::DisplayStreamStartError() QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); } -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key, - bool autostart, bool autostop, - bool start_now) +void OBSBasic::BroadcastStreamCheck() { - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string an_id = QT_TO_UTF8(id); - obs_data_set_string(settings, "stream_id", an_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) -{ - YoutubeApiWrappers *apiYouTube( - dynamic_cast(GetAuth())); - if (!apiYouTube) { + if (!serviceBroadcastFlow) { /* technically we should never get here -Lain */ QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); + broadcastStreamCheckThread->deleteLater(); blog(LOG_ERROR, "=========================================="); blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); blog(LOG_ERROR, "=========================================="); @@ -6789,9 +6645,6 @@ void OBSBasic::YoutubeStreamCheck(const std::string &key) } int timeout = 0; - json11::Json json; - QString id = key.c_str(); - while (StreamingActive()) { if (timeout == 14) { QMetaObject::invokeMethod(this, "ForceStopStreaming", @@ -6799,62 +6652,34 @@ void OBSBasic::YoutubeStreamCheck(const std::string &key) break; } - if (!apiYouTube->FindStream(id, json)) { + obs_broadcast_stream_state state = + serviceBroadcastFlow->IsBroadcastStreamActive(); + + switch (state) { + case OBS_BROADCAST_STREAM_FAILURE: QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { + case OBS_BROADCAST_STREAM_INACTIVE: + QThread::sleep(1); + timeout++; + break; + case OBS_BROADCAST_STREAM_ACTIVE: QMetaObject::invokeMethod(ui->broadcastButton, "setEnabled", Q_ARG(bool, true)); break; - } else { - QThread::sleep(1); - timeout++; } - } - - youtubeStreamCheckThread->deleteLater(); -} -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr( - "YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText( - QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GlobalConfig(), "General", - "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - } - }; - - bool warned = config_get_bool(App()->GlobalConfig(), "General", - "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, - Q_ARG(VoidFunc, msgBox)); + if (state != OBS_BROADCAST_STREAM_INACTIVE) + break; } + + broadcastStreamCheckThread->deleteLater(); } -#endif void OBSBasic::StartStreaming() { @@ -6863,8 +6688,11 @@ void OBSBasic::StartStreaming() if (disableOutputsRef) return; - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { + /* Ignore broadcast flow if bandwidth test is enabled */ + bool bwtest = obs_service_bandwidth_test_enabled(service); + if (!bwtest && serviceBroadcastFlow) { + if (serviceBroadcastFlow->BroadcastState() == + OBS_BROADCAST_NONE) { ui->streamButton->setChecked(false); QMessageBox no_broadcast(this); @@ -6881,8 +6709,8 @@ void OBSBasic::StartStreaming() no_broadcast.exec(); if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, - "SetupBroadcast"); + QMetaObject::invokeMethod( + this, "ManageBroadcastButtonClicked"); return; } } @@ -6900,7 +6728,6 @@ void OBSBasic::StartStreaming() ui->streamButton->setEnabled(false); ui->streamButton->setChecked(false); ui->streamButton->setText(QTStr("Basic.Main.Connecting")); - ui->broadcastButton->setChecked(false); if (sysTrayStream) { sysTrayStream->setEnabled(false); @@ -6912,27 +6739,22 @@ void OBSBasic::StartStreaming() return; } - if (!autoStartBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StartBroadcast")); - ui->broadcastButton->setProperty("broadcastState", "ready"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - // well, we need to disable button while stream is not active - ui->broadcastButton->setEnabled(false); - } else { - if (!autoStopBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StopBroadcast")); - } else { + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!bwtest && serviceBroadcastFlow) { + ui->broadcastButton->setChecked(false); + + if (serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); ui->broadcastButton->setText( - QTStr("Basic.Main.AutoStopEnabled")); + QTStr("Basic.Main.StartBroadcast")); + connect(ui->broadcastButton, &QPushButton::clicked, + this, &OBSBasic::StartBroadcastButtonClicked); + // well, we need to disable button while stream is not active ui->broadcastButton->setEnabled(false); + } else { + BroadcastStarted(); } - ui->broadcastButton->setProperty("broadcastState", "active"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - broadcastActive = true; } bool recordWhenStreaming = config_get_bool( @@ -6945,135 +6767,156 @@ void OBSBasic::StartStreaming() if (replayBufferWhileStreaming) StartReplayBuffer(); -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif + /* Ignore broadcast flow if bandwidth test is enabled */ + if (bwtest || !serviceBroadcastFlow || + serviceBroadcastFlow->BroadcastStartType() != + OBS_BROADCAST_START_DIFFER_FROM_STREAM) + return; + + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle( + QTStr("Basic.Main.BroadcastManualStartWarning.Title")); + msgbox.setText(QTStr("Basic.Main.BroadcastManualStartWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GlobalConfig(), "General", + "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GlobalConfig(), "General", + "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, + Q_ARG(VoidFunc, msgBox)); + } } -void OBSBasic::BroadcastButtonClicked() +void OBSBasic::ManageBroadcastButtonClicked() { - if (!broadcastReady || - (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - if (broadcastReady) - ui->broadcastButton->setChecked(true); + if (obs_service_bandwidth_test_enabled(service)) { + OBSMessageBox::warning( + this, QTStr("Basic.Main.ManageBroadcastBWTest.Title"), + QTStr("Basic.Main.ManageBroadcastBWTest.Text")); return; } - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = - dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr( - "YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, - ytAuth->GetBroadcastId()); - - OBSMessageBox::warning( - this, - QTStr("Output.BroadcastStartFailed"), - last_error, true); - ui->broadcastButton->setChecked(false); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag + bool streamingActive = outputHandler->StreamingActive(); - if (!autoStopBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StopBroadcast")); - } else { - ui->broadcastButton->setText( - QTStr("Basic.Main.AutoStopEnabled")); - ui->broadcastButton->setEnabled(false); - } + serviceBroadcastFlow->ManageBroadcast(streamingActive); - ui->broadcastButton->setProperty("broadcastState", "active"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", - "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), - QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No); + if (streamingActive) + return; - if (button == QMessageBox::No) { - ui->broadcastButton->setChecked(true); - return; - } - } + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_NONE); - std::shared_ptr ytAuth = - dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr( - "YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, - ytAuth->GetBroadcastId()); - - OBSMessageBox::warning( - this, - QTStr("Output.BroadcastStopFailed"), - last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; + if (serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_WITH_STREAM_NOW) + QMetaObject::invokeMethod(this, "StartStreaming"); +} - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - SetBroadcastFlowEnabled(true); +void OBSBasic::StartBroadcastButtonClicked() +{ + if (!serviceBroadcastFlow->DifferedStartBroadcast()) { + QString error = QString::fromStdString( + serviceBroadcastFlow->GetLastError()); + OBSMessageBox::warning( + this, QTStr("Output.BroadcastStartFailed"), + error.isEmpty() ? QTStr("Output.BroadcastUnknownError") + : error, + true); + ui->broadcastButton->setChecked(false); + return; } + + BroadcastStarted(); } -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +void OBSBasic::BroadcastStarted() { - ui->broadcastButton->setEnabled(enabled); - ui->broadcastButton->setVisible(enabled); - ui->broadcastButton->setChecked(broadcastReady); - ui->broadcastButton->setProperty("broadcastState", "idle"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast")); + switch (serviceBroadcastFlow->BroadcastStopType()) { + case OBS_BROADCAST_STOP_DIFFER_FROM_STREAM: + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); + ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast")); + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::StopBroadcastButtonClicked); + break; + case OBS_BROADCAST_STOP_WITH_STREAM: + if (serviceBroadcastFlow->AllowManagingWhileStreaming()) { + ResetBroadcastButtonState(); + break; + } + ui->broadcastButton->setText( + QTStr("Basic.Main.AutoStopEnabled")); + ui->broadcastButton->setEnabled(false); + break; + case OBS_BROADCAST_STOP_NEVER: + if (serviceBroadcastFlow->AllowManagingWhileStreaming()) { + ResetBroadcastButtonState(); + break; + } + ui->broadcastButton->setText( + QTStr("Basic.Main.SetupBroadcast")); + ui->broadcastButton->setEnabled(false); + ui->broadcastButton->setChecked(true); + break; + } } -void OBSBasic::SetupBroadcast() +void OBSBasic::StopBroadcastButtonClicked() { -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, - &OBSBasic::YouTubeActionDialogOk); - int result = dialog.Valid() ? dialog.exec() : QDialog::Rejected; - if (result != QDialog::Accepted) { - if (!broadcastReady) - ui->broadcastButton->setChecked(false); + bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", + "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), + QTStr("Basic.Main.BroadcastEndWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) { + ui->broadcastButton->setChecked(true); + return; } } -#endif + + if (!serviceBroadcastFlow->DifferedStopBroadcast()) { + QString error = QString::fromStdString( + serviceBroadcastFlow->GetLastError()); + OBSMessageBox::warning( + this, QTStr("Output.BroadcastStopFailed"), + error.isEmpty() ? QTStr("Output.BroadcastUnknownError") + : error, + true); + } + + QMetaObject::invokeMethod(this, "StopStreaming"); + + ResetBroadcastButtonState(); +} + +void OBSBasic::ResetBroadcastButtonState() +{ + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); + + ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast")); + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::ManageBroadcastButtonClicked); + + ui->broadcastButton->setEnabled(true); + ui->broadcastButton->setVisible(true); + + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_NONE); } #ifdef _WIN32 @@ -7186,19 +7029,10 @@ void OBSBasic::StopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(streamingStopping); - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow) + serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7226,19 +7060,10 @@ void OBSBasic::ForceStopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(true); - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow) + serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7324,22 +7149,18 @@ void OBSBasic::StreamingStart() sysTrayStream->setEnabled(true); } -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = - obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread( - [this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName( - "YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_DIFFER_FROM_STREAM && + !broadcastStreamCheckThread) { + broadcastStreamCheckThread = + CreateQThread([this] { BroadcastStreamCheck(); }); + broadcastStreamCheckThread->setObjectName( + "BroadcastStreamCheckThread"); + broadcastStreamCheckThread->start(); } -#endif if (api) api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTED); @@ -7454,8 +7275,11 @@ void OBSBasic::StreamingStop(int code, QString last_error) } // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_ACTIVE) + ResetBroadcastButtonState(); } void OBSBasic::AutoRemux(QString input, bool no_show) @@ -7972,27 +7796,33 @@ void OBSBasic::OnVirtualCamStop(int) void OBSBasic::on_streamButton_clicked() { + bool bwtest = obs_service_bandwidth_test_enabled(service); if (outputHandler->StreamingActive()) { bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStoppingStream"); -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && - autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), - QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No); + /* Ignore broadcast flow if bandwidth test is enabled */ + if (isVisible() && !bwtest && serviceBroadcastFlow) { + if (serviceBroadcastFlow->BroadcastStopType() == + OBS_BROADCAST_STOP_WITH_STREAM) { + QMessageBox::StandardButton button = + OBSMessageBox::question( + this, + QTStr("ConfirmStop.Title"), + QTStr("Basic.Main.BroadcastEndWarning"), + QMessageBox::Yes | + QMessageBox::No, + QMessageBox::No); + + if (button == QMessageBox::No) { + ui->streamButton->setChecked(true); + return; + } - if (button == QMessageBox::No) { - ui->streamButton->setChecked(true); - return; + confirm = false; } - - confirm = false; } -#endif + if (confirm && isVisible()) { QMessageBox::StandardButton button = OBSMessageBox::question( @@ -8014,14 +7844,8 @@ void OBSBasic::on_streamButton_clicked() return; } - Auth *auth = GetAuth(); - - auto action = - (auth && auth->external()) - ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation( - this, service); - switch (action) { + switch (UIValidation::StreamSettingsConfirmation(this, + service)) { case StreamSettingsAction::ContinueStream: break; case StreamSettingsAction::OpenSettings: @@ -8036,16 +7860,12 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); - bool bwtest = false; - - if (this->auth) { - OBSDataAutoRelease settings = - obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && - !broadcastActive) - confirm = false; + // Disable confirmation if this is going to open broadcast setup + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!bwtest && serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastState() == + OBS_BROADCAST_NONE) { + confirm = false; } if (bwtest && isVisible()) { @@ -10926,3 +10746,13 @@ void OBSBasic::ThemeChanged() if (api) api->on_event(OBS_FRONTEND_EVENT_THEME_CHANGED); } + +void OBSBasic::RestoreState(const QByteArray &state) +{ + if (isVisible() || !isMaximized()) { + if (!restoreState(state)) + on_resetDocks_triggered(true); + } else { + dockStateToRestore = state; + } +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 470196e397bdc4..4f508e1e05e18a 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -36,9 +36,9 @@ #include "window-missing-files.hpp" #include "window-projector.hpp" #include "window-basic-about.hpp" -#include "auth-base.hpp" #include "log-viewer.hpp" #include "undo-stack-obs.hpp" +#include "broadcast-flow.hpp" #include @@ -222,8 +222,6 @@ class OBSBasic : public OBSMainWindow { private: obs_frontend_callbacks *api = nullptr; - std::shared_ptr auth; - std::vector volumes; std::vector signalHandlers; @@ -544,6 +542,7 @@ class OBSBasic : public OBSMainWindow { QList visDlgPositions; QByteArray startingDockLayout; + QByteArray dockStateToRestore; obs_data_array_t *SaveProjectors(); void LoadSavedProjectors(obs_data_array_t *savedProjectors); @@ -621,20 +620,17 @@ class OBSBasic : public OBSMainWindow { void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &id, const QString &key, - bool autostart, bool autostop, - bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); + OBSBroadcastFlow *serviceBroadcastFlow = nullptr; + QList broadcastFlows; + QPointer broadcastStreamCheckThread; + + void ResetServiceBroadcastFlow(); + void LoadServiceBroadcastFlow(); + + void BroadcastStarted(); + void ResetBroadcastButtonState(); + + void BroadcastStreamCheck(); void UpdatePreviewSafeAreas(); bool drawSafeAreas = false; @@ -670,8 +666,6 @@ public slots: void DisplayStreamStartError(); - void SetupBroadcast(); - void StartStreaming(); void StopStreaming(); void ForceStopStreaming(); @@ -841,6 +835,10 @@ private slots: void RestartVirtualCam(const VCamConfig &config); void RestartingVirtualCam(); + void ManageBroadcastButtonClicked(); + void StartBroadcastButtonClicked(); + void StopBroadcastButtonClicked(); + private: /* OBS Callbacks */ static void SceneReordered(void *data, calldata_t *params); @@ -894,6 +892,10 @@ private slots: obs_service_t *GetService(); void SetService(obs_service_t *service); + bool AddBroadcastFlow(const obs_service_t *service, + const obs_frontend_broadcast_flow &flow); + void RemoveBroadcastFlow(const obs_service_t *service); + int GetTransitionDuration(); int GetTbarPosition(); @@ -939,8 +941,6 @@ private slots: void SaveService(); bool LoadService(); - inline Auth *GetAuth() { return auth.get(); } - inline void EnableOutputs(bool enable) { if (enable) { @@ -979,6 +979,9 @@ private slots: bool IsDockObjectNameUsed(const QString &name); void AddCustomDockWidget(QDockWidget *dock); + QWidget *GetBrowserWidget(const obs_frontend_browser_params ¶ms); + void DeleteCookie(const std::string &url); + static OBSBasic *Get(); const char *GetCurrentOutputPath(); @@ -1020,6 +1023,8 @@ private slots: QColor GetSelectionColor() const; + void RestoreState(const QByteArray &state); + protected: virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 052d8dd37a9500..a20acf94ae86d0 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -1,89 +1,33 @@ #include #include +#include #include "window-basic-settings.hpp" #include "obs-frontend-api.h" #include "obs-app.hpp" #include "window-basic-main.hpp" #include "qt-wrappers.hpp" -#include "url-push-button.hpp" +#include "service-sort-filter.hpp" -#ifdef BROWSER_AVAILABLE -#include -#endif - -#include "auth-oauth.hpp" - -#include "ui-config.h" - -#ifdef YOUTUBE_ENABLED -#include "youtube-api-wrappers.hpp" -#endif - -struct QCef; -struct QCefCookieManager; - -extern QCef *cef; -extern QCefCookieManager *panel_cookies; - -enum class ListOpt : int { - ShowAll = 1, - Custom, - WHIP, -}; - -enum class Section : int { - Connect, - StreamKey, -}; - -inline bool OBSBasicSettings::IsCustomService() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::Custom; -} +constexpr int SHOW_ALL = 1; +constexpr const char *TEMP_SERVICE_NAME = "temp_service"; -inline bool OBSBasicSettings::IsWHIP() const +inline bool OBSBasicSettings::IsCustomOrInternalService() const { - return ui->service->currentData().toInt() == (int)ListOpt::WHIP; + return ui->service->currentIndex() == -1 || + ui->service->currentData().toString() == "custom_service"; } void OBSBasicSettings::InitStreamPage() { - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - ui->bandwidthTestEnable->setVisible(false); - - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - int vertSpacing = ui->topStreamLayout->verticalSpacing(); QMargins m = ui->topStreamLayout->contentsMargins(); m.setBottom(vertSpacing / 2); ui->topStreamLayout->setContentsMargins(m); - m = ui->loginPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->loginPageLayout->setContentsMargins(m); - - m = ui->streamkeyPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); - LoadServices(false); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.None")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.BTTV")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.FFZ")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.Both")); - connect(ui->ignoreRecommended, SIGNAL(clicked(bool)), this, SLOT(DisplayEnforceWarning(bool))); connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this, @@ -92,755 +36,239 @@ void OBSBasicSettings::InitStreamPage() void OBSBasicSettings::LoadStream1Settings() { + loading = true; + bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + obs_service_t *service = main->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *id = obs_service_get_id(service); + uint32_t flags = obs_service_get_flags(service); - obs_service_t *service_obj = main->GetService(); - const char *type = obs_service_get_type(service_obj); - bool is_rtmp_custom = (strcmp(type, "rtmp_custom") == 0); - bool is_rtmp_common = (strcmp(type, "rtmp_common") == 0); - bool is_whip = (strcmp(type, "whip_custom") == 0); + tempService = + obs_service_create_private(id, TEMP_SERVICE_NAME, nullptr); - loading = true; - - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const char *service = obs_data_get_string(settings, "service"); - const char *server = obs_data_get_string(settings, "server"); - const char *key = obs_data_get_string(settings, "key"); - protocol = QT_UTF8(obs_service_get_protocol(service_obj)); - const char *bearer_token = - obs_data_get_string(settings, "bearer_token"); - - if (is_rtmp_custom || is_whip) - ui->customServer->setText(server); - - if (is_rtmp_custom) { - ui->service->setCurrentIndex(0); - lastServiceIdx = 0; - lastCustomServer = ui->customServer->text(); - - bool use_auth = obs_data_get_bool(settings, "use_auth"); - const char *username = - obs_data_get_string(settings, "username"); - const char *password = - obs_data_get_string(settings, "password"); - ui->authUsername->setText(QT_UTF8(username)); - ui->authPw->setText(QT_UTF8(password)); - ui->useAuth->setChecked(use_auth); - - /* add tooltips for stream key, user, password fields */ - QString file = !App()->IsThemeDark() - ? ":/res/images/help.svg" - : ":/res/images/help_light.svg"; - QString lStr = "%1 "; - - ui->streamKeyLabel->setText( - lStr.arg(ui->streamKeyLabel->text(), file)); - ui->streamKeyLabel->setToolTip( - QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); - - ui->authUsernameLabel->setText( - lStr.arg(ui->authUsernameLabel->text(), file)); - ui->authUsernameLabel->setToolTip( - QTStr("Basic.Settings.Stream.Custom.Username.ToolTip")); - - ui->authPwLabel->setText( - lStr.arg(ui->authPwLabel->text(), file)); - ui->authPwLabel->setToolTip( - QTStr("Basic.Settings.Stream.Custom.Password.ToolTip")); - } else { - int idx = ui->service->findText(service); - if (idx == -1) { - if (service && *service) - ui->service->insertItem(1, service); - idx = 1; - } - ui->service->setCurrentIndex(idx); - lastServiceIdx = idx; + /* Avoid sharing the same obs_data_t pointer, + * between the service ,the temp service and the properties view */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); + obs_service_update(tempService, settings); - bool bw_test = obs_data_get_bool(settings, "bwtest"); - ui->bandwidthTestEnable->setChecked(bw_test); - - idx = config_get_int(main->Config(), "Twitch", "AddonChoice"); - ui->twitchAddonDropdown->setCurrentIndex(idx); - } + if ((flags & OBS_SERVICE_UNCOMMON) != 0) + LoadServices(true); - UpdateServerList(); + int idx = ui->service->findData(QT_UTF8(id)); + if (idx == -1) { + QString name(obs_service_get_display_name(id)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); - if (is_rtmp_common) { - int idx = ui->server->findData(server); - if (idx == -1) { - if (server && *server) - ui->server->insertItem(0, server, server); - idx = 0; - } - ui->server->setCurrentIndex(idx); + ui->service->setPlaceholderText(name); } - if (is_whip) - ui->key->setText(bearer_token); - else - ui->key->setText(key); + QSignalBlocker s(ui->service); + QSignalBlocker i(ui->ignoreRecommended); - lastService.clear(); - ServiceChanged(); + ui->service->setCurrentIndex(idx); - UpdateKeyLink(); - UpdateMoreInfoLink(); - UpdateVodTrackSetting(); - UpdateServiceRecommendations(); + ui->ignoreRecommended->setEnabled(!IsCustomOrInternalService()); + ui->ignoreRecommended->setChecked(ignoreRecommended); - bool streamActive = obs_frontend_streaming_active(); - ui->streamPage->setEnabled(!streamActive); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(settings); + ui->serviceLayout->addWidget(streamServiceProps); - ui->ignoreRecommended->setChecked(ignoreRecommended); + DisplayEnforceWarning(ignoreRecommended); loading = false; - QMetaObject::invokeMethod(this, "UpdateResFPSLimits", + QMetaObject::invokeMethod(this, &OBSBasicSettings::UpdateResFPSLimits, Qt::QueuedConnection); + + if (obs_video_active()) { + ui->streamPage->setEnabled(false); + } } void OBSBasicSettings::SaveStream1Settings() { - bool customServer = IsCustomService(); - bool whip = IsWHIP(); - const char *service_id = "rtmp_common"; - - if (customServer) { - service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; - } + OBSDataAutoRelease settings = obs_service_get_settings(tempService); + obs_service_t *service = main->GetService(); - obs_service_t *oldService = main->GetService(); - OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService); + if (ui->service->property("changed").toBool()) { + /* Create a unique obs_data_t for the new service */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); - OBSDataAutoRelease settings = obs_data_create(); + OBSServiceAutoRelease newService = obs_service_create( + obs_service_get_id(tempService), "default_service", + settings, nullptr); - if (!customServer && !whip) { - obs_data_set_string(settings, "service", - QT_TO_UTF8(ui->service->currentText())); - obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); - } else { - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->customServer->text().trimmed())); - obs_data_set_bool(settings, "use_auth", - ui->useAuth->isChecked()); - if (ui->useAuth->isChecked()) { - obs_data_set_string( - settings, "username", - QT_TO_UTF8(ui->authUsername->text())); - obs_data_set_string(settings, "password", - QT_TO_UTF8(ui->authPw->text())); - } - } + if (!newService) + return; - if (!!auth && strcmp(auth->service(), "Twitch") == 0) { - bool choiceExists = config_has_user_value( - main->Config(), "Twitch", "AddonChoice"); - int currentChoice = - config_get_int(main->Config(), "Twitch", "AddonChoice"); - int newChoice = ui->twitchAddonDropdown->currentIndex(); + main->SetService(newService); - config_set_int(main->Config(), "Twitch", "AddonChoice", - newChoice); - - if (choiceExists && currentChoice != newChoice) - forceAuthReload = true; - - obs_data_set_bool(settings, "bwtest", - ui->bandwidthTestEnable->isChecked()); - } else { - obs_data_set_bool(settings, "bwtest", false); - } - - if (whip) { - obs_data_set_string(settings, "service", "WHIP"); - obs_data_set_string(settings, "bearer_token", - QT_TO_UTF8(ui->key->text())); } else { - obs_data_set_string(settings, "key", - QT_TO_UTF8(ui->key->text())); + obs_service_update(service, settings); } - - OBSServiceAutoRelease newService = obs_service_create( - service_id, "default_service", settings, hotkeyData); - - if (!newService) - return; - - main->SetService(newService); main->SaveService(); - main->auth = auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); } -void OBSBasicSettings::UpdateMoreInfoLink() -{ - if (IsCustomService() || IsWHIP()) { - ui->moreInfoButton->hide(); - return; - } - - QString serviceName = ui->service->currentText(); - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - const char *more_info_link = - obs_data_get_string(settings, "more_info_link"); - - if (!more_info_link || (*more_info_link == '\0')) { - ui->moreInfoButton->hide(); - } else { - ui->moreInfoButton->setTargetUrl(QUrl(more_info_link)); - ui->moreInfoButton->show(); - } - obs_properties_destroy(props); -} - -void OBSBasicSettings::UpdateKeyLink() +/* NOTE: Identical to AutoConfigStreamPage function except it shows deprecated services */ +void OBSBasicSettings::LoadServices(bool showAll) { - QString serviceName = ui->service->currentText(); - QString customServer = ui->customServer->text().trimmed(); - QString streamKeyLink; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); - - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = - "https://www.facebook.com/live/producer?ref=OBS"; - } + const char *id; + size_t idx = 0; + bool needShowAllOption = false; - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - } else if (IsWHIP()) { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.BearerToken")); - } else if (!IsCustomService()) { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - } + QSignalBlocker sb(ui->service); - if (QString(streamKeyLink).isNull() || - QString(streamKeyLink).isEmpty()) { - ui->getStreamKeyButton->hide(); - } else { - ui->getStreamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->getStreamKeyButton->show(); - } - obs_properties_destroy(props); -} + ui->service->clear(); + ui->service->setModel(new QStandardItemModel(0, 1, ui->service)); -void OBSBasicSettings::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); + while (obs_enum_service_types(idx++, &id)) { + uint32_t flags = obs_get_service_flags(id); - OBSDataAutoRelease settings = obs_data_create(); + if ((flags & OBS_SERVICE_INTERNAL) != 0) + continue; - obs_data_set_bool(settings, "show_all", showAll); + QStringList protocols = + QT_UTF8(obs_get_service_supported_protocols(id)) + .split(";"); - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); + if (protocols.empty()) { + blog(LOG_WARNING, "No protocol found for service '%s'", + id); + continue; + } - ui->service->blockSignals(true); - ui->service->clear(); + bool protocolRegistered = false; + for (uint32_t i = 0; i < protocols.size(); i++) { + protocolRegistered |= obs_is_output_protocol_registered( + QT_TO_UTF8(protocols[i])); + } - QStringList names; + if (!protocolRegistered) { + blog(LOG_WARNING, + "No registered protocol compatible with service '%s'", + id); + continue; + } - obs_property_t *services = obs_properties_get(props, "service"); - size_t services_count = obs_property_list_item_count(services); - for (size_t i = 0; i < services_count; i++) { - const char *name = obs_property_list_item_string(services, i); - names.push_back(name); - } + bool isUncommon = (flags & OBS_SERVICE_UNCOMMON) != 0; + bool isDeprecated = (flags & OBS_SERVICE_DEPRECATED) != 0; - if (showAll) - names.sort(Qt::CaseInsensitive); + QString name(obs_service_get_display_name(id)); + if (isDeprecated) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); - for (QString &name : names) - ui->service->addItem(name); + if (showAll || !(isUncommon || isDeprecated)) + ui->service->addItem(name, QT_UTF8(id)); - if (obs_is_output_protocol_registered("WHIP")) { - ui->service->addItem(QTStr("WHIP"), - QVariant((int)ListOpt::WHIP)); + if ((isUncommon || isDeprecated) && !showAll) + needShowAllOption = true; } - if (!showAll) { + if (needShowAllOption) { ui->service->addItem( QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); - } - - ui->service->insertItem( - 0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), - QVariant((int)ListOpt::Custom)); - - if (!lastService.isEmpty()) { - int idx = ui->service->findText(lastService); - if (idx != -1) - ui->service->setCurrentIndex(idx); + QVariant(SHOW_ALL)); } - obs_properties_destroy(props); + QSortFilterProxyModel *model = + new ServiceSortFilterProxyModel(ui->service); + model->setSourceModel(ui->service->model()); + // Combo's current model must be reparented, + // Otherwise QComboBox::setModel() will delete it + ui->service->model()->setParent(model); - ui->service->blockSignals(false); -} - -static inline bool is_auth_service(const std::string &service) -{ - return Auth::AuthType(service) != Auth::Type::None; -} + ui->service->setModel(model); -static inline bool is_external_oauth(const std::string &service) -{ - return Auth::External(service); + ui->service->model()->sort(0); } -static void reset_service_ui_fields(Ui::OBSBasicSettings *ui, - std::string &service, bool loading) +void OBSBasicSettings::on_service_currentIndexChanged(int) { - bool external_oauth = is_external_oauth(service); - if (external_oauth) { - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(true); - ui->useStreamKeyAdv->setVisible(true); - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && (!loading || key.isEmpty()) - ? (int)Section::Connect - : (int)Section::StreamKey; - - ui->streamStackWidget->setCurrentIndex(page); - ui->streamKeyWidget->setVisible(true); - ui->streamKeyLabel->setVisible(true); - ui->connectAccount2->setVisible(can_auth); - ui->useStreamKeyAdv->setVisible(false); - } else { - ui->connectAccount2->setVisible(false); - ui->useStreamKeyAdv->setVisible(false); - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} + ui->service->setPlaceholderText(""); -#ifdef YOUTUBE_ENABLED -static void get_yt_ch_title(Ui::OBSBasicSettings *ui) -{ - const char *name = config_get_string(OBSBasic::Get()->Config(), - "YouTube", "ChannelName"); - if (name) { - ui->connectedAccountText->setText(name); - } else { - // if we still not changed the service page - if (IsYouTubeService(QT_TO_UTF8(ui->service->currentText()))) { - ui->connectedAccountText->setText( - QTStr("Auth.LoadingChannel.Error")); - } - } -} -#endif - -void OBSBasicSettings::UseStreamKeyAdvClicked() -{ - ui->streamKeyWidget->setVisible(true); -} - -void OBSBasicSettings::on_service_currentIndexChanged(int idx) -{ - if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) { + if (ui->service->currentData().toInt() == SHOW_ALL) { LoadServices(true); ui->service->showPopup(); return; } - ServiceChanged(); - - UpdateMoreInfoLink(); - UpdateServerList(); - UpdateKeyLink(); - UpdateServiceRecommendations(); - - UpdateVodTrackSetting(); - - protocol = FindProtocol(); - UpdateAdvNetworkGroup(); - - if (ServiceSupportsCodecCheck() && UpdateResFPSLimits()) { - lastServiceIdx = idx; - if (idx == 0) - lastCustomServer = ui->customServer->text(); - } -} - -void OBSBasicSettings::on_customServer_textChanged(const QString &) -{ - UpdateKeyLink(); - - protocol = FindProtocol(); - UpdateAdvNetworkGroup(); - - if (ServiceSupportsCodecCheck()) - lastCustomServer = ui->customServer->text(); -} - -void OBSBasicSettings::ServiceChanged() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool custom = IsCustomService(); - bool whip = IsWHIP(); - - ui->disconnectAccount->setVisible(false); - ui->bandwidthTestEnable->setVisible(false); - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - - if (lastService != service.c_str()) { - reset_service_ui_fields(ui.get(), service, loading); - } - - ui->useAuth->setVisible(custom); - ui->authUsernameLabel->setVisible(custom); - ui->authUsername->setVisible(custom); - ui->authPwLabel->setVisible(custom); - ui->authPwWidget->setVisible(custom); - - if (custom || whip) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, - ui->serverStackedWidget); - - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - on_useAuth_toggled(); - } else { - ui->serverStackedWidget->setCurrentIndex(0); - } - - auth.reset(); - - if (!main->auth) { - return; - } - - auto system_auth_service = main->auth->service(); - bool service_check = service.find(system_auth_service) != - std::string::npos; -#ifdef YOUTUBE_ENABLED - service_check = service_check ? service_check - : IsYouTubeService(system_auth_service) && - IsYouTubeService(service); -#endif - if (service_check) { - auth = main->auth; - OnAuthConnected(); - } -} - -QString OBSBasicSettings::FindProtocol() -{ - if (IsCustomService()) { - if (ui->customServer->text().isEmpty()) - return QString("RTMP"); - - QString server = ui->customServer->text(); - - if (obs_is_output_protocol_registered("RTMPS") && - server.startsWith("rtmps://")) - return QString("RTMPS"); - - if (server.startsWith("ftl://")) - return QString("FTL"); - - if (server.startsWith("srt://")) - return QString("SRT"); - - if (server.startsWith("rist://")) - return QString("RIST"); - - } else { - obs_properties_t *props = - obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", - QT_TO_UTF8(ui->service->currentText())); - obs_property_modified(services, settings); - - obs_properties_destroy(props); - - const char *protocol = - obs_data_get_string(settings, "protocol"); - if (protocol && *protocol) - return QT_UTF8(protocol); - } - - return QString("RTMP"); -} - -void OBSBasicSettings::UpdateServerList() -{ - QString serviceName = ui->service->currentText(); - - lastService = serviceName; - - obs_properties_t *props = obs_get_service_properties("rtmp_common"); - obs_property_t *services = obs_properties_get(props, "service"); - - OBSDataAutoRelease settings = obs_data_create(); - - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); - - obs_property_t *servers = obs_properties_get(props, "server"); - - ui->server->clear(); - - size_t servers_count = obs_property_list_item_count(servers); - for (size_t i = 0; i < servers_count; i++) { - const char *name = obs_property_list_item_name(servers, i); - const char *server = obs_property_list_item_string(servers, i); - ui->server->addItem(name, server); - } - - obs_properties_destroy(props); -} - -void OBSBasicSettings::on_show_clicked() -{ - if (ui->key->echoMode() == QLineEdit::Password) { - ui->key->setEchoMode(QLineEdit::Normal); - ui->show->setText(QTStr("Hide")); - } else { - ui->key->setEchoMode(QLineEdit::Password); - ui->show->setText(QTStr("Show")); - } -} - -void OBSBasicSettings::on_authPwShow_clicked() -{ - if (ui->authPw->echoMode() == QLineEdit::Password) { - ui->authPw->setEchoMode(QLineEdit::Normal); - ui->authPwShow->setText(QTStr("Hide")); - } else { - ui->authPw->setEchoMode(QLineEdit::Password); - ui->authPwShow->setText(QTStr("Show")); - } -} - -OBSService OBSBasicSettings::SpawnTempService() -{ - bool custom = IsCustomService(); - bool whip = IsWHIP(); - const char *service_id = "rtmp_common"; - - if (custom) { - service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; - } - - OBSDataAutoRelease settings = obs_data_create(); - - if (!custom && !whip) { - obs_data_set_string(settings, "service", - QT_TO_UTF8(ui->service->currentText())); - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); - } else { - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->customServer->text().trimmed())); - } - - if (whip) - obs_data_set_string(settings, "bearer_token", - QT_TO_UTF8(ui->key->text())); - else - obs_data_set_string(settings, "key", - QT_TO_UTF8(ui->key->text())); - - OBSServiceAutoRelease newService = obs_service_create( - service_id, "temp_service", settings, nullptr); - return newService.Get(); -} - -void OBSBasicSettings::OnOAuthStreamKeyConnected() -{ - OAuthStreamKey *a = reinterpret_cast(auth.get()); - - if (a) { - bool validKey = !a->key().empty(); - - if (validKey) - ui->key->setText(QT_UTF8(a->key().c_str())); - - ui->streamKeyWidget->setVisible(false); - ui->streamKeyLabel->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(true); - ui->useStreamKeyAdv->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - - if (strcmp(a->service(), "Twitch") == 0) { - ui->bandwidthTestEnable->setVisible(true); - ui->twitchAddonLabel->setVisible(true); - ui->twitchAddonDropdown->setVisible(true); - } else { - ui->bandwidthTestEnable->setChecked(false); + OBSServiceAutoRelease oldService = + obs_service_get_ref(tempService.Get()); + + QString service = ui->service->currentData().toString(); + OBSDataAutoRelease newSettings = + obs_service_defaults(QT_TO_UTF8(service)); + tempService = obs_service_create_private( + QT_TO_UTF8(service), TEMP_SERVICE_NAME, newSettings); + + bool cancelChange = false; + if (!obs_service_get_protocol(tempService)) { + /* + * Cancel the change if the service happen to be without default protocol. + * + * This is better than generating dozens of obs_service_t to check + * if there is a default protocol while filling the combo box. + */ + OBSMessageBox::warning( + this, + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Title"), + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Text") + .arg(ui->service->currentText())); + cancelChange = true; + } + + if (cancelChange || + !(ServiceSupportsCodecCheck() && UpdateResFPSLimits())) { + tempService = obs_service_get_ref(oldService); + const char *id = obs_service_get_id(tempService); + uint32_t flags = obs_get_service_flags(id); + if ((flags & OBS_SERVICE_INTERNAL) != 0) { + QString name(obs_service_get_display_name(id)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); + + ui->service->setPlaceholderText(name); } -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(a->service())) { - ui->key->clear(); - - ui->connectedAccountLabel->setVisible(true); - ui->connectedAccountText->setVisible(true); - ui->connectedAccountText->setText( - QTStr("Auth.LoadingChannel.Title")); + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex( + ui->service->findData(QT_UTF8(id))); - get_yt_ch_title(ui.get()); - } -#endif - } - - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); -} - -void OBSBasicSettings::OnAuthConnected() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - Auth::Type type = Auth::AuthType(service); - - if (type == Auth::Type::OAuth_StreamKey || - type == Auth::Type::OAuth_LinkedAccount) { - OnOAuthStreamKeyConnected(); - } - - if (!loading) { - stream1Changed = true; - EnableApplyButton(true); - } -} - -void OBSBasicSettings::on_connectAccount_clicked() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - - OAuth::DeleteCookies(service); - - auth = OAuthStreamKey::Login(this, service); - if (!!auth) { - OnAuthConnected(); - - ui->useStreamKeyAdv->setVisible(false); - } -} - -#define DISCONNECT_COMFIRM_TITLE \ - "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title" -#define DISCONNECT_COMFIRM_TEXT \ - "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text" - -void OBSBasicSettings::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), - QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { return; } - main->auth.reset(); - auth.reset(); - main->SetBroadcastFlowEnabled(false); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif + ui->ignoreRecommended->setEnabled(!IsCustomOrInternalService()); - ui->bandwidthTestEnable->setChecked(false); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(nullptr, true); + ui->serviceLayout->addWidget(streamServiceProps); - reset_service_ui_fields(ui.get(), service, loading); - - ui->bandwidthTestEnable->setVisible(false); - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); -} - -void OBSBasicSettings::on_useStreamKey_clicked() -{ - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); -} - -void OBSBasicSettings::on_useAuth_toggled() -{ - if (!IsCustomService()) - return; - - bool use_auth = ui->useAuth->isChecked(); + UpdateServiceRecommendations(); - ui->authUsernameLabel->setVisible(use_auth); - ui->authUsername->setVisible(use_auth); - ui->authPwLabel->setVisible(use_auth); - ui->authPwWidget->setVisible(use_auth); + UpdateVodTrackSetting(); + UpdateAdvNetworkGroup(); } void OBSBasicSettings::UpdateVodTrackSetting() { bool enableForCustomServer = config_get_bool( GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); - bool enableVodTrack = ui->service->currentText() == "Twitch"; + bool enableVodTrack = obs_service_get_audio_track_cap(tempService) == + OBS_SERVICE_AUDIO_ARCHIVE_TRACK; bool wasEnabled = !!vodTrackCheckbox; - if (enableForCustomServer && IsCustomService()) + if (enableForCustomServer && IsCustomOrInternalService()) enableVodTrack = true; if (enableVodTrack == wasEnabled) @@ -919,28 +347,66 @@ void OBSBasicSettings::UpdateVodTrackSetting() } } -OBSService OBSBasicSettings::GetStream1Service() -{ - return stream1Changed ? SpawnTempService() - : OBSService(main->GetService()); -} +extern const char *get_simple_output_encoder(const char *name); void OBSBasicSettings::UpdateServiceRecommendations() { - bool customServer = IsCustomService(); - ui->ignoreRecommended->setVisible(!customServer); - ui->enforceSettingsLabel->setVisible(!customServer); - - OBSService service = GetStream1Service(); - - int vbitrate, abitrate; + bool simple = (ui->outputMode->currentIndex() == 0); + QString vcodec, acodec; + int vbitrate = 0, abitrate; BPtr res_list; size_t res_count; - int fps; + bool res_with_fps = false; + obs_service_resolution *best_res = nullptr; + int fps = 0; - obs_service_get_max_bitrate(service, &vbitrate, &abitrate); - obs_service_get_supported_resolutions(service, &res_list, &res_count); - obs_service_get_max_fps(service, &fps); + obs_service_get_supported_resolutions2(tempService, &res_list, + &res_count, &res_with_fps); + + if (res_count) { + int best_res_pixels = 0; + + for (size_t i = 0; i < res_count; i++) { + obs_service_resolution *res = &res_list[i]; + int res_pixels = res->cx + res->cy; + if (res_pixels > best_res_pixels || + (res_pixels == best_res_pixels && + res->fps > best_res->fps)) { + best_res = res; + best_res_pixels = res_pixels; + } + } + + if (res_with_fps) + fps = best_res->fps; + } + + if (!res_with_fps) + obs_service_get_max_fps(tempService, &fps); + + if (simple) { + QString encoder = + ui->simpleOutStrEncoder->currentData().toString(); + const char *id = get_simple_output_encoder(QT_TO_UTF8(encoder)); + vcodec = obs_get_encoder_codec(id); + acodec = ui->simpleOutStrAEncoder->currentData().toString(); + } else { + QString vencoder = ui->advOutEncoder->currentData().toString(); + QString aencoder = ui->advOutAEncoder->currentData().toString(); + vcodec = obs_get_encoder_codec(QT_TO_UTF8(vencoder)); + acodec = obs_get_encoder_codec(QT_TO_UTF8(aencoder)); + } + + if (best_res) + vbitrate = obs_service_get_max_video_bitrate( + tempService, QT_TO_UTF8(vcodec), *best_res); + + if (!vbitrate) + vbitrate = obs_service_get_max_codec_bitrate( + tempService, QT_TO_UTF8(vcodec)); + + abitrate = obs_service_get_max_codec_bitrate(tempService, + QT_TO_UTF8(acodec)); QString text; @@ -954,25 +420,13 @@ void OBSBasicSettings::UpdateServiceRecommendations() text += ENFORCE_TEXT("MaxAudioBitrate") .arg(QString::number(abitrate)); } - if (res_count) { + if (best_res) { if (!text.isEmpty()) text += "
"; - obs_service_resolution best_res = {}; - int best_res_pixels = 0; - - for (size_t i = 0; i < res_count; i++) { - obs_service_resolution res = res_list[i]; - int res_pixels = res.cx + res.cy; - if (res_pixels > best_res_pixels) { - best_res = res; - best_res_pixels = res_pixels; - } - } - QString res_str = - QString("%1x%2").arg(QString::number(best_res.cx), - QString::number(best_res.cy)); + QString("%1x%2").arg(QString::number(best_res->cx), + QString::number(best_res->cy)); text += ENFORCE_TEXT("MaxResolution").arg(res_str); } if (fps) { @@ -983,25 +437,13 @@ void OBSBasicSettings::UpdateServiceRecommendations() } #undef ENFORCE_TEXT -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(QT_TO_UTF8(ui->service->currentText()))) { - if (!text.isEmpty()) - text += "

"; - - text += "" - "YouTube Terms of Service
" - "" - "Google Privacy Policy
" - "" - "Google Third-Party Permissions"; - } -#endif + ui->enforceSettings->setVisible(!text.isEmpty()); ui->enforceSettingsLabel->setText(text); } void OBSBasicSettings::DisplayEnforceWarning(bool checked) { - if (IsCustomService()) + if (IsCustomOrInternalService()) return; if (!checked) { @@ -1101,13 +543,14 @@ bool OBSBasicSettings::UpdateResFPSLimits() bool ignoreRecommended = ui->ignoreRecommended->isChecked(); BPtr res_list; size_t res_count = 0; + bool res_with_fps = false; int max_fps = 0; - if (!IsCustomService() && !ignoreRecommended) { - OBSService service = GetStream1Service(); - obs_service_get_supported_resolutions(service, &res_list, - &res_count); - obs_service_get_max_fps(service, &max_fps); + if (!ignoreRecommended) { + obs_service_get_supported_resolutions2( + tempService, &res_list, &res_count, &res_with_fps); + if (!res_with_fps) + obs_service_get_max_fps(tempService, &max_fps); } /* ------------------------------------ */ @@ -1125,6 +568,19 @@ bool OBSBasicSettings::UpdateResFPSLimits() if (res_count) set_closest_res(cx, cy, res_list, res_count); + if (res_count && res_with_fps) { + for (size_t i = 0; i < res_count; i++) { + { + if (res_list[i].cx != cx || + res_list[i].cy != cy) + continue; + + if (res_list[i].fps > max_fps) + max_fps = res_list[i].fps; + } + } + } + if (max_fps) { int fpsType = ui->fpsType->currentIndex(); @@ -1167,16 +623,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() bool valid = ResFPSValid(res_list, res_count, max_fps); if (!valid) { - /* if the user was already on facebook with an incompatible - * resolution, assume it's an upgrade */ - if (lastServiceIdx == -1 && lastIgnoreRecommended == -1) { - ui->ignoreRecommended->setChecked(true); - ui->ignoreRecommended->setProperty("changed", true); - stream1Changed = true; - EnableApplyButton(true); - return UpdateResFPSLimits(); - } - QMessageBox::StandardButton button; #define WARNING_VAL(x) \ @@ -1196,16 +642,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() #undef WARNING_VAL if (button == QMessageBox::No) { - if (idx != lastServiceIdx) - QMetaObject::invokeMethod( - ui->service, "setCurrentIndex", - Qt::QueuedConnection, - Q_ARG(int, lastServiceIdx)); - else - QMetaObject::invokeMethod(ui->ignoreRecommended, - "setChecked", - Qt::QueuedConnection, - Q_ARG(bool, true)); return false; } } @@ -1286,8 +722,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() /* ------------------------------------ */ - lastIgnoreRecommended = (int)ignoreRecommended; - return true; } @@ -1306,7 +740,6 @@ static bool service_supports_codec(const char **codecs, const char *codec) } extern bool EncoderAvailable(const char *encoder); -extern const char *get_simple_output_encoder(const char *name); static inline bool service_supports_encoder(const char **codecs, const char *encoder) @@ -1343,15 +776,16 @@ bool OBSBasicSettings::ServiceAndVCodecCompatible() codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); } - OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_video_codecs(service); + const char **codecs = + obs_service_get_supported_video_codecs(tempService); - if (!codecs || IsCustomService()) { + if (!codecs) { const char *output; char **output_codecs; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol( + obs_service_get_protocol(tempService), &output, + return_first_id); output_codecs = strlist_split( obs_get_output_supported_video_codecs(output), ';', @@ -1382,15 +816,16 @@ bool OBSBasicSettings::ServiceAndACodecCompatible() codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); } - OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_audio_codecs(service); + const char **codecs = + obs_service_get_supported_audio_codecs(tempService); - if (!codecs || IsCustomService()) { + if (!codecs) { const char *output; char **output_codecs; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol( + obs_service_get_protocol(tempService), &output, + return_first_id); output_codecs = strlist_split( obs_get_output_supported_audio_codecs(output), ';', false); @@ -1458,9 +893,7 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() bool acodec_compat = ServiceAndACodecCompatible(); if (vcodec_compat && acodec_compat) { - if (lastServiceIdx != ui->service->currentIndex() || - IsCustomService()) - ResetEncoders(true); + ResetEncoders(true); return true; } @@ -1527,17 +960,6 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() #undef WARNING_VAL if (button == QMessageBox::No) { - if (lastServiceIdx == 0 && - lastServiceIdx == ui->service->currentIndex()) - QMetaObject::invokeMethod(ui->customServer, "setText", - Qt::QueuedConnection, - Q_ARG(QString, - lastCustomServer)); - else - QMetaObject::invokeMethod(ui->service, - "setCurrentIndex", - Qt::QueuedConnection, - Q_ARG(int, lastServiceIdx)); return false; } @@ -1556,30 +978,32 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) QString lastAdvAudioEnc = ui->advOutAEncoder->currentData().toString(); QString lastAudioEnc = ui->simpleOutStrAEncoder->currentData().toString(); - OBSService service = SpawnTempService(); - const char **vcodecs = obs_service_get_supported_video_codecs(service); - const char **acodecs = obs_service_get_supported_audio_codecs(service); + const char *protocol = obs_service_get_protocol(tempService); + const char **vcodecs = + obs_service_get_supported_video_codecs(tempService); + const char **acodecs = + obs_service_get_supported_audio_codecs(tempService); const char *type; BPtr output_vcodecs; BPtr output_acodecs; size_t idx = 0; - if (!vcodecs || IsCustomService()) { + if (!vcodecs) { const char *output; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol(protocol, &output, + return_first_id); output_vcodecs = strlist_split( obs_get_output_supported_video_codecs(output), ';', false); vcodecs = (const char **)output_vcodecs.Get(); } - if (!acodecs || IsCustomService()) { + if (!acodecs) { const char *output; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol(protocol, &output, + return_first_id); output_acodecs = strlist_split( obs_get_output_supported_audio_codecs(output), ';', false); @@ -1790,3 +1214,62 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) ui->simpleOutStrAEncoder->setCurrentIndex(idx); } } + +OBSPropertiesView * +OBSBasicSettings::CreateTempServicePropertyView(obs_data_t *settings, + bool changed) +{ + OBSDataAutoRelease defaultSettings = + obs_service_defaults(obs_service_get_id(tempService)); + OBSPropertiesView *view; + + view = new OBSPropertiesView( + settings ? settings : defaultSettings.Get(), tempService, + (PropertiesReloadCallback)obs_service_properties, nullptr, + nullptr, 170); + view->setFrameShape(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + view->setProperty("changed", QVariant(changed)); + /* NOTE: Stream1Changed is implemented inside ServicePropertyViewChanged, + * in case the settings are reverted. */ + QObject::connect(view, &OBSPropertiesView::Changed, this, + &OBSBasicSettings::ServicePropertyViewChanged); + + return view; +} + +void OBSBasicSettings::ServicePropertyViewChanged() +{ + OBSDataAutoRelease settings = obs_service_get_settings(tempService); + QString oldSettingsJson = QT_UTF8(obs_data_get_json(settings)); + obs_service_update(tempService, streamServiceProps->GetSettings()); + + if (!(ServiceSupportsCodecCheck() && UpdateResFPSLimits())) { + QMetaObject::invokeMethod(this, "RestoreServiceSettings", + Qt::QueuedConnection, + Q_ARG(QString, oldSettingsJson)); + return; + } + + UpdateVodTrackSetting(); + UpdateAdvNetworkGroup(); + + if (!loading) { + stream1Changed = true; + streamServiceProps->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::RestoreServiceSettings(QString settingsJson) +{ + OBSDataAutoRelease settings = + obs_data_create_from_json(QT_TO_UTF8(settingsJson)); + obs_service_update(tempService, settings); + + bool changed = streamServiceProps->property("changed").toBool(); + + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(settings, changed); + ui->serviceLayout->addWidget(streamServiceProps); +} diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index f29e92e5060b31..56232ee0a9f9ae 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -473,14 +473,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->multiviewDrawAreas, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); @@ -1009,9 +1001,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->advOutRecRescale->lineEdit()->setValidator(validator); ui->advOutFFRescale->lineEdit()->setValidator(validator); - connect(ui->useStreamKeyAdv, SIGNAL(clicked()), this, - SLOT(UseStreamKeyAdvClicked())); - connect(ui->simpleOutStrAEncoder, SIGNAL(currentIndexChanged(int)), this, SLOT(SimpleStreamAudioEncoderChanged())); connect(ui->advOutAEncoder, SIGNAL(currentIndexChanged(int)), this, @@ -2513,6 +2502,9 @@ void OBSBasicSettings::LoadOutputSettings() ui->advNetworkGroupBox->setEnabled(false); } + /* Services side but requires to be done once encoders are loaded */ + UpdateServiceRecommendations(); + loading = false; } @@ -4375,6 +4367,9 @@ void OBSBasicSettings::on_advOutEncoder_currentIndexChanged() ui->advOutUseRescale->setVisible(true); ui->advOutRescale->setVisible(true); + + /* Update services page test if codec has changed */ + UpdateServiceRecommendations(); } void OBSBasicSettings::on_advOutRecEncoder_currentIndexChanged(int idx) @@ -4539,12 +4534,6 @@ bool OBSBasicSettings::AskIfCanCloseSettings() if (!Changed() || QueryChanges()) canCloseSettings = true; - if (forceAuthReload) { - main->auth->Save(); - main->auth->Load(); - forceAuthReload = false; - } - if (forceUpdateCheck) { main->CheckForUpdates(false); forceUpdateCheck = false; @@ -5517,6 +5506,9 @@ void OBSBasicSettings::SimpleStreamingEncoderChanged() idx = ui->simpleOutPreset->findData(QVariant(defaultPreset)); ui->simpleOutPreset->setCurrentIndex(idx); + + /* Update services page test if codec has changed */ + UpdateServiceRecommendations(); } #define ESTIMATE_STR "Basic.Settings.Output.ReplayBuffer.Estimate" @@ -5804,11 +5796,10 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() QString qual = ui->simpleOutRecQuality->currentData().toString(); QString warning; bool enforceBitrate = !ui->ignoreRecommended->isChecked(); - OBSService service = GetStream1Service(); delete simpleOutRecWarning; - if (enforceBitrate && service) { + if (enforceBitrate && tempService) { OBSDataAutoRelease videoSettings = obs_data_create(); OBSDataAutoRelease audioSettings = obs_data_create(); int oldVBitrate = ui->simpleOutputVBitrate->value(); @@ -5817,8 +5808,31 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() obs_data_set_int(videoSettings, "bitrate", oldVBitrate); obs_data_set_int(audioSettings, "bitrate", oldABitrate); - obs_service_apply_encoder_settings(service, videoSettings, - audioSettings); + QString streamEnc = + ui->simpleOutStrEncoder->currentData().toString(); + + obs_service_apply_encoder_settings2( + tempService, + get_simple_output_encoder(QT_TO_UTF8(streamEnc)), + videoSettings); + + const char *aencoder = EncoderAvailable("ffmpeg_opus") + ? "ffmpeg_opus" + : nullptr; + if (ui->simpleOutStrAEncoder->currentData().toString() == + "aac") { + aencoder = nullptr; + if (EncoderAvailable("CoreAudio_AAC")) { + aencoder = "CoreAudio_AAC"; + } else if (EncoderAvailable("libfdk_aac")) { + aencoder = "libfdk_aac"; + } else if (EncoderAvailable("ffmpeg_aac")) { + aencoder = "ffmpeg_aac"; + } + } + + obs_service_apply_encoder_settings2(tempService, aencoder, + audioSettings); int newVBitrate = obs_data_get_int(videoSettings, "bitrate"); int newABitrate = obs_data_get_int(audioSettings, "bitrate"); @@ -6228,7 +6242,8 @@ void OBSBasicSettings::RecreateOutputResolutionWidget() void OBSBasicSettings::UpdateAdvNetworkGroup() { - bool enabled = protocol.contains("RTMP"); + bool enabled = + QT_UTF8(obs_service_get_protocol(tempService)).contains("RTMP"); ui->advNetworkDisabled->setVisible(!enabled); diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index ac99da9deaf7f1..466c471575fe6a 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -28,8 +28,6 @@ #include -#include "auth-base.hpp" - class OBSBasic; class QAbstractButton; class QRadioButton; @@ -107,8 +105,6 @@ class OBSBasicSettings : public QDialog { std::unique_ptr ui; - std::shared_ptr auth; - bool generalChanged = false; bool stream1Changed = false; bool outputsChanged = false; @@ -119,7 +115,6 @@ class OBSBasicSettings : public QDialog { bool advancedChanged = false; int pageIndex = 0; bool loading = true; - bool forceAuthReload = false; bool forceUpdateCheck = false; std::string savedTheme; int sampleRateIndex = 0; @@ -128,8 +123,6 @@ class OBSBasicSettings : public QDialog { bool hotkeysLoaded = false; int lastSimpleRecQualityIdx = 0; - int lastServiceIdx = -1; - int lastIgnoreRecommended = -1; int lastChannelSetupIdx = 0; static constexpr uint32_t ENCODER_HIDE_FLAGS = @@ -137,7 +130,8 @@ class OBSBasicSettings : public QDialog { OBSFFFormatDesc formats; - OBSPropertiesView *streamProperties = nullptr; + OBSPropertiesView *streamServiceProps = nullptr; + OBSPropertiesView *streamEncoderProps = nullptr; OBSPropertiesView *recordEncoderProps = nullptr; @@ -256,37 +250,29 @@ class OBSBasicSettings : public QDialog { void LoadBranchesList(); /* stream */ + OBSServiceAutoRelease tempService; + void InitStreamPage(); - inline bool IsCustomService() const; - inline bool IsWHIP() const; + inline bool IsCustomOrInternalService() const; void LoadServices(bool showAll); - void OnOAuthStreamKeyConnected(); - void OnAuthConnected(); - QString lastService; - QString protocol; - QString lastCustomServer; int prevLangIndex; bool prevBrowserAccel; - void ServiceChanged(); - QString FindProtocol(); - void UpdateServerList(); void UpdateKeyLink(); void UpdateVodTrackSetting(); void UpdateServiceRecommendations(); - void UpdateMoreInfoLink(); void UpdateAdvNetworkGroup(); + OBSPropertiesView *CreateTempServicePropertyView(obs_data_t *settings, + bool changed = false); + private slots: void RecreateOutputResolutionWidget(); bool UpdateResFPSLimits(); void DisplayEnforceWarning(bool checked); - void on_show_clicked(); - void on_authPwShow_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); - void on_useAuth_toggled(); + + void ServicePropertyViewChanged(); + void RestoreServiceSettings(QString settingsJson); void on_hotkeyFilterReset_clicked(); void on_hotkeyFilterSearch_textChanged(const QString text); @@ -379,8 +365,6 @@ private slots: int SimpleOutGetSelectedAudioTracks(); int AdvOutGetSelectedAudioTracks(); - OBSService GetStream1Service(); - bool ServiceAndVCodecCompatible(); bool ServiceAndACodecCompatible(); bool ServiceSupportsCodecCheck(); @@ -392,7 +376,6 @@ private slots: void on_buttonBox_clicked(QAbstractButton *button); void on_service_currentIndexChanged(int idx); - void on_customServer_textChanged(const QString &text); void on_simpleOutputBrowse_clicked(); void on_advOutRecPathBrowse_clicked(); void on_advOutFFPathBrowse_clicked(); @@ -461,8 +444,6 @@ private slots: void SimpleStreamingEncoderChanged(); - OBSService SpawnTempService(); - void SetGeneralIcon(const QIcon &icon); void SetStreamIcon(const QIcon &icon); void SetOutputIcon(const QIcon &icon); @@ -472,8 +453,6 @@ private slots: void SetAccessibilityIcon(const QIcon &icon); void SetAdvancedIcon(const QIcon &icon); - void UseStreamKeyAdvClicked(); - void SimpleStreamAudioEncoderChanged(); void AdvAudioEncodersChanged(); diff --git a/UI/window-youtube-actions.cpp b/UI/window-youtube-actions.cpp deleted file mode 100644 index 1f99ac02cf8b72..00000000000000 --- a/UI/window-youtube-actions.cpp +++ /dev/null @@ -1,861 +0,0 @@ -#include "window-basic-main.hpp" -#include "window-youtube-actions.hpp" - -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "youtube-api-wrappers.hpp" - -#include -#include -#include -#include -#include -#include - -const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'"; -const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m"; -const QString IndexOfGamingCategory = "20"; - -OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, - bool broadcastReady) - : QDialog(parent), - ui(new Ui::OBSYoutubeActions), - apiYouTube(dynamic_cast(auth)), - workerThread(new WorkerThread(apiYouTube)), - broadcastReady(broadcastReady) -{ - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - ui->setupUi(this); - - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Public"), - "public"); - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Unlisted"), - "unlisted"); - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Private"), - "private"); - - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Normal"), - "normal"); - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Low"), "low"); - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.UltraLow"), - "ultraLow"); - - UpdateOkButtonStatus(); - - connect(ui->title, &QLineEdit::textChanged, this, - [&](const QString &) { this->UpdateOkButtonStatus(); }); - connect(ui->privacyBox, &QComboBox::currentTextChanged, this, - [&](const QString &) { this->UpdateOkButtonStatus(); }); - connect(ui->yesMakeForKids, &QRadioButton::toggled, this, - [&](bool) { this->UpdateOkButtonStatus(); }); - connect(ui->notMakeForKids, &QRadioButton::toggled, this, - [&](bool) { this->UpdateOkButtonStatus(); }); - connect(ui->tabWidget, &QTabWidget::currentChanged, this, - [&](int) { this->UpdateOkButtonStatus(); }); - connect(ui->pushButton, &QPushButton::clicked, this, - &OBSYoutubeActions::OpenYouTubeDashboard); - - connect(ui->helpAutoStartStop, &QLabel::linkActivated, this, - [](const QString &) { - QToolTip::showText( - QCursor::pos(), - QTStr("YouTube.Actions.AutoStartStop.TT")); - }); - connect(ui->help360Video, &QLabel::linkActivated, this, - [](const QString &link) { QDesktopServices::openUrl(link); }); - connect(ui->helpMadeForKids, &QLabel::linkActivated, this, - [](const QString &link) { QDesktopServices::openUrl(link); }); - - ui->scheduledTime->setVisible(false); - connect(ui->checkScheduledLater, &QCheckBox::stateChanged, this, - [&](int state) { - ui->scheduledTime->setVisible(state); - if (state) { - ui->checkAutoStart->setVisible(true); - ui->checkAutoStop->setVisible(true); - ui->helpAutoStartStop->setVisible(true); - - ui->checkAutoStart->setChecked(false); - ui->checkAutoStop->setChecked(false); - } else { - ui->checkAutoStart->setVisible(false); - ui->checkAutoStop->setVisible(false); - ui->helpAutoStartStop->setVisible(false); - - ui->checkAutoStart->setChecked(true); - ui->checkAutoStop->setChecked(true); - } - UpdateOkButtonStatus(); - }); - - ui->checkAutoStart->setVisible(false); - ui->checkAutoStop->setVisible(false); - ui->helpAutoStartStop->setVisible(false); - - ui->scheduledTime->setDateTime(QDateTime::currentDateTime()); - - auto thumbSelectionHandler = [&]() { - if (thumbnailFile.isEmpty()) { - QString filePath = OpenFile( - this, - QTStr("YouTube.Actions.Thumbnail.SelectFile"), - QStandardPaths::writableLocation( - QStandardPaths::PicturesLocation), - QString("Images (*.png *.jpg *.jpeg *.gif)")); - - if (!filePath.isEmpty()) { - QFileInfo tFile(filePath); - if (!tFile.exists()) { - return ShowErrorDialog( - this, - QTStr("YouTube.Actions.Error.FileMissing")); - } else if (tFile.size() > 2 * 1024 * 1024) { - return ShowErrorDialog( - this, - QTStr("YouTube.Actions.Error.FileTooLarge")); - } - - thumbnailFile = filePath; - ui->selectedFileName->setText(thumbnailFile); - ui->selectFileButton->setText(QTStr( - "YouTube.Actions.Thumbnail.ClearFile")); - - QImageReader imgReader(filePath); - imgReader.setAutoTransform(true); - const QImage newImage = imgReader.read(); - ui->thumbnailPreview->setPixmap( - QPixmap::fromImage(newImage).scaled( - 160, 90, Qt::KeepAspectRatio, - Qt::SmoothTransformation)); - } - } else { - thumbnailFile.clear(); - ui->selectedFileName->setText(QTStr( - "YouTube.Actions.Thumbnail.NoFileSelected")); - ui->selectFileButton->setText( - QTStr("YouTube.Actions.Thumbnail.SelectFile")); - ui->thumbnailPreview->setPixmap( - GetPlaceholder().pixmap(QSize(16, 16))); - } - }; - - connect(ui->selectFileButton, &QPushButton::clicked, this, - thumbSelectionHandler); - connect(ui->thumbnailPreview, &ClickableLabel::clicked, this, - thumbSelectionHandler); - - if (!apiYouTube) { - blog(LOG_DEBUG, "YouTube API auth NOT found."); - Cancel(); - return; - } - - const char *name = config_get_string(OBSBasic::Get()->Config(), - "YouTube", "ChannelName"); - this->setWindowTitle(QTStr("YouTube.Actions.WindowTitle").arg(name)); - - QVector category_list; - if (!apiYouTube->GetVideoCategoriesList(category_list)) { - ShowErrorDialog( - parent, - apiYouTube->GetLastError().isEmpty() - ? QTStr("YouTube.Actions.Error.General") - : QTStr("YouTube.Actions.Error.Text") - .arg(apiYouTube->GetLastError())); - Cancel(); - return; - } - for (auto &category : category_list) { - ui->categoryBox->addItem(category.title, category.id); - if (category.id == IndexOfGamingCategory) { - ui->categoryBox->setCurrentText(category.title); - } - } - - connect(ui->okButton, &QPushButton::clicked, this, - &OBSYoutubeActions::InitBroadcast); - connect(ui->saveButton, &QPushButton::clicked, this, - &OBSYoutubeActions::ReadyBroadcast); - connect(ui->cancelButton, &QPushButton::clicked, this, [&]() { - blog(LOG_DEBUG, "YouTube live broadcast creation cancelled."); - // Close the dialog. - Cancel(); - }); - - qDeleteAll(ui->scrollAreaWidgetContents->findChildren( - QString(), Qt::FindDirectChildrenOnly)); - - // Add label indicating loading state - QLabel *loadingLabel = new QLabel(); - loadingLabel->setTextFormat(Qt::RichText); - loadingLabel->setAlignment(Qt::AlignHCenter); - loadingLabel->setText( - QString("%1") - .arg(QTStr("YouTube.Actions.EventsLoading"))); - ui->scrollAreaWidgetContents->layout()->addWidget(loadingLabel); - - // Delete "loading..." label on completion - connect(workerThread, &WorkerThread::finished, this, [&] { - QLayoutItem *item = - ui->scrollAreaWidgetContents->layout()->takeAt(0); - item->widget()->deleteLater(); - }); - - connect(workerThread, &WorkerThread::failed, this, [&]() { - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.Text") - .arg(last_error); - - ShowErrorDialog(this, last_error); - QDialog::reject(); - }); - - connect(workerThread, &WorkerThread::new_item, this, - [&](const QString &title, const QString &dateTimeString, - const QString &broadcast, const QString &status, - bool astart, bool astop) { - ClickableLabel *label = new ClickableLabel(); - label->setTextFormat(Qt::RichText); - - if (status == "live" || status == "testing") { - // Resumable stream - label->setText( - QString("%1
%2") - .arg(title, - QTStr("YouTube.Actions.Stream.Resume"))); - - } else if (dateTimeString.isEmpty()) { - // The broadcast created by YouTube Studio has no start time. - // Yes this does violate the restrictions set in YouTube's API - // But why would YouTube care about consistency? - label->setText( - QString("%1
%2") - .arg(title, - QTStr("YouTube.Actions.Stream.YTStudio"))); - } else { - label->setText( - QString("%1
%2") - .arg(title, - QTStr("YouTube.Actions.Stream.ScheduledFor") - .arg(dateTimeString))); - } - - label->setAlignment(Qt::AlignHCenter); - label->setMargin(4); - - connect(label, &ClickableLabel::clicked, this, - [&, label, broadcast, astart, astop]() { - for (QWidget *i : - ui->scrollAreaWidgetContents->findChildren< - QWidget *>( - QString(), - Qt::FindDirectChildrenOnly)) { - - i->setProperty( - "isSelectedEvent", - "false"); - i->style()->unpolish(i); - i->style()->polish(i); - } - label->setProperty("isSelectedEvent", - "true"); - label->style()->unpolish(label); - label->style()->polish(label); - - this->selectedBroadcast = broadcast; - this->autostart = astart; - this->autostop = astop; - UpdateOkButtonStatus(); - }); - ui->scrollAreaWidgetContents->layout()->addWidget( - label); - - if (selectedBroadcast == broadcast) - label->clicked(); - }); - workerThread->start(); - - OBSBasic *main = OBSBasic::Get(); - bool rememberSettings = config_get_bool(main->basicConfig, "YouTube", - "RememberSettings"); - if (rememberSettings) - LoadSettings(); - - // Switch to events page and select readied broadcast once loaded - if (broadcastReady) { - ui->tabWidget->setCurrentIndex(1); - selectedBroadcast = apiYouTube->GetBroadcastId(); - } - -#ifdef __APPLE__ - // MacOS theming issues - this->resize(this->width() + 200, this->height() + 120); -#endif - valid = true; -} - -void OBSYoutubeActions::showEvent(QShowEvent *event) -{ - QDialog::showEvent(event); - if (thumbnailFile.isEmpty()) - ui->thumbnailPreview->setPixmap( - GetPlaceholder().pixmap(QSize(16, 16))); -} - -OBSYoutubeActions::~OBSYoutubeActions() -{ - workerThread->stop(); - workerThread->wait(); - - delete workerThread; -} - -void WorkerThread::run() -{ - if (!pending) - return; - json11::Json broadcasts; - - for (QString broadcastStatus : {"active", "upcoming"}) { - if (!apiYouTube->GetBroadcastsList(broadcasts, "", - broadcastStatus)) { - emit failed(); - return; - } - - while (pending) { - auto items = broadcasts["items"].array_items(); - for (auto item : items) { - QString status = QString::fromStdString( - item["status"]["lifeCycleStatus"] - .string_value()); - - if (status == "live" || status == "testing") { - // Check that the attached liveStream is offline (reconnectable) - QString stream_id = QString::fromStdString( - item["contentDetails"] - ["boundStreamId"] - .string_value()); - json11::Json stream; - if (!apiYouTube->FindStream(stream_id, - stream)) - continue; - if (stream["status"]["streamStatus"] == - "active") - continue; - } - - QString title = QString::fromStdString( - item["snippet"]["title"].string_value()); - QString scheduledStartTime = - QString::fromStdString( - item["snippet"] - ["scheduledStartTime"] - .string_value()); - QString broadcast = QString::fromStdString( - item["id"].string_value()); - - // Treat already started streams as autostart for UI purposes - bool astart = - status == "live" || - item["contentDetails"]["enableAutoStart"] - .bool_value(); - bool astop = - item["contentDetails"]["enableAutoStop"] - .bool_value(); - - QDateTime utcDTime = QDateTime::fromString( - scheduledStartTime, - SchedulDateAndTimeFormat); - // DateTime parser means that input datetime is a local, so we need to move it - QDateTime dateTime = utcDTime.addSecs( - utcDTime.offsetFromUtc()); - - QString dateTimeString = QLocale().toString( - dateTime, - QString("%1 %2").arg( - QLocale().dateFormat( - QLocale::LongFormat), - QLocale().timeFormat( - QLocale::ShortFormat))); - - emit new_item(title, dateTimeString, broadcast, - status, astart, astop); - } - - auto nextPageToken = - broadcasts["nextPageToken"].string_value(); - if (nextPageToken.empty() || items.empty()) - break; - else { - if (!pending) - return; - if (!apiYouTube->GetBroadcastsList( - broadcasts, - QString::fromStdString( - nextPageToken), - broadcastStatus)) { - emit failed(); - return; - } - } - } - } - - emit ready(); -} - -void OBSYoutubeActions::UpdateOkButtonStatus() -{ - bool enable = false; - - if (ui->tabWidget->currentIndex() == 0) { - enable = !ui->title->text().isEmpty() && - !ui->privacyBox->currentText().isEmpty() && - (ui->yesMakeForKids->isChecked() || - ui->notMakeForKids->isChecked()); - ui->okButton->setEnabled(enable); - ui->saveButton->setEnabled(enable); - - if (ui->checkScheduledLater->checkState() == Qt::Checked) { - ui->okButton->setText( - QTStr("YouTube.Actions.Create_Schedule")); - ui->saveButton->setText( - QTStr("YouTube.Actions.Create_Schedule_Ready")); - } else { - ui->okButton->setText( - QTStr("YouTube.Actions.Create_GoLive")); - ui->saveButton->setText( - QTStr("YouTube.Actions.Create_Ready")); - } - ui->pushButton->setVisible(false); - } else { - enable = !selectedBroadcast.isEmpty(); - ui->okButton->setEnabled(enable); - ui->saveButton->setEnabled(enable); - ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive")); - ui->saveButton->setText(QTStr("YouTube.Actions.Choose_Ready")); - - ui->pushButton->setVisible(true); - } -} -bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, - StreamDescription &stream, - bool stream_later, - bool ready_broadcast) -{ - YoutubeApiWrappers *apiYouTube = api; - BroadcastDescription broadcast = {}; - UiToBroadcast(broadcast); - - if (stream_later) { - // DateTime parser means that input datetime is a local, so we need to move it - auto dateTime = ui->scheduledTime->dateTime(); - auto utcDTime = dateTime.addSecs(-dateTime.offsetFromUtc()); - broadcast.schedul_date_time = - utcDTime.toString(SchedulDateAndTimeFormat); - } else { - // stream now is always autostart/autostop - broadcast.auto_start = true; - broadcast.auto_stop = true; - broadcast.schedul_date_time = - QDateTime::currentDateTimeUtc().toString( - SchedulDateAndTimeFormat); - } - - autostart = broadcast.auto_start; - autostop = broadcast.auto_stop; - - blog(LOG_DEBUG, "Scheduled date and time: %s", - broadcast.schedul_date_time.toStdString().c_str()); - if (!apiYouTube->InsertBroadcast(broadcast)) { - blog(LOG_DEBUG, "No broadcast created."); - return false; - } - if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title, - broadcast.description, - broadcast.category.id)) { - blog(LOG_DEBUG, "No category set."); - return false; - } - if (!thumbnailFile.isEmpty()) { - blog(LOG_INFO, "Uploading thumbnail file \"%s\"...", - thumbnailFile.toStdString().c_str()); - if (!apiYouTube->SetVideoThumbnail(broadcast.id, - thumbnailFile)) { - blog(LOG_DEBUG, "No thumbnail set."); - return false; - } - } - - if (!stream_later || ready_broadcast) { - stream = {"", "", "OBS Studio Video Stream"}; - if (!apiYouTube->InsertStream(stream)) { - blog(LOG_DEBUG, "No stream created."); - return false; - } - json11::Json json; - if (!apiYouTube->BindStream(broadcast.id, stream.id, json)) { - blog(LOG_DEBUG, "No stream binded."); - return false; - } - - if (broadcast.privacy != "private") { - const std::string apiLiveChatId = - json["snippet"]["liveChatId"].string_value(); - apiYouTube->SetChatId(broadcast.id, apiLiveChatId); - } else { - apiYouTube->ResetChat(); - } - } - - return true; -} - -bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api, - StreamDescription &stream) -{ - YoutubeApiWrappers *apiYouTube = api; - - json11::Json json; - if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) { - blog(LOG_DEBUG, "No broadcast found."); - return false; - } - - std::string boundStreamId = - json["items"] - .array_items()[0]["contentDetails"]["boundStreamId"] - .string_value(); - std::string broadcastPrivacy = - json["items"] - .array_items()[0]["status"]["privacyStatus"] - .string_value(); - std::string apiLiveChatId = - json["items"] - .array_items()[0]["snippet"]["liveChatId"] - .string_value(); - - stream.id = boundStreamId.c_str(); - if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) { - auto item = json["items"].array_items()[0]; - auto streamName = item["cdn"]["ingestionInfo"]["streamName"] - .string_value(); - auto title = item["snippet"]["title"].string_value(); - - stream.name = streamName.c_str(); - stream.title = title.c_str(); - api->SetBroadcastId(selectedBroadcast); - } else { - stream = {"", "", "OBS Studio Video Stream"}; - if (!apiYouTube->InsertStream(stream)) { - blog(LOG_DEBUG, "No stream created."); - return false; - } - if (!apiYouTube->BindStream(selectedBroadcast, stream.id, - json)) { - blog(LOG_DEBUG, "No stream binded."); - return false; - } - } - - if (broadcastPrivacy != "private") - apiYouTube->SetChatId(selectedBroadcast, apiLiveChatId); - else - apiYouTube->ResetChat(); - - return true; -} - -void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text) -{ - QMessageBox dlg(parent); - dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); - dlg.setWindowTitle(QTStr("YouTube.Actions.Error.Title")); - dlg.setText(text); - dlg.setTextFormat(Qt::RichText); - dlg.setIcon(QMessageBox::Warning); - dlg.setStandardButtons(QMessageBox::StandardButton::Ok); - dlg.exec(); -} - -void OBSYoutubeActions::InitBroadcast() -{ - StreamDescription stream; - QMessageBox msgBox(this); - msgBox.setWindowFlags(msgBox.windowFlags() & - ~Qt::WindowCloseButtonHint); - msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title")); - msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast")); - msgBox.setStandardButtons(QMessageBox::StandardButtons()); - - bool success = false; - auto action = [&]() { - if (ui->tabWidget->currentIndex() == 0) { - success = this->CreateEventAction( - apiYouTube, stream, - ui->checkScheduledLater->isChecked()); - } else { - success = this->ChooseAnEventAction(apiYouTube, stream); - }; - QMetaObject::invokeMethod(&msgBox, "accept", - Qt::QueuedConnection); - }; - QScopedPointer thread(CreateQThread(action)); - thread->start(); - msgBox.exec(); - thread->wait(); - - if (success) { - if (ui->tabWidget->currentIndex() == 0) { - // Stream later usecase. - if (ui->checkScheduledLater->isChecked()) { - QMessageBox msg(this); - msg.setWindowTitle(QTStr( - "YouTube.Actions.EventCreated.Title")); - msg.setText(QTStr( - "YouTube.Actions.EventCreated.Text")); - msg.setStandardButtons(QMessageBox::Ok); - msg.exec(); - // Close dialog without start streaming. - Cancel(); - } else { - // Stream now usecase. - blog(LOG_DEBUG, "New valid stream: %s", - QT_TO_UTF8(stream.name)); - emit ok(QT_TO_UTF8(stream.id), - QT_TO_UTF8(stream.name), true, true, - true); - Accept(); - } - } else { - // Stream to precreated broadcast usecase. - emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name), - autostart, autostop, true); - Accept(); - } - } else { - // Fail. - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.NoBroadcastCreated") - .arg(last_error); - - ShowErrorDialog(this, last_error); - } -} - -void OBSYoutubeActions::ReadyBroadcast() -{ - StreamDescription stream; - QMessageBox msgBox(this); - msgBox.setWindowFlags(msgBox.windowFlags() & - ~Qt::WindowCloseButtonHint); - msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title")); - msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast")); - msgBox.setStandardButtons(QMessageBox::StandardButtons()); - - bool success = false; - auto action = [&]() { - if (ui->tabWidget->currentIndex() == 0) { - success = this->CreateEventAction( - apiYouTube, stream, - ui->checkScheduledLater->isChecked(), true); - } else { - success = this->ChooseAnEventAction(apiYouTube, stream); - }; - QMetaObject::invokeMethod(&msgBox, "accept", - Qt::QueuedConnection); - }; - QScopedPointer thread(CreateQThread(action)); - thread->start(); - msgBox.exec(); - thread->wait(); - - if (success) { - emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name), - autostart, autostop, false); - Accept(); - } else { - // Fail. - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.NoBroadcastCreated") - .arg(last_error); - - ShowErrorDialog(this, last_error); - } -} - -void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast) -{ - broadcast.title = ui->title->text(); - // ToDo: UI warning rather than silent truncation - broadcast.description = ui->description->toPlainText().left(5000); - broadcast.privacy = ui->privacyBox->currentData().toString(); - broadcast.category.title = ui->categoryBox->currentText(); - broadcast.category.id = ui->categoryBox->currentData().toString(); - broadcast.made_for_kids = ui->yesMakeForKids->isChecked(); - broadcast.latency = ui->latencyBox->currentData().toString(); - broadcast.auto_start = ui->checkAutoStart->isChecked(); - broadcast.auto_stop = ui->checkAutoStop->isChecked(); - broadcast.dvr = ui->checkDVR->isChecked(); - broadcast.schedul_for_later = ui->checkScheduledLater->isChecked(); - broadcast.projection = ui->check360Video->isChecked() ? "360" - : "rectangular"; - - if (ui->checkRememberSettings->isChecked()) - SaveSettings(broadcast); -} - -void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast) -{ - OBSBasic *main = OBSBasic::Get(); - - config_set_string(main->basicConfig, "YouTube", "Title", - QT_TO_UTF8(broadcast.title)); - config_set_string(main->basicConfig, "YouTube", "Description", - QT_TO_UTF8(broadcast.description)); - config_set_string(main->basicConfig, "YouTube", "Privacy", - QT_TO_UTF8(broadcast.privacy)); - config_set_string(main->basicConfig, "YouTube", "CategoryID", - QT_TO_UTF8(broadcast.category.id)); - config_set_string(main->basicConfig, "YouTube", "Latency", - QT_TO_UTF8(broadcast.latency)); - config_set_bool(main->basicConfig, "YouTube", "MadeForKids", - broadcast.made_for_kids); - config_set_bool(main->basicConfig, "YouTube", "AutoStart", - broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "AutoStop", - broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr); - config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater", - broadcast.schedul_for_later); - config_set_string(main->basicConfig, "YouTube", "Projection", - QT_TO_UTF8(broadcast.projection)); - config_set_string(main->basicConfig, "YouTube", "ThumbnailFile", - QT_TO_UTF8(thumbnailFile)); - config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true); -} - -void OBSYoutubeActions::LoadSettings() -{ - OBSBasic *main = OBSBasic::Get(); - - const char *title = - config_get_string(main->basicConfig, "YouTube", "Title"); - ui->title->setText(QT_UTF8(title)); - - const char *desc = - config_get_string(main->basicConfig, "YouTube", "Description"); - ui->description->setPlainText(QT_UTF8(desc)); - - const char *priv = - config_get_string(main->basicConfig, "YouTube", "Privacy"); - int index = ui->privacyBox->findData(priv); - ui->privacyBox->setCurrentIndex(index); - - const char *catID = - config_get_string(main->basicConfig, "YouTube", "CategoryID"); - index = ui->categoryBox->findData(catID); - ui->categoryBox->setCurrentIndex(index); - - const char *latency = - config_get_string(main->basicConfig, "YouTube", "Latency"); - index = ui->latencyBox->findData(latency); - ui->latencyBox->setCurrentIndex(index); - - bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR"); - ui->checkDVR->setChecked(dvr); - - bool forKids = - config_get_bool(main->basicConfig, "YouTube", "MadeForKids"); - if (forKids) - ui->yesMakeForKids->setChecked(true); - else - ui->notMakeForKids->setChecked(true); - - bool schedLater = config_get_bool(main->basicConfig, "YouTube", - "ScheduleForLater"); - ui->checkScheduledLater->setChecked(schedLater); - - bool autoStart = - config_get_bool(main->basicConfig, "YouTube", "AutoStart"); - ui->checkAutoStart->setChecked(autoStart); - - bool autoStop = - config_get_bool(main->basicConfig, "YouTube", "AutoStop"); - ui->checkAutoStop->setChecked(autoStop); - - const char *projection = - config_get_string(main->basicConfig, "YouTube", "Projection"); - if (projection && *projection) { - if (strcmp(projection, "360") == 0) - ui->check360Video->setChecked(true); - else - ui->check360Video->setChecked(false); - } - - const char *thumbFile = config_get_string(main->basicConfig, "YouTube", - "ThumbnailFile"); - if (thumbFile && *thumbFile) { - QFileInfo tFile(thumbFile); - // Re-check validity before setting path again - if (tFile.exists() && tFile.size() <= 2 * 1024 * 1024) { - thumbnailFile = tFile.absoluteFilePath(); - ui->selectedFileName->setText(thumbnailFile); - ui->selectFileButton->setText( - QTStr("YouTube.Actions.Thumbnail.ClearFile")); - - QImageReader imgReader(thumbnailFile); - imgReader.setAutoTransform(true); - const QImage newImage = imgReader.read(); - ui->thumbnailPreview->setPixmap( - QPixmap::fromImage(newImage).scaled( - 160, 90, Qt::KeepAspectRatio, - Qt::SmoothTransformation)); - } - } -} - -void OBSYoutubeActions::OpenYouTubeDashboard() -{ - ChannelDescription channel; - if (!apiYouTube->GetChannelDescription(channel)) { - blog(LOG_DEBUG, "Could not get channel description."); - ShowErrorDialog( - this, - apiYouTube->GetLastError().isEmpty() - ? QTStr("YouTube.Actions.Error.General") - : QTStr("YouTube.Actions.Error.Text") - .arg(apiYouTube->GetLastError())); - return; - } - - //https://studio.youtube.com/channel/UCA9bSfH3KL186kyiUsvi3IA/videos/live?filter=%5B%5D&sort=%7B%22columnType%22%3A%22date%22%2C%22sortOrder%22%3A%22DESCENDING%22%7D - QString uri = - QString("https://studio.youtube.com/channel/%1/videos/live?filter=[]&sort={\"columnType\"%3A\"date\"%2C\"sortOrder\"%3A\"DESCENDING\"}") - .arg(channel.id); - QDesktopServices::openUrl(uri); -} - -void OBSYoutubeActions::Cancel() -{ - workerThread->stop(); - reject(); -} -void OBSYoutubeActions::Accept() -{ - workerThread->stop(); - accept(); -} diff --git a/UI/youtube-api-wrappers.cpp b/UI/youtube-api-wrappers.cpp deleted file mode 100644 index 876ecad4f26495..00000000000000 --- a/UI/youtube-api-wrappers.cpp +++ /dev/null @@ -1,605 +0,0 @@ -#include "youtube-api-wrappers.hpp" - -#include -#include -#include - -#include -#include - -#include "auth-youtube.hpp" -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include "obf.h" - -using namespace json11; - -/* ------------------------------------------------------------------------- */ -#define YOUTUBE_LIVE_API_URL "https://www.googleapis.com/youtube/v3" - -#define YOUTUBE_LIVE_STREAM_URL YOUTUBE_LIVE_API_URL "/liveStreams" -#define YOUTUBE_LIVE_BROADCAST_URL YOUTUBE_LIVE_API_URL "/liveBroadcasts" -#define YOUTUBE_LIVE_BROADCAST_TRANSITION_URL \ - YOUTUBE_LIVE_BROADCAST_URL "/transition" -#define YOUTUBE_LIVE_BROADCAST_BIND_URL YOUTUBE_LIVE_BROADCAST_URL "/bind" - -#define YOUTUBE_LIVE_CHANNEL_URL YOUTUBE_LIVE_API_URL "/channels" -#define YOUTUBE_LIVE_TOKEN_URL "https://oauth2.googleapis.com/token" -#define YOUTUBE_LIVE_VIDEOCATEGORIES_URL YOUTUBE_LIVE_API_URL "/videoCategories" -#define YOUTUBE_LIVE_VIDEOS_URL YOUTUBE_LIVE_API_URL "/videos" -#define YOUTUBE_LIVE_CHAT_MESSAGES_URL YOUTUBE_LIVE_API_URL "/liveChat/messages" -#define YOUTUBE_LIVE_THUMBNAIL_URL \ - "https://www.googleapis.com/upload/youtube/v3/thumbnails/set" - -#define DEFAULT_BROADCASTS_PER_QUERY \ - "50" // acceptable values are 0 to 50, inclusive -/* ------------------------------------------------------------------------- */ - -bool IsYouTubeService(const std::string &service) -{ - auto it = find_if(youtubeServices.begin(), youtubeServices.end(), - [&service](const Auth::Def &yt) { - return service == yt.service; - }); - return it != youtubeServices.end(); -} - -bool YoutubeApiWrappers::GetTranslatedError(QString &error_message) -{ - QString translated = - QTStr("YouTube.Errors." + lastErrorReason.toUtf8()); - // No translation found - if (translated.startsWith("YouTube.Errors.")) - return false; - error_message = translated; - return true; -} - -YoutubeApiWrappers::YoutubeApiWrappers(const Def &d) : YoutubeAuth(d) {} - -bool YoutubeApiWrappers::TryInsertCommand(const char *url, - const char *content_type, - std::string request_type, - const char *data, Json &json_out, - long *error_code, int data_size) -{ - long httpStatusCode = 0; - -#ifdef _DEBUG - blog(LOG_DEBUG, "YouTube API command URL: %s", url); - if (data && data[0] == '{') // only log JSON data - blog(LOG_DEBUG, "YouTube API command data: %s", data); -#endif - if (token.empty()) - return false; - std::string output; - std::string error; - // Increase timeout by the time it takes to transfer `data_size` at 1 Mbps - int timeout = 5 + data_size / 125000; - bool success = GetRemoteFile(url, output, error, &httpStatusCode, - content_type, request_type, data, - {"Authorization: Bearer " + token}, - nullptr, timeout, false, data_size); - if (error_code) - *error_code = httpStatusCode; - - if (!success || output.empty()) { - if (!error.empty()) - blog(LOG_WARNING, "YouTube API request failed: %s", - error.c_str()); - return false; - } - - json_out = Json::parse(output, error); -#ifdef _DEBUG - blog(LOG_DEBUG, "YouTube API command answer: %s", - json_out.dump().c_str()); -#endif - if (!error.empty()) { - return false; - } - return httpStatusCode < 400; -} - -bool YoutubeApiWrappers::UpdateAccessToken() -{ - if (refresh_token.empty()) { - return false; - } - - std::string clientid = YOUTUBE_CLIENTID; - std::string secret = YOUTUBE_SECRET; - deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); - deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); - - std::string r_token = - QUrl::toPercentEncoding(refresh_token.c_str()).toStdString(); - const QString url = YOUTUBE_LIVE_TOKEN_URL; - const QString data_template = "client_id=%1" - "&client_secret=%2" - "&refresh_token=%3" - "&grant_type=refresh_token"; - const QString data = data_template.arg(QString(clientid.c_str()), - QString(secret.c_str()), - QString(r_token.c_str())); - Json json_out; - bool success = TryInsertCommand(QT_TO_UTF8(url), - "application/x-www-form-urlencoded", "", - QT_TO_UTF8(data), json_out); - - if (!success || json_out.object_items().find("error") != - json_out.object_items().end()) - return false; - token = json_out["access_token"].string_value(); - return token.empty() ? false : true; -} - -bool YoutubeApiWrappers::InsertCommand(const char *url, - const char *content_type, - std::string request_type, - const char *data, Json &json_out, - int data_size) -{ - long error_code; - bool success = TryInsertCommand(url, content_type, request_type, data, - json_out, &error_code, data_size); - - if (error_code == 401) { - // Attempt to update access token and try again - if (!UpdateAccessToken()) - return false; - success = TryInsertCommand(url, content_type, request_type, - data, json_out, &error_code, - data_size); - } - - if (json_out.object_items().find("error") != - json_out.object_items().end()) { - blog(LOG_ERROR, - "YouTube API error:\n\tHTTP status: %ld\n\tURL: %s\n\tJSON: %s", - error_code, url, json_out.dump().c_str()); - - lastError = json_out["error"]["code"].int_value(); - lastErrorReason = - QString(json_out["error"]["errors"][0]["reason"] - .string_value() - .c_str()); - lastErrorMessage = QString( - json_out["error"]["message"].string_value().c_str()); - - // The existence of an error implies non-success even if the HTTP status code disagrees. - success = false; - } - return success; -} - -bool YoutubeApiWrappers::GetChannelDescription( - ChannelDescription &channel_description) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_CHANNEL_URL - "?part=snippet,contentDetails,statistics" - "&mine=true"; - Json json_out; - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) { - return false; - } - - if (json_out["pageInfo"]["totalResults"].int_value() == 0) { - lastErrorMessage = QTStr("YouTube.Auth.NoChannels"); - return false; - } - - channel_description.id = - QString(json_out["items"][0]["id"].string_value().c_str()); - channel_description.title = QString( - json_out["items"][0]["snippet"]["title"].string_value().c_str()); - return channel_description.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::InsertBroadcast(BroadcastDescription &broadcast) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=snippet,status,contentDetails"; - const Json data = Json::object{ - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(broadcast.title)}, - {"description", QT_TO_UTF8(broadcast.description)}, - {"scheduledStartTime", - QT_TO_UTF8(broadcast.schedul_date_time)}, - }}, - {"status", - Json::object{ - {"privacyStatus", QT_TO_UTF8(broadcast.privacy)}, - {"selfDeclaredMadeForKids", broadcast.made_for_kids}, - }}, - {"contentDetails", - Json::object{ - {"latencyPreference", QT_TO_UTF8(broadcast.latency)}, - {"enableAutoStart", broadcast.auto_start}, - {"enableAutoStop", broadcast.auto_stop}, - {"enableDvr", broadcast.dvr}, - {"projection", QT_TO_UTF8(broadcast.projection)}, - { - "monitorStream", - Json::object{ - {"enableMonitorStream", false}, - }, - }, - }}, - }; - Json json_out; - if (!InsertCommand(url, "application/json", "", data.dump().c_str(), - json_out)) { - return false; - } - broadcast.id = QString(json_out["id"].string_value().c_str()); - return broadcast.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::InsertStream(StreamDescription &stream) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_STREAM_URL - "?part=snippet,cdn,status,contentDetails"; - const Json data = Json::object{ - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(stream.title)}, - }}, - {"cdn", - Json::object{ - {"frameRate", "variable"}, - {"ingestionType", "rtmp"}, - {"resolution", "variable"}, - }}, - {"contentDetails", Json::object{{"isReusable", false}}}, - }; - Json json_out; - if (!InsertCommand(url, "application/json", "", data.dump().c_str(), - json_out)) { - return false; - } - stream.id = QString(json_out["id"].string_value().c_str()); - stream.name = QString(json_out["cdn"]["ingestionInfo"]["streamName"] - .string_value() - .c_str()); - return stream.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::BindStream(const QString broadcast_id, - const QString stream_id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QString url_template = YOUTUBE_LIVE_BROADCAST_BIND_URL - "?id=%1" - "&streamId=%2" - "&part=id,snippet,contentDetails,status"; - const QString url = url_template.arg(broadcast_id, stream_id); - const Json data = Json::object{}; - this->broadcast_id = broadcast_id; - return InsertCommand(QT_TO_UTF8(url), "application/json", "", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, const QString &page, - const QString &status) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=snippet,contentDetails,status" - "&broadcastType=all&maxResults=" DEFAULT_BROADCASTS_PER_QUERY; - - if (status.isEmpty()) - url += "&mine=true"; - else - url += "&broadcastStatus=" + status.toUtf8(); - - if (!page.isEmpty()) - url += "&pageToken=" + page.toUtf8(); - return InsertCommand(url, "application/json", "", nullptr, json_out); -} - -bool YoutubeApiWrappers::GetVideoCategoriesList( - QVector &category_list_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QString url_template = YOUTUBE_LIVE_VIDEOCATEGORIES_URL - "?part=snippet" - "®ionCode=%1" - "&hl=%2"; - /* - * All OBS locale regions aside from "US" are missing category id 29 - * ("Nonprofits & Activism"), but it is still available to channels - * set to those regions via the YouTube Studio website. - * To work around this inconsistency with the API all locales will - * use the "US" region and only set the language part for localisation. - * It is worth noting that none of the regions available on YouTube - * feature any category not also available to the "US" region. - */ - QString url = url_template.arg("US", QLocale().name()); - - Json json_out; - if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr, - json_out)) { - if (lastErrorReason != "unsupportedLanguageCode" && - lastErrorReason != "invalidLanguage") - return false; - // Try again with en-US if YouTube error indicates an unsupported locale - url = url_template.arg("US", "en_US"); - if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", - nullptr, json_out)) - return false; - } - category_list_out = {}; - for (auto &j : json_out["items"].array_items()) { - // Assignable only. - if (j["snippet"]["assignable"].bool_value()) { - category_list_out.push_back( - {j["id"].string_value().c_str(), - j["snippet"]["title"].string_value().c_str()}); - } - } - return category_list_out.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id, - const QString &video_title, - const QString &video_description, - const QString &categorie_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_VIDEOS_URL "?part=snippet"; - const Json data = Json::object{ - {"id", QT_TO_UTF8(video_id)}, - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(video_title)}, - {"description", QT_TO_UTF8(video_description)}, - {"categoryId", QT_TO_UTF8(categorie_id)}, - }}, - }; - Json json_out; - return InsertCommand(url, "application/json", "PUT", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::SetVideoThumbnail(const QString &video_id, - const QString &thumbnail_file) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - // Make sure the file hasn't been deleted since originally selecting it - if (!QFile::exists(thumbnail_file)) { - lastErrorMessage = QTStr("YouTube.Actions.Error.FileMissing"); - return false; - } - - QFile thumbFile(thumbnail_file); - if (!thumbFile.open(QFile::ReadOnly)) { - lastErrorMessage = - QTStr("YouTube.Actions.Error.FileOpeningFailed"); - return false; - } - - const QByteArray fileContents = thumbFile.readAll(); - const QString mime = - QMimeDatabase().mimeTypeForData(fileContents).name(); - - const QString url = YOUTUBE_LIVE_THUMBNAIL_URL "?videoId=" + video_id; - Json json_out; - return InsertCommand(QT_TO_UTF8(url), QT_TO_UTF8(mime), "POST", - fileContents.constData(), json_out, - fileContents.size()); -} - -bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - Json json_out; - if (!FindBroadcast(broadcast_id, json_out)) - return false; - - auto lifeCycleStatus = - json_out["items"][0]["status"]["lifeCycleStatus"].string_value(); - - if (lifeCycleStatus == "live" || lifeCycleStatus == "liveStarting") - // Broadcast is already (going to be) live - return true; - else if (lifeCycleStatus == "testStarting") { - // User will need to wait a few seconds before attempting to start broadcast - lastErrorMessage = - QTStr("YouTube.Actions.Error.BroadcastTestStarting"); - lastErrorReason.clear(); - return false; - } - - // Only reset if broadcast has monitoring enabled and is not already in "testing" mode - auto monitorStreamEnabled = - json_out["items"][0]["contentDetails"]["monitorStream"] - ["enableMonitorStream"] - .bool_value(); - if (lifeCycleStatus != "testing" && monitorStreamEnabled && - !ResetBroadcast(broadcast_id, json_out)) - return false; - - const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL - "?id=%1" - "&broadcastStatus=%2" - "&part=status"; - const QString live = url_template.arg(broadcast_id, "live"); - bool success = InsertCommand(QT_TO_UTF8(live), "application/json", - "POST", "{}", json_out); - // Return a success if the command failed, but was redundant (broadcast already live) - return success || lastErrorReason == "redundantTransition"; -} - -bool YoutubeApiWrappers::StartLatestBroadcast() -{ - return StartBroadcast(this->broadcast_id); -} - -bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL - "?id=%1" - "&broadcastStatus=complete" - "&part=status"; - const QString url = url_template.arg(broadcast_id); - Json json_out; - bool success = InsertCommand(QT_TO_UTF8(url), "application/json", - "POST", "{}", json_out); - // Return a success if the command failed, but was redundant (broadcast already stopped) - return success || lastErrorReason == "redundantTransition"; -} - -bool YoutubeApiWrappers::StopLatestBroadcast() -{ - return StopBroadcast(this->broadcast_id); -} - -void YoutubeApiWrappers::SetBroadcastId(QString &broadcast_id) -{ - this->broadcast_id = broadcast_id; -} - -QString YoutubeApiWrappers::GetBroadcastId() -{ - return this->broadcast_id; -} - -bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - auto snippet = json_out["items"][0]["snippet"]; - auto status = json_out["items"][0]["status"]; - auto contentDetails = json_out["items"][0]["contentDetails"]; - auto monitorStream = contentDetails["monitorStream"]; - - const Json data = Json::object{ - {"id", QT_TO_UTF8(broadcast_id)}, - {"snippet", - Json::object{ - {"title", snippet["title"]}, - {"description", snippet["description"]}, - {"scheduledStartTime", snippet["scheduledStartTime"]}, - {"scheduledEndTime", snippet["scheduledEndTime"]}, - }}, - {"status", - Json::object{ - {"privacyStatus", status["privacyStatus"]}, - {"madeForKids", status["madeForKids"]}, - {"selfDeclaredMadeForKids", - status["selfDeclaredMadeForKids"]}, - }}, - {"contentDetails", - Json::object{ - { - "monitorStream", - Json::object{ - {"enableMonitorStream", false}, - {"broadcastStreamDelayMs", - monitorStream["broadcastStreamDelayMs"]}, - }, - }, - {"enableAutoStart", contentDetails["enableAutoStart"]}, - {"enableAutoStop", contentDetails["enableAutoStop"]}, - {"enableClosedCaptions", - contentDetails["enableClosedCaptions"]}, - {"enableDvr", contentDetails["enableDvr"]}, - {"enableContentEncryption", - contentDetails["enableContentEncryption"]}, - {"enableEmbed", contentDetails["enableEmbed"]}, - {"recordFromStart", contentDetails["recordFromStart"]}, - {"startWithSlate", contentDetails["startWithSlate"]}, - }}, - }; - - const QString put = YOUTUBE_LIVE_BROADCAST_URL - "?part=id,snippet,contentDetails,status"; - return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::FindBroadcast(const QString &id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=id,snippet,contentDetails,status" - "&broadcastType=all&maxResults=1"; - url += "&id=" + id.toUtf8(); - - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) - return false; - - auto items = json_out["items"].array_items(); - if (items.size() != 1) { - lastErrorMessage = - QTStr("YouTube.Actions.Error.BroadcastNotFound"); - return false; - } - - return true; -} - -bool YoutubeApiWrappers::FindStream(const QString &id, json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_STREAM_URL "?part=id,snippet,cdn,status" - "&maxResults=1"; - url += "&id=" + id.toUtf8(); - - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) - return false; - - auto items = json_out["items"].array_items(); - if (items.size() != 1) { - lastErrorMessage = "No active broadcast found."; - return false; - } - - return true; -} - -bool YoutubeApiWrappers::SendChatMessage(const std::string &chat_id, - const QString &message) -{ - QByteArray url = YOUTUBE_LIVE_CHAT_MESSAGES_URL "?part=snippet"; - - json11::Json json_in = Json::object{ - {"snippet", - Json::object{ - {"liveChatId", chat_id}, - {"type", "textMessageEvent"}, - {"textMessageDetails", - Json::object{{"messageText", QT_TO_UTF8(message)}}}, - }}}; - - json11::Json json_out; - return InsertCommand(url, "application/json", "POST", - json_in.dump().c_str(), json_out); -} diff --git a/UI/youtube-api-wrappers.hpp b/UI/youtube-api-wrappers.hpp deleted file mode 100644 index dcabc7020c73c4..00000000000000 --- a/UI/youtube-api-wrappers.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "auth-youtube.hpp" - -#include -#include - -struct ChannelDescription { - QString id; - QString title; -}; - -struct StreamDescription { - QString id; - QString name; - QString title; -}; - -struct CategoryDescription { - QString id; - QString title; -}; - -struct BroadcastDescription { - QString id; - QString title; - QString description; - QString privacy; - CategoryDescription category; - QString latency; - bool made_for_kids; - bool auto_start; - bool auto_stop; - bool dvr; - bool schedul_for_later; - QString schedul_date_time; - QString projection; -}; - -bool IsYouTubeService(const std::string &service); - -class YoutubeApiWrappers : public YoutubeAuth { - Q_OBJECT - - bool TryInsertCommand(const char *url, const char *content_type, - std::string request_type, const char *data, - json11::Json &ret, long *error_code = nullptr, - int data_size = 0); - bool UpdateAccessToken(); - bool InsertCommand(const char *url, const char *content_type, - std::string request_type, const char *data, - json11::Json &ret, int data_size = 0); - -public: - YoutubeApiWrappers(const Def &d); - - bool GetChannelDescription(ChannelDescription &channel_description); - bool InsertBroadcast(BroadcastDescription &broadcast); - bool InsertStream(StreamDescription &stream); - bool BindStream(const QString broadcast_id, const QString stream_id, - json11::Json &json_out); - bool GetBroadcastsList(json11::Json &json_out, const QString &page, - const QString &status); - bool - GetVideoCategoriesList(QVector &category_list_out); - bool SetVideoCategory(const QString &video_id, - const QString &video_title, - const QString &video_description, - const QString &categorie_id); - bool SetVideoThumbnail(const QString &video_id, - const QString &thumbnail_file); - bool StartBroadcast(const QString &broadcast_id); - bool StopBroadcast(const QString &broadcast_id); - bool ResetBroadcast(const QString &broadcast_id, - json11::Json &json_out); - bool StartLatestBroadcast(); - bool StopLatestBroadcast(); - bool SendChatMessage(const std::string &chat_id, - const QString &message); - - void SetBroadcastId(QString &broadcast_id); - QString GetBroadcastId(); - - bool FindBroadcast(const QString &id, json11::Json &json_out); - bool FindStream(const QString &id, json11::Json &json_out); - - QString GetLastError() { return lastErrorMessage; }; - bool GetTranslatedError(QString &error_message); - -private: - QString broadcast_id; - - int lastError; - QString lastErrorMessage; - QString lastErrorReason; -}; diff --git a/build-aux/README.md b/build-aux/README.md index 9343a02198a9a3..367e515fb77677 100644 --- a/build-aux/README.md +++ b/build-aux/README.md @@ -3,6 +3,8 @@ This folder contains: - The Flatpak manifest used to build OBS Studio - The script `format-manifest.py` which format manifest JSON files +- JSON Schemas related to plugins +- The script `regen-obs-services-json-parser.zsh` to regenerate obs-services plugin JSON parser against its schema ## OBS Studio Flatpak Manifest diff --git a/build-aux/json-schema/codecDefs.json b/build-aux/json-schema/codecDefs.json new file mode 100644 index 00000000000000..894b1de68d1a2e --- /dev/null +++ b/build-aux/json-schema/codecDefs.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/codecDefs.json", + "title": "Codec Enums, Patterns and Properties", + "description": "Codec-related enums, patterns and properties used in OBS JSON Schemas", + "$comment": "Made to allow an easy way to add codec to schemas", + "$defs": { + "videoCodecEnum": { + "$comment": "Enumeration of video codecs", + "enum": ["h264","hevc","av1"] + }, + "audioCodecEnum": { + "$comment": "Enumeration of audio codecs", + "enum": ["aac","opus"] + }, + "codecProperties": { + "$comment": "Per-codec properties meant for obs-services schema", + "type": "object", + "properties": { + "h264": + { + "type": "object", + "title": "H264 Settings", + "description": "Properties related to the H264 codec", + "properties": { + "profile": { + "type": "string", + "title": "H264 Profile", + "enum": [ "baseline", "main", "high" ] + }, + "keyint": { + "type": "integer", + "title": "Keyframe Interval (seconds)", + "minimum": 0 + }, + "bframes": + { + "type": "integer", + "title": "B-Frames", + "minimum": 0 + } + }, + "minProperties": 1 + }, + "obs_x264": { + "type": "string", + "title": "x264 Encoder Options", + "description": "Options meant for the x264 encoder implementation with the id 'obs_x264'", + "minLength": 1 + } + } + } + } +} diff --git a/build-aux/json-schema/obs-services.json b/build-aux/json-schema/obs-services.json new file mode 100644 index 00000000000000..8701e11d6a520b --- /dev/null +++ b/build-aux/json-schema/obs-services.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/obs-service.json", + "title": "OBS Services Plugin JSON Schema", + "type": "object", + "properties": { + "format_version": { + "type": "integer", + "const": 1 + }, + "services": { + "type": "array", + "items": { + "allOf": [ + { + "title": "Service", + "properties": { + "id": { + "type": "string", + "title": "Service ID", + "description": "Human readable identifier used to register the service in OBS\nMaking it human readable is meant to allow users to use it through scripts and plugins", + "pattern": "^[a-z0-9_\\-]+$", + "minLength": 1 + }, + "name": { + "type": "string", + "title": "Service Name", + "description": "Name of the streaming service. Will be displayed in the Service dropdown.", + "minLength": 1 + }, + "common": { + "type": "boolean", + "title": "Property reserved to OBS Project developers", + "description": "Whether or not the service is shown in the list before it is expanded to all services by the user.", + "default": false + }, + "github_logins": { + "type": "array", + "title": "Github logins", + "description": "Login to ping on Github when the services check is failing.\n 'obsproject' is used as a placeholder.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "$commit": "Forbid 'common' being set if false", + "if": { "properties": { "common": { "const": false } } }, + "then": { "properties": { "common": { "description": "This property is unneeded if set to false" , "const": true } } }, + "required": ["id","name", "github_logins"] + }, + { + "$comment": "Add base service JSON schema", + "$ref": "service.json" + }, + { + "$comment": "Add protocol properties", + "$ref": "protocolDefs.json#/$defs/protocolProperties" + } + ], + "unevaluatedProperties": false + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "$comment": "Support '$schema' without making it a proper property", + "if": { "not": { "maxProperties": 2 } }, + "then": { "properties": { "$schema": { "type": "string" } } }, + "required": ["format_version", "services"], + "unevaluatedProperties": false +} diff --git a/build-aux/json-schema/package-schema.json b/build-aux/json-schema/package-schema.json new file mode 100644 index 00000000000000..58db6698684fca --- /dev/null +++ b/build-aux/json-schema/package-schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "url": { + "$ref": "#/definitions/saneUrl", + "description": "Points to the base URL of hosted package.json and services.json files, used to automatically fetch the latest version." + }, + "version": { + "type": "integer" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Filename to read,, containing service definitions." + }, + "version": { + "type": "integer", + "description": "This value should be bumped any time the file defined by the 'name' field is changed. This value will be used when determining whether a new version is available." + } + }, + "required": [ + "name", + "version" + ] + }, + "description": "List of files to read, each containing a list of services.", + "additionalProperties": false + } + }, + "required": [ + "url", + "version", + "files" + ], + "definitions": { + "saneUrl": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + } +} diff --git a/build-aux/json-schema/protocolDefs.json b/build-aux/json-schema/protocolDefs.json new file mode 100644 index 00000000000000..a7a19e6ff2a81c --- /dev/null +++ b/build-aux/json-schema/protocolDefs.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/protocolDefs.json", + "title": "Protocol Enums, Patterns and Properties", + "description": "Protocol-related enums, patterns and properties used in OBS JSON Schemas", + "$comment": "Made to allow an easy way to add protocol to schemas", + "$defs": { + "protocolEnum": { + "$comment": "Enumeration of protocols", + "enum": ["RTMP","RTMPS","HLS","SRT","RIST","WHIP"] + }, + "protocolMapEnum": { + "$comment": "Enumeration of protocols (with '*' as any) that have various compatible codecs", + "enum": ["*","RTMP","RTMPS","HLS","SRT","RIST","WHIP"] + }, + "serverPattern": { + "$comment": "Pattern to enforce a supported server URL", + "pattern": "^(rtmps?|https?|srt|rist)://" + }, + "protocolProperties": { + "$comment": "Per-protocol properties meant for obs-services schema", + "type": "object", + "properties": { + "SRT": { + "type": "object", + "title": "SRT Properties", + "description": "Properties related to the SRT protocol", + "properties": { + "stream_id": { + "type": "boolean", + "default": true + }, + "encrypt_passphrase": { + "type": "boolean", + "default": false + } + }, + "required": ["stream_id", "encrypt_passphrase"] + }, + "RIST": { + "type": "object", + "title": "RIST Properties", + "description": "Properties related to the RIST protocol", + "properties": { + "encrypt_passphrase": { + "type": "boolean", + "default": true + }, + "srp_username_password": { + "type": "boolean", + "default": false + } + }, + "required": ["encrypt_passphrase", "srp_username_password"] + } + } + } + } +} diff --git a/build-aux/json-schema/service.json b/build-aux/json-schema/service.json new file mode 100644 index 00000000000000..d05d564d3bad59 --- /dev/null +++ b/build-aux/json-schema/service.json @@ -0,0 +1,255 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/service.json", + "title": "OBS Service Object Schema", + "type": "object", + "properties": { + "more_info_link": { + "type": "string", + "description": "Link that provides additional info about the service, presented in the UI as a button next to the services dropdown.", + "format": "uri", + "$ref": "#/$defs/httpPattern", + "minLength": 1 + }, + "stream_key_link": { + "type": "string", + "description": "Link where a logged-in user can find the 'stream key', presented as a button alongside the stream key field.", + "format": "uri", + "$ref": "#/$defs/httpPattern", + "minLength": 1 + }, + "servers": { + "type": "array", + "description": "Array of server objects", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the server (e.g. location, primary/backup), displayed in the Server dropdown.", + "minLength": 1 + }, + "protocol": { + "type": "string", + "title": "Server Protocol", + "description": "Protocol used by the server. Required if the URI scheme can't help identify the streaming protocol (e.g. HLS).", + "$ref": "protocolDefs.json#/$defs/protocolEnum" + }, + "url": { + "type": "string", + "title": "Server URL", + "description": "URL of the ingest server.", + "format": "uri", + "$ref": "protocolDefs.json#/$defs/serverPattern", + "minLength": 1 + } + }, + "required": ["name","url","protocol"], + "unevaluatedProperties": false + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "supported_codecs": { + "type": "object", + "title": "Supported Codecs", + "description": "Codecs that are supported by the service.", + "properties": { + "video": { + "type": "object", + "title": "Supported Video Codecs", + "description": "Video codecs that are supported by the service.", + "propertyNames": { "$ref": "protocolDefs.json#/$defs/protocolMapEnum" }, + "properties": { + "*": { + "type": "array", + "title": "Any Protocol Supported Video Codec", + "description": "Video codecs that are supported by the service and any supported protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/videoCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "additionalProperties": { + "type": "array", + "title": "Protocol Supported Video Codec", + "description": "Video codecs that are supported by the service on this protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/videoCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "minProperties": 1, + "unevaluatedProperties": false + }, + "audio": { + "type": "object", + "title": "Supported Audio Codecs", + "description": "Audio codecs that are supported by the service.", + "propertyNames": { "$ref": "protocolDefs.json#/$defs/protocolMapEnum" }, + "properties": { + "*": { + "type": "array", + "title": "Any Protocol Supported Audio Codec", + "description": "Audio codecs that are supported by the service and any supported protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/audioCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "additionalProperties": { + "type": "array", + "title": "Protocol Supported Audio Codec", + "description": "Audio codecs that are supported by the service on this protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/audioCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "minProperties": 1, + "unevaluatedProperties": false + } + }, + "minProperties": 1, + "unevaluatedProperties": false + }, + "supported_resolutions": { + "type": "array", + "title": "Supported Resolutions", + "description": "Resolution supported by the service. All with or all withtout the framerate.", + "items": { + "type": "string", + "pattern": "^[0-9]+x[0-9]+(|@[0-9]+)$" + }, + "allOf": [ + { + "if": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } }, + "then": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } } + }, + { + "if": { "items": { "$ref": "#/$defs/resolutionPattern" } }, + "then": { "items": { "$ref": "#/$defs/resolutionPattern" } } + } + ], + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "maximums": { + "type": "object", + "title": "Maximums", + "description": "Maximum values allowed by the service", + "properties": { + "framerate": { + "type": "integer", + "title": "Maximum Framerate", + "exclusiveMinimum": 0 + }, + "video_bitrate": { + "type": "object", + "title": "Maximum Video Bitrate", + "description": "Maximum video bitrate per codec", + "propertyNames": { "$ref": "codecDefs.json#/$defs/videoCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "audio_bitrate": { + "type": "object", + "title": "Maximum Audio Bitrate", + "description": "Maximum audio bitrate per codec", + "propertyNames": { "$ref": "codecDefs.json#/$defs/audioCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "video_bitrate_matrix": { + "type": "object", + "title": "Maximum Video Bitrate Matrix", + "description": "Maximum video bitrate per supported resolution with framerate and per codec", + "propertyNames": { "$ref": "#/$defs/resolutionFrameratePattern" }, + "additionalProperties": { + "type": "object", + "propertyNames": { "$ref": "codecDefs.json#/$defs/videoCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "minProperties": 1, + "unevaluatedProperties": false + } + }, + "minProperties": 1, + "unevaluatedProperties": false + }, + "recommended": { + "type": "object", + "title": "Recommended settings", + "description": "Settings that are applied only if the user wants to do so.", + "allOf": [ + { + "$comment": "Add codec properties", + "$ref": "codecDefs.json#/$defs/codecProperties" + } + ], + "minProperties": 1, + "unevaluatedProperties": true + } + }, + "allOf": [ + { + "$comment": "Make 'video_bitrate_matrix' unusable if 'supported_resolutions' is empty", + "if": { "properties": { "supported_resolutions": { "maxItems": 0 } } }, + "then": { "properties": { + "maximums": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if supported resolutions with framerates are set", "maxProperties": 0 } } }, + "recommended": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if supported resolutions with framerates are set", "maxProperties": 0 } } } + } } + }, + { + "$comment": "Make 'video_bitrate_matrix' unusable if supported resolutions are without framerate", + "if": { "not": { "properties": { "supported_resolutions": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } } } } }, + "then": { "properties": { + "maximums": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if 'supported_resolutions' is with framerates", "maxProperties": 0 } } }, + "recommended": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if 'supported_resolutions' is with framerates", "maxProperties": 0 } } } + } } + }, + { + "$comment": "Make 'framerate' unusable if supported resolutions are with framerate", + "if": { "not": {"properties": { "supported_resolutions": { "items": { "$ref": "#/$defs/resolutionPattern" } } } } }, + "then": { "properties": { + "maximums": { "properties": { "framerate": { "description": "This property is only available if 'supported_resolutions' is without framerates or not set", "const": 0 } } }, + "recommended": { "properties": { "framerate": { "description": "This property is only available if 'supported_resolutions' is without framerates or not set", "const": 0 } } } + } } + } + ], + "required": ["servers"], + "$defs": { + "httpPattern": { + "$comment": "Pattern to enforce HTTP(S) links", + "pattern": "^https?://" + }, + "resolutionPattern": { + "$comment": "Pattern for resolution without framerate", + "pattern": "^[0-9]+x[0-9]+$" + }, + "resolutionFrameratePattern": { + "$comment": "Pattern for resolution with framerate", + "pattern": "^[0-9]+x[0-9]+@[0-9]+$" + } + } +} diff --git a/build-aux/regen-obs-services-json-parser.zsh b/build-aux/regen-obs-services-json-parser.zsh new file mode 100755 index 00000000000000..2956da34632d42 --- /dev/null +++ b/build-aux/regen-obs-services-json-parser.zsh @@ -0,0 +1,149 @@ +#!/usr/bin/env zsh + +builtin emulate -L zsh +setopt EXTENDED_GLOB +setopt PUSHD_SILENT +setopt ERR_EXIT +setopt ERR_RETURN +setopt NO_UNSET +setopt PIPE_FAIL +setopt NO_AUTO_PUSHD +setopt NO_PUSHD_IGNORE_DUPS +setopt FUNCTION_ARGZERO + +## Enable for script debugging +# setopt WARN_CREATE_GLOBAL +# setopt WARN_NESTED_VAR +# setopt XTRACE + +autoload -Uz is-at-least && if ! is-at-least 5.2; then + print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade zsh to fix this issue." + exit 1 +fi + +invoke_quicktype() { + local json_parser_file="plugins/obs-services/generated/services-json.hpp" + + if (( ${+commands[quicktype]} )) { + local quicktype_version=($(quicktype --version)) + + if ! is-at-least 23.0.0 ${quicktype_version[3]}; then + log_error "quicktype is not version 23.x.x (found ${quicktype_version[3]}." + exit 2 + fi + + if is-at-least 24.0.0 ${quicktype_version[3]}; then + log_error "quicktype is more recent than version 23.x.x (found ${quicktype_version[3]})." + exit 2 + fi + + } else { + log_error "No viable quicktype version found (required 23.x.x)" + exit 2 + } + + quicktype_args+=( + ## Main schema and its dependencies + -s schema build-aux/json-schema/obs-services.json + -S build-aux/json-schema/service.json + -S build-aux/json-schema/protocolDefs.json + -S build-aux/json-schema/codecDefs.json + ## Language and language options + -l cpp --no-boost + --code-format with-struct + --type-style pascal-case + --member-style camel-case + --enumerator-style upper-underscore-case + # Global include for Nlohmann JSON + --include-location global-include + # Namespace + --namespace OBSServices + # Main struct type name + -t ServicesJson + ) + + if (( check_only )) { + if (( _loglevel > 1 )) log_info "Checking obs-services JSON parser code..." + + if ! quicktype ${quicktype_args} | diff -q "${json_parser_file}" - &> /dev/null; then + log_error "obs-services JSON parser code does not match JSON schemas." + if (( fail_on_error )) return 2; + else + if (( _loglevel > 1 )) log_status "obs-services JSON parser code matches JSON schemas." + fi + } else { + if ! quicktype ${quicktype_args} -o "${json_parser_file}"; then + log_error "An error occured while generating obs-services JSON parser code." + if (( fail_on_error )) return 2; + fi + } +} + +run_quicktype() { + if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} + + typeset -g host_os=${${(L)$(uname -s)}//darwin/macos} + local -i fail_on_error=0 + local -i check_only=0 + local -i verbosity=1 + local -r _version='1.0.0' + + fpath=("${SCRIPT_HOME}/.functions" ${fpath}) + autoload -Uz set_loglevel log_info log_error log_output log_status log_warning + + local -r _usage=" +Usage: %B${functrace[1]%:*}%b