Browse Source

UI: Add Restream integration

Closes obsproject/obs-studio#1768
SoftArch 6 years ago
parent
commit
de66aeab68
5 changed files with 339 additions and 0 deletions
  1. 19 0
      UI/CMakeLists.txt
  2. 283 0
      UI/auth-restream.cpp
  3. 29 0
      UI/auth-restream.hpp
  4. 4 0
      UI/ui-config.h.in
  5. 4 0
      UI/window-basic-main.cpp

+ 19 - 0
UI/CMakeLists.txt

@@ -39,6 +39,16 @@ else()
 	set(MIXER_ENABLED TRUE)
 endif()
 
+if(NOT DEFINED RESTREAM_CLIENTID OR "${RESTREAM_CLIENTID}" STREQUAL "" OR
+   NOT DEFINED RESTREAM_HASH     OR "${RESTREAM_HASH}"     STREQUAL "" OR
+   NOT BROWSER_AVAILABLE_INTERNAL)
+	set(RESTREAM_ENABLED FALSE)
+	set(RESTREAM_CLIENTID "")
+	set(RESTREAM_HASH "0")
+else()
+	set(RESTREAM_ENABLED TRUE)
+endif()
+
 configure_file(
 	"${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in"
 	"${CMAKE_CURRENT_BINARY_DIR}/ui-config.h")
@@ -159,6 +169,15 @@ if(BROWSER_AVAILABLE_INTERNAL)
 			auth-mixer.hpp
 			)
 	endif()
