window-dock-youtube-app.cpp 12 KB

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