1
0

window-youtube-actions.cpp 26 KB

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