UI: Add YouTube integration

This commit is contained in:
Yuriy Chumak 2021-06-27 15:30:00 -07:00 committed by jp9000
parent 0654675f32
commit e6f1daab8c
21 changed files with 2824 additions and 138 deletions

View file

@ -22,6 +22,10 @@ env:
TWITCH_HASH: ${{ secrets.TWITCH_HASH }}
RESTREAM_CLIENTID: ${{ secrets.RESTREAM_CLIENTID }}
RESTREAM_HASH: ${{ secrets.RESTREAM_HASH }}
YOUTUBE_CLIENTID: ${{ secrets.YOUTUBE_CLIENTID }}
YOUTUBE_CLIENTID_HASH: ${{ secrets.YOUTUBE_CLIENTID_HASH }}
YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
YOUTUBE_SECRET_HASH: ${{ secrets.YOUTUBE_SECRET_HASH }}
jobs:
macos64:

View file

@ -39,6 +39,28 @@ else()
set(RESTREAM_ENABLED TRUE)
endif()
if(DEFINED ENV{YOUTUBE_CLIENTID} AND NOT DEFINED YOUTUBE_CLIENTID)
set(YOUTUBE_CLIENTID "$ENV{YOUTUBE_CLIENTID}")
endif()
if(DEFINED ENV{YOUTUBE_CLIENTID_HASH} AND NOT DEFINED YOUTUBE_CLIENTID_HASH)
set(YOUTUBE_CLIENTID_HASH "$ENV{YOUTUBE_CLIENTID_HASH}")
endif()
if(DEFINED ENV{YOUTUBE_SECRET} AND NOT DEFINED YOUTUBE_SECRET)
set(YOUTUBE_SECRET "$ENV{YOUTUBE_SECRET}")
endif()
if(DEFINED ENV{YOUTUBE_SECRET_HASH} AND NOT DEFINED YOUTUBE_SECRET_HASH)
set(YOUTUBE_SECRET_HASH "$ENV{YOUTUBE_SECRET_HASH}")
endif()
if(NOT DEFINED YOUTUBE_CLIENTID OR "${YOUTUBE_CLIENTID}" STREQUAL "" OR
NOT DEFINED YOUTUBE_SECRET OR "${YOUTUBE_SECRET}" STREQUAL "" OR
NOT DEFINED YOUTUBE_CLIENTID_HASH OR "${YOUTUBE_CLIENTID_HASH}" STREQUAL "" OR
NOT DEFINED YOUTUBE_SECRET_HASH OR "${YOUTUBE_SECRET_HASH}" STREQUAL "")
set(YOUTUBE_ENABLED FALSE)
else()
set(YOUTUBE_ENABLED TRUE)
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/ui-config.h")
@ -167,6 +189,18 @@ if(BROWSER_AVAILABLE_INTERNAL)
auth-restream.hpp
)
endif()
endif()
if(YOUTUBE_ENABLED)
list(APPEND obs_PLATFORM_SOURCES
auth-youtube.cpp
youtube-api-wrappers.cpp
)
list(APPEND obs_PLATFORM_HEADERS
auth-youtube.hpp
youtube-api-wrappers.hpp
)
endif()
set(obs_libffutil_SOURCES
@ -377,6 +411,18 @@ set(obs_UI
set(obs_QRC
forms/obs.qrc)
if(YOUTUBE_ENABLED)
list(APPEND obs_SOURCES
window-youtube-actions.cpp
)
list(APPEND obs_HEADERS
window-youtube-actions.hpp
)
list(APPEND obs_UI
forms/OBSYoutubeActions.ui
)
endif()
qt5_wrap_ui(obs_UI_HEADERS ${obs_UI})
qt5_add_resources(obs_QRC_SOURCES ${obs_QRC})

209
UI/auth-youtube.cpp Normal file
View file

@ -0,0 +1,209 @@
#include "auth-youtube.hpp"
#include <iostream>
#include <QMessageBox>
#include <QThread>
#include <vector>
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QUrl>
#ifdef WIN32
#include <windows.h>
#include <shellapi.h>
#pragma comment(lib, "shell32")
#endif
#include "auth-listener.hpp"
#include "obs-app.hpp"
#include "qt-wrappers.hpp"
#include "ui-config.h"
#include "youtube-api-wrappers.hpp"
#include "window-basic-main.hpp"
#include "obf.h"
using namespace json11;
/* ------------------------------------------------------------------------- */
#define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth"
#define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token"
#define YOUTUBE_SCOPE_VERSION 1
#define YOUTUBE_API_STATE_LENGTH 32
#define SECTION_NAME "YouTube"
static const char allowedChars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static const int allowedCount = static_cast<int>(sizeof(allowedChars) - 1);
/* ------------------------------------------------------------------------- */
static inline void OpenBrowser(const QString auth_uri)
{
QUrl url(auth_uri, QUrl::StrictMode);
QDesktopServices::openUrl(url);
}
void RegisterYoutubeAuth()
{
for (auto &service : youtubeServices) {
OAuth::RegisterOAuth(
service,
[service]() {
return std::make_shared<YoutubeApiWrappers>(
service);
},
YoutubeAuth::Login, []() { return; });
}
}
YoutubeAuth::YoutubeAuth(const Def &d)
: OAuthStreamKey(d), section(SECTION_NAME)
{
}
bool YoutubeAuth::RetryLogin()
{
return true;
}
void YoutubeAuth::SaveInternal()
{
OBSBasic *main = OBSBasic::Get();
config_set_string(main->Config(), service(), "DockState",
main->saveState().toBase64().constData());
const char *section_name = section.c_str();
config_set_string(main->Config(), section_name, "RefreshToken",
refresh_token.c_str());
config_set_string(main->Config(), section_name, "Token", token.c_str());
config_set_uint(main->Config(), section_name, "ExpireTime",
expire_time);
config_set_int(main->Config(), section_name, "ScopeVer",
currentScopeVer);
}
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 : "";
}
bool YoutubeAuth::LoadInternal()
{
OBSBasic *main = OBSBasic::Get();
const char *section_name = section.c_str();
refresh_token = get_config_str(main, section_name, "RefreshToken");
token = get_config_str(main, section_name, "Token");
expire_time =
config_get_uint(main->Config(), section_name, "ExpireTime");
currentScopeVer =
(int)config_get_int(main->Config(), section_name, "ScopeVer");
return implicit ? !token.empty() : !refresh_token.empty();
}
void YoutubeAuth::LoadUI()
{
uiLoaded = true;
}
QString YoutubeAuth::GenerateState()
{
std::uniform_int_distribution<> distr(0, allowedCount);
std::string result;
result.reserve(YOUTUBE_API_STATE_LENGTH);
std::generate_n(std::back_inserter(result), YOUTUBE_API_STATE_LENGTH,
[&] {
return static_cast<char>(
allowedChars[distr(randomSeed)]);
});
return result.c_str();
}
// Static.
std::shared_ptr<Auth> YoutubeAuth::Login(QWidget *owner,
const std::string &service)
{
QString auth_code;
AuthListener server;
auto it = std::find_if(youtubeServices.begin(), youtubeServices.end(),
[service](auto &item) {
return service == item.service;
});
if (it == youtubeServices.end()) {
return nullptr;
}
const auto auth = std::make_shared<YoutubeApiWrappers>(*it);
QString redirect_uri =
QString("http://127.0.0.1:%1").arg(server.GetPort());
QMessageBox dlg(owner);
dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint);
dlg.setWindowTitle(QTStr("YouTube.Auth.WaitingAuth.Title"));
std::string clientid = YOUTUBE_CLIENTID;
std::string secret = YOUTUBE_SECRET;
deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH);
deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH);
QString url_template;
url_template += "%1";
url_template += "?response_type=code";
url_template += "&client_id=%2";
url_template += "&redirect_uri=%3";
url_template += "&state=%4";
url_template += "&scope=https://www.googleapis.com/auth/youtube";
QString url = url_template.arg(YOUTUBE_AUTH_URL, clientid.c_str(),
redirect_uri, auth->GenerateState());
QString text = QTStr("YouTube.Auth.WaitingAuth.Text");
text = text.arg(
QString("<a href='%1'>Google OAuth Service</a>").arg(url));
dlg.setText(text);
dlg.setTextFormat(Qt::RichText);
dlg.setStandardButtons(QMessageBox::StandardButton::Cancel);
connect(&dlg, &QMessageBox::buttonClicked, &dlg,
[&](QAbstractButton *) {
#ifdef _DEBUG
blog(LOG_DEBUG, "Action Cancelled.");
#endif
// TODO: Stop server.
dlg.reject();
});
// Async Login.
connect(&server, &AuthListener::ok, &dlg,
[&dlg, &auth_code](QString code) {
#ifdef _DEBUG
blog(LOG_DEBUG, "Got youtube redirected answer: %s",
QT_TO_UTF8(code));
#endif
auth_code = code;
dlg.accept();
});
connect(&server, &AuthListener::fail, &dlg, [&dlg]() {
#ifdef _DEBUG
blog(LOG_DEBUG, "No access granted");
#endif
dlg.reject();
});
auto open_external_browser = [url]() { OpenBrowser(url); };
QScopedPointer<QThread> thread(CreateQThread(open_external_browser));
thread->start();
dlg.exec();
if (!auth->GetToken(YOUTUBE_TOKEN_URL, clientid, secret,
QT_TO_UTF8(redirect_uri), YOUTUBE_SCOPE_VERSION,
QT_TO_UTF8(auth_code), true)) {
return nullptr;
}
return auth;
}

