diff --git a/frontend/cmake/feature-restream.cmake b/frontend/cmake/feature-restream.cmake index 5e77f75dda36ae..d0886e31ea0a59 100644 --- a/frontend/cmake/feature-restream.cmake +++ b/frontend/cmake/feature-restream.cmake @@ -1,5 +1,14 @@ if(RESTREAM_CLIENTID AND RESTREAM_HASH MATCHES "^(0|[a-fA-F0-9]+)$" AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE oauth/RestreamAuth.cpp oauth/RestreamAuth.hpp) + target_sources( + obs-studio + PRIVATE + oauth/RestreamAuth.cpp + oauth/RestreamAuth.hpp + forms/OBSRestreamActions.ui + dialogs/OBSRestreamActions.cpp + dialogs/OBSRestreamActions.hpp + ) + target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED) else() target_disable_feature(obs-studio "Restream API connection") diff --git a/frontend/cmake/ui-widgets.cmake b/frontend/cmake/ui-widgets.cmake index 20befdeb0c3774..e2ae98c4485414 100644 --- a/frontend/cmake/ui-widgets.cmake +++ b/frontend/cmake/ui-widgets.cmake @@ -14,6 +14,7 @@ target_sources( widgets/ColorSelect.hpp widgets/OBSBasic.cpp widgets/OBSBasic.hpp + widgets/OBSBasic_Broadcast.cpp widgets/OBSBasic_Browser.cpp widgets/OBSBasic_Clipboard.cpp widgets/OBSBasic_ContextToolbar.cpp @@ -28,6 +29,7 @@ target_sources( widgets/OBSBasic_Projectors.cpp widgets/OBSBasic_Recording.cpp widgets/OBSBasic_ReplayBuffer.cpp + widgets/OBSBasic_Restream.cpp widgets/OBSBasic_SceneCollections.cpp widgets/OBSBasic_SceneItems.cpp widgets/OBSBasic_Scenes.cpp diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index 572b15f15117fb..41eb2a94c04081 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1561,6 +1561,19 @@ YouTube.Errors.invalidTransition="The attempted transition was invalid. This may YouTube.DocksRemoval.Title="Clear Legacy YouTube Browser Docks" YouTube.DocksRemoval.Text="These browser docks will be removed as deprecated:\n\n%1\nUse \"Docks/YouTube Live Control Room\" instead." +# Restream Actions and Auth +Restream.Actions.WindowTitle="Restream Broadcast Setup" +Restream.Actions.BroadcastSelectTitle="Select Existing Broadcast" +Restream.Actions.DashboardButton="Open Restream"; +Restream.Actions.BroadcastSelectButton="Select Broadcast" +Restream.Actions.BroadcastSelectAndStartButton="Select Broadcast and Start Streaming" +Restream.Actions.BroadcastScheduled="Scheduled" +Restream.Actions.BroadcastLoadingTitle="Loading broadcast information..." +Restream.Actions.BroadcastLoadingText="Loading broadcast information for %1, please wait..." +Restream.Actions.BroadcastLoadingFailureTitle="Failed to load broadcast information" +Restream.Actions.BroadcastLoadingFailureText="Failed to load broadcast information for %1\n\n%2: %3" +Restream.Actions.BroadcastLoadingEmptyText="Failed to access Restream setup.\nPlease select a broadcast to enable streaming." + # MultitrackVideo ConfigDownload.WarningMessageTitle="Warning" FailedToStartStream.MissingConfigURL="No config URL available for the current service" diff --git a/frontend/dialogs/OBSRestreamActions.cpp b/frontend/dialogs/OBSRestreamActions.cpp new file mode 100644 index 00000000000000..cba77ebe94f095 --- /dev/null +++ b/frontend/dialogs/OBSRestreamActions.cpp @@ -0,0 +1,177 @@ +#include "OBSRestreamActions.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "moc_OBSRestreamActions.cpp" + +OBSRestreamActions::OBSRestreamActions(QWidget *parent, Auth *auth, bool broadcastReady) + : QDialog(parent), + ui(new Ui::OBSRestreamActions), + restreamAuth(dynamic_cast(auth)), + broadcastReady(broadcastReady) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + eventFilter = new OBSEventFilter([this](QObject *, QEvent *event) { + if (event->type() == QEvent::ApplicationActivate) { + auto events = this->restreamAuth->GetBroadcastInfo(); + this->UpdateBroadcastList(events); + } + return false; + }); + + App()->installEventFilter(eventFilter); + + ui->setupUi(this); + ui->okButton->setEnabled(false); + ui->saveButton->setEnabled(false); + + connect(ui->okButton, &QPushButton::clicked, this, &OBSRestreamActions::BroadcastSelectAndStartAction); + connect(ui->saveButton, &QPushButton::clicked, this, &OBSRestreamActions::BroadcastSelectAction); + connect(ui->dashboardButton, &QPushButton::clicked, this, &OBSRestreamActions::OpenRestreamDashboard); + connect(ui->cancelButton, &QPushButton::clicked, this, [&]() { + blog(LOG_DEBUG, "Restream live event creation cancelled."); + reject(); + }); + + qDeleteAll(ui->scrollAreaWidgetContents->findChildren(QString(), Qt::FindDirectChildrenOnly)); + + auto events = restreamAuth->GetBroadcastInfo(); + this->UpdateBroadcastList(events); +} + +OBSRestreamActions::~OBSRestreamActions() +{ + if (eventFilter) + App()->removeEventFilter(eventFilter); +} + +void OBSRestreamActions::UpdateBroadcastList(QVector &events) +{ + if (events.isEmpty()) { + RestreamEventDescription event; + event.id = ""; + event.title = "Live with Restream"; + event.scheduledFor = 0; + event.showId = ""; + events.push_back(event); + } + + auto tryToFindShowId = selectedShowId; + if (tryToFindShowId.empty()) + tryToFindShowId = restreamAuth->GetShowId(); + + selectedEventId = ""; + selectedShowId = ""; + + qDeleteAll(ui->scrollAreaWidgetContents->findChildren(QString(), Qt::FindDirectChildrenOnly)); + EnableOkButton(false); + + for (auto event : events) { + if (event.showId == tryToFindShowId) { + selectedEventId = event.id; + selectedShowId = event.showId; + + EnableOkButton(true); + break; + } + } + + if (selectedShowId.empty()) { + if (events.size()) { + auto event = events.at(0); + selectedEventId = event.id; + selectedShowId = event.showId; + + restreamAuth->SelectShow(selectedEventId, selectedShowId); + + EnableOkButton(true); + emit ok(false); + } else { + restreamAuth->ResetShow(); + emit ok(false); + } + } + + for (auto event : events) { + ClickableLabel *label = new ClickableLabel(); + label->setTextFormat(Qt::RichText); + label->setAlignment(Qt::AlignHCenter); + label->setMargin(4); + + QString scheduledForString; + if (event.scheduledFor > 0) { + QDateTime dateTime = QDateTime::fromSecsSinceEpoch(event.scheduledFor); + scheduledForString = QLocale().toString( + dateTime, QString("%1 %2").arg(QLocale().dateFormat(QLocale::LongFormat), + QLocale().timeFormat(QLocale::ShortFormat))); + + label->setText(QString("%1
%2: %3") + .arg(QString::fromStdString(event.title), + QTStr("Restream.Actions.BroadcastScheduled"), scheduledForString)); + } else { + label->setText( + QString("%1%2").arg(QString::fromStdString(event.title), scheduledForString)); + } + + connect(label, &ClickableLabel::clicked, this, [&, label, event]() { + for (QWidget *i : ui->scrollAreaWidgetContents->findChildren( + QString(), Qt::FindDirectChildrenOnly)) { + + i->setProperty("class", ""); + i->style()->unpolish(i); + i->style()->polish(i); + } + label->setProperty("class", "row-selected"); + label->style()->unpolish(label); + label->style()->polish(label); + + selectedEventId = event.id; + selectedShowId = event.showId; + + EnableOkButton(true); + }); + + ui->scrollAreaWidgetContents->layout()->addWidget(label); + + if (event.showId == selectedShowId) { + label->setProperty("class", "row-selected"); + label->style()->unpolish(label); + label->style()->polish(label); + } + } +} + +void OBSRestreamActions::EnableOkButton(bool state) +{ + ui->okButton->setEnabled(state); + ui->saveButton->setEnabled(state); +} + +void OBSRestreamActions::BroadcastSelectAction() +{ + restreamAuth->SelectShow(selectedEventId, selectedShowId); + emit ok(false); + accept(); +} + +void OBSRestreamActions::BroadcastSelectAndStartAction() +{ + restreamAuth->SelectShow(selectedEventId, selectedShowId); + emit ok(true); + accept(); +} + +void OBSRestreamActions::OpenRestreamDashboard() +{ + QDesktopServices::openUrl(QString("https://app.restream.io/")); +} diff --git a/frontend/dialogs/OBSRestreamActions.hpp b/frontend/dialogs/OBSRestreamActions.hpp new file mode 100644 index 00000000000000..45c5c83fe32deb --- /dev/null +++ b/frontend/dialogs/OBSRestreamActions.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "ui_OBSRestreamActions.h" +#include "oauth/RestreamAuth.hpp" + +class OBSEventFilter; + +class OBSRestreamActions : public QDialog { + Q_OBJECT + Q_PROPERTY(QIcon thumbPlaceholder READ GetPlaceholder WRITE SetPlaceholder DESIGNABLE true) + + std::unique_ptr ui; + +signals: + void ok(bool start_now); + +protected: + void EnableOkButton(bool state); + +public: + explicit OBSRestreamActions(QWidget *parent, Auth *auth, bool broadcastReady); + virtual ~OBSRestreamActions() override; + + bool Valid() { return valid; }; + +private: + void UpdateBroadcastList(QVector &newEvents); + void BroadcastSelectAction(); + void BroadcastSelectAndStartAction(); + void OpenRestreamDashboard(); + + QIcon GetPlaceholder() { return thumbPlaceholder; } + void SetPlaceholder(const QIcon &icon) { thumbPlaceholder = icon; } + + RestreamAuth *restreamAuth; + OBSEventFilter *eventFilter; + std::string selectedEventId; + std::string selectedShowId; + bool broadcastReady; + bool valid = false; + QIcon thumbPlaceholder; +}; diff --git a/frontend/forms/OBSRestreamActions.ui b/frontend/forms/OBSRestreamActions.ui new file mode 100644 index 00000000000000..7d803fb572c400 --- /dev/null +++ b/frontend/forms/OBSRestreamActions.ui @@ -0,0 +1,187 @@ + + + OBSRestreamActions + + + + 0 + 0 + 821 + 738 + + + + + 0 + 0 + + + + Restream.Actions.WindowTitle + + + true + + + + 6 + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + 0 + + + + Restream.Actions.BroadcastSelectTitle + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::ScrollBarAsNeeded + + + true + + + + + 0 + 0 + 191 + 216 + + + + + 0 + 0 + + + + + + + border: 1px solid black; + + + <big>Go live now</big> + + + Qt::RichText + + + Qt::AlignCenter + + + 4 + + + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + QLayout::SetNoConstraint + + + + + Cancel + + + + + + + Restream.Actions.DashboardButton + + + + + + + Restream.Actions.BroadcastSelectButton + + + + + + + Restream.Actions.BroadcastSelectAndStartButton + + + + + + + + + + + + + ClickableLabel + QLabel +
components/ClickableLabel.hpp
+
+
+ + +
diff --git a/frontend/oauth/RestreamAuth.cpp b/frontend/oauth/RestreamAuth.cpp index e428e889d1c210..2796f3933ee51a 100644 --- a/frontend/oauth/RestreamAuth.cpp +++ b/frontend/oauth/RestreamAuth.cpp @@ -5,10 +5,8 @@ #include #include #include - #include #include - #include #include "moc_RestreamAuth.cpp" @@ -19,14 +17,14 @@ using namespace json11; #define RESTREAM_AUTH_URL OAUTH_BASE_URL "v1/restream/redirect" #define RESTREAM_TOKEN_URL OAUTH_BASE_URL "v1/restream/token" -#define RESTREAM_STREAMKEY_URL "https://api.restream.io/v2/user/streamKey" +#define RESTREAM_API_URL "https://api.restream.io/v2/user" #define RESTREAM_SCOPE_VERSION 1 - #define RESTREAM_CHAT_DOCK_NAME "restreamChat" #define RESTREAM_INFO_DOCK_NAME "restreamInfo" #define RESTREAM_CHANNELS_DOCK_NAME "restreamChannel" +#define RESTREAM_SECTION_NAME "Restream" -static Auth::Def restreamDef = {"Restream", Auth::Type::OAuth_StreamKey}; +static Auth::Def restreamDef = {"Restream", Auth::Type::OAuth_StreamKey, false, true}; /* ------------------------------------------------------------------------- */ @@ -44,17 +42,18 @@ RestreamAuth::~RestreamAuth() main->RemoveDockWidget(RESTREAM_CHANNELS_DOCK_NAME); } -bool RestreamAuth::GetChannelInfo() +QVector RestreamAuth::GetBroadcastInfo() try { + QVector events; + std::string client_id = RESTREAM_CLIENTID; deobfuscate_str(&client_id[0], RESTREAM_HASH); if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION)) - return false; + return events; + if (token.empty()) - return false; - if (!key_.empty()) - return true; + return events; std::string auth; auth += "Authorization: Bearer "; @@ -70,14 +69,15 @@ try { bool success; auto func = [&]() { - success = GetRemoteFile(RESTREAM_STREAMKEY_URL, output, error, nullptr, "application/json", "", nullptr, - headers, nullptr, 5); + auto url = QString("%1/events/upcoming?source=2&sort=scheduled").arg(RESTREAM_API_URL); + success = GetRemoteFile(url.toUtf8(), output, error, nullptr, "application/json", "", nullptr, headers, + nullptr, 5); }; ExecThreadedWithoutBlocking(func, QTStr("Auth.LoadingChannel.Title"), QTStr("Auth.LoadingChannel.Text").arg(service())); if (!success || output.empty()) - throw ErrorInfo("Failed to get stream key from remote", error); + throw ErrorInfo("Failed to get upcoming events info from remote", error); json = Json::parse(output, error); if (!error.empty()) @@ -87,57 +87,205 @@ try { if (!error.empty()) throw ErrorInfo(error, json["error_description"].string_value()); - key_ = json["streamKey"].string_value(); + auto items = json.array_items(); + if (!items.size()) { + OBSMessageBox::warning(OBSBasic::Get(), QTStr("Restream.Actions.BroadcastLoadingFailureTitle"), + QTStr("Restream.Actions.BroadcastLoadingEmptyText")); + return QVector(); + } - return true; -} catch (ErrorInfo info) { - QString title = QTStr("Auth.ChannelFailure.Title"); - QString text = QTStr("Auth.ChannelFailure.Text").arg(service(), info.message.c_str(), info.error.c_str()); + for (auto item : items) { + RestreamEventDescription event; + event.id = item["id"].string_value(); + event.title = item["title"].string_value(); + event.scheduledFor = item["scheduledFor"].is_number() ? item["scheduledFor"].int_value() : 0; + event.showId = item["showId"].string_value(); + events.push_back(event); + } + + std::sort(events.begin(), events.end(), + [](const RestreamEventDescription &a, const RestreamEventDescription &b) { + return a.scheduledFor && (!b.scheduledFor || a.scheduledFor < b.scheduledFor); + }); + return events; +} catch (ErrorInfo info) { + QString title = QTStr("Restream.Actions.BroadcastLoadingFailureTitle"); + QString text = QTStr("Restream.Actions.BroadcastLoadingFailureText") + .arg(service(), info.message.c_str(), info.error.c_str()); QMessageBox::warning(OBSBasic::Get(), title, text); + blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), info.error.c_str()); + return QVector(); +} + +std::string RestreamAuth::GetStreamingKey(std::string eventId) +try { + std::string client_id = RESTREAM_CLIENTID; + deobfuscate_str(&client_id[0], RESTREAM_HASH); + + if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION)) + return ""; + + if (token.empty()) + return ""; + + std::string auth; + auth += "Authorization: Bearer "; + auth += token; + + std::vector headers; + headers.push_back(std::string("Client-ID: ") + client_id); + headers.push_back(std::move(auth)); + + auto url = eventId.empty() || eventId == "default" + ? QString("%1/streamKey").arg(RESTREAM_API_URL) + : QString("%1/events/%2/streamKey").arg(RESTREAM_API_URL, QString::fromStdString(eventId)); + + std::string output; + std::string error; + Json json; + bool success; + + auto func = [&, url]() { + success = GetRemoteFile(url.toUtf8(), output, error, nullptr, "application/json", "", nullptr, headers, + nullptr, 5); + }; + + ExecThreadedWithoutBlocking(func, QTStr("Auth.LoadingChannel.Title"), + QTStr("Auth.LoadingChannel.Text").arg(service())); + if (!success || output.empty()) + throw ErrorInfo("Failed to get the stream key from remote", error); + + json = Json::parse(output, error); + if (!error.empty()) + throw ErrorInfo("Failed to parse json", error); + + error = json["error"].string_value(); + if (!error.empty()) + throw ErrorInfo(error, json["error_description"].string_value()); + return json["streamKey"].string_value(); +} catch (ErrorInfo info) { + QString title = QTStr("Restream.Actions.BroadcastLoadingFailureTitle"); + QString text = QTStr("Restream.Actions.BroadcastLoadingFailureText") + .arg(service(), info.message.c_str(), info.error.c_str()); + QMessageBox::warning(OBSBasic::Get(), title, text); blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), info.error.c_str()); - return false; + return ""; +} + +void RestreamAuth::ResetShow() +{ + this->key_ = ""; + this->showId = ""; +} + +bool RestreamAuth::SelectShow(std::string eventId, std::string showId) +{ + auto key = GetStreamingKey(eventId); + if (key.empty()) { + this->key_ = ""; + this->showId = ""; + return false; + } + + if (this->key_ != key || this->showId != showId) { + this->key_ = key; + this->showId = showId; + + OBSBasic *main = OBSBasic::Get(); + obs_service_t *service = main->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + obs_data_set_string(settings, "key", key.c_str()); + obs_service_update(service, settings); + + auto showIdParam = !showId.empty() ? QString("?show-id=%1").arg(QString::fromStdString(showId)) + : QString(""); + + if (chatWidgetBrowser) { + auto url = QString("https://restream.io/chat-application%1").arg(showIdParam); + chatWidgetBrowser->setURL(url.toStdString()); + } + + if (titlesWidgetBrowser) { + auto url = QString("https://restream.io/titles/embed%1").arg(showIdParam); + titlesWidgetBrowser->setURL(url.toStdString()); + } + + if (channelWidgetBrowser) { + auto url = QString("https://restream.io/channel/embed%1").arg(showIdParam); + channelWidgetBrowser->setURL(url.toStdString()); + } + } + + return true; +} + +std::string RestreamAuth::GetShowId() +{ + return showId; +} + +bool RestreamAuth::IsBroadcastReady() +{ + return !key_.empty(); } void RestreamAuth::SaveInternal() { OBSBasic *main = OBSBasic::Get(); config_set_string(main->Config(), service(), "DockState", main->saveState().toBase64().constData()); - OAuthStreamKey::SaveInternal(); -} + config_set_string(main->Config(), service(), "ShowId", showId.c_str()); -static inline std::string get_config_str(OBSBasic *main, const char *section, const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; + OAuthStreamKey::SaveInternal(); } bool RestreamAuth::LoadInternal() { firstLoad = false; + + OBSBasic *main = OBSBasic::Get(); + auto showIdVal = config_get_string(main->Config(), service(), "ShowId"); + showId = showIdVal ? showIdVal : ""; + return OAuthStreamKey::LoadInternal(); } void RestreamAuth::LoadUI() { - if (!cef) - return; if (uiLoaded) return; - if (!GetChannelInfo()) + + // Select the previous event + if (key_.empty()) { + auto foundEventId = std::string(""); + auto foundShowId = std::string(""); + + auto events = GetBroadcastInfo(); + for (auto event : events) { + if (event.showId == showId) { + foundEventId = event.id; + foundShowId = event.showId; + break; + } + } + + if (foundShowId.empty() && events.size()) { + auto event = events.at(0); + foundEventId = event.id; + foundShowId = event.showId; + } + + SelectShow(foundEventId, foundShowId); + } + +#ifdef BROWSER_AVAILABLE + if (!cef) return; OBSBasic::InitBrowserPanelSafeBlock(); OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - /* ----------------------------------- */ - - url = "https://restream.io/chat-application"; + auto showIdParam = !showId.empty() ? QString("?show-id=%1").arg(QString::fromStdString(showId)) : QString(""); QSize size = main->frameSize(); QPoint pos = main->pos(); @@ -149,15 +297,14 @@ void RestreamAuth::LoadUI() chat->setWindowTitle(QTStr("Auth.Chat")); chat->setAllowedAreas(Qt::AllDockWidgetAreas); - browser = cef->create_widget(chat, url, panel_cookies); - chat->SetWidget(browser); + auto url = QString("https://restream.io/chat-application%1").arg(showIdParam); + chatWidgetBrowser = cef->create_widget(chat, url.toStdString(), panel_cookies); + chat->SetWidget(chatWidgetBrowser); main->AddDockWidget(chat, Qt::RightDockWidgetArea); /* ----------------------------------- */ - url = "https://restream.io/titles/embed"; - BrowserDock *info = new BrowserDock(QTStr("Auth.StreamInfo")); info->setObjectName(RESTREAM_INFO_DOCK_NAME); info->resize(410, 600); @@ -165,15 +312,14 @@ void RestreamAuth::LoadUI() info->setWindowTitle(QTStr("Auth.StreamInfo")); info->setAllowedAreas(Qt::AllDockWidgetAreas); - browser = cef->create_widget(info, url, panel_cookies); - info->SetWidget(browser); + url = QString("https://restream.io/titles/embed%1").arg(showIdParam); + titlesWidgetBrowser = cef->create_widget(info, url.toStdString(), panel_cookies); + info->SetWidget(titlesWidgetBrowser); main->AddDockWidget(info, Qt::LeftDockWidgetArea); /* ----------------------------------- */ - url = "https://restream.io/channel/embed"; - BrowserDock *channels = new BrowserDock(QTStr("RestreamAuth.Channels")); channels->setObjectName(RESTREAM_CHANNELS_DOCK_NAME); channels->resize(410, 600); @@ -181,8 +327,9 @@ void RestreamAuth::LoadUI() channels->setWindowTitle(QTStr("RestreamAuth.Channels")); channels->setAllowedAreas(Qt::AllDockWidgetAreas); - browser = cef->create_widget(channels, url, panel_cookies); - channels->SetWidget(browser); + url = QString("https://restream.io/channel/embed%1").arg(showIdParam); + channelWidgetBrowser = cef->create_widget(channels, url.toStdString(), panel_cookies); + channels->SetWidget(channelWidgetBrowser); main->AddDockWidget(channels, Qt::LeftDockWidgetArea); @@ -207,6 +354,7 @@ void RestreamAuth::LoadUI() if (main->isVisible() || !main->isMaximized()) main->restoreState(dockState); } +#endif uiLoaded = true; } @@ -232,25 +380,18 @@ std::shared_ptr RestreamAuth::Login(QWidget *parent, const std::string &) OAuthLogin login(parent, RESTREAM_AUTH_URL, false); cef->add_popup_whitelist_url("about:blank", &login); - if (login.exec() == QDialog::Rejected) { + if (login.exec() == QDialog::Rejected) return nullptr; - } - std::shared_ptr auth = std::make_shared(restreamDef); + std::shared_ptr restreamAuth = std::make_shared(restreamDef); std::string client_id = RESTREAM_CLIENTID; deobfuscate_str(&client_id[0], RESTREAM_HASH); - if (!auth->GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION, QT_TO_UTF8(login.GetCode()))) { + if (!restreamAuth->GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION, QT_TO_UTF8(login.GetCode()))) return nullptr; - } - std::string error; - if (auth->GetChannelInfo()) { - return auth; - } - - return nullptr; + return restreamAuth; } static std::shared_ptr CreateRestreamAuth() @@ -274,3 +415,8 @@ void RegisterRestreamAuth() OAuth::RegisterOAuth(restreamDef, CreateRestreamAuth, RestreamAuth::Login, DeleteCookies); } + +bool IsRestreamService(const std::string &service) +{ + return service == restreamDef.service; +} diff --git a/frontend/oauth/RestreamAuth.hpp b/frontend/oauth/RestreamAuth.hpp index 6ea9128a1418cd..42bf1a936fe93e 100644 --- a/frontend/oauth/RestreamAuth.hpp +++ b/frontend/oauth/RestreamAuth.hpp @@ -2,23 +2,44 @@ #include "OAuth.hpp" +class QCefWidget; + +struct RestreamEventDescription { + std::string id; + std::string title; + qint64 scheduledFor; + std::string showId; +}; + class RestreamAuth : public OAuthStreamKey { Q_OBJECT bool uiLoaded = false; + std::string showId; + + QCefWidget *chatWidgetBrowser = NULL; + QCefWidget *titlesWidgetBrowser = NULL; + QCefWidget *channelWidgetBrowser = NULL; virtual bool RetryLogin() override; virtual void SaveInternal() override; virtual bool LoadInternal() override; - bool GetChannelInfo(); - virtual void LoadUI() override; public: RestreamAuth(const Def &d); ~RestreamAuth(); + QVector GetBroadcastInfo(); + std::string GetStreamingKey(std::string eventId); + bool SelectShow(std::string eventId, std::string showId); + void ResetShow(); + std::string GetShowId(); + bool IsBroadcastReady(); + static std::shared_ptr Login(QWidget *parent, const std::string &service_name); }; + +bool IsRestreamService(const std::string &service); diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index e8169c50e81efb..afe737cdc99655 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -1652,6 +1652,10 @@ private slots: bool autostart, bool autostop, bool start_now); #endif +#ifdef RESTREAM_ENABLED + void RestreamActionDialogOk(bool start_now); +#endif + void BroadcastButtonClicked(); void SetBroadcastFlowEnabled(bool enabled); diff --git a/frontend/widgets/OBSBasic_Broadcast.cpp b/frontend/widgets/OBSBasic_Broadcast.cpp new file mode 100644 index 00000000000000..d48a43aab11931 --- /dev/null +++ b/frontend/widgets/OBSBasic_Broadcast.cpp @@ -0,0 +1,133 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#ifdef YOUTUBE_ENABLED +#include +#include +#include +#endif +#ifdef RESTREAM_ENABLED +#include +#endif + +#include + +using namespace std; + +void OBSBasic::BroadcastButtonClicked() +{ + if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { + SetupBroadcast(); + return; + } + + if (!autoStartBroadcast) { +#ifdef YOUTUBE_ENABLED + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StartLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); + return; + } + } +#endif + broadcastActive = true; + autoStartBroadcast = true; // and clear the flag + + emit BroadcastStreamStarted(autoStopBroadcast); + } else if (!autoStopBroadcast) { +#ifdef YOUTUBE_ENABLED + bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) + return; + } + + std::shared_ptr ytAuth = dynamic_pointer_cast(auth); + if (ytAuth.get()) { + if (!ytAuth->StopLatestBroadcast()) { + auto last_error = ytAuth->GetLastError(); + if (last_error.isEmpty()) + last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + if (!ytAuth->GetTranslatedError(last_error)) + last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") + .arg(last_error, ytAuth->GetBroadcastId()); + + OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); + } + } +#endif + broadcastActive = false; + broadcastReady = false; + + autoStopBroadcast = true; + QMetaObject::invokeMethod(this, "StopStreaming"); + emit BroadcastStreamReady(broadcastReady); + SetBroadcastFlowEnabled(true); + } +} + +void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +{ + emit BroadcastFlowEnabled(enabled); + +#ifdef RESTREAM_ENABLED + Auth *const auth = GetAuth(); + if (auth && IsRestreamService(auth->service())) { + auto restreamAuth = dynamic_cast(auth); + broadcastReady = restreamAuth->IsBroadcastReady(); + emit BroadcastStreamReady(broadcastReady); + } +#endif +} + +void OBSBasic::SetupBroadcast() +{ +#if defined YOUTUBE_ENABLED || defined RESTREAM_ENABLED + Auth *const auth = GetAuth(); +#endif +#ifdef YOUTUBE_ENABLED + if (IsYouTubeService(auth->service())) { + OBSYoutubeActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); + dialog.exec(); + } +#endif +#ifdef RESTREAM_ENABLED + if (IsRestreamService(auth->service())) { + OBSRestreamActions dialog(this, auth, broadcastReady); + connect(&dialog, &OBSRestreamActions::ok, this, &OBSBasic::RestreamActionDialogOk); + dialog.exec(); + return; + } +#endif +} diff --git a/frontend/widgets/OBSBasic_Restream.cpp b/frontend/widgets/OBSBasic_Restream.cpp new file mode 100644 index 00000000000000..1ebbdde92f42b1 --- /dev/null +++ b/frontend/widgets/OBSBasic_Restream.cpp @@ -0,0 +1,46 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + Zachary Lund + Philippe Groarke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "OBSBasic.hpp" + +#ifdef RESTREAM_ENABLED +#include +#endif + +#include + +using namespace std; + +extern bool cef_js_avail; + +#ifdef RESTREAM_ENABLED +void OBSBasic::RestreamActionDialogOk(bool start_now) +{ + auto *restreamAuth = dynamic_cast(GetAuth()); + + autoStartBroadcast = true; + autoStopBroadcast = true; + broadcastReady = restreamAuth->IsBroadcastReady(); + + emit BroadcastStreamReady(broadcastReady); + + if (broadcastReady && start_now) + QMetaObject::invokeMethod(this, "StartStreaming"); +} +#endif diff --git a/frontend/widgets/OBSBasic_Streaming.cpp b/frontend/widgets/OBSBasic_Streaming.cpp index db5a3004604d29..3e0f1f39a8596d 100644 --- a/frontend/widgets/OBSBasic_Streaming.cpp +++ b/frontend/widgets/OBSBasic_Streaming.cpp @@ -24,6 +24,9 @@ #include #include #endif +#ifdef RESTREAM_ENABLED +#include +#endif #include @@ -341,6 +344,12 @@ void OBSBasic::StreamingStop(int code, QString last_error) youtubeAppDock->IngestionStopped(); #endif +#ifdef RESTREAM_ENABLED + Auth *const auth = GetAuth(); + if (auth && IsRestreamService(auth->service())) + broadcastActive = false; +#endif + blog(LOG_INFO, STREAMING_STOP); if (encode_error) { diff --git a/frontend/widgets/OBSBasic_YouTube.cpp b/frontend/widgets/OBSBasic_YouTube.cpp index bd708bb3f87401..128b97e770438d 100644 --- a/frontend/widgets/OBSBasic_YouTube.cpp +++ b/frontend/widgets/OBSBasic_YouTube.cpp @@ -129,87 +129,6 @@ void OBSBasic::ShowYouTubeAutoStartWarning() } #endif -void OBSBasic::BroadcastButtonClicked() -{ - if (!broadcastReady || (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - return; - } - - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStartFailed"), last_error, true); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag - - emit BroadcastStreamStarted(autoStopBroadcast); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(App()->GetUserConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - if (button == QMessageBox::No) - return; - } - - std::shared_ptr ytAuth = dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, ytAuth->GetBroadcastId()); - - OBSMessageBox::warning(this, QTStr("Output.BroadcastStopFailed"), last_error, true); - } - } -#endif - broadcastActive = false; - broadcastReady = false; - - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - emit BroadcastStreamReady(broadcastReady); - SetBroadcastFlowEnabled(true); - } -} - -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) -{ - emit BroadcastFlowEnabled(enabled); -} - -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, &OBSBasic::YouTubeActionDialogOk); - dialog.exec(); - } -#endif -} - #ifdef YOUTUBE_ENABLED YouTubeAppDock *OBSBasic::GetYouTubeAppDock() {