From 81b588137aa76146b8b3faec683d08c478c10e94 Mon Sep 17 00:00:00 2001 From: Yuriy Chumak Date: Mon, 5 Jun 2023 16:16:38 +0300 Subject: [PATCH] UI: Add "YouTube Control Panel" dock panel New dock panel with integrated youtube studio live control room. This commit also modifies CI files. --- UI/cmake/feature-youtube.cmake | 12 +- UI/cmake/legacy.cmake | 12 +- UI/data/locale/en-US.ini | 3 + UI/window-basic-auto-config.cpp | 7 + UI/window-basic-main.cpp | 54 +++- UI/window-basic-main.hpp | 14 +- UI/window-basic-settings-stream.cpp | 18 ++ UI/window-basic-settings.cpp | 20 ++ UI/window-dock-youtube-app.cpp | 482 ++++++++++++++++++++++++++++ UI/window-dock-youtube-app.hpp | 54 ++++ UI/window-youtube-actions.cpp | 36 ++- UI/window-youtube-actions.hpp | 6 +- UI/youtube-api-wrappers.cpp | 13 + UI/youtube-api-wrappers.hpp | 1 + 14 files changed, 712 insertions(+), 20 deletions(-) create mode 100644 UI/window-dock-youtube-app.cpp create mode 100644 UI/window-dock-youtube-app.hpp diff --git a/UI/cmake/feature-youtube.cmake b/UI/cmake/feature-youtube.cmake index 23d460b70..2a1fb8d89 100644 --- a/UI/cmake/feature-youtube.cmake +++ b/UI/cmake/feature-youtube.cmake @@ -2,8 +2,16 @@ 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_sources( + obs-studio + PRIVATE auth-youtube.cpp + auth-youtube.hpp + window-dock-youtube-app.cpp + window-dock-youtube-app.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() diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index d55219ea4..3e5789629 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -330,8 +330,16 @@ 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) + target_sources( + obs + PRIVATE auth-youtube.cpp + auth-youtube.hpp + window-dock-youtube-app.cpp + window-dock-youtube-app.hpp + window-youtube-actions.cpp + window-youtube-actions.hpp + youtube-api-wrappers.cpp + youtube-api-wrappers.hpp) endif() if(OS_WINDOWS) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 6e2316434..8b8f37a9d 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -1526,3 +1526,6 @@ 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." +# Browser Dock +YouTube.DocksRemoval.Title="Clear Legacy YouTube Browser Docks" +YouTube.DocksRemoval.Text="These browser docks will be removed as deprecated:\n\n%1\nUse \"Docks/YouTube Live Control Room\" instead." diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 504606729..1a7a649a7 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -1050,6 +1050,13 @@ void AutoConfig::done(int result) if (type == Type::Streaming) SaveStreamSettings(); SaveSettings(); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) { + OBSBasic *main = OBSBasic::Get(); + main->NewYouTubeAppDock(); + } +#endif } } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index b12735369..4a3d0e6b3 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2170,6 +2170,9 @@ void OBSBasic::OBSInit() /* ----------------------------- */ /* add custom browser docks */ +#if defined(BROWSER_AVAILABLE) && defined(YOUTUBE_ENABLED) + YouTubeAppDock::CleanupYouTubeUrls(); +#endif #ifdef BROWSER_AVAILABLE if (cef) { @@ -2331,6 +2334,12 @@ void OBSBasic::OBSInit() UpdatePreviewProgramIndicators(); OnFirstLoad(); +#ifdef YOUTUBE_ENABLED + /* setup YouTube app dock */ + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock = new YouTubeAppDock(); +#endif + if (!hideWindowOnStart) activateWindow(); @@ -6823,9 +6832,10 @@ void OBSBasic::DisplayStreamStartError() } #ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key, - bool autostart, bool autostop, - bool start_now) +void OBSBasic::YouTubeActionDialogOk(const QString &broadcast_id, + const QString &stream_id, + const QString &key, bool autostart, + bool autostop, bool start_now) { //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); obs_service_t *service_obj = GetService(); @@ -6834,8 +6844,11 @@ void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key, 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()); + const std::string b_id = QT_TO_UTF8(broadcast_id); + obs_data_set_string(settings, "broadcast_id", b_id.c_str()); + + const std::string s_id = QT_TO_UTF8(stream_id); + obs_data_set_string(settings, "stream_id", s_id.c_str()); obs_service_update(service_obj, settings); autoStartBroadcast = autostart; @@ -7419,6 +7432,11 @@ void OBSBasic::StreamingStart() OnActivate(); +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStarted(); +#endif + blog(LOG_INFO, STREAMING_START); } @@ -7499,6 +7517,11 @@ void OBSBasic::StreamingStop(int code, QString last_error) OnDeactivate(); +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected()) + youtubeAppDock->IngestionStopped(); +#endif + blog(LOG_INFO, STREAMING_STOP); if (encode_error) { @@ -8462,6 +8485,27 @@ config_t *OBSBasic::Config() const return basicConfig; } +#ifdef YOUTUBE_ENABLED +YouTubeAppDock *OBSBasic::GetYouTubeAppDock() +{ + return youtubeAppDock; +} + +void OBSBasic::NewYouTubeAppDock() +{ + if (youtubeAppDock) + delete youtubeAppDock; + youtubeAppDock = new YouTubeAppDock(); +} + +void OBSBasic::DeleteYouTubeAppDock() +{ + if (youtubeAppDock) + delete youtubeAppDock; + youtubeAppDock = nullptr; +} +#endif + void OBSBasic::UpdateEditMenu() { QModelIndexList items = GetAllSelectedSourceItems(); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 97bf84cfa..1a1d6be0b 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -36,6 +36,9 @@ #include "window-missing-files.hpp" #include "window-projector.hpp" #include "window-basic-about.hpp" +#ifdef YOUTUBE_ENABLED +#include "window-dock-youtube-app.hpp" +#endif #include "auth-base.hpp" #include "log-viewer.hpp" #include "undo-stack-obs.hpp" @@ -262,6 +265,9 @@ private: QPointer advAudioWindow; QPointer filters; QPointer statsDock; +#ifdef YOUTUBE_ENABLED + QPointer youtubeAppDock; +#endif QPointer about; QPointer missDialog; QPointer logView; @@ -631,7 +637,8 @@ private: #ifdef YOUTUBE_ENABLED void YoutubeStreamCheck(const std::string &key); void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &id, const QString &key, + void YouTubeActionDialogOk(const QString &broadcast_id, + const QString &stream_id, const QString &key, bool autostart, bool autostop, bool start_now); #endif @@ -1253,6 +1260,11 @@ public: const char *file) const override; static void InitBrowserPanelSafeBlock(); +#ifdef YOUTUBE_ENABLED + void NewYouTubeAppDock(); + void DeleteYouTubeAppDock(); + YouTubeAppDock *GetYouTubeAppDock(); +#endif }; class SceneRenameDelegate : public QStyledItemDelegate { diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index abbdc68f7..918d23dca 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -770,6 +770,14 @@ void OBSBasicSettings::on_connectAccount_clicked() auth = OAuthStreamKey::Login(this, service); if (!!auth) { OnAuthConnected(); +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(service)) { + if (!main->GetYouTubeAppDock()) { + main->NewYouTubeAppDock(); + } + main->GetYouTubeAppDock()->AccountConnected(); + } +#endif ui->useStreamKeyAdv->setVisible(false); } @@ -812,6 +820,16 @@ void OBSBasicSettings::on_disconnectAccount_clicked() ui->connectedAccountLabel->setVisible(false); ui->connectedAccountText->setVisible(false); + +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(service)) { + if (!main->GetYouTubeAppDock()) { + main->NewYouTubeAppDock(); + } + main->GetYouTubeAppDock()->AccountDisconnected(); + main->GetYouTubeAppDock()->Update(); + } +#endif } void OBSBasicSettings::on_useStreamKey_clicked() diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index ae2a3478e..77378a7c1 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -48,6 +48,10 @@ #include "window-basic-main-outputs.hpp" #include "window-projector.hpp" +#ifdef YOUTUBE_ENABLED +#include "youtube-api-wrappers.hpp" +#endif + #include #include #include "ui-config.h" @@ -4251,6 +4255,22 @@ void OBSBasicSettings::on_buttonBox_clicked(QAbstractButton *button) return; SaveSettings(); + +#ifdef YOUTUBE_ENABLED + std::string service = ui->service->currentText().toStdString(); + if (IsYouTubeService(service)) { + if (!main->GetYouTubeAppDock()) { + main->NewYouTubeAppDock(); + } + main->GetYouTubeAppDock()->SettingsUpdated( + !IsYouTubeService(service) || stream1Changed); + } else { + if (main->GetYouTubeAppDock()) { + main->GetYouTubeAppDock()->AccountDisconnected(); + } + main->DeleteYouTubeAppDock(); + } +#endif ClearChanged(); } diff --git a/UI/window-dock-youtube-app.cpp b/UI/window-dock-youtube-app.cpp new file mode 100644 index 000000000..24e0c56f9 --- /dev/null +++ b/UI/window-dock-youtube-app.cpp @@ -0,0 +1,482 @@ +#include + +#include "window-basic-main.hpp" +#include "youtube-api-wrappers.hpp" +#include "window-dock-youtube-app.hpp" + +#include "ui-config.h" +#include "qt-wrappers.hpp" + +#include +using json = nlohmann::json; + +#ifdef YOUTUBE_WEBAPP_PLACEHOLDER +static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL = + YOUTUBE_WEBAPP_PLACEHOLDER; +#else +static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL = + "https://studio.youtube.com/live/channel/UC/console?kc=OBS"; +#endif + +#ifdef YOUTUBE_WEBAPP_ADDRESS +static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL = + YOUTUBE_WEBAPP_ADDRESS; +#else +static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL = + "https://studio.youtube.com/live/channel/%1/console?kc=OBS"; +#endif + +static constexpr const char *BROADCAST_CREATED = "BROADCAST_CREATED"; +static constexpr const char *BROADCAST_SELECTED = "BROADCAST_SELECTED"; +static constexpr const char *INGESTION_STARTED = "INGESTION_STARTED"; +static constexpr const char *INGESTION_STOPPED = "INGESTION_STOPPED"; + +YouTubeAppDock::YouTubeAppDock() + : BrowserDock(), + dockBrowser(nullptr), + cookieManager(nullptr) +{ + OBSBasic::InitBrowserPanelSafeBlock(); + AddYouTubeAppDock("YouTube Live Control Panel"); +} + +YouTubeAppDock::~YouTubeAppDock() +{ + if (cookieManager) { + cookieManager->FlushStore(); + delete cookieManager; + } +} + +bool YouTubeAppDock::IsYTServiceSelected() +{ + obs_service_t *service_obj = OBSBasic::Get()->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + const char *service = obs_data_get_string(settings, "service"); + return IsYouTubeService(service); +} + +void YouTubeAppDock::AccountConnected() +{ + channelId.clear(); // renew channel id + UpdateChannelId(); +} + +void YouTubeAppDock::AccountDisconnected() +{ + SettingsUpdated(true); +} + +void YouTubeAppDock::SettingsUpdated(bool cleanup) +{ + bool ytservice = IsYTServiceSelected(); + SetVisibleYTAppDockInMenu(ytservice); + + // definitely cleanup if YT switched off + if (!ytservice || cleanup) { + if (cookieManager) + cookieManager->DeleteCookies("", ""); + } + if (ytservice) + Update(); +} + +std::string YouTubeAppDock::InitYTUserUrl() +{ + std::string user_url(YOUTUBE_WEBAPP_PLACEHOLDER_URL); + + if (IsUserSignedIntoYT()) { + YoutubeApiWrappers *apiYouTube = GetYTApi(); + if (apiYouTube) { + ChannelDescription channel_description; + if (apiYouTube->GetChannelDescription( + channel_description)) { + QString url = + QString(YOUTUBE_WEBAPP_ADDRESS_URL) + .arg(channel_description.id); + user_url = url.toStdString(); + } else { + blog(LOG_ERROR, + "YT: InitYTUserUrl() Failed to get channel id"); + } + } + } else { + blog(LOG_ERROR, "YT: InitYTUserUrl() User is not signed"); + } + + blog(LOG_DEBUG, "YT: InitYTUserUrl() User url: %s", user_url.c_str()); + return user_url; +} + +void YouTubeAppDock::AddYouTubeAppDock(const QString &title) +{ + QString bId(QUuid::createUuid().toString()); + bId.replace(QRegularExpression("[{}-]"), ""); + this->setProperty("uuid", bId); + this->setObjectName(title + "Object"); + this->resize(580, 500); + this->setMinimumSize(400, 300); + this->setWindowTitle(title); + this->setAllowedAreas(Qt::AllDockWidgetAreas); + + OBSBasic::Get()->addDockWidget(Qt::RightDockWidgetArea, this); + actionYTAppDock = OBSBasic::Get()->AddDockWidget(this); + + if (IsYTServiceSelected()) { + const std::string url = InitYTUserUrl(); + CreateBrowserWidget(url); + + // reload panel layout + const char *dockStateStr = config_get_string( + App()->GlobalConfig(), "BasicWindow", "DockState"); + if (dockStateStr) { + QByteArray dockState = QByteArray::fromBase64( + QByteArray(dockStateStr)); + OBSBasic::Get()->restoreState(dockState); + } + } else { + this->setVisible(false); + actionYTAppDock->setVisible(false); + } +} + +void YouTubeAppDock::CreateBrowserWidget(const std::string &url) +{ + std::string dir_name = std::string("obs_profile_cookies_youtube/") + + config_get_string(OBSBasic::Get()->Config(), + "Panels", "CookieId"); + if (cookieManager) + delete cookieManager; + cookieManager = cef->create_cookie_manager(dir_name, true); + + if (dockBrowser) + delete dockBrowser; + dockBrowser = cef->create_widget(this, url, cookieManager); + if (!dockBrowser) + return; + + if (obs_browser_qcef_version() >= 1) + dockBrowser->allowAllPopups(true); + + this->SetWidget(dockBrowser); + Update(); +} + +void YouTubeAppDock::SetVisibleYTAppDockInMenu(bool visible) +{ + if (!actionYTAppDock) + return; + + actionYTAppDock->setVisible(visible); + this->setVisible(visible); +} + +// only 'ACCOUNT' mode supported +void YouTubeAppDock::BroadcastCreated(const char *stream_id) +{ + DispatchYTEvent(BROADCAST_CREATED, stream_id, YTSM_ACCOUNT); +} + +// only 'ACCOUNT' mode supported +void YouTubeAppDock::BroadcastSelected(const char *stream_id) +{ + DispatchYTEvent(BROADCAST_SELECTED, stream_id, YTSM_ACCOUNT); +} + +// both 'ACCOUNT' and 'STREAM_KEY' modes supported +void YouTubeAppDock::IngestionStarted() +{ + obs_service_t *service_obj = OBSBasic::Get()->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + const char *service = obs_data_get_string(settings, "service"); + if (IsYouTubeService(service)) { + if (IsUserSignedIntoYT()) { + const char *broadcast_id = + obs_data_get_string(settings, "broadcast_id"); + this->IngestionStarted(broadcast_id, + YouTubeAppDock::YTSM_ACCOUNT); + } else { + const char *stream_key = + obs_data_get_string(settings, "key"); + this->IngestionStarted(stream_key, + YouTubeAppDock::YTSM_STREAM_KEY); + } + } +} + +void YouTubeAppDock::IngestionStarted(const char *stream_id, + streaming_mode_t mode) +{ + DispatchYTEvent(INGESTION_STARTED, stream_id, mode); +} + +// both 'ACCOUNT' and 'STREAM_KEY' modes supported +void YouTubeAppDock::IngestionStopped() +{ + obs_service_t *service_obj = OBSBasic::Get()->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service_obj); + const char *service = obs_data_get_string(settings, "service"); + + if (IsYouTubeService(service)) { + if (IsUserSignedIntoYT()) { + const char *broadcast_id = + obs_data_get_string(settings, "broadcast_id"); + this->IngestionStopped(broadcast_id, + YouTubeAppDock::YTSM_ACCOUNT); + } else { + const char *stream_key = + obs_data_get_string(settings, "key"); + this->IngestionStopped(stream_key, + YouTubeAppDock::YTSM_STREAM_KEY); + } + } +} + +void YouTubeAppDock::IngestionStopped(const char *stream_id, + streaming_mode_t mode) +{ + DispatchYTEvent(INGESTION_STOPPED, stream_id, mode); +} + +void YouTubeAppDock::showEvent(QShowEvent *) +{ + if (!dockBrowser) + Update(); +} + +void YouTubeAppDock::closeEvent(QCloseEvent *event) +{ + BrowserDock::closeEvent(event); + this->SetWidget(nullptr); +} + +void YouTubeAppDock::DispatchYTEvent(const char *event, const char *video_id, + streaming_mode_t mode) +{ + if (!dockBrowser) + return; + + // update channelId if empty: + UpdateChannelId(); + + // notify YouTube Live Streaming API: + std::string script; + if (mode == YTSM_ACCOUNT) { + script = QString(R"""( + if (window.location.hostname == 'studio.youtube.com') { + let event = { + type: '%1', + channelId: '%2', + videoId: '%3', + }; + console.log(event); + if (window.ytlsapi && window.ytlsapi.dispatchEvent) + window.ytlsapi.dispatchEvent(event); + } + )""") + .arg(event) + .arg(channelId) + .arg(video_id) + .toStdString(); + } else { + const char *stream_key = video_id; + script = QString(R"""( + if (window.location.hostname == 'studio.youtube.com') { + let event = { + type: '%1', + streamKey: '%2', + }; + console.log(event); + if (window.ytlsapi && window.ytlsapi.dispatchEvent) + window.ytlsapi.dispatchEvent(event); + } + )""") + .arg(event) + .arg(stream_key) + .toStdString(); + } + dockBrowser->executeJavaScript(script); + + // in case of user still not logged in in dock panel, remember last event + SetInitEvent(mode, event, video_id, channelId.toStdString().c_str()); +} + +void YouTubeAppDock::Update() +{ + std::string url = InitYTUserUrl(); + + if (!dockBrowser) { + CreateBrowserWidget(url); + } else { + dockBrowser->setURL(url); + } + + // if streaming already run, let's notify YT about past event + if (OBSBasic::Get()->StreamingActive()) { + obs_service_t *service_obj = OBSBasic::Get()->GetService(); + OBSDataAutoRelease settings = + obs_service_get_settings(service_obj); + if (IsUserSignedIntoYT()) { + channelId.clear(); // renew channelId + UpdateChannelId(); + const char *broadcast_id = + obs_data_get_string(settings, "broadcast_id"); + SetInitEvent(YTSM_ACCOUNT, INGESTION_STARTED, + broadcast_id, + channelId.toStdString().c_str()); + } else { + const char *stream_key = + obs_data_get_string(settings, "key"); + SetInitEvent(YTSM_STREAM_KEY, INGESTION_STARTED, + stream_key); + } + } else { + SetInitEvent(IsUserSignedIntoYT() ? YTSM_ACCOUNT + : YTSM_STREAM_KEY); + } + + dockBrowser->reloadPage(); +} + +void YouTubeAppDock::UpdateChannelId() +{ + if (channelId.isEmpty()) { + YoutubeApiWrappers *apiYouTube = GetYTApi(); + if (apiYouTube) { + ChannelDescription channel_description; + if (apiYouTube->GetChannelDescription( + channel_description)) { + channelId = channel_description.id; + } else { + blog(LOG_ERROR, "YT: AccountConnected() Failed " + "to get channel id"); + } + } + } +} + +void YouTubeAppDock::SetInitEvent(streaming_mode_t mode, const char *event, + const char *video_id, const char *channelId) +{ + const std::string version = App()->GetVersionString(); + + QString api_event; + if (event) { + if (mode == YTSM_ACCOUNT) { + api_event = QString(R"""(, + initEvent: { + type: '%1', + channelId: '%2', + videoId: '%3', + } + )""") + .arg(event) + .arg(channelId) + .arg(video_id); + } else { + api_event = QString(R"""(, + initEvent: { + type: '%1', + streamKey: '%2', + } + )""") + .arg(event) + .arg(video_id); + } + } + + std::string script = QString(R"""( + let obs_name = '%1'; + let obs_version = '%2'; + let client_mode = %3; + if (window.location.hostname == 'studio.youtube.com') { + console.log("name:", obs_name); + console.log("version:", obs_version); + console.log("initEvent:", { + initClientMode: client_mode + %4 }); + if (window.ytlsapi && window.ytlsapi.init) + window.ytlsapi.init(obs_name, obs_version, undefined, { + initClientMode: client_mode + %4 }); + } + )""") + .arg("OBS") + .arg(version.c_str()) + .arg(mode == YTSM_ACCOUNT ? "'ACCOUNT'" + : "'STREAM_KEY'") + .arg(api_event) + .toStdString(); + dockBrowser->setStartupScript(script); +} + +YoutubeApiWrappers *YouTubeAppDock::GetYTApi() +{ + Auth *auth = OBSBasic::Get()->GetAuth(); + if (auth) { + YoutubeApiWrappers *apiYouTube( + dynamic_cast(auth)); + if (apiYouTube) { + return apiYouTube; + } else { + blog(LOG_ERROR, + "YT: GetYTApi() Failed to get YoutubeApiWrappers"); + } + } else { + blog(LOG_ERROR, "YT: GetYTApi() Failed to get Auth"); + } + return nullptr; +} + +void YouTubeAppDock::CleanupYouTubeUrls() +{ + static constexpr const char *YOUTUBE_VIDEO_URL = + "://studio.youtube.com/video/"; + // remove legacy YouTube Browser Docks (once) + + bool youtube_cleanup_done = config_get_bool( + App()->GlobalConfig(), "General", "YtDockCleanupDone"); + + if (youtube_cleanup_done) + return; + + config_set_bool(App()->GlobalConfig(), "General", "YtDockCleanupDone", + true); + + const char *jsonStr = config_get_string( + App()->GlobalConfig(), "BasicWindow", "ExtraBrowserDocks"); + if (!jsonStr) + return; + + json array = json::parse(jsonStr); + if (!array.is_array()) + return; + + json save_array; + std::string removedYTUrl; + + for (json &item : array) { + auto url = item["url"].get(); + + if (url.find(YOUTUBE_VIDEO_URL) != std::string::npos) { + blog(LOG_DEBUG, "YT: found legacy url: %s", + url.c_str()); + removedYTUrl += url; + removedYTUrl += ";\n"; + } else { + save_array.push_back(item); + } + } + + if (!removedYTUrl.empty()) { + const QString msg_title = QTStr("YouTube.DocksRemoval.Title"); + const QString msg_text = + QTStr("YouTube.DocksRemoval.Text") + .arg(QT_UTF8(removedYTUrl.c_str())); + OBSMessageBox::warning(OBSBasic::Get(), msg_title, msg_text); + + std::string output = save_array.dump(); + config_set_string(App()->GlobalConfig(), "BasicWindow", + "ExtraBrowserDocks", output.c_str()); + } +} diff --git a/UI/window-dock-youtube-app.hpp b/UI/window-dock-youtube-app.hpp new file mode 100644 index 000000000..3a45fc9b7 --- /dev/null +++ b/UI/window-dock-youtube-app.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "window-dock-browser.hpp" +#include "youtube-api-wrappers.hpp" + +class QAction; +class QCefWidget; + +class YouTubeAppDock : public BrowserDock { + Q_OBJECT + +public: + YouTubeAppDock(); + ~YouTubeAppDock(); + + enum streaming_mode_t { YTSM_ACCOUNT, YTSM_STREAM_KEY }; + + void AccountConnected(); + void AccountDisconnected(); + void SettingsUpdated(bool cleanup = false); + void Update(); + + void BroadcastCreated(const char *stream_id); + void BroadcastSelected(const char *stream_id); + void IngestionStarted(); + void IngestionStopped(); + + static bool IsYTServiceSelected(); + static YoutubeApiWrappers *GetYTApi(); + static void CleanupYouTubeUrls(); + +protected: + void IngestionStarted(const char *stream_id, streaming_mode_t mode); + void IngestionStopped(const char *stream_id, streaming_mode_t mode); + +private: + std::string InitYTUserUrl(); + void SetVisibleYTAppDockInMenu(bool visible); + void AddYouTubeAppDock(const QString &title); + void CreateBrowserWidget(const std::string &url); + virtual void showEvent(QShowEvent *event) override; + virtual void closeEvent(QCloseEvent *event) override; + void DispatchYTEvent(const char *event, const char *video_id, + streaming_mode_t mode); + void UpdateChannelId(); + void SetInitEvent(streaming_mode_t mode, const char *event = nullptr, + const char *video_id = nullptr, + const char *channelId = nullptr); + + QString channelId; + QPointer dockBrowser; + QCefCookieManager *cookieManager; // is not a Qt object + QPointer actionYTAppDock; +}; diff --git a/UI/window-youtube-actions.cpp b/UI/window-youtube-actions.cpp index 1f99ac02c..e0b27cef1 100644 --- a/UI/window-youtube-actions.cpp +++ b/UI/window-youtube-actions.cpp @@ -444,12 +444,12 @@ void OBSYoutubeActions::UpdateOkButtonStatus() } } bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, + BroadcastDescription &broadcast, StreamDescription &stream, bool stream_later, bool ready_broadcast) { YoutubeApiWrappers *apiYouTube = api; - BroadcastDescription broadcast = {}; UiToBroadcast(broadcast); if (stream_later) { @@ -513,6 +513,12 @@ bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, } } +#ifdef YOUTUBE_ENABLED + if (OBSBasic::Get()->GetYouTubeAppDock()) + OBSBasic::Get()->GetYouTubeAppDock()->BroadcastCreated( + broadcast.id.toStdString().c_str()); +#endif + return true; } @@ -568,6 +574,12 @@ bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api, else apiYouTube->ResetChat(); +#ifdef YOUTUBE_ENABLED + if (OBSBasic::Get()->GetYouTubeAppDock()) + OBSBasic::Get()->GetYouTubeAppDock()->BroadcastSelected( + selectedBroadcast.toStdString().c_str()); +#endif + return true; } @@ -585,6 +597,7 @@ void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text) void OBSYoutubeActions::InitBroadcast() { + BroadcastDescription broadcast; StreamDescription stream; QMessageBox msgBox(this); msgBox.setWindowFlags(msgBox.windowFlags() & @@ -597,10 +610,12 @@ void OBSYoutubeActions::InitBroadcast() auto action = [&]() { if (ui->tabWidget->currentIndex() == 0) { success = this->CreateEventAction( - apiYouTube, stream, + apiYouTube, broadcast, stream, ui->checkScheduledLater->isChecked()); } else { success = this->ChooseAnEventAction(apiYouTube, stream); + if (success) + broadcast.id = this->selectedBroadcast; }; QMetaObject::invokeMethod(&msgBox, "accept", Qt::QueuedConnection); @@ -627,15 +642,17 @@ void OBSYoutubeActions::InitBroadcast() // Stream now usecase. blog(LOG_DEBUG, "New valid stream: %s", QT_TO_UTF8(stream.name)); - emit ok(QT_TO_UTF8(stream.id), + emit ok(QT_TO_UTF8(broadcast.id), + 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); + emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id), + QT_TO_UTF8(stream.name), autostart, autostop, + true); Accept(); } } else { @@ -654,6 +671,7 @@ void OBSYoutubeActions::InitBroadcast() void OBSYoutubeActions::ReadyBroadcast() { + BroadcastDescription broadcast; StreamDescription stream; QMessageBox msgBox(this); msgBox.setWindowFlags(msgBox.windowFlags() & @@ -666,10 +684,12 @@ void OBSYoutubeActions::ReadyBroadcast() auto action = [&]() { if (ui->tabWidget->currentIndex() == 0) { success = this->CreateEventAction( - apiYouTube, stream, + apiYouTube, broadcast, stream, ui->checkScheduledLater->isChecked(), true); } else { success = this->ChooseAnEventAction(apiYouTube, stream); + if (success) + broadcast.id = this->selectedBroadcast; }; QMetaObject::invokeMethod(&msgBox, "accept", Qt::QueuedConnection); @@ -680,8 +700,8 @@ void OBSYoutubeActions::ReadyBroadcast() thread->wait(); if (success) { - emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name), - autostart, autostop, false); + emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id), + QT_TO_UTF8(stream.name), autostart, autostop, false); Accept(); } else { // Fail. diff --git a/UI/window-youtube-actions.hpp b/UI/window-youtube-actions.hpp index 18cbf0b8a..d74001465 100644 --- a/UI/window-youtube-actions.hpp +++ b/UI/window-youtube-actions.hpp @@ -36,14 +36,16 @@ class OBSYoutubeActions : public QDialog { std::unique_ptr ui; signals: - void ok(const QString &id, const QString &key, bool autostart, - bool autostop, bool start_now); + void ok(const QString &broadcast_id, const QString &stream_id, + const QString &key, bool autostart, bool autostop, + bool start_now); protected: void showEvent(QShowEvent *event) override; void UpdateOkButtonStatus(); bool CreateEventAction(YoutubeApiWrappers *api, + BroadcastDescription &broadcast, StreamDescription &stream, bool stream_later, bool ready_broadcast = false); bool ChooseAnEventAction(YoutubeApiWrappers *api, diff --git a/UI/youtube-api-wrappers.cpp b/UI/youtube-api-wrappers.cpp index 876ecad4f..2aaa78afc 100644 --- a/UI/youtube-api-wrappers.cpp +++ b/UI/youtube-api-wrappers.cpp @@ -9,6 +9,7 @@ #include "auth-youtube.hpp" #include "obs-app.hpp" +#include "window-basic-main.hpp" #include "qt-wrappers.hpp" #include "remote-text.hpp" #include "ui-config.h" @@ -45,6 +46,18 @@ bool IsYouTubeService(const std::string &service) }); return it != youtubeServices.end(); } +bool IsUserSignedIntoYT() +{ + Auth *auth = OBSBasic::Get()->GetAuth(); + if (auth) { + YoutubeApiWrappers *apiYouTube( + dynamic_cast(auth)); + if (apiYouTube) { + return true; + } + } + return false; +} bool YoutubeApiWrappers::GetTranslatedError(QString &error_message) { diff --git a/UI/youtube-api-wrappers.hpp b/UI/youtube-api-wrappers.hpp index dcabc7020..5e6b7f534 100644 --- a/UI/youtube-api-wrappers.hpp +++ b/UI/youtube-api-wrappers.hpp @@ -38,6 +38,7 @@ struct BroadcastDescription { }; bool IsYouTubeService(const std::string &service); +bool IsUserSignedIntoYT(); class YoutubeApiWrappers : public YoutubeAuth { Q_OBJECT