|
|
@@ -0,0 +1,482 @@
|
|
|
+#include <QUuid>
|
|
|
+
|
|
|
+#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 <nlohmann/json.hpp>
|
|
|
+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<YoutubeApiWrappers *>(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<std::string>();
|
|
|
+
|
|
|
+ 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());
|
|
|
+ }
|
|
|
+}
|