window-youtube-actions.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888
  1. #include "window-basic-main.hpp"
  2. #include "moc_window-youtube-actions.cpp"
  3. #include "obs-app.hpp"
  4. #include "youtube-api-wrappers.hpp"
  5. #include <qt-wrappers.hpp>
  6. #include <QToolTip>
  7. #include <QDateTime>
  8. #include <QDesktopServices>
  9. #include <QFileInfo>
  10. #include <QStandardPaths>
  11. #include <QImageReader>
  12. const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'";
  13. const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m";
  14. const QString IndexOfGamingCategory = "20";
  15. OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth,
  16. bool broadcastReady)
  17. : QDialog(parent),
  18. ui(new Ui::OBSYoutubeActions),
  19. apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth)),
  20. workerThread(new WorkerThread(apiYouTube)),
  21. broadcastReady(broadcastReady)
  22. {
  23. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  24. ui->setupUi(this);
  25. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Public"),
  26. "public");
  27. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Unlisted"),
  28. "unlisted");
  29. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Private"),
  30. "private");
  31. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Normal"),
  32. "normal");
  33. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Low"), "low");
  34. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.UltraLow"),
  35. "ultraLow");
  36. UpdateOkButtonStatus();
  37. connect(ui->title, &QLineEdit::textChanged, this,
  38. [&](const QString &) { this->UpdateOkButtonStatus(); });
  39. connect(ui->privacyBox, &QComboBox::currentTextChanged, this,
  40. [&](const QString &) { this->UpdateOkButtonStatus(); });
  41. connect(ui->yesMakeForKids, &QRadioButton::toggled, this,
  42. [&](bool) { this->UpdateOkButtonStatus(); });
  43. connect(ui->notMakeForKids, &QRadioButton::toggled, this,
  44. [&](bool) { this->UpdateOkButtonStatus(); });
  45. connect(ui->tabWidget, &QTabWidget::currentChanged, this,
  46. [&](int) { this->UpdateOkButtonStatus(); });
  47. connect(ui->pushButton, &QPushButton::clicked, this,
  48. &OBSYoutubeActions::OpenYouTubeDashboard);
  49. connect(ui->helpAutoStartStop, &QLabel::linkActivated, this,
  50. [](const QString &) {
  51. QToolTip::showText(
  52. QCursor::pos(),
  53. QTStr("YouTube.Actions.AutoStartStop.TT"));
  54. });
  55. connect(ui->help360Video, &QLabel::linkActivated, this,
  56. [](const QString &link) { QDesktopServices::openUrl(link); });
  57. connect(ui->helpMadeForKids, &QLabel::linkActivated, this,
  58. [](const QString &link) { QDesktopServices::openUrl(link); });
  59. ui->scheduledTime->setVisible(false);
  60. #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
  61. connect(ui->checkScheduledLater, &QCheckBox::checkStateChanged, this,
  62. [&](Qt::CheckState state)
  63. #else
  64. connect(ui->checkScheduledLater, &QCheckBox::stateChanged, this,
  65. [&](int state)
  66. #endif
  67. {
  68. const bool checked = (state == Qt::Checked);
  69. ui->scheduledTime->setVisible(checked);
  70. if (checked) {
  71. ui->checkAutoStart->setVisible(true);
  72. ui->checkAutoStop->setVisible(true);
  73. ui->helpAutoStartStop->setVisible(true);
  74. ui->checkAutoStart->setChecked(false);
  75. ui->checkAutoStop->setChecked(false);
  76. } else {
  77. ui->checkAutoStart->setVisible(false);
  78. ui->checkAutoStop->setVisible(false);
  79. ui->helpAutoStartStop->setVisible(false);
  80. ui->checkAutoStart->setChecked(true);
  81. ui->checkAutoStop->setChecked(true);
  82. }
  83. UpdateOkButtonStatus();
  84. });
  85. ui->checkAutoStart->setVisible(false);
  86. ui->checkAutoStop->setVisible(false);
  87. ui->helpAutoStartStop->setVisible(false);
  88. ui->scheduledTime->setDateTime(QDateTime::currentDateTime());
  89. auto thumbSelectionHandler = [&]() {
  90. if (thumbnailFile.isEmpty()) {
  91. QString filePath = OpenFile(
  92. this,
  93. QTStr("YouTube.Actions.Thumbnail.SelectFile"),
  94. QStandardPaths::writableLocation(
  95. QStandardPaths::PicturesLocation),
  96. QString("Images (*.png *.jpg *.jpeg *.gif)"));
  97. if (!filePath.isEmpty()) {
  98. QFileInfo tFile(filePath);
  99. if (!tFile.exists()) {
  100. return ShowErrorDialog(
  101. this,
  102. QTStr("YouTube.Actions.Error.FileMissing"));
  103. } else if (tFile.size() > 2 * 1024 * 1024) {
  104. return ShowErrorDialog(
  105. this,
  106. QTStr("YouTube.Actions.Error.FileTooLarge"));
  107. }
  108. thumbnailFile = filePath;
  109. ui->selectedFileName->setText(thumbnailFile);
  110. ui->selectFileButton->setText(QTStr(
  111. "YouTube.Actions.Thumbnail.ClearFile"));
  112. QImageReader imgReader(filePath);
  113. imgReader.setAutoTransform(true);
  114. const QImage newImage = imgReader.read();
  115. ui->thumbnailPreview->setPixmap(
  116. QPixmap::fromImage(newImage).scaled(
  117. 160, 90, Qt::KeepAspectRatio,
  118. Qt::SmoothTransformation));
  119. }
  120. } else {
  121. thumbnailFile.clear();
  122. ui->selectedFileName->setText(QTStr(
  123. "YouTube.Actions.Thumbnail.NoFileSelected"));
  124. ui->selectFileButton->setText(
  125. QTStr("YouTube.Actions.Thumbnail.SelectFile"));
  126. ui->thumbnailPreview->setPixmap(
  127. GetPlaceholder().pixmap(QSize(16, 16)));
  128. }
  129. };
  130. connect(ui->selectFileButton, &QPushButton::clicked, this,
  131. thumbSelectionHandler);
  132. connect(ui->thumbnailPreview, &ClickableLabel::clicked, this,
  133. thumbSelectionHandler);
  134. if (!apiYouTube) {
  135. blog(LOG_DEBUG, "YouTube API auth NOT found.");
  136. Cancel();
  137. return;
  138. }
  139. const char *name = config_get_string(OBSBasic::Get()->Config(),
  140. "YouTube", "ChannelName");
  141. this->setWindowTitle(QTStr("YouTube.Actions.WindowTitle").arg(name));
  142. QVector<CategoryDescription> category_list;
  143. if (!apiYouTube->GetVideoCategoriesList(category_list)) {
  144. ShowErrorDialog(
  145. parent,
  146. apiYouTube->GetLastError().isEmpty()
  147. ? QTStr("YouTube.Actions.Error.General")
  148. : QTStr("YouTube.Actions.Error.Text")
  149. .arg(apiYouTube->GetLastError()));
  150. Cancel();
  151. return;
  152. }
  153. for (auto &category : category_list) {
  154. ui->categoryBox->addItem(category.title, category.id);
  155. if (category.id == IndexOfGamingCategory) {
  156. ui->categoryBox->setCurrentText(category.title);
  157. }
  158. }
  159. connect(ui->okButton, &QPushButton::clicked, this,
  160. &OBSYoutubeActions::InitBroadcast);
  161. connect(ui->saveButton, &QPushButton::clicked, this,
  162. &OBSYoutubeActions::ReadyBroadcast);
  163. connect(ui->cancelButton, &QPushButton::clicked, this, [&]() {
  164. blog(LOG_DEBUG, "YouTube live broadcast creation cancelled.");
  165. // Close the dialog.
  166. Cancel();
  167. });
  168. qDeleteAll(ui->scrollAreaWidgetContents->findChildren<QWidget *>(
  169. QString(), Qt::FindDirectChildrenOnly));
  170. // Add label indicating loading state
  171. QLabel *loadingLabel = new QLabel();
  172. loadingLabel->setTextFormat(Qt::RichText);
  173. loadingLabel->setAlignment(Qt::AlignHCenter);
  174. loadingLabel->setText(
  175. QString("<big>%1</big>")
  176. .arg(QTStr("YouTube.Actions.EventsLoading")));
  177. ui->scrollAreaWidgetContents->layout()->addWidget(loadingLabel);
  178. // Delete "loading..." label on completion
  179. connect(workerThread, &WorkerThread::finished, this, [&] {
  180. QLayoutItem *item =
  181. ui->scrollAreaWidgetContents->layout()->takeAt(0);
  182. item->widget()->deleteLater();
  183. });
  184. connect(workerThread, &WorkerThread::failed, this, [&]() {
  185. auto last_error = apiYouTube->GetLastError();
  186. if (last_error.isEmpty())
  187. last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
  188. if (!apiYouTube->GetTranslatedError(last_error))
  189. last_error = QTStr("YouTube.Actions.Error.Text")
  190. .arg(last_error);
  191. ShowErrorDialog(this, last_error);
  192. QDialog::reject();
  193. });
  194. connect(workerThread, &WorkerThread::new_item, this,
  195. [&](const QString &title, const QString &dateTimeString,
  196. const QString &broadcast, const QString &status,
  197. bool astart, bool astop) {
  198. ClickableLabel *label = new ClickableLabel();
  199. label->setTextFormat(Qt::RichText);
  200. if (status == "live" || status == "testing") {
  201. // Resumable stream
  202. label->setText(
  203. QString("<big>%1</big><br/>%2")
  204. .arg(title,
  205. QTStr("YouTube.Actions.Stream.Resume")));
  206. } else if (dateTimeString.isEmpty()) {
  207. // The broadcast created by YouTube Studio has no start time.
  208. // Yes this does violate the restrictions set in YouTube's API
  209. // But why would YouTube care about consistency?
  210. label->setText(
  211. QString("<big>%1</big><br/>%2")
  212. .arg(title,
  213. QTStr("YouTube.Actions.Stream.YTStudio")));
  214. } else {
  215. label->setText(
  216. QString("<big>%1</big><br/>%2")
  217. .arg(title,
  218. QTStr("YouTube.Actions.Stream.ScheduledFor")
  219. .arg(dateTimeString)));
  220. }
  221. label->setAlignment(Qt::AlignHCenter);
  222. label->setMargin(4);
  223. connect(label, &ClickableLabel::clicked, this,
  224. [&, label, broadcast, astart, astop]() {
  225. for (QWidget *i :
  226. ui->scrollAreaWidgetContents->findChildren<
  227. QWidget *>(
  228. QString(),
  229. Qt::FindDirectChildrenOnly)) {
  230. i->setProperty(
  231. "isSelectedEvent",
  232. "false");
  233. i->style()->unpolish(i);
  234. i->style()->polish(i);
  235. }
  236. label->setProperty("isSelectedEvent",
  237. "true");
  238. label->style()->unpolish(label);
  239. label->style()->polish(label);
  240. this->selectedBroadcast = broadcast;
  241. this->autostart = astart;
  242. this->autostop = astop;
  243. UpdateOkButtonStatus();
  244. });
  245. ui->scrollAreaWidgetContents->layout()->addWidget(
  246. label);
  247. if (selectedBroadcast == broadcast)
  248. label->clicked();
  249. });
  250. workerThread->start();
  251. OBSBasic *main = OBSBasic::Get();
  252. bool rememberSettings = config_get_bool(main->basicConfig, "YouTube",
  253. "RememberSettings");
  254. if (rememberSettings)
  255. LoadSettings();
  256. // Switch to events page and select readied broadcast once loaded
  257. if (broadcastReady) {
  258. ui->tabWidget->setCurrentIndex(1);
  259. selectedBroadcast = apiYouTube->GetBroadcastId();
  260. }
  261. #ifdef __APPLE__
  262. // MacOS theming issues
  263. this->resize(this->width() + 200, this->height() + 120);
  264. #endif
  265. valid = true;
  266. }
  267. void OBSYoutubeActions::showEvent(QShowEvent *event)
  268. {
  269. QDialog::showEvent(event);
  270. if (thumbnailFile.isEmpty())
  271. ui->thumbnailPreview->setPixmap(
  272. GetPlaceholder().pixmap(QSize(16, 16)));
  273. }
  274. OBSYoutubeActions::~OBSYoutubeActions()
  275. {
  276. workerThread->stop();
  277. workerThread->wait();
  278. delete workerThread;
  279. }
  280. void WorkerThread::run()
  281. {
  282. if (!pending)
  283. return;
  284. json11::Json broadcasts;
  285. for (QString broadcastStatus : {"active", "upcoming"}) {
  286. if (!apiYouTube->GetBroadcastsList(broadcasts, "",
  287. broadcastStatus)) {
  288. emit failed();
  289. return;
  290. }
  291. while (pending) {
  292. auto items = broadcasts["items"].array_items();
  293. for (auto item : items) {
  294. QString status = QString::fromStdString(
  295. item["status"]["lifeCycleStatus"]
  296. .string_value());
  297. if (status == "live" || status == "testing") {
  298. // Check that the attached liveStream is offline (reconnectable)
  299. QString stream_id = QString::fromStdString(
  300. item["contentDetails"]
  301. ["boundStreamId"]
  302. .string_value());
  303. json11::Json stream;
  304. if (!apiYouTube->FindStream(stream_id,
  305. stream))
  306. continue;
  307. if (stream["status"]["streamStatus"] ==
  308. "active")
  309. continue;
  310. }
  311. QString title = QString::fromStdString(
  312. item["snippet"]["title"].string_value());
  313. QString scheduledStartTime =
  314. QString::fromStdString(
  315. item["snippet"]
  316. ["scheduledStartTime"]
  317. .string_value());
  318. QString broadcast = QString::fromStdString(
  319. item["id"].string_value());
  320. // Treat already started streams as autostart for UI purposes
  321. bool astart =
  322. status == "live" ||
  323. item["contentDetails"]["enableAutoStart"]
  324. .bool_value();
  325. bool astop =
  326. item["contentDetails"]["enableAutoStop"]
  327. .bool_value();
  328. QDateTime utcDTime = QDateTime::fromString(
  329. scheduledStartTime,
  330. SchedulDateAndTimeFormat);
  331. // DateTime parser means that input datetime is a local, so we need to move it
  332. QDateTime dateTime = utcDTime.addSecs(
  333. utcDTime.offsetFromUtc());
  334. QString dateTimeString = QLocale().toString(
  335. dateTime,
  336. QString("%1 %2").arg(
  337. QLocale().dateFormat(
  338. QLocale::LongFormat),
  339. QLocale().timeFormat(
  340. QLocale::ShortFormat)));
  341. emit new_item(title, dateTimeString, broadcast,
  342. status, astart, astop);
  343. }
  344. auto nextPageToken =
  345. broadcasts["nextPageToken"].string_value();
  346. if (nextPageToken.empty() || items.empty())
  347. break;
  348. else {
  349. if (!pending)
  350. return;
  351. if (!apiYouTube->GetBroadcastsList(
  352. broadcasts,
  353. QString::fromStdString(
  354. nextPageToken),
  355. broadcastStatus)) {
  356. emit failed();
  357. return;
  358. }
  359. }
  360. }
  361. }
  362. emit ready();
  363. }
  364. void OBSYoutubeActions::UpdateOkButtonStatus()
  365. {
  366. bool enable = false;
  367. if (ui->tabWidget->currentIndex() == 0) {
  368. enable = !ui->title->text().isEmpty() &&
  369. !ui->privacyBox->currentText().isEmpty() &&
  370. (ui->yesMakeForKids->isChecked() ||
  371. ui->notMakeForKids->isChecked());
  372. ui->okButton->setEnabled(enable);
  373. ui->saveButton->setEnabled(enable);
  374. if (ui->checkScheduledLater->checkState() == Qt::Checked) {
  375. ui->okButton->setText(
  376. QTStr("YouTube.Actions.Create_Schedule"));
  377. ui->saveButton->setText(
  378. QTStr("YouTube.Actions.Create_Schedule_Ready"));
  379. } else {
  380. ui->okButton->setText(
  381. QTStr("YouTube.Actions.Create_GoLive"));
  382. ui->saveButton->setText(
  383. QTStr("YouTube.Actions.Create_Ready"));
  384. }
  385. ui->pushButton->setVisible(false);
  386. } else {
  387. enable = !selectedBroadcast.isEmpty();
  388. ui->okButton->setEnabled(enable);
  389. ui->saveButton->setEnabled(enable);
  390. ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive"));
  391. ui->saveButton->setText(QTStr("YouTube.Actions.Choose_Ready"));
  392. ui->pushButton->setVisible(true);
  393. }
  394. }
  395. bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api,
  396. BroadcastDescription &broadcast,
  397. StreamDescription &stream,
  398. bool stream_later,
  399. bool ready_broadcast)
  400. {
  401. YoutubeApiWrappers *apiYouTube = api;
  402. UiToBroadcast(broadcast);
  403. if (stream_later) {
  404. // DateTime parser means that input datetime is a local, so we need to move it
  405. auto dateTime = ui->scheduledTime->dateTime();
  406. auto utcDTime = dateTime.addSecs(-dateTime.offsetFromUtc());
  407. broadcast.schedul_date_time =
  408. utcDTime.toString(SchedulDateAndTimeFormat);
  409. } else {
  410. // stream now is always autostart/autostop
  411. broadcast.auto_start = true;
  412. broadcast.auto_stop = true;
  413. broadcast.schedul_date_time =
  414. QDateTime::currentDateTimeUtc().toString(
  415. SchedulDateAndTimeFormat);
  416. }
  417. autostart = broadcast.auto_start;
  418. autostop = broadcast.auto_stop;
  419. blog(LOG_DEBUG, "Scheduled date and time: %s",
  420. broadcast.schedul_date_time.toStdString().c_str());
  421. if (!apiYouTube->InsertBroadcast(broadcast)) {
  422. blog(LOG_DEBUG, "No broadcast created.");
  423. return false;
  424. }
  425. if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title,
  426. broadcast.description,
  427. broadcast.category.id)) {
  428. blog(LOG_DEBUG, "No category set.");
  429. return false;
  430. }
  431. if (!thumbnailFile.isEmpty()) {
  432. blog(LOG_INFO, "Uploading thumbnail file \"%s\"...",
  433. thumbnailFile.toStdString().c_str());
  434. if (!apiYouTube->SetVideoThumbnail(broadcast.id,
  435. thumbnailFile)) {
  436. blog(LOG_DEBUG, "No thumbnail set.");
  437. return false;
  438. }
  439. }
  440. if (!stream_later || ready_broadcast) {
  441. stream = {"", "", "OBS Studio Video Stream"};
  442. if (!apiYouTube->InsertStream(stream)) {
  443. blog(LOG_DEBUG, "No stream created.");
  444. return false;
  445. }
  446. json11::Json json;
  447. if (!apiYouTube->BindStream(broadcast.id, stream.id, json)) {
  448. blog(LOG_DEBUG, "No stream binded.");
  449. return false;
  450. }
  451. if (broadcast.privacy != "private") {
  452. const std::string apiLiveChatId =
  453. json["snippet"]["liveChatId"].string_value();
  454. apiYouTube->SetChatId(broadcast.id, apiLiveChatId);
  455. } else {
  456. apiYouTube->ResetChat();
  457. }
  458. }
  459. #ifdef YOUTUBE_ENABLED
  460. if (OBSBasic::Get()->GetYouTubeAppDock())
  461. OBSBasic::Get()->GetYouTubeAppDock()->BroadcastCreated(
  462. broadcast.id.toStdString().c_str());
  463. #endif
  464. return true;
  465. }
  466. bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
  467. StreamDescription &stream)
  468. {
  469. YoutubeApiWrappers *apiYouTube = api;
  470. json11::Json json;
  471. if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) {
  472. blog(LOG_DEBUG, "No broadcast found.");
  473. return false;
  474. }
  475. std::string boundStreamId =
  476. json["items"]
  477. .array_items()[0]["contentDetails"]["boundStreamId"]
  478. .string_value();
  479. std::string broadcastPrivacy =
  480. json["items"]
  481. .array_items()[0]["status"]["privacyStatus"]
  482. .string_value();
  483. std::string apiLiveChatId =
  484. json["items"]
  485. .array_items()[0]["snippet"]["liveChatId"]
  486. .string_value();
  487. stream.id = boundStreamId.c_str();
  488. if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) {
  489. auto item = json["items"].array_items()[0];
  490. auto streamName = item["cdn"]["ingestionInfo"]["streamName"]
  491. .string_value();
  492. auto title = item["snippet"]["title"].string_value();
  493. stream.name = streamName.c_str();
  494. stream.title = title.c_str();
  495. api->SetBroadcastId(selectedBroadcast);
  496. } else {
  497. stream = {"", "", "OBS Studio Video Stream"};
  498. if (!apiYouTube->InsertStream(stream)) {
  499. blog(LOG_DEBUG, "No stream created.");
  500. return false;
  501. }
  502. if (!apiYouTube->BindStream(selectedBroadcast, stream.id,
  503. json)) {
  504. blog(LOG_DEBUG, "No stream binded.");
  505. return false;
  506. }
  507. }
  508. if (broadcastPrivacy != "private")
  509. apiYouTube->SetChatId(selectedBroadcast, apiLiveChatId);
  510. else
  511. apiYouTube->ResetChat();
  512. #ifdef YOUTUBE_ENABLED
  513. if (OBSBasic::Get()->GetYouTubeAppDock())
  514. OBSBasic::Get()->GetYouTubeAppDock()->BroadcastSelected(
  515. selectedBroadcast.toStdString().c_str());
  516. #endif
  517. return true;
  518. }
  519. void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text)
  520. {
  521. QMessageBox dlg(parent);
  522. dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint);
  523. dlg.setWindowTitle(QTStr("YouTube.Actions.Error.Title"));
  524. dlg.setText(text);
  525. dlg.setTextFormat(Qt::RichText);
  526. dlg.setIcon(QMessageBox::Warning);
  527. dlg.setStandardButtons(QMessageBox::StandardButton::Ok);
  528. dlg.exec();
  529. }
  530. void OBSYoutubeActions::InitBroadcast()
  531. {
  532. BroadcastDescription broadcast;
  533. StreamDescription stream;
  534. QMessageBox msgBox(this);
  535. msgBox.setWindowFlags(msgBox.windowFlags() &
  536. ~Qt::WindowCloseButtonHint);
  537. msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title"));
  538. msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast"));
  539. msgBox.setStandardButtons(QMessageBox::StandardButtons());
  540. bool success = false;
  541. auto action = [&]() {
  542. if (ui->tabWidget->currentIndex() == 0) {
  543. success = this->CreateEventAction(
  544. apiYouTube, broadcast, stream,
  545. ui->checkScheduledLater->isChecked());
  546. } else {
  547. success = this->ChooseAnEventAction(apiYouTube, stream);
  548. if (success)
  549. broadcast.id = this->selectedBroadcast;
  550. };
  551. QMetaObject::invokeMethod(&msgBox, "accept",
  552. Qt::QueuedConnection);
  553. };
  554. QScopedPointer<QThread> thread(CreateQThread(action));
  555. thread->start();
  556. msgBox.exec();
  557. thread->wait();
  558. if (success) {
  559. if (ui->tabWidget->currentIndex() == 0) {
  560. // Stream later usecase.
  561. if (ui->checkScheduledLater->isChecked()) {
  562. QMessageBox msg(this);
  563. msg.setWindowTitle(QTStr(
  564. "YouTube.Actions.EventCreated.Title"));
  565. msg.setText(QTStr(
  566. "YouTube.Actions.EventCreated.Text"));
  567. msg.setStandardButtons(QMessageBox::Ok);
  568. msg.exec();
  569. // Close dialog without start streaming.
  570. Cancel();
  571. } else {
  572. // Stream now usecase.
  573. blog(LOG_DEBUG, "New valid stream: %s",
  574. QT_TO_UTF8(stream.name));
  575. emit ok(QT_TO_UTF8(broadcast.id),
  576. QT_TO_UTF8(stream.id),
  577. QT_TO_UTF8(stream.name), true, true,
  578. true);
  579. Accept();
  580. }
  581. } else {
  582. // Stream to precreated broadcast usecase.
  583. emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id),
  584. QT_TO_UTF8(stream.name), autostart, autostop,
  585. true);
  586. Accept();
  587. }
  588. } else {
  589. // Fail.
  590. auto last_error = apiYouTube->GetLastError();
  591. if (last_error.isEmpty())
  592. last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
  593. if (!apiYouTube->GetTranslatedError(last_error))
  594. last_error =
  595. QTStr("YouTube.Actions.Error.NoBroadcastCreated")
  596. .arg(last_error);
  597. ShowErrorDialog(this, last_error);
  598. }
  599. }
  600. void OBSYoutubeActions::ReadyBroadcast()
  601. {
  602. BroadcastDescription broadcast;
  603. StreamDescription stream;
  604. QMessageBox msgBox(this);
  605. msgBox.setWindowFlags(msgBox.windowFlags() &
  606. ~Qt::WindowCloseButtonHint);
  607. msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title"));
  608. msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast"));
  609. msgBox.setStandardButtons(QMessageBox::StandardButtons());
  610. bool success = false;
  611. auto action = [&]() {
  612. if (ui->tabWidget->currentIndex() == 0) {
  613. success = this->CreateEventAction(
  614. apiYouTube, broadcast, stream,
  615. ui->checkScheduledLater->isChecked(), true);
  616. } else {
  617. success = this->ChooseAnEventAction(apiYouTube, stream);
  618. if (success)
  619. broadcast.id = this->selectedBroadcast;
  620. };
  621. QMetaObject::invokeMethod(&msgBox, "accept",
  622. Qt::QueuedConnection);
  623. };
  624. QScopedPointer<QThread> thread(CreateQThread(action));
  625. thread->start();
  626. msgBox.exec();
  627. thread->wait();
  628. if (success) {
  629. emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id),
  630. QT_TO_UTF8(stream.name), autostart, autostop, false);
  631. Accept();
  632. } else {
  633. // Fail.
  634. auto last_error = apiYouTube->GetLastError();
  635. if (last_error.isEmpty())
  636. last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
  637. if (!apiYouTube->GetTranslatedError(last_error))
  638. last_error =
  639. QTStr("YouTube.Actions.Error.NoBroadcastCreated")
  640. .arg(last_error);
  641. ShowErrorDialog(this, last_error);
  642. }
  643. }
  644. void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast)
  645. {
  646. broadcast.title = ui->title->text();
  647. // ToDo: UI warning rather than silent truncation
  648. broadcast.description = ui->description->toPlainText().left(5000);
  649. broadcast.privacy = ui->privacyBox->currentData().toString();
  650. broadcast.category.title = ui->categoryBox->currentText();
  651. broadcast.category.id = ui->categoryBox->currentData().toString();
  652. broadcast.made_for_kids = ui->yesMakeForKids->isChecked();
  653. broadcast.latency = ui->latencyBox->currentData().toString();
  654. broadcast.auto_start = ui->checkAutoStart->isChecked();
  655. broadcast.auto_stop = ui->checkAutoStop->isChecked();
  656. broadcast.dvr = ui->checkDVR->isChecked();
  657. broadcast.schedul_for_later = ui->checkScheduledLater->isChecked();
  658. broadcast.projection = ui->check360Video->isChecked() ? "360"
  659. : "rectangular";
  660. if (ui->checkRememberSettings->isChecked())
  661. SaveSettings(broadcast);
  662. }
  663. void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast)
  664. {
  665. OBSBasic *main = OBSBasic::Get();
  666. config_set_string(main->basicConfig, "YouTube", "Title",
  667. QT_TO_UTF8(broadcast.title));
  668. config_set_string(main->basicConfig, "YouTube", "Description",
  669. QT_TO_UTF8(broadcast.description));
  670. config_set_string(main->basicConfig, "YouTube", "Privacy",
  671. QT_TO_UTF8(broadcast.privacy));
  672. config_set_string(main->basicConfig, "YouTube", "CategoryID",
  673. QT_TO_UTF8(broadcast.category.id));
  674. config_set_string(main->basicConfig, "YouTube", "Latency",
  675. QT_TO_UTF8(broadcast.latency));
  676. config_set_bool(main->basicConfig, "YouTube", "MadeForKids",
  677. broadcast.made_for_kids);
  678. config_set_bool(main->basicConfig, "YouTube", "AutoStart",
  679. broadcast.auto_start);
  680. config_set_bool(main->basicConfig, "YouTube", "AutoStop",
  681. broadcast.auto_start);
  682. config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr);
  683. config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater",
  684. broadcast.schedul_for_later);
  685. config_set_string(main->basicConfig, "YouTube", "Projection",
  686. QT_TO_UTF8(broadcast.projection));
  687. config_set_string(main->basicConfig, "YouTube", "ThumbnailFile",
  688. QT_TO_UTF8(thumbnailFile));
  689. config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true);
  690. }
  691. void OBSYoutubeActions::LoadSettings()
  692. {
  693. OBSBasic *main = OBSBasic::Get();
  694. const char *title =
  695. config_get_string(main->basicConfig, "YouTube", "Title");
  696. ui->title->setText(QT_UTF8(title));
  697. const char *desc =
  698. config_get_string(main->basicConfig, "YouTube", "Description");
  699. ui->description->setPlainText(QT_UTF8(desc));
  700. const char *priv =
  701. config_get_string(main->basicConfig, "YouTube", "Privacy");
  702. int index = ui->privacyBox->findData(priv);
  703. ui->privacyBox->setCurrentIndex(index);
  704. const char *catID =
  705. config_get_string(main->basicConfig, "YouTube", "CategoryID");
  706. index = ui->categoryBox->findData(catID);
  707. ui->categoryBox->setCurrentIndex(index);
  708. const char *latency =
  709. config_get_string(main->basicConfig, "YouTube", "Latency");
  710. index = ui->latencyBox->findData(latency);
  711. ui->latencyBox->setCurrentIndex(index);
  712. bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR");
  713. ui->checkDVR->setChecked(dvr);
  714. bool forKids =
  715. config_get_bool(main->basicConfig, "YouTube", "MadeForKids");
  716. if (forKids)
  717. ui->yesMakeForKids->setChecked(true);
  718. else
  719. ui->notMakeForKids->setChecked(true);
  720. bool schedLater = config_get_bool(main->basicConfig, "YouTube",
  721. "ScheduleForLater");
  722. ui->checkScheduledLater->setChecked(schedLater);
  723. bool autoStart =
  724. config_get_bool(main->basicConfig, "YouTube", "AutoStart");
  725. ui->checkAutoStart->setChecked(autoStart);
  726. bool autoStop =
  727. config_get_bool(main->basicConfig, "YouTube", "AutoStop");
  728. ui->checkAutoStop->setChecked(autoStop);
  729. const char *projection =
  730. config_get_string(main->basicConfig, "YouTube", "Projection");
  731. if (projection && *projection) {
  732. if (strcmp(projection, "360") == 0)
  733. ui->check360Video->setChecked(true);
  734. else
  735. ui->check360Video->setChecked(false);
  736. }
  737. const char *thumbFile = config_get_string(main->basicConfig, "YouTube",
  738. "ThumbnailFile");
  739. if (thumbFile && *thumbFile) {
  740. QFileInfo tFile(thumbFile);
  741. // Re-check validity before setting path again
  742. if (tFile.exists() && tFile.size() <= 2 * 1024 * 1024) {
  743. thumbnailFile = tFile.absoluteFilePath();
  744. ui->selectedFileName->setText(thumbnailFile);
  745. ui->selectFileButton->setText(
  746. QTStr("YouTube.Actions.Thumbnail.ClearFile"));
  747. QImageReader imgReader(thumbnailFile);
  748. imgReader.setAutoTransform(true);
  749. const QImage newImage = imgReader.read();
  750. ui->thumbnailPreview->setPixmap(
  751. QPixmap::fromImage(newImage).scaled(
  752. 160, 90, Qt::KeepAspectRatio,
  753. Qt::SmoothTransformation));
  754. }
  755. }
  756. }
  757. void OBSYoutubeActions::OpenYouTubeDashboard()
  758. {
  759. ChannelDescription channel;
  760. if (!apiYouTube->GetChannelDescription(channel)) {
  761. blog(LOG_DEBUG, "Could not get channel description.");
  762. ShowErrorDialog(
  763. this,
  764. apiYouTube->GetLastError().isEmpty()
  765. ? QTStr("YouTube.Actions.Error.General")
  766. : QTStr("YouTube.Actions.Error.Text")
  767. .arg(apiYouTube->GetLastError()));
  768. return;
  769. }
  770. //https://studio.youtube.com/channel/UCA9bSfH3KL186kyiUsvi3IA/videos/live?filter=%5B%5D&sort=%7B%22columnType%22%3A%22date%22%2C%22sortOrder%22%3A%22DESCENDING%22%7D
  771. QString uri =
  772. QString("https://studio.youtube.com/channel/%1/videos/live?filter=[]&sort={\"columnType\"%3A\"date\"%2C\"sortOrder\"%3A\"DESCENDING\"}")
  773. .arg(channel.id);
  774. QDesktopServices::openUrl(uri);
  775. }
  776. void OBSYoutubeActions::Cancel()
  777. {
  778. workerThread->stop();
  779. reject();
  780. }
  781. void OBSYoutubeActions::Accept()
  782. {
  783. workerThread->stop();
  784. accept();
  785. }