/****************************************************************************** Copyright (C) 2014 by John R. Bradley Copyright (C) 2023 by Lain Bailey 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 . ******************************************************************************/ #include "browser-app.hpp" #include "browser-version.h" #include #ifdef _WIN32 #include #endif #ifdef ENABLE_BROWSER_QT_LOOP #include #include #include #include #endif #ifndef UNUSED_PARAMETER #define UNUSED_PARAMETER(x) \ { \ (void)x; \ } #endif #ifdef _WIN32 constexpr bool kIsPlatformWindows = true; #else constexpr bool kIsPlatformWindows = false; #endif CefRefPtr BrowserApp::GetRenderProcessHandler() { return this; } CefRefPtr BrowserApp::GetBrowserProcessHandler() { return this; } CefRefPtr BrowserApp::GetDefaultClient() { return GetDummy(); } void BrowserApp::OnRegisterCustomSchemes(CefRawPtr 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 requestContext = CefRequestContext::GetGlobalContext(); CefString errorMessage; CefRefPtr optionValue = CefValue::Create(); constexpr std::array 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 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 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 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 exposedFunctions = {"getControlLevel", "getCurrentScene", "getStatus", "startRecording", "stopRecording", "startStreaming", "stopStreaming", "pauseRecording", "unpauseRecording", "startReplayBuffer", "stopReplayBuffer", "saveReplayBuffer", "startVirtualcam", "stopVirtualcam", "getScenes", "setCurrentScene", "getTransitions", "getCurrentTransition", "setCurrentTransition"}; void BrowserApp::OnContextCreated(CefRefPtr browser, CefRefPtr, CefRefPtr context) { CefRefPtr globalObj = context->GetGlobal(); CefRefPtr obsStudioObj = CefV8Value::CreateObject(nullptr, nullptr); globalObj->SetValue("obsstudio", obsStudioObj, V8_PROPERTY_ATTRIBUTE_NONE); CefRefPtr pluginVersion = CefV8Value::CreateString(OBS_BROWSER_VERSION_STRING); obsStudioObj->SetValue("pluginVersion", pluginVersion, V8_PROPERTY_ATTRIBUTE_NONE); for (std::string name : exposedFunctions) { CefRefPtr func = CefV8Value::CreateFunction(name, this); obsStudioObj->SetValue(name, func, V8_PROPERTY_ATTRIBUTE_NONE); } UNUSED_PARAMETER(browser); } void BrowserApp::ExecuteJSFunction(CefRefPtr browser, const char *functionName, CefV8ValueList arguments) { std::vector names; browser->GetFrameNames(names); for (auto &name : names) { CefRefPtr frame = #if CHROME_VERSION_BUILD >= 6261 browser->GetFrameByName(name); #else browser->GetFrame(name); #endif CefRefPtr context = frame->GetV8Context(); context->Enter(); CefRefPtr globalObj = context->GetGlobal(); CefRefPtr obsStudioObj = globalObj->GetValue("obsstudio"); CefRefPtr jsFunction = obsStudioObj->GetValue(functionName); if (jsFunction && jsFunction->IsFunction()) jsFunction->ExecuteFunction(nullptr, arguments); context->Exit(); } } CefRefPtr CefValueToCefV8Value(CefRefPtr value) { CefRefPtr 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 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 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 browser, CefRefPtr frame, CefProcessId source_process, CefRefPtr message) { UNUSED_PARAMETER(frame); DCHECK(source_process == PID_BROWSER); CefRefPtr 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 names; browser->GetFrameNames(names); for (auto &name : names) { CefRefPtr frame = #if CHROME_VERSION_BUILD >= 6261 browser->GetFrameByName(name); #else browser->GetFrame(name); #endif CefRefPtr context = frame->GetV8Context(); context->Enter(); CefRefPtr globalObj = context->GetGlobal(); CefRefPtr returnValue; CefRefPtr 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 dispatchEvent = globalObj->GetValue("dispatchEvent"); dispatchEvent->ExecuteFunction(nullptr, arguments); context->Exit(); } } else if (message->GetName() == "executeCallback") { CefRefPtr context = browser->GetMainFrame()->GetV8Context(); context->Enter(); CefRefPtr arguments = message->GetArgumentList(); int callbackID = arguments->GetInt(0); CefString jsonString = arguments->GetString(1); CefRefPtr json = CefParseJSON(arguments->GetString(1).ToString(), {}); CefRefPtr callback = callbackMap[callbackID]; CefV8ValueList args; CefRefPtr 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::iterator iterator; iterator = std::find(exposedFunctions.begin(), exposedFunctions.end(), function); return iterator != exposedFunctions.end(); } bool BrowserApp::Execute(const CefString &name, CefRefPtr, const CefV8ValueList &arguments, CefRefPtr &, CefString &) { if (IsValidFunction(name.ToString())) { if (arguments.size() >= 1 && arguments[0]->IsFunction()) { callbackId++; callbackMap[callbackId] = arguments[0]; } CefRefPtr msg = CefProcessMessage::Create(name); CefRefPtr 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 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 browser, BrowserFunc func) { std::lock_guard lock(messageObject.browserTaskMutex); messageObject.browserTasks.emplace_back(browser, func); QMetaObject::invokeMethod(&messageObject, "ExecuteNextBrowserTask", Qt::QueuedConnection); } bool MessageObject::ExecuteNextBrowserTask() { Task nextTask; { std::lock_guard 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