UI: Add thumbnail option to YouTube broadcast setup

master
derrod 2021-09-12 11:44:00 +02:00
parent 2dd8049aef
commit 39bbbb41dc
10 changed files with 276 additions and 29 deletions

View File

@ -1205,6 +1205,11 @@ YouTube.Actions.Privacy.Public="Public"
YouTube.Actions.Privacy.Unlisted="Unlisted"
YouTube.Actions.Category="Category"
YouTube.Actions.Thumbnail="Thumbnail"
YouTube.Actions.Thumbnail.SelectFile="Select file..."
YouTube.Actions.Thumbnail.NoFileSelected="No file selected"
YouTube.Actions.Thumbnail.ClearFile="Clear"
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"
@ -1238,6 +1243,9 @@ YouTube.Actions.Error.NoBroadcastCreated="Broadcast creation error '%1'.<br/>A d
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.Error.BroadcastNotFound="The selected broadcast was not found."
YouTube.Actions.Error.FileMissing="Selected file does not exist."
YouTube.Actions.Error.FileOpeningFailed="Failed opening selected file."
YouTube.Actions.Error.FileTooLarge="Selected file is too large (Limit: 2 MiB)."
YouTube.Actions.EventsLoading="Loading list of events..."
YouTube.Actions.EventCreated.Title="Event Created"

View File

@ -1165,6 +1165,14 @@ QSlider::handle:horizontal[themeID="tBarSlider"] {
}
/* YouTube Integration */
OBSYoutubeActions {
qproperty-thumbPlaceholder: url(./Dark/sources/image.svg);
}
#thumbnailPreview {
background-color: rgb(40,40,42);
}
#ytEventList QLabel {
color: rgb(225,224,225);
background-color: #162458;

View File

@ -896,6 +896,10 @@ QSlider::handle:horizontal[themeID="tBarSlider"] {
}
/* YouTube Integration */
OBSYoutubeActions {
qproperty-thumbPlaceholder: url(./Dark/sources/image.svg);
}
#broadcastButton[broadcastState=ready] {
background: blue;
}

View File

@ -1465,6 +1465,10 @@ QPushButton#sourceFiltersButton {
}
/* YouTube Integration */
OBSYoutubeActions {
qproperty-thumbPlaceholder: url(./Dark/sources/image.svg);
}
#ytEventList QLabel {
background-color: rgb(0, 188, 212);; /* Cyan (Primary) */
color: rgb(239, 240, 241); /* White */

View File

