123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- #include <QUuid>
- #include "window-basic-main.hpp"
- #include "youtube-api-wrappers.hpp"
- #include "moc_window-dock-youtube-app.cpp"
- #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(const QString &title) : BrowserDock(title), dockBrowser(nullptr)
- {
- cef->init_browser();
- OBSBasic::InitBrowserPanelSafeBlock();
- AddYouTubeAppDock();
- }
- bool YouTubeAppDock::IsYTServiceSelected()
- {
- if (!cef_js_avail)
- return false;
- 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 (panel_cookies) {
- panel_cookies->DeleteCookies("youtube.com", "");
- panel_cookies->DeleteCookies("google.com", "");
- }
- }
- 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()
- {
- QString bId(QUuid::createUuid().toString());
- bId.replace(QRegularExpression("[{}-]"), "");
- this->setProperty("uuid", bId);
- this->setObjectName("youtubeLiveControlPanel");
- this->resize(580, 500);
- this->setMinimumSize(400, 300);
- this->setAllowedAreas(Qt::AllDockWidgetAreas);
- OBSBasic::Get()->AddDockWidget(this, Qt::RightDockWidgetArea);
- if (IsYTServiceSelected()) {
- const std::string url = InitYTUserUrl();
- CreateBrowserWidget(url);
- } else {
- this->setVisible(false);
- this->toggleViewAction()->setVisible(false);
- }
- }
- void YouTubeAppDock::CreateBrowserWidget(const std::string &url)
- {
- if (dockBrowser)
- delete dockBrowser;
- dockBrowser = cef->create_widget(this, url, panel_cookies);
- if (!dockBrowser)
- return;
- if (obs_browser_qcef_version() >= 1)
- dockBrowser->allowAllPopups(true);
- this->SetWidget(dockBrowser);
- QWidget::connect(dockBrowser.get(), &QCefWidget::urlChanged, this, &YouTubeAppDock::ReloadChatDock);
- Update();
- }
- void YouTubeAppDock::SetVisibleYTAppDockInMenu(bool visible)
- {
- if (visible && toggleViewAction()->isVisible())
- return;
- toggleViewAction()->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::ReloadChatDock()
- {
- if (IsUserSignedIntoYT()) {
- YoutubeApiWrappers *apiYouTube = GetYTApi();
- if (apiYouTube) {
- apiYouTube->ReloadChat();
- }
- }
- }
- 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()
- {
- if (!cef_js_avail)
- return;
- 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()->GetUserConfig(), "General", "YtDockCleanupDone");
- if (youtube_cleanup_done)
- return;
- config_set_bool(App()->GetUserConfig(), "General", "YtDockCleanupDone", true);
- const char *jsonStr = config_get_string(App()->GetUserConfig(), "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()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str());
- }
- }
|