window-dock-youtube-app.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. #include <QUuid>
  2. #include "window-basic-main.hpp"
  3. #include "youtube-api-wrappers.hpp"
  4. #include "moc_window-dock-youtube-app.cpp"
  5. #include "ui-config.h"
  6. #include "qt-wrappers.hpp"
  7. #include <nlohmann/json.hpp>
  8. using json = nlohmann::json;
  9. #ifdef YOUTUBE_WEBAPP_PLACEHOLDER
  10. static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL = YOUTUBE_WEBAPP_PLACEHOLDER;
  11. #else
  12. static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL =
  13. "https://studio.youtube.com/live/channel/UC/console?kc=OBS";
  14. #endif
  15. #ifdef YOUTUBE_WEBAPP_ADDRESS
  16. static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL = YOUTUBE_WEBAPP_ADDRESS;
  17. #else
  18. static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL = "https://studio.youtube.com/live/channel/%1/console?kc=OBS";
  19. #endif
  20. static constexpr const char *BROADCAST_CREATED = "BROADCAST_CREATED";
  21. static constexpr const char *BROADCAST_SELECTED = "BROADCAST_SELECTED";
  22. static constexpr const char *INGESTION_STARTED = "INGESTION_STARTED";
  23. static constexpr const char *INGESTION_STOPPED = "INGESTION_STOPPED";
  24. YouTubeAppDock::YouTubeAppDock(const QString &title) : BrowserDock(title), dockBrowser(nullptr)
  25. {
  26. cef->init_browser();
  27. OBSBasic::InitBrowserPanelSafeBlock();
  28. AddYouTubeAppDock();
  29. }
  30. bool YouTubeAppDock::IsYTServiceSelected()
  31. {
  32. if (!cef_js_avail)
  33. return false;
  34. obs_service_t *service_obj = OBSBasic::Get()->GetService();
  35. OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
  36. const char *service = obs_data_get_string(settings, "service");
  37. return IsYouTubeService(service);
  38. }
  39. void YouTubeAppDock::AccountConnected()
  40. {
  41. channelId.clear(); // renew channel id
  42. UpdateChannelId();
  43. }
  44. void YouTubeAppDock::AccountDisconnected()
  45. {
  46. SettingsUpdated(true);
  47. }
  48. void YouTubeAppDock::SettingsUpdated(bool cleanup)
  49. {
  50. bool ytservice = IsYTServiceSelected();
  51. SetVisibleYTAppDockInMenu(ytservice);
  52. // definitely cleanup if YT switched off
  53. if (!ytservice || cleanup) {
  54. if (panel_cookies) {
  55. panel_cookies->DeleteCookies("youtube.com", "");
  56. panel_cookies->DeleteCookies("google.com", "");
  57. }
  58. }
  59. if (ytservice)
  60. Update();
  61. }
  62. std::string YouTubeAppDock::InitYTUserUrl()
  63. {
  64. std::string user_url(YOUTUBE_WEBAPP_PLACEHOLDER_URL);
  65. if (IsUserSignedIntoYT()) {
  66. YoutubeApiWrappers *apiYouTube = GetYTApi();
  67. if (apiYouTube) {
  68. ChannelDescription channel_description;
  69. if (apiYouTube->GetChannelDescription(channel_description)) {
  70. QString url = QString(YOUTUBE_WEBAPP_ADDRESS_URL).arg(channel_description.id);
  71. user_url = url.toStdString();
  72. } else {
  73. blog(LOG_ERROR, "YT: InitYTUserUrl() Failed to get channel id");
  74. }
  75. }
  76. } else {
  77. blog(LOG_ERROR, "YT: InitYTUserUrl() User is not signed");
  78. }
  79. blog(LOG_DEBUG, "YT: InitYTUserUrl() User url: %s", user_url.c_str());
  80. return user_url;
  81. }
  82. void YouTubeAppDock::AddYouTubeAppDock()
  83. {
  84. QString bId(QUuid::createUuid().toString());
  85. bId.replace(QRegularExpression("[{}-]"), "");
  86. this->setProperty("uuid", bId);
  87. this->setObjectName("youtubeLiveControlPanel");
  88. this->resize(580, 500);
  89. this->setMinimumSize(400, 300);
  90. this->setAllowedAreas(Qt::AllDockWidgetAreas);
  91. OBSBasic::Get()->AddDockWidget(this, Qt::RightDockWidgetArea);
  92. if (IsYTServiceSelected()) {
  93. const std::string url = InitYTUserUrl();
  94. CreateBrowserWidget(url);
  95. } else {
  96. this->setVisible(false);
  97. this->toggleViewAction()->setVisible(false);
  98. }
  99. }
  100. void YouTubeAppDock::CreateBrowserWidget(const std::string &url)
  101. {
  102. if (dockBrowser)
  103. delete dockBrowser;
  104. dockBrowser = cef->create_widget(this, url, panel_cookies);
  105. if (!dockBrowser)
  106. return;
  107. if (obs_browser_qcef_version() >= 1)
  108. dockBrowser->allowAllPopups(true);
  109. this->SetWidget(dockBrowser);
  110. QWidget::connect(dockBrowser.get(), &QCefWidget::urlChanged, this, &YouTubeAppDock::ReloadChatDock);
  111. Update();
  112. }
  113. void YouTubeAppDock::SetVisibleYTAppDockInMenu(bool visible)
  114. {
  115. if (visible && toggleViewAction()->isVisible())
  116. return;
  117. toggleViewAction()->setVisible(visible);
  118. this->setVisible(visible);
  119. }
  120. // only 'ACCOUNT' mode supported
  121. void YouTubeAppDock::BroadcastCreated(const char *stream_id)
  122. {
  123. DispatchYTEvent(BROADCAST_CREATED, stream_id, YTSM_ACCOUNT);
  124. }
  125. // only 'ACCOUNT' mode supported
  126. void YouTubeAppDock::BroadcastSelected(const char *stream_id)
  127. {
  128. DispatchYTEvent(BROADCAST_SELECTED, stream_id, YTSM_ACCOUNT);
  129. }
  130. // both 'ACCOUNT' and 'STREAM_KEY' modes supported
  131. void YouTubeAppDock::IngestionStarted()
  132. {
  133. obs_service_t *service_obj = OBSBasic::Get()->GetService();
  134. OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
  135. const char *service = obs_data_get_string(settings, "service");
  136. if (IsYouTubeService(service)) {
  137. if (IsUserSignedIntoYT()) {
  138. const char *broadcast_id = obs_data_get_string(settings, "broadcast_id");
  139. this->IngestionStarted(broadcast_id, YouTubeAppDock::YTSM_ACCOUNT);
  140. } else {
  141. const char *stream_key = obs_data_get_string(settings, "key");
  142. this->IngestionStarted(stream_key, YouTubeAppDock::YTSM_STREAM_KEY);
  143. }
  144. }
  145. }
  146. void YouTubeAppDock::IngestionStarted(const char *stream_id, streaming_mode_t mode)
  147. {
  148. DispatchYTEvent(INGESTION_STARTED, stream_id, mode);
  149. }
  150. // both 'ACCOUNT' and 'STREAM_KEY' modes supported
  151. void YouTubeAppDock::IngestionStopped()
  152. {
  153. obs_service_t *service_obj = OBSBasic::Get()->GetService();
  154. OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
  155. const char *service = obs_data_get_string(settings, "service");
  156. if (IsYouTubeService(service)) {
  157. if (IsUserSignedIntoYT()) {
  158. const char *broadcast_id = obs_data_get_string(settings, "broadcast_id");
  159. this->IngestionStopped(broadcast_id, YouTubeAppDock::YTSM_ACCOUNT);
  160. } else {
  161. const char *stream_key = obs_data_get_string(settings, "key");
  162. this->IngestionStopped(stream_key, YouTubeAppDock::YTSM_STREAM_KEY);
  163. }
  164. }
  165. }
  166. void YouTubeAppDock::IngestionStopped(const char *stream_id, streaming_mode_t mode)
  167. {
  168. DispatchYTEvent(INGESTION_STOPPED, stream_id, mode);
  169. }
  170. void YouTubeAppDock::showEvent(QShowEvent *)
  171. {
  172. if (!dockBrowser)
  173. Update();
  174. }
  175. void YouTubeAppDock::closeEvent(QCloseEvent *event)
  176. {
  177. BrowserDock::closeEvent(event);
  178. this->SetWidget(nullptr);
  179. }
  180. void YouTubeAppDock::DispatchYTEvent(const char *event, const char *video_id, streaming_mode_t mode)
  181. {
  182. if (!dockBrowser)
  183. return;
  184. // update channelId if empty:
  185. UpdateChannelId();
  186. // notify YouTube Live Streaming API:
  187. std::string script;
  188. if (mode == YTSM_ACCOUNT) {
  189. script = QString(R"""(
  190. if (window.location.hostname == 'studio.youtube.com') {
  191. let event = {
  192. type: '%1',
  193. channelId: '%2',
  194. videoId: '%3',
  195. };
  196. console.log(event);
  197. if (window.ytlsapi && window.ytlsapi.dispatchEvent)
  198. window.ytlsapi.dispatchEvent(event);
  199. }
  200. )""")
  201. .arg(event)
  202. .arg(channelId)
  203. .arg(video_id)
  204. .toStdString();
  205. } else {
  206. const char *stream_key = video_id;
  207. script = QString(R"""(
  208. if (window.location.hostname == 'studio.youtube.com') {
  209. let event = {
  210. type: '%1',
  211. streamKey: '%2',
  212. };
  213. console.log(event);
  214. if (window.ytlsapi && window.ytlsapi.dispatchEvent)
  215. window.ytlsapi.dispatchEvent(event);
  216. }
  217. )""")
  218. .arg(event)
  219. .arg(stream_key)
  220. .toStdString();
  221. }
  222. dockBrowser->executeJavaScript(script);
  223. // in case of user still not logged in in dock panel, remember last event
  224. SetInitEvent(mode, event, video_id, channelId.toStdString().c_str());
  225. }
  226. void YouTubeAppDock::Update()
  227. {
  228. std::string url = InitYTUserUrl();
  229. if (!dockBrowser) {
  230. CreateBrowserWidget(url);
  231. } else {
  232. dockBrowser->setURL(url);
  233. }
  234. // if streaming already run, let's notify YT about past event
  235. if (OBSBasic::Get()->StreamingActive()) {
  236. obs_service_t *service_obj = OBSBasic::Get()->GetService();
  237. OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
  238. if (IsUserSignedIntoYT()) {
  239. channelId.clear(); // renew channelId
  240. UpdateChannelId();
  241. const char *broadcast_id = obs_data_get_string(settings, "broadcast_id");
  242. SetInitEvent(YTSM_ACCOUNT, INGESTION_STARTED, broadcast_id, channelId.toStdString().c_str());
  243. } else {
  244. const char *stream_key = obs_data_get_string(settings, "key");
  245. SetInitEvent(YTSM_STREAM_KEY, INGESTION_STARTED, stream_key);
  246. }
  247. } else {
  248. SetInitEvent(IsUserSignedIntoYT() ? YTSM_ACCOUNT : YTSM_STREAM_KEY);
  249. }
  250. dockBrowser->reloadPage();
  251. }
  252. void YouTubeAppDock::UpdateChannelId()
  253. {
  254. if (channelId.isEmpty()) {
  255. YoutubeApiWrappers *apiYouTube = GetYTApi();
  256. if (apiYouTube) {
  257. ChannelDescription channel_description;
  258. if (apiYouTube->GetChannelDescription(channel_description)) {
  259. channelId = channel_description.id;
  260. } else {
  261. blog(LOG_ERROR, "YT: AccountConnected() Failed "
  262. "to get channel id");
  263. }
  264. }
  265. }
  266. }
  267. void YouTubeAppDock::ReloadChatDock()
  268. {
  269. if (IsUserSignedIntoYT()) {
  270. YoutubeApiWrappers *apiYouTube = GetYTApi();
  271. if (apiYouTube) {
  272. apiYouTube->ReloadChat();
  273. }
  274. }
  275. }
  276. void YouTubeAppDock::SetInitEvent(streaming_mode_t mode, const char *event, const char *video_id, const char *channelId)
  277. {
  278. const std::string version = App()->GetVersionString();
  279. QString api_event;
  280. if (event) {
  281. if (mode == YTSM_ACCOUNT) {
  282. api_event = QString(R"""(,
  283. initEvent: {
  284. type: '%1',
  285. channelId: '%2',
  286. videoId: '%3',
  287. }
  288. )""")
  289. .arg(event)
  290. .arg(channelId)
  291. .arg(video_id);
  292. } else {
  293. api_event = QString(R"""(,
  294. initEvent: {
  295. type: '%1',
  296. streamKey: '%2',
  297. }
  298. )""")
  299. .arg(event)
  300. .arg(video_id);
  301. }
  302. }
  303. std::string script = QString(R"""(
  304. let obs_name = '%1';
  305. let obs_version = '%2';
  306. let client_mode = %3;
  307. if (window.location.hostname == 'studio.youtube.com') {
  308. console.log("name:", obs_name);
  309. console.log("version:", obs_version);
  310. console.log("initEvent:", {
  311. initClientMode: client_mode
  312. %4 });
  313. if (window.ytlsapi && window.ytlsapi.init)
  314. window.ytlsapi.init(obs_name, obs_version, undefined, {
  315. initClientMode: client_mode
  316. %4 });
  317. }
  318. )""")
  319. .arg("OBS")
  320. .arg(version.c_str())
  321. .arg(mode == YTSM_ACCOUNT ? "'ACCOUNT'" : "'STREAM_KEY'")
  322. .arg(api_event)
  323. .toStdString();
  324. dockBrowser->setStartupScript(script);
  325. }
  326. YoutubeApiWrappers *YouTubeAppDock::GetYTApi()
  327. {
  328. Auth *auth = OBSBasic::Get()->GetAuth();
  329. if (auth) {
  330. YoutubeApiWrappers *apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth));
  331. if (apiYouTube) {
  332. return apiYouTube;
  333. } else {
  334. blog(LOG_ERROR, "YT: GetYTApi() Failed to get YoutubeApiWrappers");
  335. }
  336. } else {
  337. blog(LOG_ERROR, "YT: GetYTApi() Failed to get Auth");
  338. }
  339. return nullptr;
  340. }
  341. void YouTubeAppDock::CleanupYouTubeUrls()
  342. {
  343. if (!cef_js_avail)
  344. return;
  345. static constexpr const char *YOUTUBE_VIDEO_URL = "://studio.youtube.com/video/";
  346. // remove legacy YouTube Browser Docks (once)
  347. bool youtube_cleanup_done = config_get_bool(App()->GetUserConfig(), "General", "YtDockCleanupDone");
  348. if (youtube_cleanup_done)
  349. return;
  350. config_set_bool(App()->GetUserConfig(), "General", "YtDockCleanupDone", true);
  351. const char *jsonStr = config_get_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks");
  352. if (!jsonStr)
  353. return;
  354. json array = json::parse(jsonStr);
  355. if (!array.is_array())
  356. return;
  357. json save_array;
  358. std::string removedYTUrl;
  359. for (json &item : array) {
  360. auto url = item["url"].get<std::string>();
  361. if (url.find(YOUTUBE_VIDEO_URL) != std::string::npos) {
  362. blog(LOG_DEBUG, "YT: found legacy url: %s", url.c_str());
  363. removedYTUrl += url;
  364. removedYTUrl += ";\n";
  365. } else {
  366. save_array.push_back(item);
  367. }
  368. }
  369. if (!removedYTUrl.empty()) {
  370. const QString msg_title = QTStr("YouTube.DocksRemoval.Title");
  371. const QString msg_text = QTStr("YouTube.DocksRemoval.Text").arg(QT_UTF8(removedYTUrl.c_str()));
  372. OBSMessageBox::warning(OBSBasic::Get(), msg_title, msg_text);
  373. std::string output = save_array.dump();
  374. config_set_string(App()->GetUserConfig(), "BasicWindow", "ExtraBrowserDocks", output.c_str());
  375. }
  376. }