34
UI/auth-youtube.hpp Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include <QString>
#include <random>
#include <string>
#include "auth-oauth.hpp"
const std::vector<Auth::Def> youtubeServices = {
{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true},
{"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true},
{"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true}};
class YoutubeAuth : public OAuthStreamKey {
Q_OBJECT
bool uiLoaded = false;
std::mt19937 randomSeed;
std::string section;
virtual bool RetryLogin() override;
virtual void SaveInternal() override;
virtual bool LoadInternal() override;
virtual void LoadUI() override;
QString GenerateState();
public:
YoutubeAuth(const Def &d);
static std::shared_ptr<Auth> Login(QWidget *parent,
const std::string &service);
};

View file

@ -128,6 +128,7 @@ Auth.InvalidScope.Title="Authentication Required"
Auth.InvalidScope.Text="The authentication requirements for %1 have changed. Some features may not be available."
Auth.LoadingChannel.Title="Loading channel information..."
Auth.LoadingChannel.Text="Loading channel information for %1, please wait..."
Auth.LoadingChannel.Error="Couldn't get channel information."
Auth.ChannelFailure.Title="Failed to load channel"
Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3"
Auth.Chat="Chat"
@ -178,6 +179,7 @@ Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text="This change will app
Basic.AutoConfig.StreamPage.GetStreamKey="Get Stream Key"
Basic.AutoConfig.StreamPage.MoreInfo="More Info"
Basic.AutoConfig.StreamPage.UseStreamKey="Use Stream Key"
Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced="Use Stream Key (advanced)"
Basic.AutoConfig.StreamPage.Service="Service"
Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..."
Basic.AutoConfig.StreamPage.Service.Custom="Custom..."
@ -185,6 +187,7 @@ Basic.AutoConfig.StreamPage.Server="Server"
Basic.AutoConfig.StreamPage.StreamKey="Stream Key"
Basic.AutoConfig.StreamPage.StreamKey.LinkToSite="(Link)"
Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key"
Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account"
Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)"
Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding"
Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardware Encoding eliminates most CPU usage, but may require more bitrate to obtain the same level of quality."
@ -626,6 +629,7 @@ Basic.Main.StartRecording="Start Recording"
Basic.Main.StartReplayBuffer="Start Replay Buffer"
Basic.Main.SaveReplay="Save Replay"
Basic.Main.StartStreaming="Start Streaming"
Basic.Main.StartBroadcast="GO LIVE"
Basic.Main.StartVirtualCam="Start Virtual Camera"
Basic.Main.StopRecording="Stop Recording"
Basic.Main.PauseRecording="Pause Recording"
@ -634,6 +638,7 @@ Basic.Main.StoppingRecording="Stopping Recording..."
Basic.Main.StopReplayBuffer="Stop Replay Buffer"
Basic.Main.StoppingReplayBuffer="Stopping Replay Buffer..."
Basic.Main.StopStreaming="Stop Streaming"
Basic.Main.StopBroadcast="END STREAM"
Basic.Main.StoppingStreaming="Stopping Stream..."
Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)"
Basic.Main.ShowContextBar="Show Source Toolbar"
@ -1167,3 +1172,60 @@ ContextBar.MediaControls.PlaylistNext="Next in Playlist"
ContextBar.MediaControls.PlaylistPrevious="Previous in Playlist"
ContextBar.MediaControls.MediaProperties="Media Properties"
ContextBar.MediaControls.BlindSeek="Media Seek Widget"
# YouTube Actions and Auth
YouTube.Auth.Ok="Authorization completed successfully.\nYou can now close this page."
YouTube.Auth.NoCode="The authorization process was not completed."
YouTube.Auth.WaitingAuth.Title="YouTube User Authorization"
YouTube.Auth.WaitingAuth.Text="Please complete the authorization in your external browser.<br>If the external browser does not open, follow this link and complete the authorization:<br>%1"
YouTube.Actions.CreateNewEvent="Create a new event"
YouTube.Actions.Title="Title*"
YouTube.Actions.MyBroadcast="My Broadcast"
YouTube.Actions.Description="Description"
YouTube.Actions.Privacy="Privacy*"
YouTube.Actions.Privacy.Private="Private"
YouTube.Actions.Privacy.Public="Public"
YouTube.Actions.Privacy.Unlisted="Unlisted"
YouTube.Actions.Category="Category"
YouTube.Actions.MadeForKids="Is this video made for kids?*"
YouTube.Actions.MadeForKids.Yes="Yes, it's made for kids"
YouTube.Actions.MadeForKids.No="No, it's not made for kids"
YouTube.Actions.MadeForKids.Help="<a href='https://support.google.com/youtube/topic/9689353'>(?)</a>"
YouTube.Actions.AdditionalSettings="Additional settings:"
YouTube.Actions.Latency="Latency"
YouTube.Actions.Latency.Normal="Normal"
YouTube.Actions.Latency.Low="Low"
YouTube.Actions.Latency.UltraLow="Ultra low"
YouTube.Actions.EnableAutoStart="Enable Auto-start"
YouTube.Actions.EnableAutoStop="Enable Auto-stop"
YouTube.Actions.AutoStartStop.Help="<a href='https://developers.google.com/youtube/v3/live/docs/liveBroadcasts#contentDetails.enableAutoStart'>(?)</a>"
YouTube.Actions.EnableDVR="Enable DVR"
YouTube.Actions.360Video="360 video"
YouTube.Actions.360Video.Help="<a href='https://vr.youtube.com/create/360/'>(?)</a>"
YouTube.Actions.ScheduleForLater="Schedule for later"
YouTube.Actions.Create_GoLive="Create and Go Live"
YouTube.Actions.Choose_GoLive="Choose and Go Live"
YouTube.Actions.Create_Save="Create && Save"
YouTube.Actions.Dashboard="YouTube Studio..."
YouTube.Actions.Error.Title="Live broadcast creation error"
YouTube.Actions.Error.Text="YouTube access error '%1'.<br/>A detailed error description can be found at <a href='https://developers.google.com/youtube/v3/live/docs/errors'>https://developers.google.com/youtube/v3/live/docs/errors</a>"
YouTube.Actions.Error.General="YouTube access error. Please check your network connection or your YouTube server access."
YouTube.Actions.Error.NoBroadcastCreated="Broadcast creation error '%1'.<br/>A detailed error description can be found at <a href='https://developers.google.com/youtube/v3/live/docs/errors'>https://developers.google.com/youtube/v3/live/docs/errors</a>"
YouTube.Actions.Error.NoStreamCreated="No stream created. Please relink your account."
YouTube.Actions.Error.YouTubeApi="YouTube API Error. Please see the log file for more information."
YouTube.Actions.EventCreated.Title="Event Created"
YouTube.Actions.EventCreated.Text="Event successfully created."
YouTube.Actions.ChooseEvent="Choose an Event"
YouTube.Actions.Stream="Stream"
YouTube.Actions.Stream.ScheduledFor="scheduled for"
YouTube.Actions.Notify.Title="YouTube"
YouTube.Actions.Notify.CreatingBroadcast="Creating a new Live Broadcast, please wait..."
YouTube.Actions.AutoStartStreamingWarning="Auto start is disabled for this stream, you should click \"GO LIVE\"."
YouTube.Actions.AutoStopStreamingWarning="You will not be able to reconnect.<br>Your stream will stop and you will no longer be live."

View file

@ -418,18 +418,14 @@
</item>
<item row="7" column="1">
<widget class="QPushButton" name="connectAccount2">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<string>Basic.AutoConfig.StreamPage.ConnectAccount</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="disconnectAccount">
<property name="text">
<string>Basic.AutoConfig.StreamPage.DisconnectAccount</string>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
@ -443,6 +439,63 @@
</property>
</spacer>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="leftMargin">
<number>7</number>
</property>
<property name="rightMargin">
<number>7</number>
</property>
<item>
<widget class="QLabel" name="connectedAccountText">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Auth.LoadingChannel.Title</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="disconnectAccount">
<property name="text">
<string>Basic.AutoConfig.StreamPage.DisconnectAccount</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<widget class="QLabel" name="connectedAccountLabel">
<property name="text">
<string>Basic.AutoConfig.StreamPage.ConnectedAccount</string>
</property>
</widget>
</item>
<item row="9" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="9" column="1">
<widget class="QPushButton" name="useStreamKeyAdv">
<property name="text">
<string>Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View file

@ -1280,29 +1280,58 @@
<number>4</number>
</property>
<item>
<widget class="QPushButton" name="streamButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Basic.Main.StartStreaming</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="streamButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Basic.Main.StartStreaming</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="broadcastButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Basic.Main.StartBroadcast</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="recordingLayout">

View file

@ -1115,32 +1115,27 @@
</property>
</spacer>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_23">
<property name="spacing">
<number>8</number>
</property>
<property name="leftMargin">
<number>7</number>
</property>
<property name="rightMargin">
<number>7</number>
</property>
<item>
<widget class="QPushButton" name="connectAccount2">
<widget class="QLabel" name="connectedAccountText">
<property name="styleSheet">
<string notr="true">font-weight: bold</string>
</property>
<property name="text">
<string>Basic.AutoConfig.StreamPage.ConnectAccount</string>
<string>Auth.LoadingChannel.Title</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_19">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_23">
<item>
<widget class="QPushButton" name="disconnectAccount">
<property name="text">
@ -1163,21 +1158,21 @@
</item>
</layout>
</item>
<item row="5" column="1">
<item row="6" column="1">
<widget class="QCheckBox" name="bandwidthTestEnable">
<property name="text">
<string>Basic.Settings.Stream.BandwidthTestMode</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="7" column="1">
<widget class="QCheckBox" name="useAuth">
<property name="text">
<string>Basic.Settings.Stream.Custom.UseAuthentication</string>
</property>
</widget>
</item>
<item row="8" column="0">
<item row="9" column="0">
<widget class="QLabel" name="authUsernameLabel">
<property name="text">
<string>Basic.Settings.Stream.Custom.Username</string>
@ -1187,10 +1182,10 @@
</property>
</widget>
</item>
<item row="8" column="1">
<item row="9" column="1">
<widget class="QLineEdit" name="authUsername"/>
</item>
<item row="9" column="0">
<item row="10" column="0">
<widget class="QLabel" name="authPwLabel">
<property name="text">
<string>Basic.Settings.Stream.Custom.Password</string>
@ -1200,7 +1195,7 @@
</property>
</widget>
</item>
<item row="9" column="1">
<item row="10" column="1">
<widget class="QWidget" name="authPwWidget">
<layout class="QHBoxLayout" name="horizontalLayout_25">
<property name="leftMargin">
@ -1232,10 +1227,10 @@
</layout>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="QComboBox" name="twitchAddonDropdown"/>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLabel" name="twitchAddonLabel">
<property name="text">
<string>Basic.Settings.Stream.TTVAddon</string>
@ -1245,20 +1240,78 @@
</property>
</widget>
</item>
<item row="10" column="1">
<item row="11" column="1">
<widget class="QCheckBox" name="ignoreRecommended">
<property name="text">
<string>Basic.Settings.Stream.IgnoreRecommended</string>
</property>
</widget>
</item>
<item row="11" column="1">
<item row="12" column="1">
<widget class="QLabel" name="enforceSettingsLabel">
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_28">
<item>
<widget class="QPushButton" name="useStreamKeyAdv">
<property name="text">
<string>Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_28">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="connectedAccountLabel">
<property name="text">
<string>Basic.AutoConfig.StreamPage.ConnectedAccount</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QPushButton" name="connectAccount2">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<string>Basic.AutoConfig.StreamPage.ConnectAccount</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_19">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>

View file

@ -0,0 +1,601 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OBSYoutubeActions</class>
<widget class="QDialog" name="OBSYoutubeActions">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>583</width>
<height>452</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>465</width>
<height>346</height>
</size>
</property>
<property name="windowTitle">
<string>YouTube Actions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>YouTube.Actions.CreateNewEvent</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>YouTube.Actions.Title</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="title">
<property name="text">
<string>YouTube.Actions.MyBroadcast</string>
</property>
<property name="maxLength">
<number>100</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>YouTube.Actions.Description</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="description">
<property name="maxLength">
<number>5000</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>YouTube.Actions.Privacy</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="privacyBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContentsOnFirstShow</enum>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>YouTube.Actions.Category</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="categoryBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>YouTube.Actions.MadeForKids</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QRadioButton" name="notMakeForKids">
<property name="text">
<string>YouTube.Actions.MadeForKids.No</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="helpMadeForKids">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>YouTube.Actions.MadeForKids.Help</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QRadioButton" name="yesMakeForKids">
<property name="text">
<string>YouTube.Actions.MadeForKids.Yes</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>YouTube.Actions.AdditionalSettings</string>
</property>
</widget>
</item>
<item row="6" column="1">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>YouTube.Actions.Latency</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="latencyBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="11" column="0">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="11" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QCheckBox" name="checkAutoStart">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>YouTube.Actions.EnableAutoStart</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="helpAutoStartStop">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>YouTube.Actions.AutoStartStop.Help</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="12" column="0">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="12" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="checkAutoStop">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>YouTube.Actions.EnableAutoStop</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="checkScheduledLater">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>YouTube.Actions.ScheduleForLater</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<spacer name="horizontalSpacer_12">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="13" column="0">
<spacer name="horizontalSpacer_13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="13" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QDateTimeEdit" name="scheduledTime">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="calendarPopup">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="checkDVR">
<property name="text">
<string>YouTube.Actions.EnableDVR</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<spacer name="horizontalSpacer_14">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="9" column="0">
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QCheckBox" name="check360Video">
<property name="text">
<string>YouTube.Actions.360Video</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="help360Video">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>YouTube.Actions.360Video.Help</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>YouTube.Actions.ChooseEvent</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>179</width>
<height>192</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_11">
<property name="styleSheet">
<string notr="true">border: 1px solid black;</string>
</property>
<property name="text">
<string>&lt;big&gt;Friday Fortnite Stream&lt;/big&gt;&lt;br/&gt;scheduled for 11/11/20 2:00pm</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="styleSheet">
<string notr="true">border: 1px solid black;</string>
</property>
<property name="text">
<string>&lt;big&gt;Friday Fortnite Stream&lt;/big&gt;&lt;br/&gt;scheduled for 11/11/20 2:00pm</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_13">
<property name="styleSheet">
<string notr="true">border: 1px solid black;</string>
</property>
<property name="text">
<string>&lt;big&gt;Friday Fortnite Stream&lt;/big&gt;&lt;br/&gt;scheduled for 11/11/20 2:00pm</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_14">
<property name="styleSheet">
<string notr="true">border: 1px solid black;</string>
</property>
<property name="text">
<string>&lt;big&gt;Friday Fortnite Stream&lt;/big&gt;&lt;br/&gt;scheduled for 11/11/20 2:00pm</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,2,6">
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>YouTube.Actions.Dashboard</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="okButton">
<property name="text">
<string>YouTube.Actions.Create_GoLive</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -24,4 +24,10 @@
#define RESTREAM_CLIENTID "@RESTREAM_CLIENTID@"
#define RESTREAM_HASH 0x@RESTREAM_HASH@
#define YOUTUBE_ENABLED @YOUTUBE_ENABLED@
#define YOUTUBE_CLIENTID "@YOUTUBE_CLIENTID@"
#define YOUTUBE_SECRET "@YOUTUBE_SECRET@"
#define YOUTUBE_CLIENTID_HASH 0x@YOUTUBE_CLIENTID_HASH@
#define YOUTUBE_SECRET_HASH 0x@YOUTUBE_SECRET_HASH@
#define DEFAULT_THEME "Dark"

View file

@ -15,7 +15,12 @@
#ifdef BROWSER_AVAILABLE
#include <browser-panel.hpp>
#endif
#include "auth-oauth.hpp"
#include "ui-config.h"
#if YOUTUBE_ENABLED
#include "youtube-api-wrappers.hpp"
#endif
struct QCef;
@ -257,6 +262,9 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent)
ui->connectAccount2->setVisible(false);
ui->disconnectAccount->setVisible(false);
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
int vertSpacing = ui->topLayout->verticalSpacing();
QMargins m = ui->topLayout->contentsMargins();
@ -295,6 +303,9 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent)
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
SLOT(UpdateMoreInfoLink()));
connect(ui->useStreamKeyAdv, &QPushButton::clicked, this,
[&]() { ui->streamKeyWidget->setVisible(true); });
connect(ui->key, SIGNAL(textChanged(const QString &)), this,
SLOT(UpdateCompleted()));
connect(ui->regionUS, SIGNAL(toggled(bool)), this,
@ -413,7 +424,6 @@ void AutoConfigStreamPage::on_show_clicked()
void AutoConfigStreamPage::OnOAuthStreamKeyConnected()
{
#ifdef BROWSER_AVAILABLE
OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
if (a) {
@ -422,15 +432,45 @@ void AutoConfigStreamPage::OnOAuthStreamKeyConnected()
if (validKey)
ui->key->setText(QT_UTF8(a->key().c_str()));
ui->streamKeyWidget->setVisible(!validKey);
ui->streamKeyLabel->setVisible(!validKey);
ui->connectAccount2->setVisible(!validKey);
ui->disconnectAccount->setVisible(validKey);
ui->streamKeyWidget->setVisible(false);
ui->streamKeyLabel->setVisible(false);
ui->connectAccount2->setVisible(false);
ui->disconnectAccount->setVisible(true);
ui->useStreamKeyAdv->setVisible(false);
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
#if YOUTUBE_ENABLED
if (IsYouTubeService(a->service())) {
ui->key->clear();
ui->connectedAccountLabel->setVisible(true);
ui->connectedAccountText->setVisible(true);
ui->connectedAccountText->setText(
QTStr("Auth.LoadingChannel.Title"));
QScopedPointer<QThread> thread(CreateQThread([&]() {
std::shared_ptr<YoutubeApiWrappers> ytAuth =
std::dynamic_pointer_cast<
YoutubeApiWrappers>(auth);
if (ytAuth.get()) {
ChannelDescription cd;
if (ytAuth->GetChannelDescription(cd)) {
ui->connectedAccountText
->setText(cd.title);
}
}
}));
thread->start();
thread->wait();
}
#endif
}
ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
UpdateCompleted();
#endif
}
void AutoConfigStreamPage::OnAuthConnected()
@ -446,15 +486,16 @@ void AutoConfigStreamPage::OnAuthConnected()
void AutoConfigStreamPage::on_connectAccount_clicked()
{
#ifdef BROWSER_AVAILABLE
std::string service = QT_TO_UTF8(ui->service->currentText());
OAuth::DeleteCookies(service);
auth = OAuthStreamKey::Login(this, service);
if (!!auth)
if (!!auth) {
OnAuthConnected();
#endif
ui->useStreamKeyAdv->setVisible(false);
}
}
#define DISCONNECT_COMFIRM_TITLE \
@ -484,11 +525,14 @@ void AutoConfigStreamPage::on_disconnectAccount_clicked()
OAuth::DeleteCookies(service);
#endif
reset_service_ui_fields(service);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(true);
ui->disconnectAccount->setVisible(false);
ui->key->setText("");
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
}
void AutoConfigStreamPage::on_useStreamKey_clicked()
@ -502,6 +546,55 @@ static inline bool is_auth_service(const std::string &service)
return Auth::AuthType(service) != Auth::Type::None;
}
static inline bool is_external_oauth(const std::string &service)
{
return Auth::External(service);
}
void AutoConfigStreamPage::reset_service_ui_fields(std::string &service)
{
// when account is already connected:
OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
#if YOUTUBE_ENABLED
if (a && service == a->service() && IsYouTubeService(a->service())) {
ui->connectedAccountLabel->setVisible(true);
ui->connectedAccountText->setVisible(true);
ui->connectAccount2->setVisible(false);
ui->disconnectAccount->setVisible(true);
return;
}
#endif
bool external_oauth = is_external_oauth(service);
if (external_oauth) {
ui->streamKeyWidget->setVisible(false);
ui->streamKeyLabel->setVisible(false);
ui->connectAccount2->setVisible(true);
ui->useStreamKeyAdv->setVisible(true);
ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
} else if (cef) {
QString key = ui->key->text();
bool can_auth = is_auth_service(service);
int page = can_auth && key.isEmpty() ? (int)Section::Connect
: (int)Section::StreamKey;
ui->stackedWidget->setCurrentIndex(page);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(can_auth);
ui->useStreamKeyAdv->setVisible(false);
} else {
ui->connectAccount2->setVisible(false);
ui->useStreamKeyAdv->setVisible(false);
}
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
ui->disconnectAccount->setVisible(false);
}
void AutoConfigStreamPage::ServiceChanged()
{
bool showMore = ui->service->currentData().toInt() ==
@ -514,30 +607,7 @@ void AutoConfigStreamPage::ServiceChanged()
bool testBandwidth = ui->doBandwidthTest->isChecked();
bool custom = IsCustomService();
ui->disconnectAccount->setVisible(false);
#ifdef BROWSER_AVAILABLE
if (cef) {
if (lastService != service.c_str()) {
bool can_auth = is_auth_service(service);
int page = can_auth ? (int)Section::Connect
: (int)Section::StreamKey;
ui->stackedWidget->setCurrentIndex(page);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(can_auth);
auth.reset();
if (lastService.isEmpty())
lastService = service.c_str();
}
} else {
ui->connectAccount2->setVisible(false);
}
#else
ui->connectAccount2->setVisible(false);
#endif
reset_service_ui_fields(service);
/* Test three closest servers if "Auto" is available for Twitch */
if (service == "Twitch" && wiz->twitchAuto)
@ -570,15 +640,23 @@ void AutoConfigStreamPage::ServiceChanged()
ui->bitrateLabel->setHidden(testBandwidth);
ui->bitrate->setHidden(testBandwidth);
#ifdef BROWSER_AVAILABLE
OBSBasic *main = OBSBasic::Get();
if (!!main->auth &&
service.find(main->auth->service()) != std::string::npos) {
auth = main->auth;
OnAuthConnected();
}
if (main->auth) {
auto system_auth_service = main->auth->service();
bool service_check = service == system_auth_service;
#if YOUTUBE_ENABLED
service_check =
service_check ? service_check
: IsYouTubeService(system_auth_service) &&
IsYouTubeService(service);
#endif
if (service_check) {
auth.reset();
auth = main->auth;
OnAuthConnected();
}
}
UpdateCompleted();
}

