// Copyright 2022 The Chromium Embedded Framework Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be found // in the LICENSE file. #include "cef/libcef/browser/media_access_query.h" #include "base/command_line.h" #include "base/functional/callback_helpers.h" #include "cef/include/cef_permission_handler.h" #include "cef/libcef/browser/browser_host_base.h" #include "cef/libcef/browser/media_stream_registrar.h" #include "cef/libcef/browser/thread_util.h" #include "cef/libcef/common/cef_switches.h" #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" #include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h" namespace media_access_query { namespace { const blink::MediaStreamDevice* FindDefaultDeviceWithId( const blink::MediaStreamDevices& devices, const std::string& device_id) { if (devices.empty()) { return nullptr; } blink::MediaStreamDevices::const_iterator iter = devices.begin(); for (; iter != devices.end(); ++iter) { if (iter->id == device_id) { return &(*iter); } } return &(*devices.begin()); } // Helper for picking the device that was requested for an OpenDevice request. // If the device requested is not available it will revert to using the first // available one instead or will return an empty list if no devices of the // requested kind are present. Called on the UI thread. void GetRequestedDevice(const std::string& requested_device_id, bool audio, bool video, blink::MediaStreamDevices* devices) { CEF_REQUIRE_UIT(); DCHECK(audio || video); auto* dispatcher = MediaCaptureDevicesDispatcher::GetInstance(); if (audio) { const blink::MediaStreamDevices& audio_devices = dispatcher->GetAudioCaptureDevices(); const blink::MediaStreamDevice* const device = FindDefaultDeviceWithId(audio_devices, requested_device_id); if (device) { devices->push_back(*device); } } if (video) { const blink::MediaStreamDevices& video_devices = dispatcher->GetVideoCaptureDevices(); const blink::MediaStreamDevice* const device = FindDefaultDeviceWithId(video_devices, requested_device_id); if (device) { devices->push_back(*device); } } } class CefMediaAccessQuery { public: using CallbackType = content::MediaResponseCallback; CefMediaAccessQuery(CefBrowserHostBase* const browser, const content::MediaStreamRequest& request, CallbackType&& callback) : browser_(browser), request_(request), callback_(std::move(callback)) {} CefMediaAccessQuery(CefMediaAccessQuery&& query) : browser_(query.browser_), request_(query.request_), callback_(std::move(query.callback_)) {} CefMediaAccessQuery& operator=(CefMediaAccessQuery&& query) { browser_ = query.browser_; request_ = query.request_; callback_ = std::move(query.callback_); return *this; } CefMediaAccessQuery(const CefMediaAccessQuery&) = delete; CefMediaAccessQuery& operator=(const CefMediaAccessQuery&) = delete; bool is_null() const { return callback_.is_null(); } uint32_t requested_permissions() const { int requested_permissions = CEF_MEDIA_PERMISSION_NONE; if (device_audio_requested()) { requested_permissions |= CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE; } if (device_video_requested()) { requested_permissions |= CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE; } if (desktop_audio_requested()) { requested_permissions |= CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE; } if (desktop_video_requested()) { requested_permissions |= CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE; } return requested_permissions; } [[nodiscard]] CallbackType DisconnectCallback() { return std::move(callback_); } void ExecuteCallback(uint32_t allowed_permissions) { CEF_REQUIRE_UIT(); blink::mojom::MediaStreamRequestResult result; blink::mojom::StreamDevicesSetPtr stream_devices_set; if (allowed_permissions == CEF_MEDIA_PERMISSION_NONE) { result = blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED; stream_devices_set = blink::mojom::StreamDevicesSet::New(); } else { bool error = false; if (allowed_permissions == requested_permissions()) { stream_devices_set = GetRequestedMediaDevices(); } else { stream_devices_set = GetAllowedMediaDevices(allowed_permissions, error); } result = error ? blink::mojom::MediaStreamRequestResult::INVALID_STATE : blink::mojom::MediaStreamRequestResult::OK; } bool has_video = false; bool has_audio = false; if (!stream_devices_set->stream_devices.empty()) { blink::mojom::StreamDevices& devices = *stream_devices_set->stream_devices[0]; has_video = devices.video_device.has_value(); has_audio = devices.audio_device.has_value(); } auto media_stream_ui = browser_->GetMediaStreamRegistrar()->MaybeCreateMediaStreamUI( has_video, has_audio); std::move(callback_).Run(*stream_devices_set, result, std::move(media_stream_ui)); } private: bool device_audio_requested() const { return request_.audio_type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE; } bool device_video_requested() const { return request_.video_type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE; } bool desktop_audio_requested() const { return (request_.audio_type == blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE) || (request_.audio_type == blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE); } bool desktop_video_requested() const { return (request_.video_type == blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE) || (request_.video_type == blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE); } blink::mojom::StreamDevicesSetPtr GetRequestedMediaDevices() const { CEF_REQUIRE_UIT(); blink::MediaStreamDevices audio_devices; blink::MediaStreamDevices video_devices; if (device_audio_requested() && !request_.requested_audio_device_ids.empty() && !request_.requested_audio_device_ids.front().empty()) { // Pick the desired device or fall back to the first available of the // given type. GetRequestedDevice(request_.requested_audio_device_ids.front(), true, false, &audio_devices); } if (device_video_requested() && !request_.requested_video_device_ids.empty() && !request_.requested_video_device_ids.front().empty()) { // Pick the desired device or fall back to the first available of the // given type. GetRequestedDevice(request_.requested_video_device_ids.front(), false, true, &video_devices); } if (desktop_audio_requested()) { audio_devices.emplace_back(request_.audio_type, "loopback", "System Audio"); } if (desktop_video_requested()) { content::DesktopMediaID media_id; if (!request_.requested_video_device_ids.empty() && !request_.requested_video_device_ids.front().empty()) { media_id = content::DesktopMediaID::Parse( request_.requested_video_device_ids.front()); } else { media_id = content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, -1 /* webrtc::kFullDesktopScreenId */); } video_devices.emplace_back(request_.video_type, media_id.ToString(), "Screen"); } blink::mojom::StreamDevicesSetPtr stream_devices_set = blink::mojom::StreamDevicesSet::New(); stream_devices_set->stream_devices.emplace_back( blink::mojom::StreamDevices::New()); blink::mojom::StreamDevices& devices = *stream_devices_set->stream_devices[0]; // At most one audio device and one video device can be used in a stream. if (!audio_devices.empty()) { devices.audio_device = audio_devices.front(); } if (!video_devices.empty()) { devices.video_device = video_devices.front(); } return stream_devices_set; } blink::mojom::StreamDevicesSetPtr GetAllowedMediaDevices( uint32_t allowed_permissions, bool& error) { error = false; const auto req_permissions = requested_permissions(); const bool device_audio_allowed = allowed_permissions & CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE; const bool device_video_allowed = allowed_permissions & CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE; const bool desktop_audio_allowed = allowed_permissions & CEF_MEDIA_PERMISSION_DESKTOP_AUDIO_CAPTURE; const bool desktop_video_allowed = allowed_permissions & CEF_MEDIA_PERMISSION_DESKTOP_VIDEO_CAPTURE; blink::mojom::StreamDevicesSetPtr stream_devices_set; // getDisplayMedia must always request video if (desktop_video_requested() && (!desktop_video_allowed && desktop_audio_allowed)) { LOG(WARNING) << "Response to getDisplayMedia is not allowed to only " "return Audio"; error = true; } else if (!desktop_video_requested() && req_permissions != allowed_permissions) { LOG(WARNING) << "Response to getUserMedia must match requested permissions (" << req_permissions << " vs " << allowed_permissions << ")"; error = true; } if (error) { stream_devices_set = blink::mojom::StreamDevicesSet::New(); } else { if (!device_audio_allowed && !desktop_audio_allowed) { request_.audio_type = blink::mojom::MediaStreamType::NO_SERVICE; } if (!device_video_allowed && !desktop_video_allowed) { request_.video_type = blink::mojom::MediaStreamType::NO_SERVICE; } stream_devices_set = GetRequestedMediaDevices(); } return stream_devices_set; } CefRefPtr browser_; content::MediaStreamRequest request_; CallbackType callback_; }; class CefMediaAccessCallbackImpl : public CefMediaAccessCallback { public: using CallbackType = CefMediaAccessQuery; explicit CefMediaAccessCallbackImpl(CallbackType&& callback) : callback_(std::move(callback)) {} CefMediaAccessCallbackImpl(const CefMediaAccessCallbackImpl&) = delete; CefMediaAccessCallbackImpl& operator=(const CefMediaAccessCallbackImpl&) = delete; ~CefMediaAccessCallbackImpl() override { if (!callback_.is_null()) { // The callback is still pending. Cancel it now. if (CEF_CURRENTLY_ON_UIT()) { RunNow(std::move(callback_), CEF_MEDIA_PERMISSION_NONE); } else { CEF_POST_TASK( CEF_UIT, base::BindOnce(&CefMediaAccessCallbackImpl::RunNow, std::move(callback_), CEF_MEDIA_PERMISSION_NONE)); } } } void Continue(uint32_t allowed_permissions) override { if (CEF_CURRENTLY_ON_UIT()) { if (!callback_.is_null()) { RunNow(std::move(callback_), allowed_permissions); } } else { CEF_POST_TASK(CEF_UIT, base::BindOnce(&CefMediaAccessCallbackImpl::Continue, this, allowed_permissions)); } } void Cancel() override { Continue(CEF_MEDIA_PERMISSION_NONE); } [[nodiscard]] CallbackType Disconnect() { return std::move(callback_); } bool IsDisconnected() const { return callback_.is_null(); } private: static void RunNow(CallbackType callback, uint32_t allowed_permissions) { callback.ExecuteCallback(allowed_permissions); } CallbackType callback_; IMPLEMENT_REFCOUNTING(CefMediaAccessCallbackImpl); }; bool CheckCommandLinePermission() { const base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); return command_line->HasSwitch(switches::kEnableMediaStream); } } // namespace bool CheckMediaAccessPermission(CefBrowserHostBase* browser, content::RenderFrameHost* render_frame_host, const url::Origin& security_origin, blink::mojom::MediaStreamType type) { // Always allowed here. RequestMediaAccessPermission will be called. return true; } content::MediaResponseCallback RequestMediaAccessPermission( CefBrowserHostBase* browser, const content::MediaStreamRequest& request, content::MediaResponseCallback callback, bool default_disallow) { CEF_REQUIRE_UIT(); CefMediaAccessQuery query(browser, request, std::move(callback)); if (CheckCommandLinePermission()) { // Allow all requested permissions. query.ExecuteCallback(query.requested_permissions()); return base::NullCallback(); } bool handled = false; if (auto client = browser->GetClient()) { if (auto handler = client->GetPermissionHandler()) { const auto requested_permissions = query.requested_permissions(); CefRefPtr callbackImpl( new CefMediaAccessCallbackImpl(std::move(query))); auto frame = browser->GetFrameForGlobalId(content::GlobalRenderFrameHostId( request.render_process_id, request.render_frame_id)); if (!frame) { frame = browser->GetMainFrame(); } handled = handler->OnRequestMediaAccessPermission( browser, frame, request.security_origin.spec(), requested_permissions, callbackImpl.get()); if (!handled) { LOG_IF(ERROR, callbackImpl->IsDisconnected()) << "Should return true from OnRequestMediaAccessPermission when " "executing the callback"; query = callbackImpl->Disconnect(); } } } if (!query.is_null()) { if (default_disallow && !handled) { // Disallow access by default. query.ExecuteCallback(CEF_MEDIA_PERMISSION_NONE); } else { // Proceed with default handling. return query.DisconnectCallback(); } } return base::NullCallback(); } } // namespace media_access_query