window-dock-youtube-app.cpp 12 KB

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