blob: 40f9025f98f63d229db0a1db259da8370eacf22f [file] [log] [blame]
// 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