@ -302,6 +302,10 @@ QSlider::handle:horizontal[themeID="tBarSlider"] {
}
/* YouTube Integration */
OBSYoutubeActions {
qproperty-thumbPlaceholder: url(:res/images/sources/image.svg);
}
#ytEventList QLabel {
background-color: #e1e1e1;
border: 1px solid #ddd;

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>585</width>
<height>536</height>
<width>616</width>
<height>645</height>
</rect>
</property>
<property name="sizePolicy">
@ -22,6 +22,12 @@
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>720</width>
<height>880</height>
</size>
</property>
<property name="windowTitle">
<string>YouTube.Actions.WindowTitle</string>
</property>
@ -37,7 +43,7 @@
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<widget class="QWidget" name="ytEventCreate">
<attribute name="title">
<string>YouTube.Actions.CreateNewEvent</string>
</attribute>
@ -175,13 +181,20 @@
</layout>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>YouTube.Actions.Thumbnail</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>YouTube.Actions.AdditionalSettings</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="9" column="1">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -194,7 +207,7 @@
</property>
</spacer>
</item>
<item row="7" column="0">
<item row="10" column="0">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -207,7 +220,7 @@
</property>
</spacer>
</item>
<item row="7" column="1">
<item row="10" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_3">
@ -228,7 +241,7 @@
</item>
</layout>
</item>
<item row="8" column="0">
<item row="11" column="0">
<spacer name="horizontalSpacer_14">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -241,7 +254,7 @@
</property>
</spacer>
</item>
<item row="8" column="1">
<item row="11" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QCheckBox" name="checkDVR">
@ -255,7 +268,7 @@
</item>
</layout>
</item>
<item row="9" column="0">
<item row="12" column="0">
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -268,7 +281,7 @@
</property>
</spacer>
</item>
<item row="9" column="1">
<item row="12" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QCheckBox" name="check360Video">
@ -308,7 +321,7 @@
</item>
</layout>
</item>
<item row="10" column="0">
<item row="13" column="0">
<spacer name="horizontalSpacer_12">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -321,7 +334,7 @@
</property>
</spacer>
</item>
<item row="10" column="1">
<item row="13" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="checkScheduledLater">
@ -335,7 +348,7 @@
</item>
</layout>
</item>
<item row="11" column="0">
<item row="14" column="0">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -348,7 +361,7 @@
</property>
</spacer>
</item>
<item row="11" column="1">
<item row="14" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QCheckBox" name="checkAutoStart">
@ -397,7 +410,7 @@
</item>
</layout>
</item>
<item row="12" column="0">
<item row="15" column="0">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -410,7 +423,7 @@
</property>
</spacer>
</item>
<item row="12" column="1">
<item row="15" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QCheckBox" name="checkAutoStop">
@ -427,7 +440,7 @@
</item>
</layout>
</item>
<item row="13" column="0">
<item row="16" column="0">
<spacer name="horizontalSpacer_13">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -440,7 +453,7 @@
</property>
</spacer>
</item>
<item row="13" column="1">
<item row="16" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QDateTimeEdit" name="scheduledTime">
@ -460,7 +473,7 @@
</item>
</layout>
</item>
<item row="14" column="1">
<item row="17" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QCheckBox" name="checkRememberSettings">
@ -471,7 +484,7 @@
</item>
</layout>
</item>
<item row="14" column="0">
<item row="17" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -484,6 +497,64 @@
</property>
</spacer>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_13">
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="selectFileButton">
<property name="text">
<string>YouTube.Actions.Thumbnail.SelectFile</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="selectedFileName">
<property name="text">
<string>YouTube.Actions.Thumbnail.NoFileSelected</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="1">
<widget class="ClickableLabel" name="thumbnailPreview">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>162</width>
<height>92</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>162</width>
<height>92</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="ytEventList">
@ -504,7 +575,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>524</width>
<width>555</width>
<height>192</height>
</rect>
</property>
@ -633,6 +704,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ClickableLabel</class>
<extends>QLabel</extends>
<header>clickable-label.hpp</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -8,6 +8,9 @@
#include <QToolTip>
#include <QDateTime>
#include <QDesktopServices>
#include <QFileInfo>
#include <QStandardPaths>
#include <QImageReader>
const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'";
const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m";
@ -91,6 +94,55 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth,
ui->scheduledTime->setDateTime(QDateTime::currentDateTime());
auto thumbSelectionHandler = [&]() {
if (thumbnailFile.isEmpty()) {
QString filePath = OpenFile(
this,
QTStr("YouTube.Actions.Thumbnail.SelectFile"),
QStandardPaths::writableLocation(
QStandardPaths::PicturesLocation),
QString("Images (*.png *.jpg *.jpeg *.gif)"));
if (!filePath.isEmpty()) {
QFileInfo tFile(filePath);
if (!tFile.exists()) {
return ShowErrorDialog(
this,
QTStr("YouTube.Actions.Error.FileMissing"));
} else if (tFile.size() > 2 * 1024 * 1024) {
return ShowErrorDialog(
this,
QTStr("YouTube.Actions.Error.FileTooLarge"));
}
thumbnailFile = filePath;
ui->selectedFileName->setText(thumbnailFile);
ui->selectFileButton->setText(QTStr(
"YouTube.Actions.Thumbnail.ClearFile"));
QImageReader imgReader(filePath);
imgReader.setAutoTransform(true);
const QImage newImage = imgReader.read();
ui->thumbnailPreview->setPixmap(
QPixmap::fromImage(newImage).scaled(
160, 90, Qt::KeepAspectRatio));
}
} else {
thumbnailFile.clear();
ui->selectedFileName->setText(QTStr(
"YouTube.Actions.Thumbnail.NoFileSelected"));
ui->selectFileButton->setText(
QTStr("YouTube.Actions.Thumbnail.SelectFile"));
ui->thumbnailPreview->setPixmap(
GetPlaceholder().pixmap(QSize(16, 16)));
}
};
connect(ui->selectFileButton, &QPushButton::clicked, this,
thumbSelectionHandler);
connect(ui->thumbnailPreview, &ClickableLabel::clicked, this,
thumbSelectionHandler);
if (!apiYouTube) {
blog(LOG_DEBUG, "YouTube API auth NOT found.");
Cancel();
@ -245,6 +297,14 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth,
valid = true;
}
void OBSYoutubeActions::showEvent(QShowEvent *event)
{
QDialog::showEvent(event);
if (thumbnailFile.isEmpty())
ui->thumbnailPreview->setPixmap(
GetPlaceholder().pixmap(QSize(16, 16)));
}
OBSYoutubeActions::~OBSYoutubeActions()
{
workerThread->stop();
@ -421,6 +481,15 @@ bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api,
blog(LOG_DEBUG, "No category set.");
return false;
}
if (!thumbnailFile.isEmpty()) {
blog(LOG_INFO, "Uploading thumbnail file \"%s\"...",
thumbnailFile.toStdString().c_str());
if (!apiYouTube->SetVideoThumbnail(broadcast.id,
thumbnailFile)) {
blog(LOG_DEBUG, "No thumbnail set.");
return false;
}
}
if (!stream_later || ready_broadcast) {
stream = {"", "", "OBS Studio Video Stream"};
@ -664,6 +733,8 @@ void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast)
broadcast.schedul_for_later);
config_set_string(main->basicConfig, "YouTube", "Projection",
QT_TO_UTF8(broadcast.projection));
config_set_string(main->basicConfig, "YouTube", "ThumbnailFile",
QT_TO_UTF8(thumbnailFile));
config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true);
}
@ -724,6 +795,26 @@ void OBSYoutubeActions::LoadSettings()
else
ui->check360Video->setChecked(false);
}
const char *thumbFile = config_get_string(main->basicConfig, "YouTube",
"ThumbnailFile");
if (thumbFile && *thumbFile) {
QFileInfo tFile(thumbFile);
// Re-check validity before setting path again
if (tFile.exists() && tFile.size() <= 2 * 1024 * 1024) {
thumbnailFile = tFile.absoluteFilePath();
ui->selectedFileName->setText(thumbnailFile);
ui->selectFileButton->setText(
QTStr("YouTube.Actions.Thumbnail.ClearFile"));
QImageReader imgReader(thumbnailFile);
imgReader.setAutoTransform(true);
const QImage newImage = imgReader.read();
ui->thumbnailPreview->setPixmap(
QPixmap::fromImage(newImage).scaled(
160, 90, Qt::KeepAspectRatio));
}
}
}
void OBSYoutubeActions::OpenYouTubeDashboard()