View file

@ -197,6 +197,8 @@ public slots:
void UpdateMoreInfoLink();
void UpdateServerList();
void UpdateCompleted();
void reset_service_ui_fields(std::string &service);
};
class AutoConfigTestPage : public QWizardPage {

View file

@ -16,6 +16,7 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "ui-config.h"
#include <cstddef>
#include <ctime>
@ -54,6 +55,11 @@
#include "window-log-reply.hpp"
#include "window-projector.hpp"
#include "window-remux.hpp"
#if YOUTUBE_ENABLED
#include "auth-youtube.hpp"
#include "window-youtube-actions.hpp"
#include "youtube-api-wrappers.hpp"
#endif
#include "qt-wrappers.hpp"
#include "context-bar-controls.hpp"
#include "obs-proxy-style.hpp"
@ -203,6 +209,9 @@ void assignDockToggle(QDockWidget *dock, QAction *action)
extern void RegisterTwitchAuth();
extern void RegisterRestreamAuth();
#if YOUTUBE_ENABLED
extern void RegisterYoutubeAuth();
#endif
OBSBasic::OBSBasic(QWidget *parent)
: OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
@ -220,6 +229,9 @@ OBSBasic::OBSBasic(QWidget *parent)
#if RESTREAM_ENABLED
RegisterRestreamAuth();
#endif
#if YOUTUBE_ENABLED
RegisterYoutubeAuth();
#endif
setAcceptDrops(true);
@ -232,6 +244,7 @@ OBSBasic::OBSBasic(QWidget *parent)
ui->setupUi(this);
ui->previewDisabledWidget->setVisible(false);
ui->contextContainer->setStyle(new OBSProxyStyle);
ui->broadcastButton->setVisible(false);
/* XXX: Disable drag/drop on Linux until Qt issues are fixed */
#if !defined(_WIN32) && !defined(__APPLE__)
@ -447,6 +460,9 @@ OBSBasic::OBSBasic(QWidget *parent)
connect(ui->scenes, SIGNAL(scenesReordered()), this,
SLOT(ScenesReordered()));
connect(ui->broadcastButton, &QPushButton::clicked, this,
&OBSBasic::BroadcastButtonClicked);
}
static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent,
@ -4446,6 +4462,15 @@ void OBSBasic::closeEvent(QCloseEvent *event)
return;
}
#if YOUTUBE_ENABLED
/* Also don't close the window if the youtube stream check is active */
if (youtubeStreamCheckThread) {
QTimer::singleShot(1000, this, SLOT(close()));
event->ignore();
return;
}
#endif
if (isVisible())
config_set_string(App()->GlobalConfig(), "BasicWindow",
"geometry",
@ -6019,6 +6044,77 @@ void OBSBasic::DisplayStreamStartError()
QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message);
}
#if YOUTUBE_ENABLED
void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key,
bool autostart, bool autostop)
{
//blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key));
obs_service_t *service_obj = GetService();
obs_data_t *settings = obs_service_get_settings(service_obj);
const std::string a_key = QT_TO_UTF8(key);
obs_data_set_string(settings, "key", a_key.c_str());
const std::string an_id = QT_TO_UTF8(id);
obs_data_set_string(settings, "stream_id", an_id.c_str());
obs_service_update(service_obj, settings);
autoStartBroadcast = autostart;
autoStopBroadcast = autostop;
}
void OBSBasic::YoutubeStreamCheck(const std::string &key)
{
YoutubeApiWrappers *apiYouTube(
dynamic_cast<YoutubeApiWrappers *>(GetAuth()));
if (!apiYouTube) {
/* technically we should never get here -Jim */
QMetaObject::invokeMethod(this, "ForceStopStreaming",
Qt::QueuedConnection);
youtubeStreamCheckThread->deleteLater();
blog(LOG_ERROR, "==========================================");
blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__);
blog(LOG_ERROR, "==========================================");
return;
}
int timeout = 0;
json11::Json json;
QString id = key.c_str();
for (;;) {
if (timeout == 14) {
QMetaObject::invokeMethod(this, "ForceStopStreaming",
Qt::QueuedConnection);
break;
}
if (!apiYouTube->FindStream(id, json)) {
QMetaObject::invokeMethod(this,
"DisplayStreamStartError",
Qt::QueuedConnection);
QMetaObject::invokeMethod(this, "StopStreaming",
Qt::QueuedConnection);
break;
}
auto item = json["items"][0];
auto status = item["status"]["streamStatus"].string_value();
if (status == "active") {
QMetaObject::invokeMethod(ui->broadcastButton,
"setEnabled",
Q_ARG(bool, true));
break;
} else {
QThread::sleep(1);
timeout++;
}
}
youtubeStreamCheckThread->deleteLater();
}
#endif
void OBSBasic::StartStreaming()
{
if (outputHandler->StreamingActive())
@ -6026,6 +6122,35 @@ void OBSBasic::StartStreaming()
if (disableOutputsRef)
return;
Auth *auth = GetAuth();
if (auth) {
auth->OnStreamConfig();
#if YOUTUBE_ENABLED
if (!broadcastActive && autoStartBroadcast &&
IsYouTubeService(auth->service())) {
OBSYoutubeActions *dialog;
dialog = new OBSYoutubeActions(this, auth);
connect(dialog, &OBSYoutubeActions::ok, this,
&OBSBasic::YouTubeActionDialogOk);
int result = dialog->Valid() ? dialog->exec()
: QDialog::Rejected;
if (result != QDialog::Accepted) {
ui->streamButton->setText(
QTStr("Basic.Main.StartStreaming"));
ui->streamButton->setEnabled(true);
ui->streamButton->setChecked(false);
if (sysTrayStream) {
sysTrayStream->setText(
ui->streamButton->text());
sysTrayStream->setEnabled(true);
}
return;
}
}
#endif
}
if (!outputHandler->SetupStreaming(service)) {
DisplayStreamStartError();
return;
@ -6050,6 +6175,33 @@ void OBSBasic::StartStreaming()
return;
}
if (!autoStartBroadcast) {
ui->broadcastButton->setVisible(true);
ui->broadcastButton->setText(
QTStr("Basic.Main.StartBroadcast"));
ui->broadcastButton->setStyleSheet("background-color:#6699cc");
// well, we need to disable button while stream is not active
#if YOUTUBE_ENABLED
// get a current stream key
obs_service_t *service_obj = GetService();
obs_data_t *settings = obs_service_get_settings(service_obj);
std::string key = obs_data_get_string(settings, "stream_id");
if (!key.empty() && !youtubeStreamCheckThread) {
ui->broadcastButton->setEnabled(false);
youtubeStreamCheckThread = CreateQThread(
[this, key] { YoutubeStreamCheck(key); });
youtubeStreamCheckThread->setObjectName(
"YouTubeStreamCheckThread");
youtubeStreamCheckThread->start();
}
#endif
} else if (!autoStopBroadcast) {
broadcastActive = true;
ui->broadcastButton->setVisible(true);
ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast"));
ui->broadcastButton->setStyleSheet("background-color:#ff0000");
}
bool recordWhenStreaming = config_get_bool(
GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
if (recordWhenStreaming)
@ -6059,6 +6211,65 @@ void OBSBasic::StartStreaming()
GetGlobalConfig(), "BasicWindow", "ReplayBufferWhileStreaming");
if (replayBufferWhileStreaming)
StartReplayBuffer();
if (!autoStartBroadcast) {
OBSMessageBox::warning(
this, "Warning",
QTStr("YouTube.Actions.AutoStartStreamingWarning"),
false);
}
}
void OBSBasic::BroadcastButtonClicked()
{
if (!autoStartBroadcast) {
#if YOUTUBE_ENABLED
std::shared_ptr<YoutubeApiWrappers> ytAuth =
dynamic_pointer_cast<YoutubeApiWrappers>(auth);
if (ytAuth.get()) {
ytAuth->StartLatestBroadcast();
}
#endif
broadcastActive = true;
autoStartBroadcast = true; // and clear the flag
if (!autoStopBroadcast) {
ui->broadcastButton->setText(
QTStr("Basic.Main.StopBroadcast"));
ui->broadcastButton->setStyleSheet(
"background-color:#ff0000");
} else {
ui->broadcastButton->setVisible(false);
}
} else if (!autoStopBroadcast) {
#if YOUTUBE_ENABLED
bool confirm = config_get_bool(GetGlobalConfig(), "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<YoutubeApiWrappers> ytAuth =
dynamic_pointer_cast<YoutubeApiWrappers>(auth);
if (ytAuth.get()) {
ytAuth->StopLatestBroadcast();
}
#endif
broadcastActive = false;
autoStopBroadcast = true;
ui->broadcastButton->setVisible(false);
QMetaObject::invokeMethod(this, "StopStreaming");
}
}
#ifdef _WIN32
@ -6167,6 +6378,18 @@ void OBSBasic::StopStreaming()
if (outputHandler->StreamingActive())
outputHandler->StopStreaming(streamingStopping);
// special case: force reset broadcast state if
// no autostart and no autostop selected
if (!autoStartBroadcast && !broadcastActive) {
broadcastActive = false;
autoStartBroadcast = true;
autoStopBroadcast = true;
ui->broadcastButton->setVisible(false);
}
if (autoStopBroadcast)
broadcastActive = false;
OnDeactivate();
bool recordWhenStreaming = config_get_bool(
@ -6827,6 +7050,23 @@ void OBSBasic::on_streamButton_clicked()
bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
"WarnBeforeStoppingStream");
#if YOUTUBE_ENABLED
if (isVisible() && auth && IsYouTubeService(auth->service()) &&
autoStopBroadcast) {
QMessageBox::StandardButton button = OBSMessageBox::question(
this, QTStr("ConfirmStop.Title"),
QTStr("YouTube.Actions.AutoStopStreamingWarning"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (button == QMessageBox::No) {
ui->streamButton->setChecked(true);
return;
}
confirm = false;
}
#endif
if (confirm && isVisible()) {
QMessageBox::StandardButton button =
OBSMessageBox::question(
@ -6848,8 +7088,13 @@ void OBSBasic::on_streamButton_clicked()
return;
}
Auth *auth = GetAuth();
auto action =
UIValidation::StreamSettingsConfirmation(this, service);
(auth && auth->external())
? StreamSettingsAction::ContinueStream
: UIValidation::StreamSettingsConfirmation(
this, service);
switch (action) {
case StreamSettingsAction::ContinueStream:
break;

View file

@ -19,6 +19,7 @@
#include <QBuffer>
#include <QAction>
#include <QThread>
#include <QWidgetAction>
#include <QSystemTrayIcon>
#include <QStyledItemDelegate>
@ -556,6 +557,17 @@ private:
void MoveSceneItem(enum obs_order_movement movement,
const QString &action_name);
bool autoStartBroadcast = true;
bool autoStopBroadcast = true;
bool broadcastActive = false;
QPointer<QThread> youtubeStreamCheckThread;
#if YOUTUBE_ENABLED
void YoutubeStreamCheck(const std::string &key);
void YouTubeActionDialogOk(const QString &id, const QString &key,
bool autostart, bool autostop);
#endif
void BroadcastButtonClicked();
public slots:
void DeferSaveBegin();
void DeferSaveEnd();

View file

@ -10,7 +10,14 @@
#ifdef BROWSER_AVAILABLE
#include <browser-panel.hpp>
#endif
#include "auth-oauth.hpp"
#include "ui-config.h"
#if YOUTUBE_ENABLED
#include "youtube-api-wrappers.hpp"
#endif
struct QCef;
@ -39,9 +46,13 @@ void OBSBasicSettings::InitStreamPage()
ui->connectAccount2->setVisible(false);
ui->disconnectAccount->setVisible(false);
ui->bandwidthTestEnable->setVisible(false);
ui->twitchAddonDropdown->setVisible(false);
ui->twitchAddonLabel->setVisible(false);
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
int vertSpacing = ui->topStreamLayout->verticalSpacing();
QMargins m = ui->topStreamLayout->contentsMargins();
@ -375,6 +386,68 @@ static inline bool is_auth_service(const std::string &service)
return Auth::AuthType(service) != Auth::Type::None;
}
static inline bool is_external_oauth(const std::string &service)
{
return Auth::External(service);
}
static void reset_service_ui_fields(Ui::OBSBasicSettings *ui,
std::string &service, bool loading)
{
bool external_oauth = is_external_oauth(service);
if (external_oauth) {
ui->streamKeyWidget->setVisible(false);
ui->streamKeyLabel->setVisible(false);
ui->connectAccount2->setVisible(true);
ui->useStreamKeyAdv->setVisible(true);
ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey);
} else if (cef) {
QString key = ui->key->text();
bool can_auth = is_auth_service(service);
int page = can_auth && (!loading || key.isEmpty())
? (int)Section::Connect
: (int)Section::StreamKey;
ui->streamStackWidget->setCurrentIndex(page);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(can_auth);
ui->useStreamKeyAdv->setVisible(false);
} else {
ui->connectAccount2->setVisible(false);
ui->useStreamKeyAdv->setVisible(false);
}
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
ui->disconnectAccount->setVisible(false);
}
#if YOUTUBE_ENABLED
static void get_yt_ch_title(Ui::OBSBasicSettings *ui,
YoutubeApiWrappers *ytAuth)
{
if (ytAuth) {
ChannelDescription cd;
if (ytAuth->GetChannelDescription(cd)) {
ui->connectedAccountText->setText(cd.title);
} else {
// if we still not changed the service page
if (IsYouTubeService(
QT_TO_UTF8(ui->service->currentText()))) {
ui->connectedAccountText->setText(
QTStr("Auth.LoadingChannel.Error"));
}
}
}
}
#endif
void OBSBasicSettings::UseStreamKeyAdvClicked()
{
ui->streamKeyWidget->setVisible(true);
}
void OBSBasicSettings::on_service_currentIndexChanged(int)
{
bool showMore = ui->service->currentData().toInt() ==
@ -390,26 +463,9 @@ void OBSBasicSettings::on_service_currentIndexChanged(int)
ui->twitchAddonDropdown->setVisible(false);
ui->twitchAddonLabel->setVisible(false);
#ifdef BROWSER_AVAILABLE
if (cef) {
if (lastService != service.c_str()) {
QString key = ui->key->text();
bool can_auth = is_auth_service(service);
int page = can_auth && (!loading || key.isEmpty())
? (int)Section::Connect
: (int)Section::StreamKey;
ui->streamStackWidget->setCurrentIndex(page);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(can_auth);
}
} else {
ui->connectAccount2->setVisible(false);
if (lastService != service.c_str()) {
reset_service_ui_fields(ui.get(), service, loading);
}
#else
ui->connectAccount2->setVisible(false);
#endif
ui->useAuth->setVisible(custom);
ui->authUsernameLabel->setVisible(custom);
@ -429,15 +485,22 @@ void OBSBasicSettings::on_service_currentIndexChanged(int)
ui->serverStackedWidget->setCurrentIndex(0);
}
#ifdef BROWSER_AVAILABLE
auth.reset();
if (!main->auth) {
return;
}
if (!!main->auth &&
service.find(main->auth->service()) != std::string::npos) {
auto system_auth_service = main->auth->service();
bool service_check = service == system_auth_service;
#if YOUTUBE_ENABLED
service_check = service_check ? service_check
: IsYouTubeService(system_auth_service) &&
IsYouTubeService(service);
#endif
if (service_check) {
auth.reset();
auth = main->auth;
OnAuthConnected();
}
#endif
}
void OBSBasicSettings::UpdateServerList()
@ -528,7 +591,6 @@ OBSService OBSBasicSettings::SpawnTempService()
void OBSBasicSettings::OnOAuthStreamKeyConnected()
{
#ifdef BROWSER_AVAILABLE
OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
if (a) {
@ -541,18 +603,43 @@ void OBSBasicSettings::OnOAuthStreamKeyConnected()
ui->streamKeyLabel->setVisible(false);
ui->connectAccount2->setVisible(false);
ui->disconnectAccount->setVisible(true);
ui->useStreamKeyAdv->setVisible(false);
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
if (strcmp(a->service(), "Twitch") == 0) {
ui->bandwidthTestEnable->setVisible(true);
ui->twitchAddonLabel->setVisible(true);
ui->twitchAddonDropdown->setVisible(true);
} else {
ui->bandwidthTestEnable->setChecked(false);
}
#if YOUTUBE_ENABLED
if (IsYouTubeService(a->service())) {
ui->key->clear();
ui->connectedAccountLabel->setVisible(true);
ui->connectedAccountText->setVisible(true);
ui->connectedAccountText->setText(
QTStr("Auth.LoadingChannel.Title"));
std::string a_service = a->service();
std::shared_ptr<YoutubeApiWrappers> ytAuth =
std::dynamic_pointer_cast<YoutubeApiWrappers>(
auth);
auto act = [&]() {
get_yt_ch_title(ui.get(), ytAuth.get());
};
QScopedPointer<QThread> thread(CreateQThread(act));
thread->start();
thread->wait();
}
#endif
ui->bandwidthTestEnable->setChecked(false);
}
ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey);
#endif
}
void OBSBasicSettings::OnAuthConnected()
@ -573,15 +660,16 @@ void OBSBasicSettings::OnAuthConnected()
void OBSBasicSettings::on_connectAccount_clicked()
{
#ifdef BROWSER_AVAILABLE
std::string service = QT_TO_UTF8(ui->service->currentText());
OAuth::DeleteCookies(service);
auth = OAuthStreamKey::Login(this, service);
if (!!auth)
if (!!auth) {
OnAuthConnected();
#endif
ui->useStreamKeyAdv->setVisible(false);
}
}
#define DISCONNECT_COMFIRM_TITLE \
@ -611,14 +699,15 @@ void OBSBasicSettings::on_disconnectAccount_clicked()
ui->bandwidthTestEnable->setChecked(false);
ui->streamKeyWidget->setVisible(true);
ui->streamKeyLabel->setVisible(true);
ui->connectAccount2->setVisible(true);
ui->disconnectAccount->setVisible(false);
reset_service_ui_fields(ui.get(), service, loading);
ui->bandwidthTestEnable->setVisible(false);
ui->twitchAddonDropdown->setVisible(false);
ui->twitchAddonLabel->setVisible(false);
ui->key->setText("");
ui->connectedAccountLabel->setVisible(false);
ui->connectedAccountText->setVisible(false);
}
void OBSBasicSettings::on_useStreamKey_clicked()

View file

@ -908,6 +908,9 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
QValidator *validator = new QRegularExpressionValidator(rx, this);
ui->baseResolution->lineEdit()->setValidator(validator);
ui->outputResolution->lineEdit()->setValidator(validator);
connect(ui->useStreamKeyAdv, SIGNAL(clicked()), this,
SLOT(UseStreamKeyAdvClicked()));
}
OBSBasicSettings::~OBSBasicSettings()

View file

@ -399,6 +399,8 @@ private slots:
void SetHotkeysIcon(const QIcon &icon);
void SetAdvancedIcon(const QIcon &icon);
void UseStreamKeyAdvClicked();
protected:
virtual void closeEvent(QCloseEvent *event) override;
void reject() override;

View file

@ -0,0 +1,520 @@
#include "window-basic-main.hpp"
#include "window-youtube-actions.hpp"
#include "obs-app.hpp"
#include "qt-wrappers.hpp"
#include "youtube-api-wrappers.hpp"
#include <QDateTime>
#include <QDesktopServices>
const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'";
const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m";
const QString NormalStylesheet = "border: 1px solid black; border-radius: 5px;";
const QString SelectedStylesheet =
"border: 2px solid black; border-radius: 5px;";
const QString IndexOfGamingCategory = "20";
OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth)
: QDialog(parent),
ui(new Ui::OBSYoutubeActions),
apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth)),
workerThread(new WorkerThread(apiYouTube))
{
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");
ui->checkAutoStart->setEnabled(false);
ui->checkAutoStop->setEnabled(false);
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 &link) { QDesktopServices::openUrl(link); });
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);
connect(ui->checkScheduledLater, &QCheckBox::stateChanged, this,
[&](int state) {
ui->scheduledTime->setVisible(state);
if (state) {
ui->checkAutoStart->setEnabled(true);
ui->checkAutoStop->setEnabled(true);
ui->checkAutoStart->setChecked(false);
ui->checkAutoStop->setChecked(false);
} else {
ui->checkAutoStart->setEnabled(false);
ui->checkAutoStop->setEnabled(false);
ui->checkAutoStart->setChecked(true);
ui->checkAutoStop->setChecked(true);
}
UpdateOkButtonStatus();
});
ui->scheduledTime->setDateTime(QDateTime::currentDateTime());
if (!apiYouTube) {
blog(LOG_DEBUG, "YouTube API auth NOT found.");
Cancel();
return;
}
ChannelDescription channel;
if (!apiYouTube->GetChannelDescription(channel)) {
blog(LOG_DEBUG, "Could not get channel description.");
ShowErrorDialog(
parent,
apiYouTube->GetLastError().isEmpty()
? QTStr("YouTube.Actions.Error.General")
: QTStr("YouTube.Actions.Error.Text")
.arg(apiYouTube->GetLastError()));
Cancel();
return;
}
this->setWindowTitle(channel.title);
QVector<CategoryDescription> category_list;
if (!apiYouTube->GetVideoCategoriesList(
channel.country, channel.language, category_list)) {
blog(LOG_DEBUG, "Could not get video category for country; %s.",
channel.country.toStdString().c_str());
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->cancelButton, &QPushButton::clicked, this, [&]() {
blog(LOG_DEBUG, "YouTube live broadcast creation cancelled.");
// Close the dialog.
Cancel();
});
qDeleteAll(ui->scrollAreaWidgetContents->findChildren<QWidget *>(
QString(), Qt::FindDirectChildrenOnly));
connect(workerThread, &WorkerThread::failed, this, &QDialog::reject);
connect(workerThread, &WorkerThread::new_item, this,
[&](const QString &title, const QString &dateTimeString,
const QString &broadcast, bool astart, bool astop) {
ClickableLabel *label = new ClickableLabel();
label->setStyleSheet(NormalStylesheet);
label->setTextFormat(Qt::RichText);
label->setText(
QString("<big>%1 %2</big><br/>%3 %4")
.arg(title,
QTStr("YouTube.Actions.Stream"),
QTStr("YouTube.Actions.Stream.ScheduledFor"),
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->setStyleSheet(
NormalStylesheet);
label->setStyleSheet(
SelectedStylesheet);
this->selectedBroadcast = broadcast;
this->autostart = astart;
this->autostop = astop;
UpdateOkButtonStatus();
});
ui->scrollAreaWidgetContents->layout()->addWidget(
label);
});
workerThread->start();
#ifdef __APPLE__
// MacOS theming issues
this->resize(this->width() + 200, this->height() + 120);
#endif
valid = true;
}
OBSYoutubeActions::~OBSYoutubeActions()
{
workerThread->stop();
workerThread->wait();
delete workerThread;
}
void WorkerThread::run()
{
if (!pending)
return;
json11::Json broadcasts;
if (!apiYouTube->GetBroadcastsList(broadcasts, "")) {
emit failed();
return;
}
while (pending) {
auto items = broadcasts["items"].array_items();
for (auto item = items.begin(); item != items.end(); item++) {
auto status = (*item)["status"]["lifeCycleStatus"]
.string_value();
if (status == "created" || status == "ready") {
auto title = QString::fromStdString(
(*item)["snippet"]["title"]
.string_value());
auto scheduledStartTime = QString::fromStdString(
(*item)["snippet"]["scheduledStartTime"]
.string_value());
auto broadcast = QString::fromStdString(
(*item)["id"].string_value());
auto astart = (*item)["contentDetails"]
["enableAutoStart"]
.bool_value();
auto astop = (*item)["contentDetails"]
["enableAutoStop"]
.bool_value();
auto utcDTime = QDateTime::fromString(
scheduledStartTime,
SchedulDateAndTimeFormat);
// DateTime parser means that input datetime is a local, so we need to move it
auto dateTime = utcDTime.addSecs(
utcDTime.offsetFromUtc());
auto dateTimeString = QLocale().toString(
dateTime,
QString("%1 %2").arg(
QLocale().dateFormat(
QLocale::LongFormat),
QLocale().timeFormat(
QLocale::ShortFormat)));
emit new_item(title, dateTimeString, broadcast,
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))) {
emit failed();
return;
}
}
}
emit ready();
}
void OBSYoutubeActions::UpdateOkButtonStatus()
{
if (ui->tabWidget->currentIndex() == 0) {
ui->okButton->setEnabled(
!ui->title->text().isEmpty() &&
!ui->privacyBox->currentText().isEmpty() &&
(ui->yesMakeForKids->isChecked() ||
ui->notMakeForKids->isChecked()));
if (ui->checkScheduledLater->checkState() == Qt::Checked) {
ui->okButton->setText(
QTStr("YouTube.Actions.Create_Save"));
} else {
ui->okButton->setText(
QTStr("YouTube.Actions.Create_GoLive"));
}
ui->pushButton->setVisible(false);
} else {
ui->okButton->setEnabled(!selectedBroadcast.isEmpty());
ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive"));
ui->pushButton->setVisible(true);
}
}
bool OBSYoutubeActions::StreamNowAction(YoutubeApiWrappers *api,
StreamDescription &stream)
{
YoutubeApiWrappers *apiYouTube = api;
BroadcastDescription broadcast = {};
UiToBroadcast(broadcast);
// stream now is always autostart/autostop
broadcast.auto_start = true;
broadcast.auto_stop = true;
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;
}
stream = {"", "", "OBS Studio Video Stream", ""};
if (!apiYouTube->InsertStream(stream)) {
blog(LOG_DEBUG, "No stream created.");
return false;
}
if (!apiYouTube->BindStream(broadcast.id, stream.id)) {
blog(LOG_DEBUG, "No stream binded.");
return false;
}
if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title,
broadcast.description,
broadcast.category.id)) {
blog(LOG_DEBUG, "No category set.");
return false;
}
return true;
}
bool OBSYoutubeActions::StreamLaterAction(YoutubeApiWrappers *api)
{
YoutubeApiWrappers *apiYouTube = api;
BroadcastDescription broadcast = {};
UiToBroadcast(broadcast);
// 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);
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;
}
return true;
}
bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
StreamDescription &stream,
bool start)
{
YoutubeApiWrappers *apiYouTube = api;
std::string boundStreamId;
{
json11::Json json;
if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) {
blog(LOG_DEBUG, "No broadcast found.");
return false;
}
auto item = json["items"].array_items()[0];
auto boundStreamId =
item["contentDetails"]["boundStreamId"].string_value();
}
stream.id = boundStreamId.c_str();
json11::Json json;
if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) {
auto item = json["items"].array_items()[0];
auto streamName = item["cdn"]["streamName"].string_value();
auto title = item["snippet"]["title"].string_value();
auto description =
item["snippet"]["description"].string_value();
stream.name = streamName.c_str();
stream.title = title.c_str();
stream.description = description.c_str();
} else {
stream = {"", "", "OBS Studio Video Stream", ""};
if (!apiYouTube->InsertStream(stream)) {
blog(LOG_DEBUG, "No stream created.");
return false;
}
if (!apiYouTube->BindStream(selectedBroadcast, stream.id)) {
blog(LOG_DEBUG, "No stream binded.");
return false;
}
}
if (start)
api->StartBroadcast(selectedBroadcast);
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()
{
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) {
if (ui->checkScheduledLater->isChecked()) {
success = this->StreamLaterAction(apiYouTube);
} else {
success = this->StreamNowAction(apiYouTube,
stream);
}
} else {
success = this->ChooseAnEventAction(apiYouTube, stream,
this->autostart);
};
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(stream.id),
QT_TO_UTF8(stream.name), true, true);
Accept();
}
} else {
// Stream to precreated broadcast usecase.
emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name),
autostart, autostop);
Accept();
}
} else {
// Fail.
auto last_error = apiYouTube->GetLastError();
if (last_error.isEmpty()) {
last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
}
ShowErrorDialog(
this, QTStr("YouTube.Actions.Error.NoBroadcastCreated")
.arg(last_error));
}
}
void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast)
{
broadcast.title = ui->title->text();
broadcast.description = ui->description->text();
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";
// Current time by default.
broadcast.schedul_date_time = QDateTime::currentDateTimeUtc().toString(
SchedulDateAndTimeFormat);
}
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();
}