+
+	if(RESTREAM_ENABLED)
+		list(APPEND obs_PLATFORM_SOURCES
+			auth-restream.cpp
+			)
+		list(APPEND obs_PLATFORM_HEADERS
+			auth-restream.hpp
+			)
+	endif()
 endif()
 
 set(obs_libffutil_SOURCES

+ 283 - 0
UI/auth-restream.cpp

@@ -0,0 +1,283 @@
+#include "auth-restream.hpp"
+
+#include <QPushButton>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <qt-wrappers.hpp>
+#include <json11.hpp>
+#include <ctime>
+#include <sstream>
+
+#include <obs-app.hpp>
+#include "window-basic-main.hpp"
+#include "remote-text.hpp"
+#include "ui-config.h"
+#include "obf.h"
+#include <browser-panel.hpp>
+
+using namespace json11;
+
+extern QCef *cef;
+extern QCefCookieManager *panel_cookies;
+
+/* ------------------------------------------------------------------------- */
+
+#define RESTREAM_AUTH_URL "https://obsproject.com/app-auth/restream?action=redirect"
+#define RESTREAM_TOKEN_URL "https://obsproject.com/app-auth/restream-token"
+#define RESTREAM_STREAMKEY_URL "https://api.restream.io/v2/user/streamKey"
+#define RESTREAM_SCOPE_VERSION 1
+
+
+static Auth::Def restreamDef = {
+	"Restream",
+	Auth::Type::OAuth_StreamKey
+};
+
+/* ------------------------------------------------------------------------- */
+
+RestreamAuth::RestreamAuth(const Def &d)
+	: OAuthStreamKey(d)
+{
+}
+
+bool RestreamAuth::GetChannelInfo()
+try {
+	std::string client_id = RESTREAM_CLIENTID;
+	deobfuscate_str(&client_id[0], RESTREAM_HASH);
+
+	if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION))
+		return false;
+	if (token.empty())
+		return false;
+	if (!key_.empty())
+		return true;
+
+	std::string auth;
+	auth += "Authorization: Bearer ";
+	auth += token;
+
+	std::vector<std::string> headers;
+	headers.push_back(std::string("Client-ID: ") + client_id);
+	headers.push_back(std::move(auth));
+
+	std::string output;
+	std::string error;
+	Json json;
+	bool success;
+
+	auto func = [&] () {
+		success = GetRemoteFile(
+				RESTREAM_STREAMKEY_URL,
+				output,
+				error,
+				nullptr,
+				"application/json",
+				nullptr,
+				headers,
+				nullptr,
+				5);
+	};
+
+	ExecThreadedWithoutBlocking(
+			func,
+			QTStr("Auth.LoadingChannel.Title"),
+			QTStr("Auth.LoadingChannel.Text").arg(service()));
+	if (!success || output.empty())
+		throw ErrorInfo("Failed to get stream key from remote", error);
+
+	json = Json::parse(output, error);
+	if (!error.empty())
+		throw ErrorInfo("Failed to parse json", error);
+
+	error = json["error"].string_value();
+	if (!error.empty())
+		throw ErrorInfo(error, json["error_description"].string_value());
+
+	key_ = json["streamKey"].string_value();
+
+	return true;
+} catch (ErrorInfo info) {
+	QString title = QTStr("Auth.ChannelFailure.Title");
+	QString text = QTStr("Auth.ChannelFailure.Text")
+		.arg(service(), info.message.c_str(), info.error.c_str());
+
+	QMessageBox::warning(OBSBasic::Get(), title, text);
+
+	blog(LOG_WARNING, "%s: %s: %s",
+			__FUNCTION__,
+			info.message.c_str(),
+			info.error.c_str());
+	return false;
+}
+
+void RestreamAuth::SaveInternal()
+{
+	OBSBasic *main = OBSBasic::Get();
+	config_set_string(main->Config(), service(), "DockState",
+			main->saveState().toBase64().constData());
+	OAuthStreamKey::SaveInternal();
+}
+
+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 RestreamAuth::LoadInternal()
+{
+	firstLoad = false;
+	return OAuthStreamKey::LoadInternal();
+}
+
+class RestreamWidget : public QDockWidget {
+public:
+	inline RestreamWidget() : QDockWidget() {}
+
+	QScopedPointer<QCefWidget> widget;
+};
+
+void RestreamAuth::LoadUI()
+{
+	if (uiLoaded)
+		return;
+	if (!GetChannelInfo())
+		return;
+
+	OBSBasic::InitBrowserPanelSafeBlock();
+	OBSBasic *main = OBSBasic::Get();
+
+	QCefWidget *browser;
+	std::string url;
+	std::string script;
+
+	/* ----------------------------------- */
+
+	url = "https://restream.io/chat-application";
+
+	QSize size = main->frameSize();
+	QPoint pos = main->pos();
+
+	chat.reset(new RestreamWidget());
+	chat->setObjectName("restreamChat");
+	chat->resize(420, 600);
+	chat->setMinimumSize(380, 300);
+	chat->setWindowTitle(QTStr("Auth.Chat"));
+	chat->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+	browser = cef->create_widget(nullptr, url, panel_cookies);
+	chat->setWidget(browser);
+
+	main->addDockWidget(Qt::RightDockWidgetArea, chat.data());
+	chatMenu.reset(main->AddDockWidget(chat.data()));
+
+	/* ----------------------------------- */
+
+	url = "https://restream.io/titles/embed";
+
+	info.reset(new RestreamWidget());
+	info->setObjectName("restreamInfo");
+	info->resize(410, 600);
+	info->setMinimumSize(380, 300);
+	info->setWindowTitle(QTStr("Auth.StreamInfo"));
+	info->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+	browser = cef->create_widget(nullptr, url, panel_cookies);
+	info->setWidget(browser);
+
+	main->addDockWidget(Qt::RightDockWidgetArea, info.data());
+	infoMenu.reset(main->AddDockWidget(info.data()));
+
+	/* ----------------------------------- */
+
+	chat->setFloating(true);
+	info->setFloating(true);
+	chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50);
+	info->move(pos.x() + 40, pos.y() + 50);
+
+	if (firstLoad) {
+		chat->setVisible(true);
+		info->setVisible(true);
+	}
+	else {
+		const char *dockStateStr = config_get_string(main->Config(),
+			service(), "DockState");
+		QByteArray dockState =
+			QByteArray::fromBase64(QByteArray(dockStateStr));
+		main->restoreState(dockState);
+	}
+
+	uiLoaded = true;
+}
+
+bool RestreamAuth::RetryLogin()
+{
+	OAuthLogin login(OBSBasic::Get(), RESTREAM_AUTH_URL, false);
+	cef->add_popup_whitelist_url("about:blank", &login);
+	if (login.exec() == QDialog::Rejected) {
+		return false;
+	}
+
+	std::shared_ptr<RestreamAuth> auth =
+		std::make_shared<RestreamAuth>(restreamDef);
+
+	std::string client_id = RESTREAM_CLIENTID;
+	deobfuscate_str(&client_id[0], RESTREAM_HASH);
+
+	return GetToken(RESTREAM_TOKEN_URL, client_id,
+			RESTREAM_SCOPE_VERSION,
+			QT_TO_UTF8(login.GetCode()), true);
+}
+
+std::shared_ptr<Auth> RestreamAuth::Login(QWidget *parent)
+{
+	OAuthLogin login(parent, RESTREAM_AUTH_URL, false);
+	cef->add_popup_whitelist_url("about:blank", &login);
+
+	if (login.exec() == QDialog::Rejected) {
+		return nullptr;
+	}
+
+	std::shared_ptr<RestreamAuth> auth =
+		std::make_shared<RestreamAuth>(restreamDef);
+
+	std::string client_id = RESTREAM_CLIENTID;
+	deobfuscate_str(&client_id[0], RESTREAM_HASH);
+
+	if (!auth->GetToken(RESTREAM_TOKEN_URL, client_id,
+				RESTREAM_SCOPE_VERSION,
+				QT_TO_UTF8(login.GetCode()))) {
+		return nullptr;
+	}
+
+	std::string error;
+	if (auth->GetChannelInfo()) {
+		return auth;
+	}
+
+	return nullptr;
+}
+
+static std::shared_ptr<Auth> CreateRestreamAuth()
+{
+	return std::make_shared<RestreamAuth>(restreamDef);
+}
+
+static void DeleteCookies()
+{
+	if (panel_cookies) {
+		panel_cookies->DeleteCookies("restream.io", std::string());
+	}
+}
+
+void RegisterRestreamAuth()
+{
+	OAuth::RegisterOAuth(
+		restreamDef,
+		CreateRestreamAuth,
+		RestreamAuth::Login,
+		DeleteCookies);
+}

