1
0

YouTubeAppDock.cpp 12 KB

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