View file

@ -0,0 +1,68 @@
#pragma once
#include <QDialog>
#include <QString>
#include <QThread>
#include "ui_OBSYoutubeActions.h"
#include "youtube-api-wrappers.hpp"
class WorkerThread : public QThread {
Q_OBJECT
public:
WorkerThread(YoutubeApiWrappers *api) : QThread(), apiYouTube(api) {}
void stop() { pending = false; }
protected:
YoutubeApiWrappers *apiYouTube;
bool pending = true;
public slots:
void run() override;
signals:
void ready();
void new_item(const QString &title, const QString &dateTimeString,
const QString &broadcast, bool astart, bool astop);
void failed();
};
class OBSYoutubeActions : public QDialog {
Q_OBJECT
std::unique_ptr<Ui::OBSYoutubeActions> ui;
signals:
void ok(const QString &id, const QString &key, bool autostart,
bool autostop);
protected:
void UpdateOkButtonStatus();
bool StreamNowAction(YoutubeApiWrappers *api,
StreamDescription &stream);
bool StreamLaterAction(YoutubeApiWrappers *api);
bool ChooseAnEventAction(YoutubeApiWrappers *api,
StreamDescription &stream, bool start);
void ShowErrorDialog(QWidget *parent, QString text);
public:
explicit OBSYoutubeActions(QWidget *parent, Auth *auth);
virtual ~OBSYoutubeActions() override;
bool Valid() { return valid; };
private:
void InitBroadcast();
void UiToBroadcast(BroadcastDescription &broadcast);
void OpenYouTubeDashboard();
void Cancel();
void Accept();
QString selectedBroadcast;
bool autostart, autostop;
bool valid = false;
YoutubeApiWrappers *apiYouTube;
WorkerThread *workerThread;
};

