window-youtube-actions.cpp 23 KB

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