瀏覽代碼

UI: Add YouTube Chat Dock

Since embedded browsers are no longer allowed to log into Google from the chat
dock is effectively read-only.
To prevent users from even trying to log in the input field is hidden
via custom CSS.
derrod 4 年之前
父節點
當前提交
18f40be820
共有 3 個文件被更改,包括 114 次插入0 次删除
  1. 82 0
      UI/auth-youtube.cpp
  2. 12 0
      UI/auth-youtube.hpp
  3. 20 0
      UI/window-youtube-actions.cpp

+ 82 - 0
UI/auth-youtube.cpp

@@ -23,6 +23,10 @@
 #include "window-basic-main.hpp"
 #include "obf.h"
 
+#ifdef BROWSER_AVAILABLE
+#include "window-dock-browser.hpp"
+#endif
+
 using namespace json11;
 
 /* ------------------------------------------------------------------------- */
@@ -32,6 +36,11 @@ using namespace json11;
 #define YOUTUBE_API_STATE_LENGTH 32
 #define SECTION_NAME "YouTube"
 
+#define YOUTUBE_CHAT_PLACEHOLDER_URL \
+	"https://obsproject.com/placeholders/youtube-chat"
+#define YOUTUBE_CHAT_POPOUT_URL \
+	"https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v=%1"
+
 static const char allowedChars[] =
 	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 static const int allowedCount = static_cast<int>(sizeof(allowedChars) - 1);
@@ -100,14 +109,87 @@ bool YoutubeAuth::LoadInternal()
 		config_get_uint(main->Config(), section_name, "ExpireTime");
 	currentScopeVer =
 		(int)config_get_int(main->Config(), section_name, "ScopeVer");
+	firstLoad = false;
 	return implicit ? !token.empty() : !refresh_token.empty();
 }
 
+#ifdef BROWSER_AVAILABLE
+static const char *ytchat_script = "\
+const obsCSS = document.createElement('style');\
+obsCSS.innerHTML = \"#panel-pages.yt-live-chat-renderer {display: none;}\
+yt-live-chat-viewer-engagement-message-renderer {display: none;}\";\
+document.querySelector('head').appendChild(obsCSS);";
+#endif
+
 void YoutubeAuth::LoadUI()
 {
+	if (uiLoaded)
+		return;
+
+#ifdef BROWSER_AVAILABLE
+	if (!cef)
+		return;
+
+	OBSBasic::InitBrowserPanelSafeBlock();
+	OBSBasic *main = OBSBasic::Get();
+
+	QCefWidget *browser;
+
+	QSize size = main->frameSize();
+	QPoint pos = main->pos();
+
+	chat.reset(new BrowserDock());
+	chat->setObjectName("ytChat");
+	chat->resize(300, 600);
+	chat->setMinimumSize(200, 300);
+	chat->setWindowTitle(QTStr("Auth.Chat"));
+	chat->setAllowedAreas(Qt::AllDockWidgetAreas);
+
+	browser = cef->create_widget(nullptr, YOUTUBE_CHAT_PLACEHOLDER_URL,
+				     panel_cookies);
+	browser->setStartupScript(ytchat_script);
+
+	chat->SetWidget(browser);
+	main->addDockWidget(Qt::RightDockWidgetArea, chat.data());
+	chatMenu.reset(main->AddDockWidget(chat.data()));
+
+	chat->setFloating(true);
+	chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50);
+
+	if (firstLoad) {
+		chat->setVisible(true);
+	} else {
+		const char *dockStateStr = config_get_string(
+			main->Config(), service(), "DockState");
+		QByteArray dockState =
+			QByteArray::fromBase64(QByteArray(dockStateStr));
+		main->restoreState(dockState);
+	}
+#endif
+
 	uiLoaded = true;
 }
 
+void YoutubeAuth::SetChatId(QString &chat_id)
+{
+#ifdef BROWSER_AVAILABLE
+	QString chat_url = QString(YOUTUBE_CHAT_POPOUT_URL).arg(chat_id);
+
+	if (chat && chat->cefWidget) {
+		chat->cefWidget->setURL(chat_url.toStdString());
+	}
+#endif
+}
+
+void YoutubeAuth::ResetChat()
+{
+#ifdef BROWSER_AVAILABLE
+	if (chat && chat->cefWidget) {
+		chat->cefWidget->setURL(YOUTUBE_CHAT_PLACEHOLDER_URL);
+	}
+#endif
+}
+
 QString YoutubeAuth::GenerateState()
 {
 	std::uniform_int_distribution<> distr(0, allowedCount);

+ 12 - 0
UI/auth-youtube.hpp

@@ -7,6 +7,10 @@
 
 #include "auth-oauth.hpp"
 
+#ifdef BROWSER_AVAILABLE
+class BrowserDock;
+#endif
+
 inline const std::vector<Auth::Def> youtubeServices = {
 	{"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true},
 	{"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true},
@@ -19,6 +23,11 @@ class YoutubeAuth : public OAuthStreamKey {
 	std::mt19937 randomSeed;
 	std::string section;
 
+#ifdef BROWSER_AVAILABLE
+	QSharedPointer<BrowserDock> chat;
+	QSharedPointer<QAction> chatMenu;
+#endif
+
 	virtual bool RetryLogin() override;
 	virtual void SaveInternal() override;
 	virtual bool LoadInternal() override;
@@ -29,6 +38,9 @@ class YoutubeAuth : public OAuthStreamKey {
 public:
 	YoutubeAuth(const Def &d);
 
+	void SetChatId(QString &chat_id);
+	void ResetChat();
+
 	static std::shared_ptr<Auth> Login(QWidget *parent,
 					   const std::string &service);
 };

+ 20 - 0
UI/window-youtube-actions.cpp

@@ -402,6 +402,12 @@ bool OBSYoutubeActions::StreamNowAction(YoutubeApiWrappers *api,
 		blog(LOG_DEBUG, "No category set.");
 		return false;
 	}
+
+	if (broadcast.privacy != "private")
+		apiYouTube->SetChatId(broadcast.id);
+	else
+		apiYouTube->ResetChat();
+
 	return true;
 }
 
@@ -429,6 +435,12 @@ bool OBSYoutubeActions::StreamLaterAction(YoutubeApiWrappers *api)
 		blog(LOG_DEBUG, "No category set.");
 		return false;
 	}
+
+	if (broadcast.privacy != "private")
+		apiYouTube->SetChatId(broadcast.id);
+	else
+		apiYouTube->ResetChat();
+
 	return true;
 }
 
@@ -448,6 +460,9 @@ bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
 			.array_items()[0]["contentDetails"]["boundStreamId"]
 			.string_value();
 
+	std::string broadcastPrivacy =
+		json["status"]["privacyStatus"].string_value();
+
 	stream.id = boundStreamId.c_str();
 	if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) {
 		auto item = json["items"].array_items()[0];
@@ -473,6 +488,11 @@ bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
 		}
 	}
 
+	if (broadcastPrivacy != "private")
+		apiYouTube->SetChatId(selectedBroadcast);
+	else
+		apiYouTube->ResetChat();
+
 	return true;
 }