478
UI/youtube-api-wrappers.cpp Normal file
View file

@ -0,0 +1,478 @@
#include "youtube-api-wrappers.hpp"
#include <QUrl>
#include <string>
#include <iostream>
#include "auth-youtube.hpp"
#include "obs-app.hpp"
#include "qt-wrappers.hpp"
#include "remote-text.hpp"
#include "ui-config.h"
#include "obf.h"
using namespace json11;
/* ------------------------------------------------------------------------- */
#define YOUTUBE_LIVE_API_URL "https://www.googleapis.com/youtube/v3"
#define YOUTUBE_LIVE_STREAM_URL YOUTUBE_LIVE_API_URL "/liveStreams"
#define YOUTUBE_LIVE_BROADCAST_URL YOUTUBE_LIVE_API_URL "/liveBroadcasts"
#define YOUTUBE_LIVE_BROADCAST_TRANSITION_URL \
YOUTUBE_LIVE_BROADCAST_URL "/transition"
#define YOUTUBE_LIVE_BROADCAST_BIND_URL YOUTUBE_LIVE_BROADCAST_URL "/bind"
#define YOUTUBE_LIVE_CHANNEL_URL YOUTUBE_LIVE_API_URL "/channels"
#define YOUTUBE_LIVE_TOKEN_URL "https://oauth2.googleapis.com/token"
#define YOUTUBE_LIVE_VIDEOCATEGORIES_URL YOUTUBE_LIVE_API_URL "/videoCategories"
#define YOUTUBE_LIVE_VIDEOS_URL YOUTUBE_LIVE_API_URL "/videos"
#define DEFAULT_BROADCASTS_PER_QUERY \
"7" // acceptable values are 0 to 50, inclusive
/* ------------------------------------------------------------------------- */
bool IsYouTubeService(const std::string &service)
{
auto it = find_if(youtubeServices.begin(), youtubeServices.end(),
[&service](const Auth::Def &yt) {
return service == yt.service;
});
return it != youtubeServices.end();
}
YoutubeApiWrappers::YoutubeApiWrappers(const Def &d) : YoutubeAuth(d) {}
bool YoutubeApiWrappers::TryInsertCommand(const char *url,
const char *content_type,
std::string request_type,
const char *data, Json &json_out,
long *error_code)
{
if (error_code)
*error_code = 0;
#ifdef _DEBUG
blog(LOG_DEBUG, "YouTube API command URL: %s", url);
blog(LOG_DEBUG, "YouTube API command data: %s", data);
#endif
if (token.empty())
return false;
std::string output;
std::string error;
bool success = GetRemoteFile(url, output, error, error_code,
content_type, request_type, data,
{"Authorization: Bearer " + token},
nullptr, 5);
if (!success || output.empty())
return false;
json_out = Json::parse(output, error);
#ifdef _DEBUG
blog(LOG_DEBUG, "YouTube API command answer: %s",
json_out.dump().c_str());
#endif
if (!error.empty()) {
return false;
}
return true;
}
bool YoutubeApiWrappers::UpdateAccessToken()
{
if (refresh_token.empty()) {
return false;
}
std::string clientid = YOUTUBE_CLIENTID;
std::string secret = YOUTUBE_SECRET;
deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH);
deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH);
std::string r_token =
QUrl::toPercentEncoding(refresh_token.c_str()).toStdString();
const QString url = YOUTUBE_LIVE_TOKEN_URL;
const QString data_template = "client_id=%1"
"&client_secret=%2"
"&refresh_token=%3"
"&grant_type=refresh_token";
const QString data = data_template.arg(QString(clientid.c_str()),
QString(secret.c_str()),
QString(r_token.c_str()));
Json json_out;
bool success = TryInsertCommand(QT_TO_UTF8(url),
"application/x-www-form-urlencoded", "",
QT_TO_UTF8(data), json_out);
if (!success || json_out.object_items().find("error") !=
json_out.object_items().end())
return false;
token = json_out["access_token"].string_value();
return token.empty() ? false : true;
}
bool YoutubeApiWrappers::InsertCommand(const char *url,
const char *content_type,
std::string request_type,
const char *data, Json &json_out)
{
long error_code;
if (!TryInsertCommand(url, content_type, request_type, data, json_out,
&error_code)) {
if (error_code == 401) {
if (!UpdateAccessToken()) {
return false;
}
//The second try after update token.
return TryInsertCommand(url, content_type, request_type,
data, json_out);
}
return false;
}
if (json_out.object_items().find("error") !=
json_out.object_items().end()) {
lastError = json_out["error"]["code"].int_value();
lastErrorMessage = QString(
json_out["error"]["message"].string_value().c_str());
if (json_out["error"]["code"] == 401) {
if (!UpdateAccessToken()) {
return false;
}
//The second try after update token.
return TryInsertCommand(url, content_type, request_type,
data, json_out);
}
return false;
}
return true;
}
bool YoutubeApiWrappers::GetChannelDescription(
ChannelDescription &channel_description)
{
lastErrorMessage.clear();
const QByteArray url = YOUTUBE_LIVE_CHANNEL_URL
"?part=snippet,contentDetails,statistics"
"&mine=true";
Json json_out;
if (!InsertCommand(url, "application/json", "", nullptr, json_out)) {
return false;
}
channel_description.id =
QString(json_out["items"][0]["id"].string_value().c_str());
channel_description.country =
QString(json_out["items"][0]["snippet"]["country"]
.string_value()
.c_str());
channel_description.language =
QString(json_out["items"][0]["snippet"]["defaultLanguage"]
.string_value()
.c_str());
channel_description.title = QString(
json_out["items"][0]["snippet"]["title"].string_value().c_str());
return channel_description.id.isEmpty() ? false : true;
}
bool YoutubeApiWrappers::InsertBroadcast(BroadcastDescription &broadcast)
{
// Youtube API: The Title property's value must be between 1 and 100 characters long.
if (broadcast.title.isEmpty() || broadcast.title.length() > 100) {
blog(LOG_ERROR, "Insert broadcast FAIL: Wrong title.");
lastErrorMessage = "Broadcast title too long.";
return false;
}
// Youtube API: The property's value can contain up to 5000 characters.
if (broadcast.description.length() > 5000) {
blog(LOG_ERROR, "Insert broadcast FAIL: Description too long.");
lastErrorMessage = "Broadcast description too long.";
return false;
}
const QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
"?part=snippet,status,contentDetails";
const Json data = Json::object{
{"snippet",
Json::object{
{"title", QT_TO_UTF8(broadcast.title)},
{"description", QT_TO_UTF8(broadcast.description)},
{"scheduledStartTime",
QT_TO_UTF8(broadcast.schedul_date_time)},
}},
{"status",
Json::object{
{"privacyStatus", QT_TO_UTF8(broadcast.privacy)},
{"selfDeclaredMadeForKids", broadcast.made_for_kids},
}},
{"contentDetails",
Json::object{
{"latencyPreference", QT_TO_UTF8(broadcast.latency)},
{"enableAutoStart", broadcast.auto_start},
{"enableAutoStop", broadcast.auto_stop},
{"enableDvr", broadcast.dvr},
{"projection", QT_TO_UTF8(broadcast.projection)},
{
"monitorStream",
Json::object{
{"enableMonitorStream", false},
},
},
}},
};
Json json_out;
if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
json_out)) {
return false;
}
broadcast.id = QString(json_out["id"].string_value().c_str());
return broadcast.id.isEmpty() ? false : true;
}
bool YoutubeApiWrappers::InsertStream(StreamDescription &stream)
{
// Youtube API documentation: The snippet.title property's value in the liveStream resource must be between 1 and 128 characters long.
if (stream.title.isEmpty() || stream.title.length() > 128) {
blog(LOG_ERROR, "Insert stream FAIL: wrong argument");
return false;
}
// Youtube API: The snippet.description property's value in the liveStream resource can have up to 10000 characters.
if (stream.description.length() > 10000) {
blog(LOG_ERROR, "Insert stream FAIL: Description too long.");
return false;
}
const QByteArray url = YOUTUBE_LIVE_STREAM_URL
"?part=snippet,cdn,status";
const Json data = Json::object{
{"snippet",
Json::object{
{"title", QT_TO_UTF8(stream.title)},
}},
{"cdn",
Json::object{
{"frameRate", "variable"},
{"ingestionType", "rtmp"},
{"resolution", "variable"},
}},
};
Json json_out;
if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
json_out)) {
return false;
}
stream.id = QString(json_out["id"].string_value().c_str());
stream.name = QString(json_out["cdn"]["ingestionInfo"]["streamName"]
.string_value()
.c_str());
return stream.id.isEmpty() ? false : true;
}
bool YoutubeApiWrappers::BindStream(const QString broadcast_id,
const QString stream_id)
{
const QString url_template = YOUTUBE_LIVE_BROADCAST_BIND_URL
"?id=%1"
"&streamId=%2"
"&part=id,snippet,contentDetails,status";
const QString url = url_template.arg(broadcast_id, stream_id);
const Json data = Json::object{};
this->broadcast_id = broadcast_id;
Json json_out;
return InsertCommand(QT_TO_UTF8(url), "application/json", "",
data.dump().c_str(), json_out);
}
bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, QString page)
{
lastErrorMessage.clear();
QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
"?part=snippet,contentDetails,status"
"&broadcastType=all&maxResults=" DEFAULT_BROADCASTS_PER_QUERY
"&mine=true";
if (!page.isEmpty())
url += "&pageToken=" + page.toUtf8();
return InsertCommand(url, "application/json", "", nullptr, json_out);
}
bool YoutubeApiWrappers::GetVideoCategoriesList(
const QString &country, const QString &language,
QVector<CategoryDescription> &category_list_out)
{
const QString url_template = YOUTUBE_LIVE_VIDEOCATEGORIES_URL
"?part=snippet"
"&regionCode=%1"
"&hl=%2";
const QString url =
url_template.arg(country.isEmpty() ? "US" : country,
language.isEmpty() ? "en" : language);
Json json_out;
if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
json_out)) {
return false;
}
category_list_out = {};
for (auto &j : json_out["items"].array_items()) {
// Assignable only.
if (j["snippet"]["assignable"].bool_value()) {
category_list_out.push_back(
{j["id"].string_value().c_str(),
j["snippet"]["title"].string_value().c_str()});
}
}
return category_list_out.isEmpty() ? false : true;
}
bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id,
const QString &video_title,
const QString &video_description,
const QString &categorie_id)
{
const QByteArray url = YOUTUBE_LIVE_VIDEOS_URL "?part=snippet";
const Json data = Json::object{
{"id", QT_TO_UTF8(video_id)},
{"snippet",
Json::object{
{"title", QT_TO_UTF8(video_title)},
{"description", QT_TO_UTF8(video_description)},
{"categoryId", QT_TO_UTF8(categorie_id)},
}},
};
Json json_out;
return InsertCommand(url, "application/json", "PUT",
data.dump().c_str(), json_out);
}
bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
{
lastErrorMessage.clear();
if (!ResetBroadcast(broadcast_id))
return false;
const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
"?id=%1"
"&broadcastStatus=%2"
"&part=status";
const QString live = url_template.arg(broadcast_id, "live");
Json json_out;
return InsertCommand(QT_TO_UTF8(live), "application/json", "POST", "{}",
json_out);
}
bool YoutubeApiWrappers::StartLatestBroadcast()
{
return StartBroadcast(this->broadcast_id);
}
bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id)
{
lastErrorMessage.clear();
const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
"?id=%1"
"&broadcastStatus=complete"
"&part=status";
const QString url = url_template.arg(broadcast_id);
Json json_out;
return InsertCommand(QT_TO_UTF8(url), "application/json", "POST", "{}",
json_out);
}
bool YoutubeApiWrappers::StopLatestBroadcast()
{
return StopBroadcast(this->broadcast_id);
}
bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id)
{
lastErrorMessage.clear();
const QString url_template = YOUTUBE_LIVE_BROADCAST_URL
"?part=id,snippet,contentDetails,status"
"&id=%1";
const QString url = url_template.arg(broadcast_id);
Json json_out;
if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
json_out))
return false;
const QString put = YOUTUBE_LIVE_BROADCAST_URL
"?part=id,snippet,contentDetails,status";
auto snippet = json_out["items"][0]["snippet"];
auto status = json_out["items"][0]["status"];
auto contentDetails = json_out["items"][0]["contentDetails"];
auto monitorStream = contentDetails["monitorStream"];
const Json data = Json::object{
{"id", QT_TO_UTF8(broadcast_id)},
{"snippet",
Json::object{
{"title", snippet["title"]},
{"scheduledStartTime", snippet["scheduledStartTime"]},
}},
{"status",
Json::object{
{"privacyStatus", status["privacyStatus"]},
{"madeForKids", status["madeForKids"]},
{"selfDeclaredMadeForKids",
status["selfDeclaredMadeForKids"]},
}},
{"contentDetails",
Json::object{
{
"monitorStream",
Json::object{
{"enableMonitorStream", false},
{"broadcastStreamDelayMs",
monitorStream["broadcastStreamDelayMs"]},
},
},
{"enableDvr", contentDetails["enableDvr"]},
{"enableContentEncryption",
contentDetails["enableContentEncryption"]},
{"enableEmbed", contentDetails["enableEmbed"]},
{"recordFromStart", contentDetails["recordFromStart"]},
{"startWithSlate", contentDetails["startWithSlate"]},
}},
};
return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT",
data.dump().c_str(), json_out);
}
bool YoutubeApiWrappers::FindBroadcast(const QString &id,
json11::Json &json_out)
{
lastErrorMessage.clear();
QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
"?part=id,snippet,contentDetails,status"
"&broadcastType=all&maxResults=1";
url += "&id=" + id.toUtf8();
if (!InsertCommand(url, "application/json", "", nullptr, json_out))
return false;
auto items = json_out["items"].array_items();
if (items.size() != 1) {
lastErrorMessage = "No active broadcast found.";
return false;
}
return true;
}
bool YoutubeApiWrappers::FindStream(const QString &id, json11::Json &json_out)
{
lastErrorMessage.clear();
QByteArray url = YOUTUBE_LIVE_STREAM_URL "?part=id,snippet,cdn,status"
"&maxResults=1";
url += "&id=" + id.toUtf8();
if (!InsertCommand(url, "application/json", "", nullptr, json_out))
return false;
auto items = json_out["items"].array_items();
if (items.size() != 1) {
lastErrorMessage = "No active broadcast found.";
return false;
}
return true;
}

