OBSForCaiic/obs-studio/plugins/obs-browser/obs-browser-plugin.cpp
evan.liu d0074858bf CAIIC 定制版 v1.0.0:英文默认界面、2K/4K 支持、集成抠图插件、更换品牌图标
界面与品牌
- 默认 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/ 发布目录与构建日志
2026-09-18 20:41:13 +08:00

915 lines
27 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 <obs-frontend-api.h>
#include <util/threading.h>
#include <util/platform.h>
#include <util/util.hpp>
#include <util/dstr.hpp>
#include <obs-module.h>
#include <obs.hpp>
#include <functional>
#include <sstream>
#include <thread>
#include <mutex>
#include <nlohmann/json.hpp>
#include <obs-websocket-api.h>
#include "obs-browser-source.hpp"
#include "browser-scheme.hpp"
#include "browser-app.hpp"
#include "browser-version.h"
#include "cef-headers.hpp"
#ifdef _WIN32
#include <util/windows/ComPtr.hpp>
#include <dxgi.h>
#include <dxgi1_2.h>
#include <d3d11.h>
#else
#include "signal-restore.hpp"
#endif
#ifdef ENABLE_WAYLAND
#include <obs-nix-platform.h>
#endif
#ifdef ENABLE_BROWSER_QT_LOOP
#include <QApplication>
#include <QThread>
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
#include "drm-format.hpp"
#endif
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("obs-browser", "en-US")
MODULE_EXPORT const char *obs_module_description(void)
{
return "CEF-based web browser source & panels";
}
using namespace std;
static thread manager_thread;
static bool manager_initialized = false;
os_event_t *cef_started_event = nullptr;
#if defined(_WIN32)
static int adapterCount = 0;
#endif
static std::wstring deviceId;
bool hwaccel = false;
/* ========================================================================= */
#ifdef ENABLE_BROWSER_QT_LOOP
extern MessageObject messageObject;
#endif
class BrowserTask : public CefTask {
public:
std::function<void()> task;
inline BrowserTask(std::function<void()> task_) : task(task_) {}
virtual void Execute() override
{
#ifdef ENABLE_BROWSER_QT_LOOP
/* you have to put the tasks on the Qt event queue after this
* call otherwise the CEF message pump may stop functioning
* correctly, it's only supposed to take 10ms max */
QMetaObject::invokeMethod(&messageObject, "ExecuteTask", Qt::QueuedConnection,
Q_ARG(MessageTask, task));
#else
task();
#endif
}
IMPLEMENT_REFCOUNTING(BrowserTask);
};
bool QueueCEFTask(std::function<void()> task)
{
return CefPostTask(TID_UI, CefRefPtr<BrowserTask>(new BrowserTask(task)));
}
/* ========================================================================= */
static const char *default_css = "\
body { \
background-color: rgba(0, 0, 0, 0); \
margin: 0px auto; \
overflow: hidden; \
}";
static void browser_source_get_defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "url", "https://obsproject.com/browser-source");
obs_data_set_default_int(settings, "width", 800);
obs_data_set_default_int(settings, "height", 600);
obs_data_set_default_int(settings, "fps", 30);
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
obs_data_set_default_bool(settings, "fps_custom", false);
#else
obs_data_set_default_bool(settings, "fps_custom", true);
#endif
obs_data_set_default_bool(settings, "shutdown", false);
obs_data_set_default_bool(settings, "restart_when_active", false);
obs_data_set_default_int(settings, "webpage_control_level", (int)DEFAULT_CONTROL_LEVEL);
obs_data_set_default_string(settings, "css", default_css);
obs_data_set_default_bool(settings, "reroute_audio", false);
}
static bool is_local_file_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings)
{
bool enabled = obs_data_get_bool(settings, "is_local_file");
obs_property_t *url = obs_properties_get(props, "url");
obs_property_t *local_file = obs_properties_get(props, "local_file");
obs_property_set_visible(url, !enabled);
obs_property_set_visible(local_file, enabled);
return true;
}
static bool is_fps_custom(obs_properties_t *props, obs_property_t *, obs_data_t *settings)
{
bool enabled = obs_data_get_bool(settings, "fps_custom");
obs_property_t *fps = obs_properties_get(props, "fps");
obs_property_set_visible(fps, enabled);
return true;
}
static obs_properties_t *browser_source_get_properties(void *data)
{
obs_properties_t *props = obs_properties_create();
BrowserSource *bs = static_cast<BrowserSource *>(data);
DStr path;
obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE);
obs_property_t *prop = obs_properties_add_bool(props, "is_local_file", obs_module_text("LocalFile"));
if (bs && !bs->url.empty()) {
const char *slash;
dstr_copy(path, bs->url.c_str());
dstr_replace(path, "\\", "/");
slash = strrchr(path->array, '/');
if (slash)
dstr_resize(path, slash - path->array + 1);
}
obs_property_set_modified_callback(prop, is_local_file_modified);
obs_properties_add_path(props, "local_file", obs_module_text("LocalFile"), OBS_PATH_FILE, "*.*", path->array);
obs_properties_add_text(props, "url", obs_module_text("URL"), OBS_TEXT_DEFAULT);
obs_properties_add_int(props, "width", obs_module_text("Width"), 1, 8192, 1);
obs_properties_add_int(props, "height", obs_module_text("Height"), 1, 8192, 1);
obs_properties_add_bool(props, "reroute_audio", obs_module_text("RerouteAudio"));
obs_property_t *fps_set = obs_properties_add_bool(props, "fps_custom", obs_module_text("CustomFrameRate"));
obs_property_set_modified_callback(fps_set, is_fps_custom);
#ifndef ENABLE_BROWSER_SHARED_TEXTURE
obs_property_set_enabled(fps_set, false);
#endif
obs_properties_add_int(props, "fps", obs_module_text("FPS"), 1, 60, 1);
obs_property_t *p = obs_properties_add_text(props, "css", obs_module_text("CSS"), OBS_TEXT_MULTILINE);
obs_property_text_set_monospace(p, true);
obs_properties_add_bool(props, "shutdown", obs_module_text("ShutdownSourceNotVisible"));
obs_properties_add_bool(props, "restart_when_active", obs_module_text("RefreshBrowserActive"));
obs_property_t *controlLevel = obs_properties_add_list(props, "webpage_control_level",
obs_module_text("WebpageControlLevel"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.None"),
(int)ControlLevel::None);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.ReadObs"),
(int)ControlLevel::ReadObs);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.ReadUser"),
(int)ControlLevel::ReadUser);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.Basic"),
(int)ControlLevel::Basic);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.Advanced"),
(int)ControlLevel::Advanced);
obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.All"),
(int)ControlLevel::All);
obs_properties_add_button2(
props, "refreshnocache", obs_module_text("RefreshNoCache"),
[](obs_properties_t *, obs_property_t *, void *data) {
static_cast<BrowserSource *>(data)->Refresh();
return false;
},
bs);
return props;
}
static void missing_file_callback(void *src, const char *new_path, void *data)
{
BrowserSource *bs = static_cast<BrowserSource *>(src);
if (bs) {
obs_source_t *source = bs->source;
OBSDataAutoRelease settings = obs_source_get_settings(source);
obs_data_set_string(settings, "local_file", new_path);
obs_source_update(source, settings);
}
UNUSED_PARAMETER(data);
}
static obs_missing_files_t *browser_source_missingfiles(void *data)
{
BrowserSource *bs = static_cast<BrowserSource *>(data);
obs_missing_files_t *files = obs_missing_files_create();
if (bs) {
obs_source_t *source = bs->source;
OBSDataAutoRelease settings = obs_source_get_settings(source);
bool enabled = obs_data_get_bool(settings, "is_local_file");
const char *path = obs_data_get_string(settings, "local_file");
if (enabled && strcmp(path, "") != 0) {
if (!os_file_exists(path)) {
obs_missing_file_t *file = obs_missing_file_create(
path, missing_file_callback, OBS_MISSING_FILE_SOURCE, bs->source, NULL);
obs_missing_files_add_file(files, file);
}
}
}
return files;
}
static CefRefPtr<BrowserApp> app;
static void BrowserInit(void)
{
string path = obs_get_module_binary_path(obs_current_module());
path = path.substr(0, path.find_last_of('/') + 1);
path += "//obs-browser-page";
#ifdef _WIN32
path += ".exe";
CefMainArgs args;
#else
/* On non-windows platforms, ie macOS, we'll want to pass thru flags to
* CEF */
struct obs_cmdline_args cmdline_args = obs_get_cmdline_args();
CefMainArgs args(cmdline_args.argc, cmdline_args.argv);
#endif
BPtr<char> conf_path = obs_module_config_path("");
os_mkdir(conf_path);
CefSettings settings;
settings.log_severity = LOGSEVERITY_FATAL;
BPtr<char> log_path = obs_module_config_path("debug.log");
BPtr<char> log_path_abs = os_get_abs_path_ptr(log_path);
CefString(&settings.log_file) = log_path_abs;
settings.windowless_rendering_enabled = true;
settings.no_sandbox = true;
#if CHROME_VERSION_BUILD > 6533 && CHROME_VERSION_BUILD <= 6613
settings.chrome_runtime = true;
#endif
uint32_t obs_ver = obs_get_version();
uint32_t obs_maj = obs_ver >> 24;
uint32_t obs_min = (obs_ver >> 16) & 0xFF;
uint32_t obs_pat = obs_ver & 0xFFFF;
/* This allows servers the ability to determine that browser panels and
* browser sources are coming from OBS. */
std::stringstream prod_ver;
prod_ver << "Chrome/";
prod_ver << std::to_string(cef_version_info(4)) << "." << std::to_string(cef_version_info(5)) << "."
<< std::to_string(cef_version_info(6)) << "." << std::to_string(cef_version_info(7));
prod_ver << " OBS/";
prod_ver << std::to_string(obs_maj) << "." << std::to_string(obs_min) << "." << std::to_string(obs_pat);
CefString(&settings.user_agent_product) = prod_ver.str();
#ifdef ENABLE_BROWSER_QT_LOOP
settings.external_message_pump = true;
settings.multi_threaded_message_loop = false;
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
// Override locale path from OBS binary path to plugin binary path
string locales = obs_get_module_binary_path(obs_current_module());
locales = locales.substr(0, locales.find_last_of('/') + 1);
locales += "locales";
BPtr<char> abs_locales = os_get_abs_path_ptr(locales.c_str());
CefString(&settings.locales_dir_path) = abs_locales;
#endif
std::string obs_locale = obs_get_locale();
std::string accepted_languages;
if (obs_locale != "en-US") {
accepted_languages = obs_locale;
accepted_languages += ",";
accepted_languages += "en-US,en";
} else {
accepted_languages = "en-US,en";
}
BPtr<char> conf_path_abs = os_get_abs_path_ptr(conf_path);
CefString(&settings.locale) = obs_get_locale();
CefString(&settings.accept_language_list) = accepted_languages;
#if CHROME_VERSION_BUILD <= 6533
settings.persist_user_preferences = 1;
#endif
CefString(&settings.cache_path) = conf_path_abs;
#if CHROME_VERSION_BUILD > 6533
CefString(&settings.root_cache_path) = conf_path_abs;
#endif
#if !defined(__APPLE__) || defined(ENABLE_BROWSER_LEGACY)
char *abs_path = os_get_abs_path_ptr(path.c_str());
CefString(&settings.browser_subprocess_path) = abs_path;
bfree(abs_path);
#endif
bool tex_sharing_avail = false;
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
if (hwaccel) {
obs_enter_graphics();
#if defined(__APPLE__) || defined(_WIN32)
hwaccel = tex_sharing_avail = gs_shared_texture_available();
#else
hwaccel = tex_sharing_avail = obs_cef_all_drm_formats_supported();
#endif
obs_leave_graphics();
}
#endif
#if defined(__APPLE__) || defined(_WIN32) || !defined(ENABLE_WAYLAND)
app = new BrowserApp(tex_sharing_avail);
#else
app = new BrowserApp(tex_sharing_avail, obs_get_nix_platform() == OBS_NIX_PLATFORM_WAYLAND);
#endif
#ifdef _WIN32
CefExecuteProcess(args, app, nullptr);
#endif
#if !defined(_WIN32)
BackupSignalHandlers();
bool success = CefInitialize(args, settings, app, nullptr);
RestoreSignalHandlers();
#else
bool success = CefInitialize(args, settings, app, nullptr);
#endif
if (!success) {
#if CHROME_VERSION_BUILD >= 6367
blog(LOG_ERROR, "[obs-browser]: CEF failed to initialize. Exit code: %d", CefGetExitCode());
#else
blog(LOG_ERROR, "[obs-browser]: CEF failed to initialize.");
#endif
return;
}
// Register custom scheme handler for local browser sources
CefRegisterSchemeHandlerFactory("http", "absolute", new BrowserSchemeHandlerFactory());
os_event_signal(cef_started_event);
}
static void BrowserShutdown(void)
{
CefClearSchemeHandlerFactories();
#ifdef ENABLE_BROWSER_QT_LOOP
while (messageObject.ExecuteNextBrowserTask())
;
CefDoMessageLoopWork();
#endif
CefShutdown();
app = nullptr;
}
#ifndef ENABLE_BROWSER_QT_LOOP
static void BrowserManagerThread(void)
{
BrowserInit();
CefRunMessageLoop();
BrowserShutdown();
}
#endif
extern "C" EXPORT void obs_browser_initialize(void)
{
if (!os_atomic_set_bool(&manager_initialized, true)) {
#ifdef ENABLE_BROWSER_QT_LOOP
BrowserInit();
#else
manager_thread = thread(BrowserManagerThread);
#endif
}
}
void RegisterBrowserSource()
{
struct obs_source_info info = {};
info.id = "browser_source";
info.type = OBS_SOURCE_TYPE_INPUT;
info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_INTERACTION |
OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB;
info.get_properties = browser_source_get_properties;
info.get_defaults = browser_source_get_defaults;
info.icon_type = OBS_ICON_TYPE_BROWSER;
info.get_name = [](void *) {
return obs_module_text("BrowserSource");
};
info.create = [](obs_data_t *settings, obs_source_t *source) -> void * {
obs_browser_initialize();
return new BrowserSource(settings, source);
};
info.destroy = [](void *data) {
static_cast<BrowserSource *>(data)->Destroy();
};
info.missing_files = browser_source_missingfiles;
info.update = [](void *data, obs_data_t *settings) {
static_cast<BrowserSource *>(data)->Update(settings);
};
info.get_width = [](void *data) {
return (uint32_t)static_cast<BrowserSource *>(data)->width;
};
info.get_height = [](void *data) {
return (uint32_t)static_cast<BrowserSource *>(data)->height;
};
info.video_tick = [](void *data, float) {
static_cast<BrowserSource *>(data)->Tick();
};
info.video_render = [](void *data, gs_effect_t *) {
static_cast<BrowserSource *>(data)->Render();
};
info.mouse_click = [](void *data, const struct obs_mouse_event *event, int32_t type, bool mouse_up,
uint32_t click_count) {
static_cast<BrowserSource *>(data)->SendMouseClick(event, type, mouse_up, click_count);
};
info.mouse_move = [](void *data, const struct obs_mouse_event *event, bool mouse_leave) {
static_cast<BrowserSource *>(data)->SendMouseMove(event, mouse_leave);
};
info.mouse_wheel = [](void *data, const struct obs_mouse_event *event, int x_delta, int y_delta) {
static_cast<BrowserSource *>(data)->SendMouseWheel(event, x_delta, y_delta);
};
info.focus = [](void *data, bool focus) {
static_cast<BrowserSource *>(data)->SendFocus(focus);
};
info.key_click = [](void *data, const struct obs_key_event *event, bool key_up) {
static_cast<BrowserSource *>(data)->SendKeyClick(event, key_up);
};
info.show = [](void *data) {
static_cast<BrowserSource *>(data)->SetShowing(true);
};
info.hide = [](void *data) {
static_cast<BrowserSource *>(data)->SetShowing(false);
};
info.activate = [](void *data) {
BrowserSource *bs = static_cast<BrowserSource *>(data);
if (bs->restart)
bs->Refresh();
bs->SetActive(true);
};
info.deactivate = [](void *data) {
static_cast<BrowserSource *>(data)->SetActive(false);
};
obs_register_source(&info);
}
/* ========================================================================= */
extern void DispatchJSEvent(std::string eventName, std::string jsonString, BrowserSource *browser = nullptr);
static void handle_obs_frontend_event(enum obs_frontend_event event, void *)
{
switch (event) {
case OBS_FRONTEND_EVENT_STREAMING_STARTING:
DispatchJSEvent("obsStreamingStarting", "null");
break;
case OBS_FRONTEND_EVENT_STREAMING_STARTED:
DispatchJSEvent("obsStreamingStarted", "null");
break;
case OBS_FRONTEND_EVENT_STREAMING_STOPPING:
DispatchJSEvent("obsStreamingStopping", "null");
break;
case OBS_FRONTEND_EVENT_STREAMING_STOPPED:
DispatchJSEvent("obsStreamingStopped", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTING:
DispatchJSEvent("obsRecordingStarting", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTED:
DispatchJSEvent("obsRecordingStarted", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_PAUSED:
DispatchJSEvent("obsRecordingPaused", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED:
DispatchJSEvent("obsRecordingUnpaused", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_STOPPING:
DispatchJSEvent("obsRecordingStopping", "null");
break;
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
DispatchJSEvent("obsRecordingStopped", "null");
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING:
DispatchJSEvent("obsReplaybufferStarting", "null");
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED:
DispatchJSEvent("obsReplaybufferStarted", "null");
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED:
DispatchJSEvent("obsReplaybufferSaved", "null");
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING:
DispatchJSEvent("obsReplaybufferStopping", "null");
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED:
DispatchJSEvent("obsReplaybufferStopped", "null");
break;
case OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED:
DispatchJSEvent("obsVirtualcamStarted", "null");
break;
case OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED:
DispatchJSEvent("obsVirtualcamStopped", "null");
break;
case OBS_FRONTEND_EVENT_SCENE_CHANGED: {
OBSSourceAutoRelease source = obs_frontend_get_current_scene();
if (!source)
break;
const char *name = obs_source_get_name(source);
if (!name)
break;
nlohmann::json json = {{"name", name},
{"width", obs_source_get_width(source)},
{"height", obs_source_get_height(source)}};
DispatchJSEvent("obsSceneChanged", json.dump());
break;
}
case OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED: {
struct obs_frontend_source_list list = {};
obs_frontend_get_scenes(&list);
std::vector<const char *> scenes_vector;
for (size_t i = 0; i < list.sources.num; i++) {
obs_source_t *source = list.sources.array[i];
scenes_vector.push_back(obs_source_get_name(source));
}
nlohmann::json json = scenes_vector;
obs_frontend_source_list_free(&list);
DispatchJSEvent("obsSceneListChanged", json.dump());
break;
}
case OBS_FRONTEND_EVENT_TRANSITION_CHANGED: {
OBSSourceAutoRelease source = obs_frontend_get_current_transition();
if (!source)
break;
const char *name = obs_source_get_name(source);
if (!name)
break;
nlohmann::json json = {{"name", name}};
DispatchJSEvent("obsTransitionChanged", json.dump());
break;
}
case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED: {
struct obs_frontend_source_list list = {};
obs_frontend_get_transitions(&list);
std::vector<const char *> transitions_vector;
for (size_t i = 0; i < list.sources.num; i++) {
obs_source_t *source = list.sources.array[i];
transitions_vector.push_back(obs_source_get_name(source));
}
nlohmann::json json = transitions_vector;
obs_frontend_source_list_free(&list);
DispatchJSEvent("obsTransitionListChanged", json.dump());
break;
}
case OBS_FRONTEND_EVENT_EXIT:
DispatchJSEvent("obsExit", "null");
break;
default:;
}
}
#ifdef _WIN32
static inline void EnumAdapterCount()
{
ComPtr<IDXGIFactory1> factory;
ComPtr<IDXGIAdapter1> adapter;
HRESULT hr;
UINT i = 0;
hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void **)&factory);
if (FAILED(hr))
return;
while (factory->EnumAdapters1(i++, &adapter) == S_OK) {
DXGI_ADAPTER_DESC desc;
hr = adapter->GetDesc(&desc);
if (FAILED(hr))
continue;
if (i == 1)
deviceId = desc.Description;
/* ignore Microsoft's 'basic' renderer' */
if (desc.VendorId == 0x1414 && desc.DeviceId == 0x8c)
continue;
adapterCount++;
}
}
#endif
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
#ifdef _WIN32
static const wchar_t *blacklisted_devices[] = {L"Intel", L"Microsoft", L"Radeon HD 8850M", L"Radeon HD 7660", nullptr};
static inline bool is_intel(const std::wstring &str)
{
return wstrstri(str.c_str(), L"Intel") != 0;
}
static void check_hwaccel_support(void)
{
/* do not use hardware acceleration if a blacklisted device is the
* default and on 2 or more adapters */
const wchar_t **device = blacklisted_devices;
if (adapterCount >= 2 || !is_intel(deviceId)) {
while (*device) {
if (!!wstrstri(deviceId.c_str(), *device)) {
hwaccel = false;
blog(LOG_INFO, "[obs-browser]: "
"Blacklisted device "
"detected, "
"disabling browser "
"source hardware "
"acceleration.");
break;
}
device++;
}
}
}
#elif __linux__
static void check_hwaccel_support(void)
{
/* NOTE: GL_VERSION returns a string that contains the driver vendor */
const char *glVersion = NULL;
obs_enter_graphics();
glVersion = gs_get_driver_version();
obs_leave_graphics();
if (!glVersion)
return;
if (strstr(glVersion, "NVIDIA") != NULL) {
hwaccel = false;
blog(LOG_INFO,
"[obs-browser]: Blacklisted driver detected, disabling browser source hardware acceleration.");
}
return;
}
#else
static void check_hwaccel_support(void)
{
return;
}
#endif
#endif
#if CHROME_VERSION_BUILD > 6533
static void MigrateProfileConfigDir(std::string oldDir, std::string newDir, bool makeDirs)
{
namespace fs = std::filesystem;
BPtr<char> configPath = obs_module_config_path(".");
fs::path rootPath{configPath.Get()};
fs::path oldPath = rootPath / fs::path(oldDir);
fs::path newPath = rootPath / fs::path(newDir);
if (!fs::exists(oldPath)) {
return;
}
try {
if (makeDirs && !fs::exists(newPath)) {
fs::create_directories(newPath);
}
const auto copyOptions = fs::copy_options::recursive;
fs::copy(oldPath, newPath, copyOptions);
} catch (const fs::filesystem_error &error) {
blog(LOG_WARNING, "[obs-browser]: Error migrating cookies for '%s': %s", oldPath.c_str(), error.what());
}
}
static void MigrateDefaultProfileSubdirs(std::string parent, bool makeDirs)
{
constexpr std::array<std::string_view, 3> kMigrationDirectories{
"Local Storage",
"Session Storage",
"Network",
};
constexpr std::string_view kDefaultDirectoryPrefix{"Default/"};
for (std::string_view directory : kMigrationDirectories) {
std::string oldDirectory{parent};
oldDirectory.append(directory);
std::string newDirectory{parent};
newDirectory.append(kDefaultDirectoryPrefix);
newDirectory.append(directory);
MigrateProfileConfigDir(oldDirectory, newDirectory, makeDirs);
}
}
static void MigrateToChromeRuntime()
{
namespace fs = std::filesystem;
BPtr<char> defaultConfigPath = obs_module_config_path("Default");
fs::path defaultPath{defaultConfigPath.Get()};
// User has already previously launched with Chrome Runtime
if (fs::exists(defaultPath)) {
blog(LOG_INFO, "[obs-browser]: Chrome Runtime Default profile detected. No migration necessary.");
return;
}
BPtr<char> localPrefsConfigPath = obs_module_config_path("LocalPrefs.json");
fs::path localPrefs{localPrefsConfigPath.Get()};
// User has never launched old OBS Browser before
if (!fs::exists(localPrefs)) {
blog(LOG_DEBUG, "[obs-browser]: Alloy Runtime preferences not found. No migration necessary.");
return;
}
blog(LOG_INFO, "[obs-browser]: Migrating files to Chrome Runtime...");
try {
fs::create_directories(defaultPath);
} catch (const fs::filesystem_error &error) {
blog(LOG_WARNING, "[obs-browser]: Error creating Default profile: %s", error.what());
}
MigrateDefaultProfileSubdirs("", false);
BPtr<char> newLocalPrefsConfigPath = obs_module_config_path("Local State");
fs::path newLocalPrefs{newLocalPrefsConfigPath.Get()};
try {
fs::copy(localPrefs, newLocalPrefs);
} catch (const fs::filesystem_error &error) {
blog(LOG_WARNING, "[obs-browser]: Error migrating preferences: %s", error.what());
}
constexpr std::string_view prefix = "obs_profile_cookies";
BPtr<char> serviceProfilesPath = obs_module_config_path(prefix.data());
fs::path serviceProfiles{serviceProfilesPath.Get()};
if (fs::exists(serviceProfiles)) {
// User has service integration cookies that also must be copied
for (auto const &dir_entry : std::filesystem::directory_iterator{serviceProfiles}) {
if (!dir_entry.is_directory()) {
continue;
}
std::string name{dir_entry.path().filename().string()};
std::string oldDir{prefix};
oldDir.append("/");
oldDir.append(name);
std::string newDir{prefix};
newDir.append("_");
newDir.append(name);
MigrateProfileConfigDir(oldDir, newDir, true);
blog(LOG_INFO, "[obs-browser]: Migrated cookies for %s.", name.c_str());
}
}
blog(LOG_INFO, "[obs-browser]: Migration of browser cookies and session completed.");
}
#endif
bool obs_module_load(void)
{
#if CHROME_VERSION_BUILD > 6533
MigrateToChromeRuntime();
#endif
#ifdef ENABLE_BROWSER_QT_LOOP
qRegisterMetaType<MessageTask>("MessageTask");
#endif
os_event_init(&cef_started_event, OS_EVENT_TYPE_MANUAL);
#if defined(_WIN32) && CHROME_VERSION_BUILD < 5615
/* CefEnableHighDPISupport doesn't do anything on OS other than Windows. Would also crash macOS at this point as CEF is not directly linked */
CefEnableHighDPISupport();
#endif
#ifdef _WIN32
EnumAdapterCount();
#else
#if defined(__APPLE__) && !defined(ENABLE_BROWSER_LEGACY)
/* Load CEF at runtime as required on macOS */
CefScopedLibraryLoader library_loader;
if (!library_loader.LoadInMain())
return false;
#endif
#endif
blog(LOG_INFO, "[obs-browser]: Version %s", OBS_BROWSER_VERSION_STRING);
blog(LOG_INFO, "[obs-browser]: CEF Version %i.%i.%i.%i (runtime), %s (compiled)", cef_version_info(4),
cef_version_info(5), cef_version_info(6), cef_version_info(7), CEF_VERSION);
RegisterBrowserSource();
obs_frontend_add_event_callback(handle_obs_frontend_event, nullptr);
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
OBSDataAutoRelease private_data = obs_get_private_data();
hwaccel = obs_data_get_bool(private_data, "BrowserHWAccel");
if (hwaccel) {
check_hwaccel_support();
}
#endif
return true;
}
void obs_module_post_load(void)
{
auto vendor = obs_websocket_register_vendor("obs-browser");
if (!vendor)
return;
auto emit_event_request_cb = [](obs_data_t *request_data, obs_data_t *, void *) {
const char *event_name = obs_data_get_string(request_data, "event_name");
if (!event_name)
return;
OBSDataAutoRelease event_data = obs_data_get_obj(request_data, "event_data");
const char *event_data_string = event_data ? obs_data_get_json(event_data) : "{}";
DispatchJSEvent(event_name, event_data_string, nullptr);
};
if (!obs_websocket_vendor_register_request(vendor, "emit_event", emit_event_request_cb, nullptr))
blog(LOG_WARNING, "[obs-browser]: Failed to register obs-websocket request emit_event");
}
void obs_module_unload(void)
{
#ifdef ENABLE_BROWSER_QT_LOOP
BrowserShutdown();
#else
if (manager_thread.joinable()) {
if (!QueueCEFTask([]() { CefQuitMessageLoop(); }))
blog(LOG_DEBUG, "[obs-browser]: Failed to post CefQuit task to loop");
manager_thread.join();
}
#endif
os_event_destroy(cef_started_event);
}