window-youtube-actions.cpp 26 KB

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