界面与品牌 - 默认 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/ 发布目录与构建日志
667 lines
20 KiB
C++
667 lines
20 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-client.hpp"
|
|
#include "obs-browser-source.hpp"
|
|
#include "base64/base64.hpp"
|
|
#include <nlohmann/json.hpp>
|
|
#include <obs-frontend-api.h>
|
|
#include <obs.hpp>
|
|
#include <util/platform.h>
|
|
#include <QApplication>
|
|
#include <QThread>
|
|
#include <QToolTip>
|
|
#if defined(__APPLE__) && CHROME_VERSION_BUILD > 4430
|
|
#include <IOSurface/IOSurface.h>
|
|
#endif
|
|
|
|
#if !defined(_WIN32) && !defined(__APPLE__)
|
|
#include <obs-nix-platform.h>
|
|
|
|
#include "drm-format.hpp"
|
|
#endif
|
|
|
|
inline bool BrowserClient::valid() const
|
|
{
|
|
return !!bs && !bs->destroying;
|
|
}
|
|
|
|
CefRefPtr<CefLoadHandler> BrowserClient::GetLoadHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefRenderHandler> BrowserClient::GetRenderHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefDisplayHandler> BrowserClient::GetDisplayHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefLifeSpanHandler> BrowserClient::GetLifeSpanHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefContextMenuHandler> BrowserClient::GetContextMenuHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefAudioHandler> BrowserClient::GetAudioHandler()
|
|
{
|
|
return reroute_audio ? this : nullptr;
|
|
}
|
|
|
|
CefRefPtr<CefRequestHandler> BrowserClient::GetRequestHandler()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
CefRefPtr<CefResourceRequestHandler> BrowserClient::GetResourceRequestHandler(CefRefPtr<CefBrowser>,
|
|
CefRefPtr<CefFrame>,
|
|
CefRefPtr<CefRequest> request, bool, bool,
|
|
const CefString &, bool &)
|
|
{
|
|
if (request->GetHeaderByName("origin") == "null") {
|
|
return this;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void BrowserClient::OnRenderProcessTerminated(CefRefPtr<CefBrowser>, TerminationStatus
|
|
#if CHROME_VERSION_BUILD >= 6367
|
|
,
|
|
int, const CefString &error_string
|
|
#endif
|
|
)
|
|
{
|
|
if (!valid())
|
|
return;
|
|
|
|
#if CHROME_VERSION_BUILD >= 6367
|
|
std::string str_text = error_string;
|
|
#else
|
|
std::string str_text = "<unknown>";
|
|
#endif
|
|
|
|
const char *sourceName = "<unknown>";
|
|
|
|
if (bs && bs->source)
|
|
sourceName = obs_source_get_name(bs->source);
|
|
|
|
blog(LOG_ERROR, "[obs-browser: '%s'] Webpage has crashed unexpectedly! Reason: '%s'", sourceName,
|
|
str_text.c_str());
|
|
}
|
|
|
|
CefResourceRequestHandler::ReturnValue BrowserClient::OnBeforeResourceLoad(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>,
|
|
CefRefPtr<CefRequest>,
|
|
CefRefPtr<CefCallback>)
|
|
{
|
|
return RV_CONTINUE;
|
|
}
|
|
|
|
bool BrowserClient::OnBeforePopup(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>,
|
|
#if CHROME_VERSION_BUILD >= 6834
|
|
int,
|
|
#endif
|
|
const CefString &, const CefString &, cef_window_open_disposition_t, bool,
|
|
const CefPopupFeatures &, CefWindowInfo &, CefRefPtr<CefClient> &,
|
|
CefBrowserSettings &, CefRefPtr<CefDictionaryValue> &, bool *)
|
|
{
|
|
/* block popups */
|
|
return true;
|
|
}
|
|
|
|
void BrowserClient::OnBeforeContextMenu(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame>, CefRefPtr<CefContextMenuParams>,
|
|
CefRefPtr<CefMenuModel> model)
|
|
{
|
|
/* remove all context menu contributions */
|
|
model->Clear();
|
|
}
|
|
|
|
bool BrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame>, CefProcessId,
|
|
CefRefPtr<CefProcessMessage> message)
|
|
{
|
|
const std::string &name = message->GetName();
|
|
CefRefPtr<CefListValue> input_args = message->GetArgumentList();
|
|
nlohmann::json json;
|
|
|
|
if (!valid()) {
|
|
return false;
|
|
}
|
|
|
|
// Fall-through switch, so that higher levels also have lower-level rights
|
|
switch (webpage_control_level) {
|
|
case ControlLevel::All:
|
|
if (name == "startRecording") {
|
|
obs_frontend_recording_start();
|
|
} else if (name == "stopRecording") {
|
|
obs_frontend_recording_stop();
|
|
} else if (name == "startStreaming") {
|
|
obs_frontend_streaming_start();
|
|
} else if (name == "stopStreaming") {
|
|
obs_frontend_streaming_stop();
|
|
} else if (name == "pauseRecording") {
|
|
obs_frontend_recording_pause(true);
|
|
} else if (name == "unpauseRecording") {
|
|
obs_frontend_recording_pause(false);
|
|
} else if (name == "startVirtualcam") {
|
|
obs_frontend_start_virtualcam();
|
|
} else if (name == "stopVirtualcam") {
|
|
obs_frontend_stop_virtualcam();
|
|
}
|
|
[[fallthrough]];
|
|
case ControlLevel::Advanced:
|
|
if (name == "startReplayBuffer") {
|
|
obs_frontend_replay_buffer_start();
|
|
} else if (name == "stopReplayBuffer") {
|
|
obs_frontend_replay_buffer_stop();
|
|
} else if (name == "setCurrentScene") {
|
|
const std::string scene_name = input_args->GetString(1).ToString();
|
|
OBSSourceAutoRelease source = obs_get_source_by_name(scene_name.c_str());
|
|
if (!source) {
|
|
blog(LOG_WARNING,
|
|
"Browser source '%s' tried to switch to scene '%s' which doesn't exist",
|
|
obs_source_get_name(bs->source), scene_name.c_str());
|
|
} else if (!obs_source_is_scene(source)) {
|
|
blog(LOG_WARNING, "Browser source '%s' tried to switch to '%s' which isn't a scene",
|
|
obs_source_get_name(bs->source), scene_name.c_str());
|
|
} else {
|
|
obs_frontend_set_current_scene(source);
|
|
}
|
|
} else if (name == "setCurrentTransition") {
|
|
const std::string transition_name = input_args->GetString(1).ToString();
|
|
obs_frontend_source_list transitions = {};
|
|
obs_frontend_get_transitions(&transitions);
|
|
|
|
OBSSourceAutoRelease transition;
|
|
for (size_t i = 0; i < transitions.sources.num; i++) {
|
|
obs_source_t *source = transitions.sources.array[i];
|
|
if (obs_source_get_name(source) == transition_name) {
|
|
transition = obs_source_get_ref(source);
|
|
break;
|
|
}
|
|
}
|
|
|
|
obs_frontend_source_list_free(&transitions);
|
|
|
|
if (transition)
|
|
obs_frontend_set_current_transition(transition);
|
|
else
|
|
blog(LOG_WARNING,
|
|
"Browser source '%s' tried to change the current transition to '%s' which doesn't exist",
|
|
obs_source_get_name(bs->source), transition_name.c_str());
|
|
}
|
|
[[fallthrough]];
|
|
case ControlLevel::Basic:
|
|
if (name == "saveReplayBuffer") {
|
|
obs_frontend_replay_buffer_save();
|
|
}
|
|
[[fallthrough]];
|
|
case ControlLevel::ReadUser:
|
|
if (name == "getScenes") {
|
|
struct obs_frontend_source_list list = {};
|
|
obs_frontend_get_scenes(&list);
|
|
std::vector<nlohmann::json> 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));
|
|
}
|
|
json = scenes_vector;
|
|
obs_frontend_source_list_free(&list);
|
|
} else if (name == "getCurrentScene") {
|
|
OBSSourceAutoRelease current_scene = obs_frontend_get_current_scene();
|
|
|
|
if (!current_scene)
|
|
return false;
|
|
|
|
const char *name = obs_source_get_name(current_scene);
|
|
if (!name)
|
|
return false;
|
|
|
|
json = {{"name", name},
|
|
{"width", obs_source_get_width(current_scene)},
|
|
{"height", obs_source_get_height(current_scene)}};
|
|
} else if (name == "getTransitions") {
|
|
struct obs_frontend_source_list list = {};
|
|
obs_frontend_get_transitions(&list);
|
|
std::vector<nlohmann::json> 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));
|
|
}
|
|
json = transitions_vector;
|
|
obs_frontend_source_list_free(&list);
|
|
} else if (name == "getCurrentTransition") {
|
|
OBSSourceAutoRelease source = obs_frontend_get_current_transition();
|
|
json = obs_source_get_name(source);
|
|
}
|
|
[[fallthrough]];
|
|
case ControlLevel::ReadObs:
|
|
if (name == "getStatus") {
|
|
json = {{"recording", obs_frontend_recording_active()},
|
|
{"streaming", obs_frontend_streaming_active()},
|
|
{"recordingPaused", obs_frontend_recording_paused()},
|
|
{"replaybuffer", obs_frontend_replay_buffer_active()},
|
|
{"virtualcam", obs_frontend_virtualcam_active()}};
|
|
}
|
|
[[fallthrough]];
|
|
case ControlLevel::None:
|
|
if (name == "getControlLevel") {
|
|
json = (int)webpage_control_level;
|
|
}
|
|
}
|
|
|
|
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("executeCallback");
|
|
|
|
CefRefPtr<CefListValue> execute_args = msg->GetArgumentList();
|
|
execute_args->SetInt(0, input_args->GetInt(0));
|
|
execute_args->SetString(1, json.dump());
|
|
|
|
SendBrowserProcessMessage(browser, PID_RENDERER, msg);
|
|
|
|
return true;
|
|
}
|
|
|
|
void BrowserClient::GetViewRect(CefRefPtr<CefBrowser>, CefRect &rect)
|
|
{
|
|
if (!valid()) {
|
|
rect.Set(0, 0, 16, 16);
|
|
return;
|
|
}
|
|
|
|
rect.Set(0, 0, bs->width < 1 ? 1 : bs->width, bs->height < 1 ? 1 : bs->height);
|
|
}
|
|
|
|
bool BrowserClient::OnTooltip(CefRefPtr<CefBrowser>, CefString &text)
|
|
{
|
|
std::string str_text = text;
|
|
QMetaObject::invokeMethod(QCoreApplication::instance()->thread(),
|
|
[str_text]() { QToolTip::showText(QCursor::pos(), str_text.c_str()); });
|
|
return true;
|
|
}
|
|
|
|
void BrowserClient::OnPaint(CefRefPtr<CefBrowser>, PaintElementType type, const RectList &, const void *buffer,
|
|
int width, int height)
|
|
{
|
|
if (type != PET_VIEW) {
|
|
// TODO Overlay texture on top of bs->texture
|
|
return;
|
|
}
|
|
|
|
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
|
|
if (sharing_available) {
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (!valid()) {
|
|
return;
|
|
}
|
|
|
|
if (bs->width != width || bs->height != height) {
|
|
obs_enter_graphics();
|
|
bs->DestroyTextures();
|
|
obs_leave_graphics();
|
|
}
|
|
|
|
if (!bs->texture && width && height) {
|
|
obs_enter_graphics();
|
|
bs->texture = gs_texture_create(width, height, GS_BGRA, 1, (const uint8_t **)&buffer, GS_DYNAMIC);
|
|
bs->width = width;
|
|
bs->height = height;
|
|
obs_leave_graphics();
|
|
} else {
|
|
obs_enter_graphics();
|
|
gs_texture_set_image(bs->texture, (const uint8_t *)buffer, width * 4, false);
|
|
obs_leave_graphics();
|
|
}
|
|
}
|
|
|
|
#ifdef ENABLE_BROWSER_SHARED_TEXTURE
|
|
void BrowserClient::UpdateExtraTexture()
|
|
{
|
|
if (bs->texture) {
|
|
const uint32_t cx = gs_texture_get_width(bs->texture);
|
|
const uint32_t cy = gs_texture_get_height(bs->texture);
|
|
const gs_color_format format = gs_texture_get_color_format(bs->texture);
|
|
const gs_color_format linear_format = gs_generalize_format(format);
|
|
|
|
if (linear_format != format) {
|
|
if (!bs->extra_texture || bs->last_format != linear_format || bs->last_cx != cx ||
|
|
bs->last_cy != cy) {
|
|
if (bs->extra_texture) {
|
|
gs_texture_destroy(bs->extra_texture);
|
|
bs->extra_texture = nullptr;
|
|
}
|
|
bs->extra_texture = gs_texture_create(cx, cy, linear_format, 1, nullptr, 0);
|
|
bs->last_cx = cx;
|
|
bs->last_cy = cy;
|
|
bs->last_format = linear_format;
|
|
}
|
|
} else if (bs->extra_texture) {
|
|
gs_texture_destroy(bs->extra_texture);
|
|
bs->extra_texture = nullptr;
|
|
bs->last_cx = 0;
|
|
bs->last_cy = 0;
|
|
bs->last_format = GS_UNKNOWN;
|
|
}
|
|
}
|
|
}
|
|
|
|
void BrowserClient::OnAcceleratedPaint(CefRefPtr<CefBrowser>, PaintElementType type, const RectList &,
|
|
#if CHROME_VERSION_BUILD >= 6367
|
|
const CefAcceleratedPaintInfo &info)
|
|
#else
|
|
void *shared_handle)
|
|
#endif
|
|
{
|
|
if (type != PET_VIEW) {
|
|
// TODO Overlay texture on top of bs->texture
|
|
return;
|
|
}
|
|
|
|
if (!valid()) {
|
|
return;
|
|
}
|
|
|
|
#if !defined(_WIN32) && !defined(__APPLE__)
|
|
if (info.plane_count == 0)
|
|
return;
|
|
|
|
struct obs_cef_video_format format = obs_cef_format_from_cef_type(info.format);
|
|
uint64_t modifier = info.modifier;
|
|
|
|
if (format.gs_format == GS_UNKNOWN)
|
|
return;
|
|
|
|
uint32_t *strides = (uint32_t *)alloca(info.plane_count * sizeof(uint32_t));
|
|
uint32_t *offsets = (uint32_t *)alloca(info.plane_count * sizeof(uint32_t));
|
|
uint64_t *modifiers = (uint64_t *)alloca(info.plane_count * sizeof(uint64_t));
|
|
int *fds = (int *)alloca(info.plane_count * sizeof(int));
|
|
|
|
/* NOTE: This a workaround under X11 where the modifier is always invalid where it can mean "no modifier" in
|
|
* Chromium's code. */
|
|
if (obs_get_nix_platform() == OBS_NIX_PLATFORM_X11_EGL && modifier == DRM_FORMAT_MOD_INVALID)
|
|
modifier = DRM_FORMAT_MOD_LINEAR;
|
|
|
|
for (size_t i = 0; i < kAcceleratedPaintMaxPlanes; i++) {
|
|
auto *plane = &info.planes[i];
|
|
|
|
strides[i] = plane->stride;
|
|
offsets[i] = plane->offset;
|
|
fds[i] = plane->fd;
|
|
|
|
modifiers[i] = modifier;
|
|
}
|
|
#endif
|
|
|
|
#if !defined(_WIN32) && CHROME_VERSION_BUILD < 6367
|
|
if (shared_handle == bs->last_handle)
|
|
return;
|
|
#endif
|
|
|
|
obs_enter_graphics();
|
|
|
|
if (bs->texture) {
|
|
#ifdef _WIN32
|
|
//gs_texture_release_sync(bs->texture, 0);
|
|
#endif
|
|
gs_texture_destroy(bs->texture);
|
|
bs->texture = nullptr;
|
|
}
|
|
|
|
#if defined(__APPLE__) && CHROME_VERSION_BUILD > 6367
|
|
bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)info.shared_texture_io_surface);
|
|
#elif defined(__APPLE__) && CHROME_VERSION_BUILD > 4183
|
|
bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)shared_handle);
|
|
#elif defined(_WIN32) && CHROME_VERSION_BUILD > 4183
|
|
bs->texture =
|
|
#if CHROME_VERSION_BUILD >= 6367
|
|
gs_texture_open_nt_shared((uint32_t)(uintptr_t)info.shared_texture_handle);
|
|
#else
|
|
gs_texture_open_nt_shared((uint32_t)(uintptr_t)shared_handle);
|
|
#endif
|
|
//if (bs->texture)
|
|
// gs_texture_acquire_sync(bs->texture, 1, INFINITE);
|
|
|
|
#elif defined(_WIN32)
|
|
bs->texture = gs_texture_open_shared((uint32_t)(uintptr_t)shared_handle);
|
|
#else
|
|
bs->texture = gs_texture_create_from_dmabuf(info.extra.coded_size.width, info.extra.coded_size.height,
|
|
format.drm_format, format.gs_format, info.plane_count, fds, strides,
|
|
offsets, modifier != DRM_FORMAT_MOD_INVALID ? modifiers : NULL);
|
|
#endif
|
|
UpdateExtraTexture();
|
|
obs_leave_graphics();
|
|
|
|
#if defined(__APPLE__) && CHROME_VERSION_BUILD >= 6367
|
|
bs->last_handle = info.shared_texture_io_surface;
|
|
#elif defined(_WIN32) && CHROME_VERSION_BUILD >= 6367
|
|
bs->last_handle = info.shared_texture_handle;
|
|
#elif defined(__APPLE__) || defined(_WIN32)
|
|
bs->last_handle = shared_handle;
|
|
#endif
|
|
}
|
|
|
|
#ifdef CEF_ON_ACCELERATED_PAINT2
|
|
void BrowserClient::OnAcceleratedPaint2(CefRefPtr<CefBrowser>, PaintElementType type, const RectList &,
|
|
void *shared_handle, bool new_texture)
|
|
{
|
|
if (type != PET_VIEW) {
|
|
// TODO Overlay texture on top of bs->texture
|
|
return;
|
|
}
|
|
|
|
if (!valid()) {
|
|
return;
|
|
}
|
|
|
|
if (!new_texture) {
|
|
return;
|
|
}
|
|
|
|
obs_enter_graphics();
|
|
|
|
if (bs->texture) {
|
|
gs_texture_destroy(bs->texture);
|
|
bs->texture = nullptr;
|
|
}
|
|
|
|
#if defined(__APPLE__) && CHROME_VERSION_BUILD > 4183
|
|
bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)shared_handle);
|
|
#elif defined(_WIN32) && CHROME_VERSION_BUILD > 4183
|
|
bs->texture = gs_texture_open_nt_shared((uint32_t)(uintptr_t)shared_handle);
|
|
|
|
#else
|
|
bs->texture = gs_texture_open_shared((uint32_t)(uintptr_t)shared_handle);
|
|
#endif
|
|
UpdateExtraTexture();
|
|
obs_leave_graphics();
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
static speaker_layout GetSpeakerLayout(CefAudioHandler::ChannelLayout cefLayout)
|
|
{
|
|
switch (cefLayout) {
|
|
case CEF_CHANNEL_LAYOUT_MONO:
|
|
return SPEAKERS_MONO; /**< Channels: MONO */
|
|
case CEF_CHANNEL_LAYOUT_STEREO:
|
|
return SPEAKERS_STEREO; /**< Channels: FL, FR */
|
|
case CEF_CHANNEL_LAYOUT_2POINT1:
|
|
case CEF_CHANNEL_LAYOUT_2_1:
|
|
case CEF_CHANNEL_LAYOUT_SURROUND:
|
|
return SPEAKERS_2POINT1; /**< Channels: FL, FR, LFE */
|
|
case CEF_CHANNEL_LAYOUT_2_2:
|
|
case CEF_CHANNEL_LAYOUT_QUAD:
|
|
case CEF_CHANNEL_LAYOUT_4_0:
|
|
return SPEAKERS_4POINT0; /**< Channels: FL, FR, FC, RC */
|
|
case CEF_CHANNEL_LAYOUT_4_1:
|
|
case CEF_CHANNEL_LAYOUT_5_0:
|
|
case CEF_CHANNEL_LAYOUT_5_0_BACK:
|
|
return SPEAKERS_4POINT1; /**< Channels: FL, FR, FC, LFE, RC */
|
|
case CEF_CHANNEL_LAYOUT_5_1:
|
|
case CEF_CHANNEL_LAYOUT_5_1_BACK:
|
|
return SPEAKERS_5POINT1; /**< Channels: FL, FR, FC, LFE, RL, RR */
|
|
case CEF_CHANNEL_LAYOUT_7_1:
|
|
case CEF_CHANNEL_LAYOUT_7_1_WIDE_BACK:
|
|
case CEF_CHANNEL_LAYOUT_7_1_WIDE:
|
|
return SPEAKERS_7POINT1; /**< Channels: FL, FR, FC, LFE, RL, RR, SL, SR */
|
|
default:
|
|
return SPEAKERS_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
void BrowserClient::OnAudioStreamStarted(CefRefPtr<CefBrowser> browser, const CefAudioParameters ¶ms_,
|
|
int channels_)
|
|
{
|
|
UNUSED_PARAMETER(browser);
|
|
channels = channels_;
|
|
channel_layout = (ChannelLayout)params_.channel_layout;
|
|
sample_rate = params_.sample_rate;
|
|
frames_per_buffer = params_.frames_per_buffer;
|
|
}
|
|
|
|
void BrowserClient::OnAudioStreamPacket(CefRefPtr<CefBrowser> browser, const float **data, int frames, int64_t pts)
|
|
{
|
|
UNUSED_PARAMETER(browser);
|
|
if (!valid()) {
|
|
return;
|
|
}
|
|
struct obs_source_audio audio = {};
|
|
const uint8_t **pcm = (const uint8_t **)data;
|
|
speaker_layout speakers = GetSpeakerLayout(channel_layout);
|
|
int speaker_count = get_audio_channels(speakers);
|
|
for (int i = 0; i < speaker_count; i++)
|
|
audio.data[i] = pcm[i];
|
|
audio.samples_per_sec = sample_rate;
|
|
audio.frames = frames;
|
|
audio.format = AUDIO_FORMAT_FLOAT_PLANAR;
|
|
audio.speakers = speakers;
|
|
audio.timestamp = (uint64_t)pts * 1000000LLU;
|
|
obs_source_output_audio(bs->source, &audio);
|
|
}
|
|
|
|
void BrowserClient::OnAudioStreamStopped(CefRefPtr<CefBrowser> browser)
|
|
{
|
|
UNUSED_PARAMETER(browser);
|
|
}
|
|
|
|
void BrowserClient::OnAudioStreamError(CefRefPtr<CefBrowser> browser, const CefString &message)
|
|
{
|
|
UNUSED_PARAMETER(browser);
|
|
UNUSED_PARAMETER(message);
|
|
}
|
|
|
|
static CefAudioHandler::ChannelLayout Convert2CEFSpeakerLayout(int channels)
|
|
{
|
|
switch (channels) {
|
|
case 1:
|
|
return CEF_CHANNEL_LAYOUT_MONO;
|
|
case 2:
|
|
return CEF_CHANNEL_LAYOUT_STEREO;
|
|
case 3:
|
|
return CEF_CHANNEL_LAYOUT_2_1;
|
|
case 4:
|
|
return CEF_CHANNEL_LAYOUT_4_0;
|
|
case 5:
|
|
return CEF_CHANNEL_LAYOUT_4_1;
|
|
case 6:
|
|
return CEF_CHANNEL_LAYOUT_5_1;
|
|
case 8:
|
|
return CEF_CHANNEL_LAYOUT_7_1;
|
|
default:
|
|
return CEF_CHANNEL_LAYOUT_UNSUPPORTED;
|
|
}
|
|
}
|
|
|
|
bool BrowserClient::GetAudioParameters(CefRefPtr<CefBrowser> browser, CefAudioParameters ¶ms)
|
|
{
|
|
UNUSED_PARAMETER(browser);
|
|
int channels = (int)audio_output_get_channels(obs_get_audio());
|
|
params.channel_layout = Convert2CEFSpeakerLayout(channels);
|
|
params.sample_rate = (int)audio_output_get_sample_rate(obs_get_audio());
|
|
params.frames_per_buffer = kFramesPerBuffer;
|
|
return true;
|
|
}
|
|
|
|
void BrowserClient::OnLoadEnd(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame> frame, int)
|
|
{
|
|
if (!valid()) {
|
|
return;
|
|
}
|
|
|
|
if (frame->IsMain() && bs->css.length()) {
|
|
std::string uriEncodedCSS = CefURIEncode(bs->css, false).ToString();
|
|
|
|
std::string script;
|
|
script += "const obsCSS = document.createElement('style');";
|
|
script += "obsCSS.appendChild(document.createTextNode("
|
|
"decodeURIComponent(\"" +
|
|
uriEncodedCSS + "\")));";
|
|
script += "document.querySelector('head').appendChild(obsCSS);";
|
|
|
|
frame->ExecuteJavaScript(script, "", 0);
|
|
}
|
|
}
|
|
|
|
void BrowserClient::OnLoadError(CefRefPtr<CefBrowser>, [[maybe_unused]] CefRefPtr<CefFrame> frame,
|
|
CefLoadHandler::ErrorCode, const CefString &, const CefString &)
|
|
{
|
|
#if CHROME_VERSION_BUILD > 6533
|
|
// CEF doesn't currently provide a way to properly disable/override the default Chrome error page.
|
|
// https://github.com/obsproject/obs-studio/issues/13499
|
|
// FIXME: https://github.com/chromiumembedded/cef/issues/3852
|
|
frame->LoadURL("about:blank");
|
|
#endif
|
|
}
|
|
|
|
bool BrowserClient::OnConsoleMessage(CefRefPtr<CefBrowser>, cef_log_severity_t level, const CefString &message,
|
|
const CefString &source, int line)
|
|
{
|
|
int errorLevel = LOG_INFO;
|
|
const char *code = "Info";
|
|
switch (level) {
|
|
case LOGSEVERITY_ERROR:
|
|
errorLevel = LOG_WARNING;
|
|
code = "Error";
|
|
break;
|
|
case LOGSEVERITY_FATAL:
|
|
errorLevel = LOG_ERROR;
|
|
code = "Fatal";
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
const char *sourceName = "<unknown>";
|
|
|
|
if (bs && bs->source)
|
|
sourceName = obs_source_get_name(bs->source);
|
|
|
|
blog(errorLevel, "[obs-browser: '%s'] %s: %s (%s:%d)", sourceName, code, message.ToString().c_str(),
|
|
source.ToString().c_str(), line);
|
|
return false;
|
|
}
|