界面与品牌 - 默认 UI 语言固定为英文,不再跟随系统区域设置自动切换(frontend/OBSApp.cpp),用户仍可在设置中手动切换 - 程序图标更换为 CAIIC logo:exe 图标 obs-studio.ico 及界面/托盘/About 图标 obs.png - About 窗口精简为 logo + 名称 + 版本号,移除项目介绍、赞助名单、Authors/License 标签页 - 版本号定为 1.0.0(构建参数 OBS_VERSION_OVERRIDE=1.0.0) 分辨率支持 - 移除首次启动将画布强制限制为 1920x1080 的逻辑,默认使用显示器原生分辨率 - 设置-视频 与自动配置向导新增 3840x2160 / 2560x1440 预设 - 自动配置向导性能测试上限放宽至 4K60(软硬件编码数据率上限及起始分辨率钳制) 插件 - 集成 obs-shaderfilter 2.6.0(绿幕抠图/自定义着色器滤镜),并适配新版 libobs 的 gs_image_file_ex API - 补齐缺失的源码:obs-browser、obs-websocket、libdshowcapture(含 external/capture-device-support) 工程 - 新增 .gitignore,排除 deploy/ 发布目录与构建日志
499 lines
15 KiB
C++
499 lines
15 KiB
C++
/******************************************************************************
|
|
Copyright (C) 2014 by John R. Bradley <jrb@turrettech.com>
|
|
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
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 "browser-app.hpp"
|
|
#include "browser-version.h"
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#ifdef ENABLE_BROWSER_QT_LOOP
|
|
#include <util/base.h>
|
|
#include <util/platform.h>
|
|
#include <util/threading.h>
|
|
#include <QTimer>
|
|
#endif
|
|
|
|
#ifndef UNUSED_PARAMETER
|
|
#define UNUSED_PARAMETER(x) \
|
|
{ \
|
|
(void)x; \
|
|
}
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
constexpr bool kIsPlatformWindows = true;
|
|
#else
|
|
constexpr bool kIsPlatformWindows = false;
|
|
#endif
|
|
|
|
CefRefPtr<CefRenderProcessHandler> BrowserApp::GetRenderProcessHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefBrowserProcessHandler> BrowserApp::GetBrowserProcessHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefClient> BrowserApp::GetDefaultClient()
|
|
{
|
|
return GetDummy();
|
|
}
|
|
|
|
void BrowserApp::OnRegisterCustomSchemes(CefRawPtr<CefSchemeRegistrar> registrar)
|
|
{
|
|
registrar->AddCustomScheme("http", CEF_SCHEME_OPTION_STANDARD | CEF_SCHEME_OPTION_CORS_ENABLED);
|
|
}
|
|
|
|
void BrowserApp::OnContextInitialized()
|
|
{
|
|
// Without a default client, CefBrowser is unmanaged, allowing full-blown Chromium windows outside of our control.
|
|
// We don't actually want those, so define a dummy client which will automatically close any such windows.
|
|
dummy = new BrowserDummyClient();
|
|
|
|
CefRefPtr<CefRequestContext> requestContext = CefRequestContext::GetGlobalContext();
|
|
CefString errorMessage;
|
|
CefRefPtr<CefValue> optionValue = CefValue::Create();
|
|
|
|
constexpr std::array<std::string_view, 20> kBrowserFeaturesToDisable{
|
|
"autofill.credit_card_enabled",
|
|
"autofill.enabled",
|
|
"autofill.iban_enabled",
|
|
"autofill.payment_card_benefits",
|
|
"autofill.payment_cvc_storage",
|
|
"autofill.profile_enabled",
|
|
"autologin.enabled",
|
|
"browser_labs_enabled",
|
|
"credentials_enable_autosignin",
|
|
"credentials_enable_service",
|
|
"payments.can_make_payment_enabled",
|
|
"printing.enabled",
|
|
"search.suggest_enabled",
|
|
"shopping_list_enabled",
|
|
"side_panel.google_search_side_panel_enabled",
|
|
"side_search.enabled",
|
|
"signin.allowed",
|
|
"signin.allowed_on_next_startup",
|
|
"translate",
|
|
"url_keyed_anonymized_data_collection.enabled"};
|
|
|
|
constexpr std::array<std::string_view, 2> kBrowserFeaturesToEnable{"extensions.block_external_extensions",
|
|
"extensions.disabled"};
|
|
|
|
optionValue->SetBool(false);
|
|
for (std::string_view feature : kBrowserFeaturesToDisable) {
|
|
requestContext->SetPreference(feature.data(), optionValue.get(), errorMessage);
|
|
}
|
|
|
|
optionValue->SetBool(true);
|
|
for (std::string_view feature : kBrowserFeaturesToEnable) {
|
|
requestContext->SetPreference(feature.data(), optionValue.get(), errorMessage);
|
|
}
|
|
}
|
|
|
|
void BrowserApp::OnBeforeChildProcessLaunch(CefRefPtr<CefCommandLine> command_line)
|
|
{
|
|
#ifdef _WIN32
|
|
std::string pid = std::to_string(GetCurrentProcessId());
|
|
command_line->AppendSwitchWithValue("parent_pid", pid);
|
|
#else
|
|
(void)command_line;
|
|
#endif
|
|
}
|
|
|
|
void BrowserApp::OnBeforeCommandLineProcessing(const CefString &, CefRefPtr<CefCommandLine> command_line)
|
|
{
|
|
if (!shared_texture_available) {
|
|
bool enableGPU = command_line->HasSwitch("enable-gpu");
|
|
CefString type = command_line->GetSwitchValue("type");
|
|
|
|
if (!enableGPU && type.empty()) {
|
|
command_line->AppendSwitch("disable-gpu-compositing");
|
|
}
|
|
}
|
|
|
|
// See https://github.com/chromiumembedded/cef/issues/3966 for 'StorageNotificationService' requirement.
|
|
constexpr std::string_view kDefaultDisabledFeatures{
|
|
"CalculateNativeWinOcclusion,GlobalShortcutsPortal,HardwareMediaKeyHandling,LiveCaption,"
|
|
"LocalNetworkAccessChecksWebSockets,MediaRouter,StorageNotificationService,WebBluetooth,"
|
|
"EnableWindowsGamingInputDataFetcher"};
|
|
|
|
std::string disableFeatures{};
|
|
|
|
if constexpr (kIsPlatformWindows) {
|
|
disableFeatures.append(kDefaultDisabledFeatures);
|
|
} else {
|
|
constexpr std::string_view kWindowsFeature{",EnableWindowsGamingInputDataFetcher"};
|
|
constexpr size_t kNonWindowsLength = kDefaultDisabledFeatures.size() - kWindowsFeature.size();
|
|
disableFeatures.append(kDefaultDisabledFeatures.substr(0, kNonWindowsLength));
|
|
}
|
|
|
|
if (command_line->HasSwitch("disable-features")) {
|
|
// Don't override existing, as this can break OSR
|
|
disableFeatures.append(",");
|
|
disableFeatures.append(command_line->GetSwitchValue("disable-features"));
|
|
}
|
|
|
|
if (command_line->HasSwitch("disable-blink-features")) {
|
|
std::string disableBlinkFeatures = command_line->GetSwitchValue("disable-blink-features");
|
|
disableBlinkFeatures += ",DocumentPictureInPictureAPI";
|
|
command_line->AppendSwitchWithValue("disable-blink-features", disableBlinkFeatures);
|
|
} else {
|
|
command_line->AppendSwitchWithValue("disable-blink-features", "DocumentPictureInPictureAPI");
|
|
}
|
|
|
|
command_line->AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required");
|
|
command_line->AppendSwitch("disable-extensions");
|
|
command_line->AppendSwitchWithValue("disable-features", disableFeatures);
|
|
command_line->AppendSwitch("hide-crash-restore-bubble");
|
|
#ifdef __APPLE__
|
|
command_line->AppendSwitch("use-mock-keychain");
|
|
#elif !defined(_WIN32)
|
|
command_line->AppendSwitchWithValue("ozone-platform", wayland ? "wayland" : "x11");
|
|
#endif
|
|
}
|
|
|
|
std::vector<std::string> exposedFunctions = {"getControlLevel", "getCurrentScene", "getStatus",
|
|
"startRecording", "stopRecording", "startStreaming",
|
|
"stopStreaming", "pauseRecording", "unpauseRecording",
|
|
"startReplayBuffer", "stopReplayBuffer", "saveReplayBuffer",
|
|
"startVirtualcam", "stopVirtualcam", "getScenes",
|
|
"setCurrentScene", "getTransitions", "getCurrentTransition",
|
|
"setCurrentTransition"};
|
|
|
|
void BrowserApp::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame>, CefRefPtr<CefV8Context> context)
|
|
{
|
|
CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
|
|
|
|
CefRefPtr<CefV8Value> obsStudioObj = CefV8Value::CreateObject(nullptr, nullptr);
|
|
globalObj->SetValue("obsstudio", obsStudioObj, V8_PROPERTY_ATTRIBUTE_NONE);
|
|
|
|
CefRefPtr<CefV8Value> pluginVersion = CefV8Value::CreateString(OBS_BROWSER_VERSION_STRING);
|
|
obsStudioObj->SetValue("pluginVersion", pluginVersion, V8_PROPERTY_ATTRIBUTE_NONE);
|
|
|
|
for (std::string name : exposedFunctions) {
|
|
CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction(name, this);
|
|
obsStudioObj->SetValue(name, func, V8_PROPERTY_ATTRIBUTE_NONE);
|
|
}
|
|
|
|
UNUSED_PARAMETER(browser);
|
|
}
|
|
|
|
void BrowserApp::ExecuteJSFunction(CefRefPtr<CefBrowser> browser, const char *functionName, CefV8ValueList arguments)
|
|
{
|
|
std::vector<CefString> names;
|
|
browser->GetFrameNames(names);
|
|
for (auto &name : names) {
|
|
CefRefPtr<CefFrame> frame =
|
|
#if CHROME_VERSION_BUILD >= 6261
|
|
browser->GetFrameByName(name);
|
|
#else
|
|
browser->GetFrame(name);
|
|
#endif
|
|
CefRefPtr<CefV8Context> context = frame->GetV8Context();
|
|
|
|
context->Enter();
|
|
|
|
CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
|
|
CefRefPtr<CefV8Value> obsStudioObj = globalObj->GetValue("obsstudio");
|
|
CefRefPtr<CefV8Value> jsFunction = obsStudioObj->GetValue(functionName);
|
|
|
|
if (jsFunction && jsFunction->IsFunction())
|
|
jsFunction->ExecuteFunction(nullptr, arguments);
|
|
|
|
context->Exit();
|
|
}
|
|
}
|
|
|
|
CefRefPtr<CefV8Value> CefValueToCefV8Value(CefRefPtr<CefValue> value)
|
|
{
|
|
CefRefPtr<CefV8Value> result;
|
|
switch (value->GetType()) {
|
|
case VTYPE_INVALID:
|
|
result = CefV8Value::CreateNull();
|
|
break;
|
|
case VTYPE_NULL:
|
|
result = CefV8Value::CreateNull();
|
|
break;
|
|
case VTYPE_BOOL:
|
|
result = CefV8Value::CreateBool(value->GetBool());
|
|
break;
|
|
case VTYPE_INT:
|
|
result = CefV8Value::CreateInt(value->GetInt());
|
|
break;
|
|
case VTYPE_DOUBLE:
|
|
result = CefV8Value::CreateDouble(value->GetDouble());
|
|
break;
|
|
case VTYPE_STRING:
|
|
result = CefV8Value::CreateString(value->GetString());
|
|
break;
|
|
case VTYPE_BINARY:
|
|
result = CefV8Value::CreateNull();
|
|
break;
|
|
case VTYPE_DICTIONARY: {
|
|
result = CefV8Value::CreateObject(nullptr, nullptr);
|
|
CefRefPtr<CefDictionaryValue> dict = value->GetDictionary();
|
|
CefDictionaryValue::KeyList keys;
|
|
dict->GetKeys(keys);
|
|
for (unsigned int i = 0; i < keys.size(); i++) {
|
|
CefString key = keys[i];
|
|
result->SetValue(key, CefValueToCefV8Value(dict->GetValue(key)), V8_PROPERTY_ATTRIBUTE_NONE);
|
|
}
|
|
} break;
|
|
case VTYPE_LIST: {
|
|
CefRefPtr<CefListValue> list = value->GetList();
|
|
size_t size = list->GetSize();
|
|
result = CefV8Value::CreateArray((int)size);
|
|
for (size_t i = 0; i < size; i++) {
|
|
result->SetValue((int)i, CefValueToCefV8Value(list->GetValue(i)));
|
|
}
|
|
} break;
|
|
#if !defined(_WIN32) && CHROME_VERSION_BUILD >= 6943
|
|
case VTYPE_NUM_VALUES:
|
|
result = CefV8Value::CreateNull();
|
|
break;
|
|
#endif
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool BrowserApp::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame,
|
|
CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
|
|
{
|
|
UNUSED_PARAMETER(frame);
|
|
DCHECK(source_process == PID_BROWSER);
|
|
|
|
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
|
|
|
if (message->GetName() == "Visibility") {
|
|
CefV8ValueList arguments;
|
|
arguments.push_back(CefV8Value::CreateBool(args->GetBool(0)));
|
|
|
|
ExecuteJSFunction(browser, "onVisibilityChange", arguments);
|
|
|
|
} else if (message->GetName() == "Active") {
|
|
CefV8ValueList arguments;
|
|
arguments.push_back(CefV8Value::CreateBool(args->GetBool(0)));
|
|
|
|
ExecuteJSFunction(browser, "onActiveChange", arguments);
|
|
|
|
} else if (message->GetName() == "DispatchJSEvent") {
|
|
nlohmann::json payloadJson = nlohmann::json::parse(args->GetString(1).ToString(), nullptr, false);
|
|
|
|
nlohmann::json wrapperJson;
|
|
if (args->GetSize() > 1)
|
|
wrapperJson["detail"] = payloadJson;
|
|
std::string wrapperJsonString = wrapperJson.dump();
|
|
std::string script;
|
|
|
|
script += "new CustomEvent('";
|
|
script += args->GetString(0).ToString();
|
|
script += "', ";
|
|
script += wrapperJsonString;
|
|
script += ");";
|
|
|
|
std::vector<CefString> names;
|
|
browser->GetFrameNames(names);
|
|
for (auto &name : names) {
|
|
CefRefPtr<CefFrame> frame =
|
|
#if CHROME_VERSION_BUILD >= 6261
|
|
browser->GetFrameByName(name);
|
|
#else
|
|
browser->GetFrame(name);
|
|
#endif
|
|
CefRefPtr<CefV8Context> context = frame->GetV8Context();
|
|
|
|
context->Enter();
|
|
|
|
CefRefPtr<CefV8Value> globalObj = context->GetGlobal();
|
|
|
|
CefRefPtr<CefV8Value> returnValue;
|
|
CefRefPtr<CefV8Exception> exception;
|
|
|
|
/* Create the CustomEvent object
|
|
* We have to use eval to invoke the new operator */
|
|
context->Eval(script, browser->GetMainFrame()->GetURL(), 0, returnValue, exception);
|
|
|
|
CefV8ValueList arguments;
|
|
arguments.push_back(returnValue);
|
|
|
|
CefRefPtr<CefV8Value> dispatchEvent = globalObj->GetValue("dispatchEvent");
|
|
dispatchEvent->ExecuteFunction(nullptr, arguments);
|
|
|
|
context->Exit();
|
|
}
|
|
|
|
} else if (message->GetName() == "executeCallback") {
|
|
CefRefPtr<CefV8Context> context = browser->GetMainFrame()->GetV8Context();
|
|
|
|
context->Enter();
|
|
|
|
CefRefPtr<CefListValue> arguments = message->GetArgumentList();
|
|
int callbackID = arguments->GetInt(0);
|
|
CefString jsonString = arguments->GetString(1);
|
|
|
|
CefRefPtr<CefValue> json = CefParseJSON(arguments->GetString(1).ToString(), {});
|
|
|
|
CefRefPtr<CefV8Value> callback = callbackMap[callbackID];
|
|
CefV8ValueList args;
|
|
|
|
CefRefPtr<CefV8Value> retval = CefValueToCefV8Value(json);
|
|
|
|
args.push_back(retval);
|
|
|
|
if (callback)
|
|
callback->ExecuteFunction(nullptr, args);
|
|
|
|
context->Exit();
|
|
|
|
callbackMap.erase(callbackID);
|
|
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsValidFunction(std::string function)
|
|
{
|
|
std::vector<std::string>::iterator iterator;
|
|
iterator = std::find(exposedFunctions.begin(), exposedFunctions.end(), function);
|
|
return iterator != exposedFunctions.end();
|
|
}
|
|
|
|
bool BrowserApp::Execute(const CefString &name, CefRefPtr<CefV8Value>, const CefV8ValueList &arguments,
|
|
CefRefPtr<CefV8Value> &, CefString &)
|
|
{
|
|
if (IsValidFunction(name.ToString())) {
|
|
if (arguments.size() >= 1 && arguments[0]->IsFunction()) {
|
|
callbackId++;
|
|
callbackMap[callbackId] = arguments[0];
|
|
}
|
|
|
|
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(name);
|
|
CefRefPtr<CefListValue> args = msg->GetArgumentList();
|
|
args->SetInt(0, callbackId);
|
|
|
|
/* Pass on arguments */
|
|
for (u_long l = 0; l < arguments.size(); l++) {
|
|
u_long pos;
|
|
if (arguments[0]->IsFunction())
|
|
pos = l;
|
|
else
|
|
pos = l + 1;
|
|
|
|
if (arguments[l]->IsString())
|
|
args->SetString(pos, arguments[l]->GetStringValue());
|
|
else if (arguments[l]->IsInt())
|
|
args->SetInt(pos, arguments[l]->GetIntValue());
|
|
else if (arguments[l]->IsBool())
|
|
args->SetBool(pos, arguments[l]->GetBoolValue());
|
|
else if (arguments[l]->IsDouble())
|
|
args->SetDouble(pos, arguments[l]->GetDoubleValue());
|
|
}
|
|
|
|
CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
|
|
SendBrowserProcessMessage(browser, PID_BROWSER, msg);
|
|
|
|
} else {
|
|
/* Function does not exist. */
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#ifdef ENABLE_BROWSER_QT_LOOP
|
|
Q_DECLARE_METATYPE(MessageTask);
|
|
MessageObject messageObject;
|
|
|
|
void QueueBrowserTask(CefRefPtr<CefBrowser> browser, BrowserFunc func)
|
|
{
|
|
std::lock_guard<std::mutex> lock(messageObject.browserTaskMutex);
|
|
messageObject.browserTasks.emplace_back(browser, func);
|
|
|
|
QMetaObject::invokeMethod(&messageObject, "ExecuteNextBrowserTask", Qt::QueuedConnection);
|
|
}
|
|
|
|
bool MessageObject::ExecuteNextBrowserTask()
|
|
{
|
|
Task nextTask;
|
|
{
|
|
std::lock_guard<std::mutex> lock(browserTaskMutex);
|
|
if (!browserTasks.size())
|
|
return false;
|
|
|
|
nextTask = browserTasks[0];
|
|
browserTasks.pop_front();
|
|
}
|
|
|
|
nextTask.func(nextTask.browser);
|
|
return true;
|
|
}
|
|
|
|
void MessageObject::ExecuteTask(MessageTask task)
|
|
{
|
|
task();
|
|
}
|
|
|
|
void MessageObject::DoCefMessageLoop(int ms)
|
|
{
|
|
if (ms)
|
|
QTimer::singleShot((int)ms + 2, []() { CefDoMessageLoopWork(); });
|
|
else
|
|
CefDoMessageLoopWork();
|
|
}
|
|
|
|
void MessageObject::Process()
|
|
{
|
|
CefDoMessageLoopWork();
|
|
}
|
|
|
|
void ProcessCef()
|
|
{
|
|
QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop", Qt::QueuedConnection, Q_ARG(int, (int)0));
|
|
}
|
|
|
|
#define MAX_DELAY (1000 / 30)
|
|
|
|
#if CHROME_VERSION_BUILD < 5938
|
|
void BrowserApp::OnScheduleMessagePumpWork(int64 delay_ms)
|
|
#else
|
|
void BrowserApp::OnScheduleMessagePumpWork(int64_t delay_ms)
|
|
#endif
|
|
{
|
|
if (delay_ms < 0)
|
|
delay_ms = 0;
|
|
else if (delay_ms > MAX_DELAY)
|
|
delay_ms = MAX_DELAY;
|
|
|
|
if (!frameTimer.isActive()) {
|
|
QObject::connect(&frameTimer, &QTimer::timeout, &messageObject, &MessageObject::Process);
|
|
frameTimer.setSingleShot(false);
|
|
frameTimer.start(33);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop", Qt::QueuedConnection, Q_ARG(int, (int)delay_ms));
|
|
}
|
|
#endif
|