mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-07-04 10:33:30 +00:00
UI: Add YouTube integration
This commit is contained in:
parent
0654675f32
commit
e6f1daab8c
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -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
209
UI/auth-youtube.cpp
Normal 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
34
UI/auth-youtube.hpp
Normal 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);
|
||||
};
|
|
@ -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."
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
601
UI/forms/OBSYoutubeActions.ui
Normal file
601
UI/forms/OBSYoutubeActions.ui
Normal 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><big>Friday Fortnite Stream</big><br/>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><big>Friday Fortnite Stream</big><br/>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><big>Friday Fortnite Stream</big><br/>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><big>Friday Fortnite Stream</big><br/>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>
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -197,6 +197,8 @@ public slots:
|
|||
void UpdateMoreInfoLink();
|
||||
void UpdateServerList();
|
||||
void UpdateCompleted();
|
||||
|
||||
void reset_service_ui_fields(std::string &service);
|
||||
};
|
||||
|
||||
class AutoConfigTestPage : public QWizardPage {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
520
UI/window-youtube-actions.cpp
Normal file
520
UI/window-youtube-actions.cpp
Normal 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();
|
||||
}
|
68
UI/window-youtube-actions.hpp
Normal file
68
UI/window-youtube-actions.hpp
Normal 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
478
UI/youtube-api-wrappers.cpp
Normal 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"
|
||||
"®ionCode=%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;
|
||||
}
|
92
UI/youtube-api-wrappers.hpp
Normal file
92
UI/youtube-api-wrappers.hpp
Normal 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;
|
||||
};
|
Loading…
Reference in a new issue