mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-02-19 07:29:18 -05:00
When a source file contains an explicit include with a filename following the "moc_<actual-filename>.cpp" pattern, then CMake's AUTOMOC generation tool will recognize the matching pair and generate the replacement header file and add the required include directory entries. For all files which do contain Q_OBJECT or similar declarations but do not have an explicit include directive, the global mocs_compilation.cpp file will still be generated (which groups all "missing" generated headers). The larger this global file is, the more expensive incremental compilation will be as this file (and all its contained generated headers) will be re-generated regardless of whether actual changes occurred.
889 lines
26 KiB
C++
889 lines
26 KiB
C++
#include "window-basic-main.hpp"
|
|
#include "moc_window-youtube-actions.cpp"
|
|
|
|
#include "obs-app.hpp"
|
|
#include "youtube-api-wrappers.hpp"
|
|
|
|
#include <qt-wrappers.hpp>
|
|
#include <QToolTip>
|
|
#include <QDateTime>
|
|
#include <QDesktopServices>
|
|
#include <QFileInfo>
|
|
#include <QStandardPaths>
|
|
#include <QImageReader>
|
|
|
|
const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'";
|
|
const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m";
|
|
const QString IndexOfGamingCategory = "20";
|
|
|
|
OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth,
|
|
bool broadcastReady)
|
|
: QDialog(parent),
|
|
ui(new Ui::OBSYoutubeActions),
|
|
apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth)),
|
|
workerThread(new WorkerThread(apiYouTube)),
|
|
broadcastReady(broadcastReady)
|
|
{
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
ui->setupUi(this);
|
|
|
|
ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Public"),
|
|
"public");
|
|
ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Unlisted"),
|
|
"unlisted");
|
|
ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Private"),
|
|
"private");
|
|
|
|
ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Normal"),
|
|
"normal");
|
|
ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Low"), "low");
|
|
ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.UltraLow"),
|
|
"ultraLow");
|
|
|
|
UpdateOkButtonStatus();
|
|
|
|
connect(ui->title, &QLineEdit::textChanged, this,
|
|
[&](const QString &) { this->UpdateOkButtonStatus(); });
|
|
connect(ui->privacyBox, &QComboBox::currentTextChanged, this,
|
|
[&](const QString &) { this->UpdateOkButtonStatus(); });
|
|
connect(ui->yesMakeForKids, &QRadioButton::toggled, this,
|
|
[&](bool) { this->UpdateOkButtonStatus(); });
|
|
connect(ui->notMakeForKids, &QRadioButton::toggled, this,
|
|
[&](bool) { this->UpdateOkButtonStatus(); });
|
|
connect(ui->tabWidget, &QTabWidget::currentChanged, this,
|
|
[&](int) { this->UpdateOkButtonStatus(); });
|
|
connect(ui->pushButton, &QPushButton::clicked, this,
|
|
&OBSYoutubeActions::OpenYouTubeDashboard);
|
|
|
|
connect(ui->helpAutoStartStop, &QLabel::linkActivated, this,
|
|
[](const QString &) {
|
|
QToolTip::showText(
|
|
QCursor::pos(),
|
|
QTStr("YouTube.Actions.AutoStartStop.TT"));
|
|
});
|
|
connect(ui->help360Video, &QLabel::linkActivated, this,
|
|
[](const QString &link) { QDesktopServices::openUrl(link); });
|
|
connect(ui->helpMadeForKids, &QLabel::linkActivated, this,
|
|
[](const QString &link) { QDesktopServices::openUrl(link); });
|
|
|
|
ui->scheduledTime->setVisible(false);
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
|
connect(ui->checkScheduledLater, &QCheckBox::checkStateChanged, this,
|
|
[&](Qt::CheckState state)
|
|
#else
|
|
connect(ui->checkScheduledLater, &QCheckBox::stateChanged, this,
|
|
[&](int state)
|
|
#endif
|
|
{
|
|
const bool checked = (state == Qt::Checked);
|
|
ui->scheduledTime->setVisible(checked);
|
|
if (checked) {
|
|
ui->checkAutoStart->setVisible(true);
|
|
ui->checkAutoStop->setVisible(true);
|
|
ui->helpAutoStartStop->setVisible(true);
|
|
|
|
ui->checkAutoStart->setChecked(false);
|
|
ui->checkAutoStop->setChecked(false);
|
|
} else {
|
|
ui->checkAutoStart->setVisible(false);
|
|
ui->checkAutoStop->setVisible(false);
|
|
ui->helpAutoStartStop->setVisible(false);
|
|
|
|
ui->checkAutoStart->setChecked(true);
|
|
ui->checkAutoStop->setChecked(true);
|
|
}
|
|
UpdateOkButtonStatus();
|
|
});
|
|
|
|
ui->checkAutoStart->setVisible(false);
|
|
ui->checkAutoStop->setVisible(false);
|
|
ui->helpAutoStartStop->setVisible(false);
|
|
|
|
ui->scheduledTime->setDateTime(QDateTime::currentDateTime());
|
|
|
|
auto thumbSelectionHandler = [&]() {
|
|
if (thumbnailFile.isEmpty()) {
|
|
QString filePath = OpenFile(
|
|
this,
|
|
QTStr("YouTube.Actions.Thumbnail.SelectFile"),
|
|
QStandardPaths::writableLocation(
|
|
QStandardPaths::PicturesLocation),
|
|
QString("Images (*.png *.jpg *.jpeg *.gif)"));
|
|
|
|
if (!filePath.isEmpty()) {
|
|
QFileInfo tFile(filePath);
|
|
if (!tFile.exists()) {
|
|
return ShowErrorDialog(
|
|
this,
|
|
QTStr("YouTube.Actions.Error.FileMissing"));
|
|
} else if (tFile.size() > 2 * 1024 * 1024) {
|
|
return ShowErrorDialog(
|
|
this,
|
|
QTStr("YouTube.Actions.Error.FileTooLarge"));
|
|
}
|
|
|
|
thumbnailFile = filePath;
|
|
ui->selectedFileName->setText(thumbnailFile);
|
|
ui->selectFileButton->setText(QTStr(
|
|
"YouTube.Actions.Thumbnail.ClearFile"));
|
|
|
|
QImageReader imgReader(filePath);
|
|
imgReader.setAutoTransform(true);
|
|
const QImage newImage = imgReader.read();
|
|
ui->thumbnailPreview->setPixmap(
|
|
QPixmap::fromImage(newImage).scaled(
|
|
160, 90, Qt::KeepAspectRatio,
|
|
Qt::SmoothTransformation));
|
|
}
|
|
} else {
|
|
thumbnailFile.clear();
|
|
ui->selectedFileName->setText(QTStr(
|
|
"YouTube.Actions.Thumbnail.NoFileSelected"));
|
|
ui->selectFileButton->setText(
|
|
QTStr("YouTube.Actions.Thumbnail.SelectFile"));
|
|
ui->thumbnailPreview->setPixmap(
|
|
GetPlaceholder().pixmap(QSize(16, 16)));
|
|
}
|
|
};
|
|
|
|
connect(ui->selectFileButton, &QPushButton::clicked, this,
|
|
thumbSelectionHandler);
|
|
connect(ui->thumbnailPreview, &ClickableLabel::clicked, this,
|
|
thumbSelectionHandler);
|
|
|
|
if (!apiYouTube) {
|
|
blog(LOG_DEBUG, "YouTube API auth NOT found.");
|
|
Cancel();
|
|
return;
|
|
}
|
|
|
|
const char *name = config_get_string(OBSBasic::Get()->Config(),
|
|
"YouTube", "ChannelName");
|
|
this->setWindowTitle(QTStr("YouTube.Actions.WindowTitle").arg(name));
|
|
|
|
QVector<CategoryDescription> category_list;
|
|
if (!apiYouTube->GetVideoCategoriesList(category_list)) {
|
|
ShowErrorDialog(
|
|
parent,
|
|
apiYouTube->GetLastError().isEmpty()
|
|
? QTStr("YouTube.Actions.Error.General")
|
|
: QTStr("YouTube.Actions.Error.Text")
|
|
.arg(apiYouTube->GetLastError()));
|
|
Cancel();
|
|
return;
|
|
}
|
|
for (auto &category : category_list) {
|
|
ui->categoryBox->addItem(category.title, category.id);
|
|
if (category.id == IndexOfGamingCategory) {
|
|
ui->categoryBox->setCurrentText(category.title);
|
|
}
|
|
}
|
|
|
|
connect(ui->okButton, &QPushButton::clicked, this,
|
|
&OBSYoutubeActions::InitBroadcast);
|
|
connect(ui->saveButton, &QPushButton::clicked, this,
|
|
&OBSYoutubeActions::ReadyBroadcast);
|
|
connect(ui->cancelButton, &QPushButton::clicked, this, [&]() {
|
|
blog(LOG_DEBUG, "YouTube live broadcast creation cancelled.");
|
|
// Close the dialog.
|
|
Cancel();
|
|
});
|
|
|
|
qDeleteAll(ui->scrollAreaWidgetContents->findChildren<QWidget *>(
|
|
QString(), Qt::FindDirectChildrenOnly));
|
|
|
|
// Add label indicating loading state
|
|
QLabel *loadingLabel = new QLabel();
|
|
loadingLabel->setTextFormat(Qt::RichText);
|
|
loadingLabel->setAlignment(Qt::AlignHCenter);
|
|
loadingLabel->setText(
|
|
QString("<big>%1</big>")
|
|
.arg(QTStr("YouTube.Actions.EventsLoading")));
|
|
ui->scrollAreaWidgetContents->layout()->addWidget(loadingLabel);
|
|
|
|
// Delete "loading..." label on completion
|
|
connect(workerThread, &WorkerThread::finished, this, [&] {
|
|
QLayoutItem *item =
|
|
ui->scrollAreaWidgetContents->layout()->takeAt(0);
|
|
item->widget()->deleteLater();
|
|
});
|
|
|
|
connect(workerThread, &WorkerThread::failed, this, [&]() {
|
|
auto last_error = apiYouTube->GetLastError();
|
|
if (last_error.isEmpty())
|
|
last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
|
|
|
|
if (!apiYouTube->GetTranslatedError(last_error))
|
|
last_error = QTStr("YouTube.Actions.Error.Text")
|
|
.arg(last_error);
|
|
|
|
ShowErrorDialog(this, last_error);
|
|
QDialog::reject();
|
|
});
|
|
|
|
connect(workerThread, &WorkerThread::new_item, this,
|
|
[&](const QString &title, const QString &dateTimeString,
|
|
const QString &broadcast, const QString &status,
|
|
bool astart, bool astop) {
|
|
ClickableLabel *label = new ClickableLabel();
|
|
label->setTextFormat(Qt::RichText);
|
|
|
|
if (status == "live" || status == "testing") {
|
|
// Resumable stream
|
|
label->setText(
|
|
QString("<big>%1</big><br/>%2")
|
|
.arg(title,
|
|
QTStr("YouTube.Actions.Stream.Resume")));
|
|
|
|
} else if (dateTimeString.isEmpty()) {
|
|
// The broadcast created by YouTube Studio has no start time.
|
|
// Yes this does violate the restrictions set in YouTube's API
|
|
// But why would YouTube care about consistency?
|
|
label->setText(
|
|
QString("<big>%1</big><br/>%2")
|
|
.arg(title,
|
|
QTStr("YouTube.Actions.Stream.YTStudio")));
|
|
} else {
|
|
label->setText(
|
|
QString("<big>%1</big><br/>%2")
|
|
.arg(title,
|
|
QTStr("YouTube.Actions.Stream.ScheduledFor")
|
|
.arg(dateTimeString)));
|
|
}
|
|
|
|
label->setAlignment(Qt::AlignHCenter);
|
|
label->setMargin(4);
|
|
|
|
connect(label, &ClickableLabel::clicked, this,
|
|
[&, label, broadcast, astart, astop]() {
|
|
for (QWidget *i :
|
|
ui->scrollAreaWidgetContents->findChildren<
|
|
QWidget *>(
|
|
QString(),
|
|
Qt::FindDirectChildrenOnly)) {
|
|
|
|
i->setProperty(
|
|
"isSelectedEvent",
|
|
"false");
|
|
i->style()->unpolish(i);
|
|
i->style()->polish(i);
|
|
}
|
|
label->setProperty("isSelectedEvent",
|
|
"true");
|
|
label->style()->unpolish(label);
|
|
label->style()->polish(label);
|
|
|
|
this->selectedBroadcast = broadcast;
|
|
this->autostart = astart;
|
|
this->autostop = astop;
|
|
UpdateOkButtonStatus();
|
|
});
|
|
ui->scrollAreaWidgetContents->layout()->addWidget(
|
|
label);
|
|
|
|
if (selectedBroadcast == broadcast)
|
|
label->clicked();
|
|
});
|
|
workerThread->start();
|
|
|
|
OBSBasic *main = OBSBasic::Get();
|
|
bool rememberSettings = config_get_bool(main->basicConfig, "YouTube",
|
|
"RememberSettings");
|
|
if (rememberSettings)
|
|
LoadSettings();
|
|
|
|
// Switch to events page and select readied broadcast once loaded
|
|
if (broadcastReady) {
|
|
ui->tabWidget->setCurrentIndex(1);
|
|
selectedBroadcast = apiYouTube->GetBroadcastId();
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
// MacOS theming issues
|
|
this->resize(this->width() + 200, this->height() + 120);
|
|
#endif
|
|
valid = true;
|
|
}
|
|
|
|
void OBSYoutubeActions::showEvent(QShowEvent *event)
|
|
{
|
|
QDialog::showEvent(event);
|
|
if (thumbnailFile.isEmpty())
|
|
ui->thumbnailPreview->setPixmap(
|
|
GetPlaceholder().pixmap(QSize(16, 16)));
|
|
}
|
|
|
|
OBSYoutubeActions::~OBSYoutubeActions()
|
|
{
|
|
workerThread->stop();
|
|
workerThread->wait();
|
|
|
|
delete workerThread;
|
|
}
|
|
|
|
void WorkerThread::run()
|
|
{
|
|
if (!pending)
|
|
return;
|
|
json11::Json broadcasts;
|
|
|
|
for (QString broadcastStatus : {"active", "upcoming"}) {
|
|
if (!apiYouTube->GetBroadcastsList(broadcasts, "",
|
|
broadcastStatus)) {
|
|
emit failed();
|
|
return;
|
|
}
|
|
|
|
while (pending) {
|
|
auto items = broadcasts["items"].array_items();
|
|
for (auto item : items) {
|
|
QString status = QString::fromStdString(
|
|
item["status"]["lifeCycleStatus"]
|
|
.string_value());
|
|
|
|
if (status == "live" || status == "testing") {
|
|
// Check that the attached liveStream is offline (reconnectable)
|
|
QString stream_id = QString::fromStdString(
|
|
item["contentDetails"]
|
|
["boundStreamId"]
|
|
.string_value());
|
|
json11::Json stream;
|
|
if (!apiYouTube->FindStream(stream_id,
|
|
stream))
|
|
continue;
|
|
if (stream["status"]["streamStatus"] ==
|
|
"active")
|
|
continue;
|
|
}
|
|
|
|
QString title = QString::fromStdString(
|
|
item["snippet"]["title"].string_value());
|
|
QString scheduledStartTime =
|
|
QString::fromStdString(
|
|
item["snippet"]
|
|
["scheduledStartTime"]
|
|
.string_value());
|
|
QString broadcast = QString::fromStdString(
|
|
item["id"].string_value());
|
|
|
|
// Treat already started streams as autostart for UI purposes
|
|
bool astart =
|
|
status == "live" ||
|
|
item["contentDetails"]["enableAutoStart"]
|
|
.bool_value();
|
|
bool astop =
|
|
item["contentDetails"]["enableAutoStop"]
|
|
.bool_value();
|
|
|
|
QDateTime utcDTime = QDateTime::fromString(
|
|
scheduledStartTime,
|
|
SchedulDateAndTimeFormat);
|
|
// DateTime parser means that input datetime is a local, so we need to move it
|
|
QDateTime dateTime = utcDTime.addSecs(
|
|
utcDTime.offsetFromUtc());
|
|
|
|
QString dateTimeString = QLocale().toString(
|
|
dateTime,
|
|
QString("%1 %2").arg(
|
|
QLocale().dateFormat(
|
|
QLocale::LongFormat),
|
|
QLocale().timeFormat(
|
|
QLocale::ShortFormat)));
|
|
|
|
emit new_item(title, dateTimeString, broadcast,
|
|
status, astart, astop);
|
|
}
|
|
|
|
auto nextPageToken =
|
|
broadcasts["nextPageToken"].string_value();
|
|
if (nextPageToken.empty() || items.empty())
|
|
break;
|
|
else {
|
|
if (!pending)
|
|
return;
|
|
if (!apiYouTube->GetBroadcastsList(
|
|
broadcasts,
|
|
QString::fromStdString(
|
|
nextPageToken),
|
|
broadcastStatus)) {
|
|
emit failed();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
emit ready();
|
|
}
|
|
|
|
void OBSYoutubeActions::UpdateOkButtonStatus()
|
|
{
|
|
bool enable = false;
|
|
|
|
if (ui->tabWidget->currentIndex() == 0) {
|
|
enable = !ui->title->text().isEmpty() &&
|
|
!ui->privacyBox->currentText().isEmpty() &&
|
|
(ui->yesMakeForKids->isChecked() ||
|
|
ui->notMakeForKids->isChecked());
|
|
ui->okButton->setEnabled(enable);
|
|
ui->saveButton->setEnabled(enable);
|
|
|
|
if (ui->checkScheduledLater->checkState() == Qt::Checked) {
|
|
ui->okButton->setText(
|
|
QTStr("YouTube.Actions.Create_Schedule"));
|
|
ui->saveButton->setText(
|
|
QTStr("YouTube.Actions.Create_Schedule_Ready"));
|
|
} else {
|
|
ui->okButton->setText(
|
|
QTStr("YouTube.Actions.Create_GoLive"));
|
|
ui->saveButton->setText(
|
|
QTStr("YouTube.Actions.Create_Ready"));
|
|
}
|
|
ui->pushButton->setVisible(false);
|
|
} else {
|
|
enable = !selectedBroadcast.isEmpty();
|
|
ui->okButton->setEnabled(enable);
|
|
ui->saveButton->setEnabled(enable);
|
|
ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive"));
|
|
ui->saveButton->setText(QTStr("YouTube.Actions.Choose_Ready"));
|
|
|
|
ui->pushButton->setVisible(true);
|
|
}
|
|
}
|
|
bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api,
|
|
BroadcastDescription &broadcast,
|
|
StreamDescription &stream,
|
|
bool stream_later,
|
|
bool ready_broadcast)
|
|
{
|
|
YoutubeApiWrappers *apiYouTube = api;
|
|
UiToBroadcast(broadcast);
|
|
|
|
if (stream_later) {
|
|
// DateTime parser means that input datetime is a local, so we need to move it
|
|
auto dateTime = ui->scheduledTime->dateTime();
|
|
auto utcDTime = dateTime.addSecs(-dateTime.offsetFromUtc());
|
|
broadcast.schedul_date_time =
|
|
utcDTime.toString(SchedulDateAndTimeFormat);
|
|
} else {
|
|
// stream now is always autostart/autostop
|
|
broadcast.auto_start = true;
|
|
broadcast.auto_stop = true;
|
|
broadcast.schedul_date_time =
|
|
QDateTime::currentDateTimeUtc().toString(
|
|
SchedulDateAndTimeFormat);
|
|
}
|
|
|
|
autostart = broadcast.auto_start;
|
|
autostop = broadcast.auto_stop;
|
|
|
|
blog(LOG_DEBUG, "Scheduled date and time: %s",
|
|
broadcast.schedul_date_time.toStdString().c_str());
|
|
if (!apiYouTube->InsertBroadcast(broadcast)) {
|
|
blog(LOG_DEBUG, "No broadcast created.");
|
|
return false;
|
|
}
|
|
if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title,
|
|
broadcast.description,
|
|
broadcast.category.id)) {
|
|
blog(LOG_DEBUG, "No category set.");
|
|
return false;
|
|
}
|
|
if (!thumbnailFile.isEmpty()) {
|
|
blog(LOG_INFO, "Uploading thumbnail file \"%s\"...",
|
|
thumbnailFile.toStdString().c_str());
|
|
if (!apiYouTube->SetVideoThumbnail(broadcast.id,
|
|
thumbnailFile)) {
|
|
blog(LOG_DEBUG, "No thumbnail set.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!stream_later || ready_broadcast) {
|
|
stream = {"", "", "OBS Studio Video Stream"};
|
|
if (!apiYouTube->InsertStream(stream)) {
|
|
blog(LOG_DEBUG, "No stream created.");
|
|
return false;
|
|
}
|
|
json11::Json json;
|
|
if (!apiYouTube->BindStream(broadcast.id, stream.id, json)) {
|
|
blog(LOG_DEBUG, "No stream binded.");
|
|
return false;
|
|
}
|
|
|
|
if (broadcast.privacy != "private") {
|
|
const std::string apiLiveChatId =
|
|
json["snippet"]["liveChatId"].string_value();
|
|
apiYouTube->SetChatId(broadcast.id, apiLiveChatId);
|
|
} else {
|
|
apiYouTube->ResetChat();
|
|
}
|
|
}
|
|
|
|
#ifdef YOUTUBE_ENABLED
|
|
if (OBSBasic::Get()->GetYouTubeAppDock())
|
|
OBSBasic::Get()->GetYouTubeAppDock()->BroadcastCreated(
|
|
broadcast.id.toStdString().c_str());
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
|
|
StreamDescription &stream)
|
|
{
|
|
YoutubeApiWrappers *apiYouTube = api;
|
|
|
|
json11::Json json;
|
|
if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) {
|
|
blog(LOG_DEBUG, "No broadcast found.");
|
|
return false;
|
|
}
|
|
|
|
std::string boundStreamId =
|
|
json["items"]
|
|
.array_items()[0]["contentDetails"]["boundStreamId"]
|
|
.string_value();
|
|
std::string broadcastPrivacy =
|
|
json["items"]
|
|
.array_items()[0]["status"]["privacyStatus"]
|
|
.string_value();
|
|
std::string apiLiveChatId =
|
|
json["items"]
|
|
.array_items()[0]["snippet"]["liveChatId"]
|
|
.string_value();
|
|
|
|
stream.id = boundStreamId.c_str();
|
|
if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) {
|
|
auto item = json["items"].array_items()[0];
|
|
auto streamName = item["cdn"]["ingestionInfo"]["streamName"]
|
|
.string_value();
|
|
auto title = item["snippet"]["title"].string_value();
|
|
|
|
stream.name = streamName.c_str();
|
|
stream.title = title.c_str();
|
|
api->SetBroadcastId(selectedBroadcast);
|
|
} else {
|
|
stream = {"", "", "OBS Studio Video Stream"};
|
|
if (!apiYouTube->InsertStream(stream)) {
|
|
blog(LOG_DEBUG, "No stream created.");
|
|
return false;
|
|
}
|
|
if (!apiYouTube->BindStream(selectedBroadcast, stream.id,
|
|
json)) {
|
|
blog(LOG_DEBUG, "No stream binded.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (broadcastPrivacy != "private")
|
|
apiYouTube->SetChatId(selectedBroadcast, apiLiveChatId);
|
|
else
|
|
apiYouTube->ResetChat();
|
|
|
|
#ifdef YOUTUBE_ENABLED
|
|
if (OBSBasic::Get()->GetYouTubeAppDock())
|
|
OBSBasic::Get()->GetYouTubeAppDock()->BroadcastSelected(
|
|
selectedBroadcast.toStdString().c_str());
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text)
|
|
{
|
|
QMessageBox dlg(parent);
|
|
dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint);
|
|
dlg.setWindowTitle(QTStr("YouTube.Actions.Error.Title"));
|
|
dlg.setText(text);
|
|
dlg.setTextFormat(Qt::RichText);
|
|
dlg.setIcon(QMessageBox::Warning);
|
|
dlg.setStandardButtons(QMessageBox::StandardButton::Ok);
|
|
dlg.exec();
|
|
}
|
|
|
|
void OBSYoutubeActions::InitBroadcast()
|
|
{
|
|
BroadcastDescription broadcast;
|
|
StreamDescription stream;
|
|
QMessageBox msgBox(this);
|
|
msgBox.setWindowFlags(msgBox.windowFlags() &
|
|
~Qt::WindowCloseButtonHint);
|
|
msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title"));
|
|
msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast"));
|
|
msgBox.setStandardButtons(QMessageBox::StandardButtons());
|
|
|
|
bool success = false;
|
|
auto action = [&]() {
|
|
if (ui->tabWidget->currentIndex() == 0) {
|
|
success = this->CreateEventAction(
|
|
apiYouTube, broadcast, stream,
|
|
ui->checkScheduledLater->isChecked());
|
|
} else {
|
|
success = this->ChooseAnEventAction(apiYouTube, stream);
|
|
if (success)
|
|
broadcast.id = this->selectedBroadcast;
|
|
};
|
|
QMetaObject::invokeMethod(&msgBox, "accept",
|
|
Qt::QueuedConnection);
|
|
};
|
|
QScopedPointer<QThread> thread(CreateQThread(action));
|
|
thread->start();
|
|
msgBox.exec();
|
|
thread->wait();
|
|
|
|
if (success) {
|
|
if (ui->tabWidget->currentIndex() == 0) {
|
|
// Stream later usecase.
|
|
if (ui->checkScheduledLater->isChecked()) {
|
|
QMessageBox msg(this);
|
|
msg.setWindowTitle(QTStr(
|
|
"YouTube.Actions.EventCreated.Title"));
|
|
msg.setText(QTStr(
|
|
"YouTube.Actions.EventCreated.Text"));
|
|
msg.setStandardButtons(QMessageBox::Ok);
|
|
msg.exec();
|
|
// Close dialog without start streaming.
|
|
Cancel();
|
|
} else {
|
|
// Stream now usecase.
|
|
blog(LOG_DEBUG, "New valid stream: %s",
|
|
QT_TO_UTF8(stream.name));
|
|
emit ok(QT_TO_UTF8(broadcast.id),
|
|
QT_TO_UTF8(stream.id),
|
|
QT_TO_UTF8(stream.name), true, true,
|
|
true);
|
|
Accept();
|
|
}
|
|
} else {
|
|
// Stream to precreated broadcast usecase.
|
|
emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id),
|
|
QT_TO_UTF8(stream.name), autostart, autostop,
|
|
true);
|
|
Accept();
|
|
}
|
|
} else {
|
|
// Fail.
|
|
auto last_error = apiYouTube->GetLastError();
|
|
if (last_error.isEmpty())
|
|
last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
|
|
if (!apiYouTube->GetTranslatedError(last_error))
|
|
last_error =
|
|
QTStr("YouTube.Actions.Error.NoBroadcastCreated")
|
|
.arg(last_error);
|
|
|
|
ShowErrorDialog(this, last_error);
|
|
}
|
|
}
|
|
|
|
void OBSYoutubeActions::ReadyBroadcast()
|
|
{
|
|
BroadcastDescription broadcast;
|
|
StreamDescription stream;
|
|
QMessageBox msgBox(this);
|
|
msgBox.setWindowFlags(msgBox.windowFlags() &
|
|
~Qt::WindowCloseButtonHint);
|
|
msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title"));
|
|
msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast"));
|
|
msgBox.setStandardButtons(QMessageBox::StandardButtons());
|
|
|
|
bool success = false;
|
|
auto action = [&]() {
|
|
if (ui->tabWidget->currentIndex() == 0) {
|
|
success = this->CreateEventAction(
|
|
apiYouTube, broadcast, stream,
|
|
ui->checkScheduledLater->isChecked(), true);
|
|
} else {
|
|
success = this->ChooseAnEventAction(apiYouTube, stream);
|
|
if (success)
|
|
broadcast.id = this->selectedBroadcast;
|
|
};
|
|
QMetaObject::invokeMethod(&msgBox, "accept",
|
|
Qt::QueuedConnection);
|
|
};
|
|
QScopedPointer<QThread> thread(CreateQThread(action));
|
|
thread->start();
|
|
msgBox.exec();
|
|
thread->wait();
|
|
|
|
if (success) {
|
|
emit ok(QT_TO_UTF8(broadcast.id), QT_TO_UTF8(stream.id),
|
|
QT_TO_UTF8(stream.name), autostart, autostop, false);
|
|
Accept();
|
|
} else {
|
|
// Fail.
|
|
auto last_error = apiYouTube->GetLastError();
|
|
if (last_error.isEmpty())
|
|
last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
|
|
if (!apiYouTube->GetTranslatedError(last_error))
|
|
last_error =
|
|
QTStr("YouTube.Actions.Error.NoBroadcastCreated")
|
|
.arg(last_error);
|
|
|
|
ShowErrorDialog(this, last_error);
|
|
}
|
|
}
|
|
|
|
void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast)
|
|
{
|
|
broadcast.title = ui->title->text();
|
|
// ToDo: UI warning rather than silent truncation
|
|
broadcast.description = ui->description->toPlainText().left(5000);
|
|
broadcast.privacy = ui->privacyBox->currentData().toString();
|
|
broadcast.category.title = ui->categoryBox->currentText();
|
|
broadcast.category.id = ui->categoryBox->currentData().toString();
|
|
broadcast.made_for_kids = ui->yesMakeForKids->isChecked();
|
|
broadcast.latency = ui->latencyBox->currentData().toString();
|
|
broadcast.auto_start = ui->checkAutoStart->isChecked();
|
|
broadcast.auto_stop = ui->checkAutoStop->isChecked();
|
|
broadcast.dvr = ui->checkDVR->isChecked();
|
|
broadcast.schedul_for_later = ui->checkScheduledLater->isChecked();
|
|
broadcast.projection = ui->check360Video->isChecked() ? "360"
|
|
: "rectangular";
|
|
|
|
if (ui->checkRememberSettings->isChecked())
|
|
SaveSettings(broadcast);
|
|
}
|
|
|
|
void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast)
|
|
{
|
|
OBSBasic *main = OBSBasic::Get();
|
|
|
|
config_set_string(main->basicConfig, "YouTube", "Title",
|
|
QT_TO_UTF8(broadcast.title));
|
|
config_set_string(main->basicConfig, "YouTube", "Description",
|
|
QT_TO_UTF8(broadcast.description));
|
|
config_set_string(main->basicConfig, "YouTube", "Privacy",
|
|
QT_TO_UTF8(broadcast.privacy));
|
|
config_set_string(main->basicConfig, "YouTube", "CategoryID",
|
|
QT_TO_UTF8(broadcast.category.id));
|
|
config_set_string(main->basicConfig, "YouTube", "Latency",
|
|
QT_TO_UTF8(broadcast.latency));
|
|
config_set_bool(main->basicConfig, "YouTube", "MadeForKids",
|
|
broadcast.made_for_kids);
|
|
config_set_bool(main->basicConfig, "YouTube", "AutoStart",
|
|
broadcast.auto_start);
|
|
config_set_bool(main->basicConfig, "YouTube", "AutoStop",
|
|
broadcast.auto_start);
|
|
config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr);
|
|
config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater",
|
|
broadcast.schedul_for_later);
|
|
config_set_string(main->basicConfig, "YouTube", "Projection",
|
|
QT_TO_UTF8(broadcast.projection));
|
|
config_set_string(main->basicConfig, "YouTube", "ThumbnailFile",
|
|
QT_TO_UTF8(thumbnailFile));
|
|
config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true);
|
|
}
|
|
|
|
void OBSYoutubeActions::LoadSettings()
|
|
{
|
|
OBSBasic *main = OBSBasic::Get();
|
|
|
|
const char *title =
|
|
config_get_string(main->basicConfig, "YouTube", "Title");
|
|
ui->title->setText(QT_UTF8(title));
|
|
|
|
const char *desc =
|
|
config_get_string(main->basicConfig, "YouTube", "Description");
|
|
ui->description->setPlainText(QT_UTF8(desc));
|
|
|
|
const char *priv =
|
|
config_get_string(main->basicConfig, "YouTube", "Privacy");
|
|
int index = ui->privacyBox->findData(priv);
|
|
ui->privacyBox->setCurrentIndex(index);
|
|
|
|
const char *catID =
|
|
config_get_string(main->basicConfig, "YouTube", "CategoryID");
|
|
index = ui->categoryBox->findData(catID);
|
|
ui->categoryBox->setCurrentIndex(index);
|
|
|
|
const char *latency =
|
|
config_get_string(main->basicConfig, "YouTube", "Latency");
|
|
index = ui->latencyBox->findData(latency);
|
|
ui->latencyBox->setCurrentIndex(index);
|
|
|
|
bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR");
|
|
ui->checkDVR->setChecked(dvr);
|
|
|
|
bool forKids =
|
|
config_get_bool(main->basicConfig, "YouTube", "MadeForKids");
|
|
if (forKids)
|
|
ui->yesMakeForKids->setChecked(true);
|
|
else
|
|
ui->notMakeForKids->setChecked(true);
|
|
|
|
bool schedLater = config_get_bool(main->basicConfig, "YouTube",
|
|
"ScheduleForLater");
|
|
ui->checkScheduledLater->setChecked(schedLater);
|
|
|
|
bool autoStart =
|
|
config_get_bool(main->basicConfig, "YouTube", "AutoStart");
|
|
ui->checkAutoStart->setChecked(autoStart);
|
|
|
|
bool autoStop =
|
|
config_get_bool(main->basicConfig, "YouTube", "AutoStop");
|
|
ui->checkAutoStop->setChecked(autoStop);
|
|
|
|
const char *projection =
|
|
config_get_string(main->basicConfig, "YouTube", "Projection");
|
|
if (projection && *projection) {
|
|
if (strcmp(projection, "360") == 0)
|
|
ui->check360Video->setChecked(true);
|
|
else
|
|
ui->check360Video->setChecked(false);
|
|
}
|
|
|
|
const char *thumbFile = config_get_string(main->basicConfig, "YouTube",
|
|
"ThumbnailFile");
|
|
if (thumbFile && *thumbFile) {
|
|
QFileInfo tFile(thumbFile);
|
|
// Re-check validity before setting path again
|
|
if (tFile.exists() && tFile.size() <= 2 * 1024 * 1024) {
|
|
thumbnailFile = tFile.absoluteFilePath();
|
|
ui->selectedFileName->setText(thumbnailFile);
|
|
ui->selectFileButton->setText(
|
|
QTStr("YouTube.Actions.Thumbnail.ClearFile"));
|
|
|
|
QImageReader imgReader(thumbnailFile);
|
|
imgReader.setAutoTransform(true);
|
|
const QImage newImage = imgReader.read();
|
|
ui->thumbnailPreview->setPixmap(
|
|
QPixmap::fromImage(newImage).scaled(
|
|
160, 90, Qt::KeepAspectRatio,
|
|
Qt::SmoothTransformation));
|
|
}
|
|
}
|
|
}
|
|
|
|
void OBSYoutubeActions::OpenYouTubeDashboard()
|
|
{
|
|
ChannelDescription channel;
|
|
if (!apiYouTube->GetChannelDescription(channel)) {
|
|
blog(LOG_DEBUG, "Could not get channel description.");
|
|
ShowErrorDialog(
|
|
this,
|
|
apiYouTube->GetLastError().isEmpty()
|
|
? QTStr("YouTube.Actions.Error.General")
|
|
: QTStr("YouTube.Actions.Error.Text")
|
|
.arg(apiYouTube->GetLastError()));
|
|
return;
|
|
}
|
|
|
|
//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
|
|
QString uri =
|
|
QString("https://studio.youtube.com/channel/%1/videos/live?filter=[]&sort={\"columnType\"%3A\"date\"%2C\"sortOrder\"%3A\"DESCENDING\"}")
|
|
.arg(channel.id);
|
|
QDesktopServices::openUrl(uri);
|
|
}
|
|
|
|
void OBSYoutubeActions::Cancel()
|
|
{
|
|
workerThread->stop();
|
|
reject();
|
|
}
|
|
void OBSYoutubeActions::Accept()
|
|
{
|
|
workerThread->stop();
|
|
accept();
|
|
}
|