View file

@ -0,0 +1,92 @@
#pragma once
#include "auth-youtube.hpp"
#include <json11.hpp>
#include <QString>
struct ChannelDescription {
QString id;
QString title;
QString country;
QString language;
};
struct StreamDescription {
QString id;
QString name;
QString title;
QString description;
};
struct CategoryDescription {
QString id;
QString title;
};
struct BroadcastDescription {
QString id;
QString title;
QString description;
QString privacy;
CategoryDescription category;
QString latency;
bool made_for_kids;
bool auto_start;
bool auto_stop;
bool dvr;
bool schedul_for_later;
QString schedul_date_time;
QString projection;
};
struct BindDescription {
const QString id;
const QString stream_name;
};
bool IsYouTubeService(const std::string &service);
class YoutubeApiWrappers : public YoutubeAuth {
Q_OBJECT
bool TryInsertCommand(const char *url, const char *content_type,
std::string request_type, const char *data,
json11::Json &ret, long *error_code = nullptr);
bool UpdateAccessToken();
bool InsertCommand(const char *url, const char *content_type,
std::string request_type, const char *data,
json11::Json &ret);
public:
YoutubeApiWrappers(const Def &d);
bool GetChannelDescription(ChannelDescription &channel_description);
bool InsertBroadcast(BroadcastDescription &broadcast);
bool InsertStream(StreamDescription &stream);
bool BindStream(const QString broadcast_id, const QString stream_id);
bool GetBroadcastsList(json11::Json &json_out, QString page);
bool
GetVideoCategoriesList(const QString &country, const QString &language,
QVector<CategoryDescription> &category_list_out);
bool SetVideoCategory(const QString &video_id,
const QString &video_title,
const QString &video_description,
const QString &categorie_id);
bool StartBroadcast(const QString &broadcast_id);
bool StopBroadcast(const QString &broadcast_id);
bool ResetBroadcast(const QString &broadcast_id);
bool StartLatestBroadcast();
bool StopLatestBroadcast();
bool FindBroadcast(const QString &id, json11::Json &json_out);
bool FindStream(const QString &id, json11::Json &json_out);
QString GetLastError() { return lastErrorMessage; };
private:
QString broadcast_id;
int lastError;
QString lastErrorMessage;
};