| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/media/capture_handle_manager.h" |
| |
| #include <algorithm> |
| |
| #include "base/memory/ptr_util.h" |
| #include "content/browser/renderer_host/render_frame_host_delegate.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| // TODO(crbug.com/40181897): Eliminate code duplication with |
| // desktop_capture_devices_util.cc. |
| media::mojom::CaptureHandlePtr CreateCaptureHandle( |
| RenderFrameHostImpl* capturer, |
| WebContents* captured, |
| const blink::mojom::CaptureHandleConfig& capture_handle_config) { |
| if (!captured) { |
| return nullptr; |
| } |
| |
| if (!capture_handle_config.expose_origin && |
| capture_handle_config.capture_handle.empty()) { |
| return nullptr; |
| } |
| |
| const url::Origin& capturer_origin = capturer->GetLastCommittedOrigin(); |
| if (!capture_handle_config.all_origins_permitted && |
| std::ranges::none_of( |
| capture_handle_config.permitted_origins, |
| [capturer_origin](const url::Origin& permitted_origin) { |
| return capturer_origin.IsSameOriginWith(permitted_origin); |
| })) { |
| return nullptr; |
| } |
| |
| // Observing CaptureHandle wheneither the capturing or the captured party |
| // is incognito is disallowed, except for self-capture. |
| if (capturer->GetMainFrame() != captured->GetPrimaryMainFrame()) { |
| if (capturer->GetBrowserContext()->IsOffTheRecord() || |
| captured->GetBrowserContext()->IsOffTheRecord()) { |
| return nullptr; |
| } |
| } |
| |
| auto result = media::mojom::CaptureHandle::New(); |
| if (capture_handle_config.expose_origin) { |
| result->origin = captured->GetPrimaryMainFrame()->GetLastCommittedOrigin(); |
| } |
| result->capture_handle = capture_handle_config.capture_handle; |
| |
| return result; |
| } |
| |
| bool IsEqual(const media::mojom::CaptureHandlePtr& lhs, |
| const media::mojom::CaptureHandlePtr& rhs) { |
| if (!lhs || !rhs) { // If either is null, equal only if both are null. |
| return !lhs && !rhs; |
| } |
| |
| if (lhs->origin.opaque() != rhs->origin.opaque()) { |
| return false; // One is empty, the other is non-empty. |
| } |
| |
| // Either both are opaque or neither is. We only compare non-opaque origins. |
| if (!lhs->origin.opaque()) { |
| if (lhs->origin != rhs->origin) { |
| return false; |
| } |
| } |
| |
| return lhs->capture_handle == rhs->capture_handle; |
| } |
| } // namespace |
| |
| class CaptureHandleManager::Observer final : public WebContentsObserver { |
| public: |
| static std::unique_ptr<Observer> Create( |
| const CaptureKey& capture_key, |
| GlobalRenderFrameHostId captured, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback); |
| |
| ~Observer() override; |
| |
| // Implements WebContentsObserver. |
| void OnCaptureHandleConfigUpdate( |
| const blink::mojom::CaptureHandleConfig& config) override; |
| |
| // Forces an immediate polling of the captured tab for the current config. |
| // Reports it back via |handle_change_callback_|. |
| void UpdateCaptureHandleConfig(); |
| |
| private: |
| Observer(WebContents* web_contents, |
| const CaptureKey& capture_key, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback); |
| |
| const CaptureKey capture_key_; |
| const GlobalRenderFrameHostId capturer_; |
| const DeviceCaptureHandleChangeCallback handle_change_callback_; |
| }; |
| |
| std::unique_ptr<CaptureHandleManager::Observer> |
| CaptureHandleManager::Observer::Create( |
| const CaptureKey& capture_key, |
| GlobalRenderFrameHostId captured, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto* const capturer_rfhi = RenderFrameHostImpl::FromID(capturer); |
| if (!capturer_rfhi || !capturer_rfhi->IsActive()) { |
| return nullptr; |
| } |
| |
| auto* const captured_rfhi = RenderFrameHostImpl::FromID(captured); |
| if (!captured_rfhi || !captured_rfhi->IsActive()) { |
| return nullptr; |
| } |
| |
| auto* const captured_web_contents = |
| WebContents::FromRenderFrameHost(captured_rfhi); |
| if (!captured_web_contents) { |
| return nullptr; |
| } |
| |
| return base::WrapUnique(new Observer(captured_web_contents, capture_key, |
| capturer, |
| std::move(handle_change_callback))); |
| } |
| |
| CaptureHandleManager::Observer::Observer( |
| WebContents* web_contents, |
| const CaptureKey& capture_key, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback) |
| : WebContentsObserver(web_contents), |
| capture_key_(capture_key), |
| capturer_(capturer), |
| handle_change_callback_(std::move(handle_change_callback)) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(handle_change_callback_); |
| } |
| |
| CaptureHandleManager::Observer::~Observer() = default; |
| |
| void CaptureHandleManager::Observer::OnCaptureHandleConfigUpdate( |
| const blink::mojom::CaptureHandleConfig& config) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto* const capturer_rfhi = RenderFrameHostImpl::FromID(capturer_); |
| if (!capturer_rfhi || !capturer_rfhi->IsActive()) { |
| DVLOG(1) << "Invalid capturer: " << capturer_ << "."; |
| return; |
| } |
| |
| handle_change_callback_.Run( |
| capture_key_.label, capture_key_.type, |
| CreateCaptureHandle(capturer_rfhi, web_contents(), config)); |
| } |
| |
| void CaptureHandleManager::Observer::UpdateCaptureHandleConfig() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto* wc = web_contents(); |
| if (wc) { |
| OnCaptureHandleConfigUpdate(wc->GetCaptureHandleConfig()); |
| } |
| } |
| |
| CaptureHandleManager::CaptureInfo::CaptureInfo( |
| std::unique_ptr<Observer> observer, |
| media::mojom::CaptureHandlePtr last_capture_handle, |
| DeviceCaptureHandleChangeCallback callback) |
| : observer(std::move(observer)), |
| last_capture_handle(std::move(last_capture_handle)), |
| callback(std::move(callback)) {} |
| |
| CaptureHandleManager::CaptureInfo::~CaptureInfo() = default; |
| |
| CaptureHandleManager::CaptureHandleManager() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| } |
| |
| CaptureHandleManager::~CaptureHandleManager() { |
| DCHECK(!BrowserThread::IsThreadInitialized(BrowserThread::IO)); |
| } |
| |
| void CaptureHandleManager::OnTabCaptureStarted( |
| const std::string& label, |
| const blink::MediaStreamDevice& captured_device, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| WebContentsMediaCaptureId captured_tab_id; |
| if (!WebContentsMediaCaptureId::Parse(captured_device.id, &captured_tab_id)) { |
| DVLOG(1) << "Not a tab-capture ID:" << captured_device.id << "."; |
| return; |
| } |
| const GlobalRenderFrameHostId captured(captured_tab_id.render_process_id, |
| captured_tab_id.main_render_frame_id); |
| |
| const CaptureKey capture_key{label, captured_device.type}; |
| |
| // base::Unretained(this) is safe because the observer is owned by |this| |
| // and both live on the UI thread together. |
| std::unique_ptr<Observer> observer = Observer::Create( |
| capture_key, captured, capturer, |
| base::BindRepeating(&CaptureHandleManager::OnCaptureHandleConfigUpdate, |
| base::Unretained(this))); |
| if (!observer) { |
| DVLOG(1) << "Observer creation failed."; |
| return; |
| } |
| |
| auto iter = captures_.find(capture_key); |
| if (iter == captures_.end()) { |
| // Creating a new tracking session. |
| const media::mojom::DisplayMediaInformationPtr& info = |
| captured_device.display_media_info; |
| media::mojom::CaptureHandlePtr capture_handle = |
| info ? info->capture_handle.Clone() : nullptr; |
| captures_[capture_key] = std::make_unique<CaptureInfo>( |
| std::move(observer), std::move(capture_handle), |
| std::move(handle_change_callback)); |
| } else { |
| // Updating an existing tracking session in response to a device change. |
| iter->second->observer = std::move(observer); |
| } |
| |
| // The currently executing task comes in response to a chain of tasks juggled |
| // between the IO and UI thread. During this time, the CaptureHandleConfig |
| // might have changed. Fetch the latest handle. |
| captures_[capture_key]->observer->UpdateCaptureHandleConfig(); |
| } |
| |
| void CaptureHandleManager::OnTabCaptureStopped( |
| const std::string& label, |
| const blink::MediaStreamDevice& captured_device) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| captures_.erase({label, captured_device.type}); |
| } |
| |
| void CaptureHandleManager::OnTabCaptureDevicesUpdated( |
| const std::string& label, |
| blink::mojom::StreamDevicesSetPtr new_stream_devices_set, |
| GlobalRenderFrameHostId capturer, |
| DeviceCaptureHandleChangeCallback handle_change_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(new_stream_devices_set); |
| DCHECK_EQ(1u, new_stream_devices_set->stream_devices.size()); |
| |
| // Pause tracking of all old devices. |
| for (auto& capture : captures_) { |
| if (capture.first.label == label) { |
| capture.second->observer = nullptr; |
| } |
| } |
| |
| // Start tracking any new devices; resume tracking of changed devices. |
| const blink::mojom::StreamDevices& new_devices = |
| *new_stream_devices_set->stream_devices[0]; |
| if (new_devices.audio_device.has_value()) { |
| OnTabCaptureStarted(label, new_devices.audio_device.value(), capturer, |
| handle_change_callback); |
| } |
| if (new_devices.video_device.has_value()) { |
| OnTabCaptureStarted(label, new_devices.video_device.value(), capturer, |
| handle_change_callback); |
| } |
| |
| // Forget any old device which was not in |new_devices|. |
| for (auto iter = captures_.begin(); iter != captures_.end();) { |
| if (!iter->second->observer) { |
| iter = captures_.erase(iter); |
| } else { |
| ++iter; |
| } |
| } |
| } |
| |
| void CaptureHandleManager::OnCaptureHandleConfigUpdate( |
| const std::string& label, |
| blink::mojom::MediaStreamType type, |
| media::mojom::CaptureHandlePtr capture_handle) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto iter = captures_.find({label, type}); |
| if (iter == captures_.end()) { |
| DVLOG(1) << "Unknown session."; |
| return; |
| } |
| |
| const CaptureKey& key = iter->first; |
| CaptureInfo& info = *iter->second; |
| |
| if (IsEqual(capture_handle, info.last_capture_handle)) { |
| // Nothing has changed -> do not report. This avoids exposing navigation |
| // between non-exposing sites to a potentially malicious render process. |
| return; |
| } |
| |
| iter->second->last_capture_handle = capture_handle.Clone(); |
| |
| info.callback.Run(key.label, key.type, std::move(capture_handle)); |
| } |
| |
| } // namespace content |