/****************************************************************************** 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-client.hpp" #include "obs-browser-source.hpp" #include "base64/base64.hpp" #include #include #include #include #include #include #include #if defined(__APPLE__) && CHROME_VERSION_BUILD > 4430 #include #endif #if !defined(_WIN32) && !defined(__APPLE__) #include #include "drm-format.hpp" #endif inline bool BrowserClient::valid() const { return !!bs && !bs->destroying; } CefRefPtr BrowserClient::GetLoadHandler() { return this; } CefRefPtr BrowserClient::GetRenderHandler() { return this; } CefRefPtr BrowserClient::GetDisplayHandler() { return this; } CefRefPtr BrowserClient::GetLifeSpanHandler() { return this; } CefRefPtr BrowserClient::GetContextMenuHandler() { return this; } CefRefPtr BrowserClient::GetAudioHandler() { return reroute_audio ? this : nullptr; } CefRefPtr BrowserClient::GetRequestHandler() { return this; } CefRefPtr BrowserClient::GetResourceRequestHandler(CefRefPtr, CefRefPtr, CefRefPtr request, bool, bool, const CefString &, bool &) { if (request->GetHeaderByName("origin") == "null") { return this; } return nullptr; } void BrowserClient::OnRenderProcessTerminated(CefRefPtr, 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 = ""; #endif const char *sourceName = ""; 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, CefRefPtr, CefRefPtr, CefRefPtr) { return RV_CONTINUE; } bool BrowserClient::OnBeforePopup(CefRefPtr, CefRefPtr, #if CHROME_VERSION_BUILD >= 6834 int, #endif const CefString &, const CefString &, cef_window_open_disposition_t, bool, const CefPopupFeatures &, CefWindowInfo &, CefRefPtr &, CefBrowserSettings &, CefRefPtr &, bool *) { /* block popups */ return true; } void BrowserClient::OnBeforeContextMenu(CefRefPtr, CefRefPtr, CefRefPtr, CefRefPtr model) { /* remove all context menu contributions */ model->Clear(); } bool BrowserClient::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr, CefProcessId, CefRefPtr message) { const std::string &name = message->GetName(); CefRefPtr 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 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 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 msg = CefProcessMessage::Create("executeCallback"); CefRefPtr 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, 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, 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, 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, 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, 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 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 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 browser) { UNUSED_PARAMETER(browser); } void BrowserClient::OnAudioStreamError(CefRefPtr 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 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, CefRefPtr 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, [[maybe_unused]] CefRefPtr 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, 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 = ""; 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; }