View File

@ -30,6 +30,8 @@ signals:
class OBSYoutubeActions : public QDialog {
Q_OBJECT
Q_PROPERTY(QIcon thumbPlaceholder READ GetPlaceholder WRITE
SetPlaceholder DESIGNABLE true)
std::unique_ptr<Ui::OBSYoutubeActions> ui;
@ -38,6 +40,7 @@ signals:
bool autostop, bool start_now);
protected:
void showEvent(QShowEvent *event);
void UpdateOkButtonStatus();
bool CreateEventAction(YoutubeApiWrappers *api,
@ -65,10 +68,15 @@ private:
void SaveSettings(BroadcastDescription &broadcast);
void LoadSettings();
QIcon GetPlaceholder() { return thumbPlaceholder; }
void SetPlaceholder(const QIcon &icon) { thumbPlaceholder = icon; }
QString selectedBroadcast;
bool autostart, autostop;
bool valid = false;
bool broadcastReady = false;
YoutubeApiWrappers *apiYouTube;
WorkerThread *workerThread;
QString thumbnailFile;
QIcon thumbPlaceholder;
};

View File

@ -1,6 +1,8 @@
#include "youtube-api-wrappers.hpp"
#include <QUrl>
#include <QMimeDatabase>
#include <QFile>
#include <string>
#include <iostream>
@ -27,6 +29,8 @@ using namespace json11;
#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 YOUTUBE_LIVE_THUMBNAIL_URL \
"https://www.googleapis.com/upload/youtube/v3/thumbnails/set"
#define DEFAULT_BROADCASTS_PER_QUERY \
"50" // acceptable values are 0 to 50, inclusive
@ -58,22 +62,25 @@ bool YoutubeApiWrappers::TryInsertCommand(const char *url,
const char *content_type,
std::string request_type,
const char *data, Json &json_out,
long *error_code)
long *error_code, int data_size)
{
long httpStatusCode = 0;
#ifdef _DEBUG
blog(LOG_DEBUG, "YouTube API command URL: %s", url);
blog(LOG_DEBUG, "YouTube API command data: %s", data);
if (data && data[0] == '{') // only log JSON data
blog(LOG_DEBUG, "YouTube API command data: %s", data);
#endif
if (token.empty())
return false;
std::string output;
std::string error;
// Increase timeout by the time it takes to transfer `data_size` at 1 Mbps
int timeout = 5 + data_size / 125000;
bool success = GetRemoteFile(url, output, error, &httpStatusCode,
content_type, request_type, data,
{"Authorization: Bearer " + token},
nullptr, 5, false);
nullptr, timeout, false, data_size);
if (error_code)
*error_code = httpStatusCode;
@ -126,18 +133,20 @@ bool YoutubeApiWrappers::UpdateAccessToken()
bool YoutubeApiWrappers::InsertCommand(const char *url,
const char *content_type,
std::string request_type,
const char *data, Json &json_out)
const char *data, Json &json_out,
int data_size)
{
long error_code;
bool success = TryInsertCommand(url, content_type, request_type, data,
json_out, &error_code);
json_out, &error_code, data_size);
if (error_code == 401) {
// Attempt to update access token and try again
if (!UpdateAccessToken())
return false;
success = TryInsertCommand(url, content_type, request_type,
data, json_out, &error_code);
data, json_out, &error_code,
data_size);
}
if (json_out.object_items().find("error") !=
@ -361,6 +370,36 @@ bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id,
data.dump().c_str(), json_out);
}
bool YoutubeApiWrappers::SetVideoThumbnail(const QString &video_id,
const QString &thumbnail_file)
{
lastErrorMessage.clear();
lastErrorReason.clear();
// Make sure the file hasn't been deleted since originally selecting it
if (!QFile::exists(thumbnail_file)) {
lastErrorMessage = QTStr("YouTube.Actions.Error.FileMissing");
return false;
}
QFile thumbFile(thumbnail_file);
if (!thumbFile.open(QFile::ReadOnly)) {
lastErrorMessage =
QTStr("YouTube.Actions.Error.FileOpeningFailed");
return false;
}
const QByteArray fileContents = thumbFile.readAll();
const QString mime =
QMimeDatabase().mimeTypeForData(fileContents).name();
const QString url = YOUTUBE_LIVE_THUMBNAIL_URL "?videoId=" + video_id;
Json json_out;
return InsertCommand(QT_TO_UTF8(url), QT_TO_UTF8(mime), "POST",
fileContents.constData(), json_out,
fileContents.size());
}
bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
{
lastErrorMessage.clear();

View File

@ -44,11 +44,12 @@ class YoutubeApiWrappers : public YoutubeAuth {
bool TryInsertCommand(const char *url, const char *content_type,
std::string request_type, const char *data,
json11::Json &ret, long *error_code = nullptr);
json11::Json &ret, long *error_code = nullptr,
int data_size = 0);
bool UpdateAccessToken();
bool InsertCommand(const char *url, const char *content_type,
std::string request_type, const char *data,
json11::Json &ret);
json11::Json &ret, int data_size = 0);
public:
YoutubeApiWrappers(const Def &d);
@ -65,6 +66,8 @@ public:
const QString &video_title,
const QString &video_description,
const QString &categorie_id);
bool SetVideoThumbnail(const QString &video_id,
const QString &thumbnail_file);
bool StartBroadcast(const QString &broadcast_id);
bool StopBroadcast(const QString &broadcast_id);
bool ResetBroadcast(const QString &broadcast_id);