Files
obs-studio/UI/window-dock-youtube-app.cpp
Lain 0c6fa0a2ba UI: Fix YouTubeAppDock crash
Apparently the YouTube app dock thing didn't take into consideration the
fact that the user may have the browser source available, but may not
have browser docks available (via the `cef` global). This is because
certain system configurations do not support browser widgets, such as
Wayland on Linux.

(Lain note: There's multiple things I'm unhappy about here. I'm mostly
going to blame myself for multiple reasons.)
2023-08-03 18:25:25 -07:00

489 lines
12 KiB
C++

#include <QUuid>
#include "window-basic-main.hpp"
#include "youtube-api-wrappers.hpp"
#include "window-dock-youtube-app.hpp"
#include "ui-config.h"
#include "qt-wrappers.hpp"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#ifdef YOUTUBE_WEBAPP_PLACEHOLDER
static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL =
YOUTUBE_WEBAPP_PLACEHOLDER;
#else
static constexpr const char *YOUTUBE_WEBAPP_PLACEHOLDER_URL =
"https://studio.youtube.com/live/channel/UC/console?kc=OBS";
#endif
#ifdef YOUTUBE_WEBAPP_ADDRESS
static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL =
YOUTUBE_WEBAPP_ADDRESS;
#else
static constexpr const char *YOUTUBE_WEBAPP_ADDRESS_URL =
"https://studio.youtube.com/live/channel/%1/console?kc=OBS";
#endif
static constexpr const char *BROADCAST_CREATED = "BROADCAST_CREATED";
static constexpr const char *BROADCAST_SELECTED = "BROADCAST_SELECTED";
static constexpr const char *INGESTION_STARTED = "INGESTION_STARTED";
static constexpr const char *INGESTION_STOPPED = "INGESTION_STOPPED";
YouTubeAppDock::YouTubeAppDock()
: BrowserDock(),
dockBrowser(nullptr),
cookieManager(nullptr)
{
OBSBasic::InitBrowserPanelSafeBlock();
AddYouTubeAppDock("YouTube Live Control Panel");
}
YouTubeAppDock::~YouTubeAppDock()
{
if (cookieManager) {
cookieManager->FlushStore();
delete cookieManager;
}
}
bool YouTubeAppDock::IsYTServiceSelected()
{
if (!cef)
return false;
obs_service_t *service_obj = OBSBasic::Get()->GetService();
OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
const char *service = obs_data_get_string(settings, "service");
return IsYouTubeService(service);
}
void YouTubeAppDock::AccountConnected()
{
channelId.clear(); // renew channel id
UpdateChannelId();
}
void YouTubeAppDock::AccountDisconnected()
{
SettingsUpdated(true);
}
void YouTubeAppDock::SettingsUpdated(bool cleanup)
{
bool ytservice = IsYTServiceSelected();
SetVisibleYTAppDockInMenu(ytservice);
// definitely cleanup if YT switched off
if (!ytservice || cleanup) {
if (cookieManager)
cookieManager->DeleteCookies("", "");
}
if (ytservice)
Update();
}
std::string YouTubeAppDock::InitYTUserUrl()
{
std::string user_url(YOUTUBE_WEBAPP_PLACEHOLDER_URL);
if (IsUserSignedIntoYT()) {
YoutubeApiWrappers *apiYouTube = GetYTApi();
if (apiYouTube) {
ChannelDescription channel_description;
if (apiYouTube->GetChannelDescription(
channel_description)) {
QString url =
QString(YOUTUBE_WEBAPP_ADDRESS_URL)
.arg(channel_description.id);
user_url = url.toStdString();
} else {
blog(LOG_ERROR,
"YT: InitYTUserUrl() Failed to get channel id");
}
}
} else {
blog(LOG_ERROR, "YT: InitYTUserUrl() User is not signed");
}
blog(LOG_DEBUG, "YT: InitYTUserUrl() User url: %s", user_url.c_str());
return user_url;
}
void YouTubeAppDock::AddYouTubeAppDock(const QString &title)
{
QString bId(QUuid::createUuid().toString());
bId.replace(QRegularExpression("[{}-]"), "");
this->setProperty("uuid", bId);
this->setObjectName(title + "Object");
this->resize(580, 500);
this->setMinimumSize(400, 300);
this->setWindowTitle(title);
this->setAllowedAreas(Qt::AllDockWidgetAreas);
OBSBasic::Get()->addDockWidget(Qt::RightDockWidgetArea, this);
actionYTAppDock = OBSBasic::Get()->AddDockWidget(this);
if (IsYTServiceSelected()) {
const std::string url = InitYTUserUrl();
CreateBrowserWidget(url);
// reload panel layout
const char *dockStateStr = config_get_string(
App()->GlobalConfig(), "BasicWindow", "DockState");
if (dockStateStr) {
QByteArray dockState = QByteArray::fromBase64(
QByteArray(dockStateStr));
OBSBasic::Get()->restoreState(dockState);
}
} else {
this->setVisible(false);
actionYTAppDock->setVisible(false);
}
}
void YouTubeAppDock::CreateBrowserWidget(const std::string &url)
{
std::string dir_name = std::string("obs_profile_cookies_youtube/") +
config_get_string(OBSBasic::Get()->Config(),
"Panels", "CookieId");
if (cookieManager)
delete cookieManager;
cookieManager = cef->create_cookie_manager(dir_name, true);
if (dockBrowser)
delete dockBrowser;
dockBrowser = cef->create_widget(this, url, cookieManager);
if (!dockBrowser)
return;
if (obs_browser_qcef_version() >= 1)
dockBrowser->allowAllPopups(true);
this->SetWidget(dockBrowser);
Update();
}
void YouTubeAppDock::SetVisibleYTAppDockInMenu(bool visible)
{
if (!actionYTAppDock)
return;
actionYTAppDock->setVisible(visible);
this->setVisible(visible);
}
// only 'ACCOUNT' mode supported
void YouTubeAppDock::BroadcastCreated(const char *stream_id)
{
DispatchYTEvent(BROADCAST_CREATED, stream_id, YTSM_ACCOUNT);
}
// only 'ACCOUNT' mode supported
void YouTubeAppDock::BroadcastSelected(const char *stream_id)
{
DispatchYTEvent(BROADCAST_SELECTED, stream_id, YTSM_ACCOUNT);
}
// both 'ACCOUNT' and 'STREAM_KEY' modes supported
void YouTubeAppDock::IngestionStarted()
{
obs_service_t *service_obj = OBSBasic::Get()->GetService();
OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
const char *service = obs_data_get_string(settings, "service");
if (IsYouTubeService(service)) {
if (IsUserSignedIntoYT()) {
const char *broadcast_id =
obs_data_get_string(settings, "broadcast_id");
this->IngestionStarted(broadcast_id,
YouTubeAppDock::YTSM_ACCOUNT);
} else {
const char *stream_key =
obs_data_get_string(settings, "key");
this->IngestionStarted(stream_key,
YouTubeAppDock::YTSM_STREAM_KEY);
}
}
}
void YouTubeAppDock::IngestionStarted(const char *stream_id,
streaming_mode_t mode)
{
DispatchYTEvent(INGESTION_STARTED, stream_id, mode);
}
// both 'ACCOUNT' and 'STREAM_KEY' modes supported
void YouTubeAppDock::IngestionStopped()
{
obs_service_t *service_obj = OBSBasic::Get()->GetService();
OBSDataAutoRelease settings = obs_service_get_settings(service_obj);
const char *service = obs_data_get_string(settings, "service");
if (IsYouTubeService(service)) {
if (IsUserSignedIntoYT()) {
const char *broadcast_id =
obs_data_get_string(settings, "broadcast_id");
this->IngestionStopped(broadcast_id,
YouTubeAppDock::YTSM_ACCOUNT);
} else {
const char *stream_key =
obs_data_get_string(settings, "key");
this->IngestionStopped(stream_key,
YouTubeAppDock::YTSM_STREAM_KEY);
}
}
}
void YouTubeAppDock::IngestionStopped(const char *stream_id,
streaming_mode_t mode)
{
DispatchYTEvent(INGESTION_STOPPED, stream_id, mode);
}
void YouTubeAppDock::showEvent(QShowEvent *)
{
if (!dockBrowser)
Update();
}
void YouTubeAppDock::closeEvent(QCloseEvent *event)
{
BrowserDock::closeEvent(event);
this->SetWidget(nullptr);
}
void YouTubeAppDock::DispatchYTEvent(const char *event, const char *video_id,
streaming_mode_t mode)
{
if (!dockBrowser)
return;
// update channelId if empty:
UpdateChannelId();
// notify YouTube Live Streaming API:
std::string script;
if (mode == YTSM_ACCOUNT) {
script = QString(R"""(
if (window.location.hostname == 'studio.youtube.com') {
let event = {
type: '%1',
channelId: '%2',
videoId: '%3',
};
console.log(event);
if (window.ytlsapi && window.ytlsapi.dispatchEvent)
window.ytlsapi.dispatchEvent(event);
}
)""")
.arg(event)
.arg(channelId)
.arg(video_id)
.toStdString();
} else {
const char *stream_key = video_id;
script = QString(R"""(
if (window.location.hostname == 'studio.youtube.com') {
let event = {
type: '%1',
streamKey: '%2',
};
console.log(event);
if (window.ytlsapi && window.ytlsapi.dispatchEvent)
window.ytlsapi.dispatchEvent(event);
}
)""")
.arg(event)
.arg(stream_key)
.toStdString();
}
dockBrowser->executeJavaScript(script);
// in case of user still not logged in in dock panel, remember last event
SetInitEvent(mode, event, video_id, channelId.toStdString().c_str());
}
void YouTubeAppDock::Update()
{
std::string url = InitYTUserUrl();
if (!dockBrowser) {
CreateBrowserWidget(url);
} else {
dockBrowser->setURL(url);
}
// if streaming already run, let's notify YT about past event
if (OBSBasic::Get()->StreamingActive()) {
obs_service_t *service_obj = OBSBasic::Get()->GetService();
OBSDataAutoRelease settings =
obs_service_get_settings(service_obj);
if (IsUserSignedIntoYT()) {
channelId.clear(); // renew channelId
UpdateChannelId();
const char *broadcast_id =
obs_data_get_string(settings, "broadcast_id");
SetInitEvent(YTSM_ACCOUNT, INGESTION_STARTED,
broadcast_id,
channelId.toStdString().c_str());
} else {
const char *stream_key =
obs_data_get_string(settings, "key");
SetInitEvent(YTSM_STREAM_KEY, INGESTION_STARTED,
stream_key);
}
} else {
SetInitEvent(IsUserSignedIntoYT() ? YTSM_ACCOUNT
: YTSM_STREAM_KEY);
}
dockBrowser->reloadPage();
}
void YouTubeAppDock::UpdateChannelId()
{
if (channelId.isEmpty()) {
YoutubeApiWrappers *apiYouTube = GetYTApi();
if (apiYouTube) {
ChannelDescription channel_description;
if (apiYouTube->GetChannelDescription(
channel_description)) {
channelId = channel_description.id;
} else {
blog(LOG_ERROR, "YT: AccountConnected() Failed "
"to get channel id");
}
}
}
}
void YouTubeAppDock::SetInitEvent(streaming_mode_t mode, const char *event,
const char *video_id, const char *channelId)
{
const std::string version = App()->GetVersionString();
QString api_event;
if (event) {
if (mode == YTSM_ACCOUNT) {
api_event = QString(R"""(,
initEvent: {
type: '%1',
channelId: '%2',
videoId: '%3',
}
)""")
.arg(event)
.arg(channelId)
.arg(video_id);
} else {
api_event = QString(R"""(,
initEvent: {
type: '%1',
streamKey: '%2',
}
)""")
.arg(event)
.arg(video_id);
}
}
std::string script = QString(R"""(
let obs_name = '%1';
let obs_version = '%2';
let client_mode = %3;
if (window.location.hostname == 'studio.youtube.com') {
console.log("name:", obs_name);
console.log("version:", obs_version);
console.log("initEvent:", {
initClientMode: client_mode
%4 });
if (window.ytlsapi && window.ytlsapi.init)
window.ytlsapi.init(obs_name, obs_version, undefined, {
initClientMode: client_mode
%4 });
}
)""")
.arg("OBS")
.arg(version.c_str())
.arg(mode == YTSM_ACCOUNT ? "'ACCOUNT'"
: "'STREAM_KEY'")
.arg(api_event)
.toStdString();
dockBrowser->setStartupScript(script);
}
YoutubeApiWrappers *YouTubeAppDock::GetYTApi()
{
Auth *auth = OBSBasic::Get()->GetAuth();
if (auth) {
YoutubeApiWrappers *apiYouTube(
dynamic_cast<YoutubeApiWrappers *>(auth));
if (apiYouTube) {
return apiYouTube;
} else {
blog(LOG_ERROR,
"YT: GetYTApi() Failed to get YoutubeApiWrappers");
}
} else {
blog(LOG_ERROR, "YT: GetYTApi() Failed to get Auth");
}
return nullptr;
}
void YouTubeAppDock::CleanupYouTubeUrls()
{
if (!cef)
return;
static constexpr const char *YOUTUBE_VIDEO_URL =
"://studio.youtube.com/video/";
// remove legacy YouTube Browser Docks (once)
bool youtube_cleanup_done = config_get_bool(
App()->GlobalConfig(), "General", "YtDockCleanupDone");
if (youtube_cleanup_done)
return;
config_set_bool(App()->GlobalConfig(), "General", "YtDockCleanupDone",
true);
const char *jsonStr = config_get_string(
App()->GlobalConfig(), "BasicWindow", "ExtraBrowserDocks");
if (!jsonStr)
return;
json array = json::parse(jsonStr);
if (!array.is_array())
return;
json save_array;
std::string removedYTUrl;
for (json &item : array) {
auto url = item["url"].get<std::string>();
if (url.find(YOUTUBE_VIDEO_URL) != std::string::npos) {
blog(LOG_DEBUG, "YT: found legacy url: %s",
url.c_str());
removedYTUrl += url;
removedYTUrl += ";\n";
} else {
save_array.push_back(item);
}
}
if (!removedYTUrl.empty()) {
const QString msg_title = QTStr("YouTube.DocksRemoval.Title");
const QString msg_text =
QTStr("YouTube.DocksRemoval.Text")
.arg(QT_UTF8(removedYTUrl.c_str()));
OBSMessageBox::warning(OBSBasic::Get(), msg_title, msg_text);
std::string output = save_array.dump();
config_set_string(App()->GlobalConfig(), "BasicWindow",
"ExtraBrowserDocks", output.c_str());
}
}