+ 29 - 0
UI/auth-restream.hpp

@@ -0,0 +1,29 @@
+#pragma once
+
+#include "auth-oauth.hpp"
+
+class RestreamWidget;
+
+class RestreamAuth : public OAuthStreamKey {
+	Q_OBJECT
+
+	QSharedPointer<RestreamWidget> chat;
+	QSharedPointer<RestreamWidget> info;
+	QSharedPointer<QAction> chatMenu;
+	QSharedPointer<QAction> infoMenu;
+	bool uiLoaded = false;
+
+	virtual bool RetryLogin() override;
+
+	virtual void SaveInternal() override;
+	virtual bool LoadInternal() override;
+
+	bool GetChannelInfo();
+
+	virtual void LoadUI() override;
+
+public:
+	RestreamAuth(const Def &d);
+
+	static std::shared_ptr<Auth> Login(QWidget *parent);
+};

+ 4 - 0
UI/ui-config.h.in

@@ -23,3 +23,7 @@
 #define MIXER_ENABLED  @MIXER_ENABLED@
 #define MIXER_CLIENTID "@MIXER_CLIENTID@"
 #define MIXER_HASH     0x@MIXER_HASH@
+
+#define RESTREAM_ENABLED  @RESTREAM_ENABLED@
+#define RESTREAM_CLIENTID "@RESTREAM_CLIENTID@"
+#define RESTREAM_HASH     0x@RESTREAM_HASH@

+ 4 - 0
UI/window-basic-main.cpp

@@ -193,6 +193,7 @@ void assignDockToggle(QDockWidget *dock, QAction *action)
 
 extern void RegisterTwitchAuth();
 extern void RegisterMixerAuth();
+extern void RegisterRestreamAuth();
 
 OBSBasic::OBSBasic(QWidget *parent)
 	: OBSMainWindow  (parent),
@@ -206,6 +207,9 @@ OBSBasic::OBSBasic(QWidget *parent)
 #if MIXER_ENABLED
 	RegisterMixerAuth();
 #endif
+#if RESTREAM_ENABLED
+	RegisterRestreamAuth();
+#endif
 
 	setAcceptDrops(true);