blob: 7f782ce33ac70b8729edcf875b1d0672c9ee7217 [file] [log] [blame]
// Copyright 2013 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/renderer_host/media/media_stream_ui_proxy.h"
#include <utility>
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.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_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/common/content_switches.h"
#include "media/capture/video/fake_video_capture_device.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
bool IsFeatureEnabled(RenderFrameHost* rfh,
bool tests_use_fake_render_frame_hosts,
network::mojom::PermissionsPolicyFeature feature) {
// Some tests don't (or can't) set up the RenderFrameHost. In these cases we
// just ignore permissions policy checks (there is no permissions policy to
// test).
if (!rfh && tests_use_fake_render_frame_hosts)
return true;
if (!rfh)
return false;
return rfh->IsFeatureEnabled(feature);
}
class MediaStreamUIProxy::Core {
public:
explicit Core(const base::WeakPtr<MediaStreamUIProxy>& proxy,
RenderFrameHostDelegate* test_render_delegate);
Core(const Core&) = delete;
Core& operator=(const Core&) = delete;
~Core();
void RequestAccess(std::unique_ptr<MediaStreamRequest> request);
void RequestSelectAudioOutput(
std::unique_ptr<SelectAudioOutputRequest> request,
SelectAudioOutputCallback callback);
void OnStarted(gfx::NativeViewId* window_id,
bool has_source_callback,
const std::string& label,
std::vector<DesktopMediaID> screen_capture_ids);
void OnDeviceStopped(const std::string& label,
const DesktopMediaID& media_id);
void OnDeviceStoppedForSourceChange(const std::string& label,
const DesktopMediaID& old_media_id,
const DesktopMediaID& new_media_id,
bool captured_surface_control_active);
void OnRegionCaptureRectChanged(
const std::optional<gfx::Rect>& region_capture_rect);
void SetFocus(const DesktopMediaID& media_id,
bool focus,
bool is_from_microtask,
bool is_from_timer);
// The type blink::mojom::StreamDevices is not movable, therefore stream
// devices cannot be captured for usage with PostTask.
void ProcessAccessRequestResponseForPostTask(
int render_process_id,
int render_frame_id,
blink::mojom::StreamDevicesSetPtr stream_devices_set_ptr,
blink::mojom::MediaStreamRequestResult result,
std::unique_ptr<MediaStreamUI> stream_ui);
void ProcessAccessRequestResponse(
int render_process_id,
int render_frame_id,
const blink::mojom::StreamDevicesSet& stream_devices_set,
blink::mojom::MediaStreamRequestResult result,
std::unique_ptr<MediaStreamUI> stream_ui);
base::WeakPtr<Core> GetWeakPtr() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// This weak pointer is created in the ctor, which runs on the IO thread.
// This pointer is always posted from the IO thread to the UI thread,
// meaning reading |weak_this_| happens on the IO thead, but dereferencing
// the actual pointer happens in the UI thread. Invalidation happens in the
// destructor, which runs on the UI thread, so this is safe.
return weak_this_;
}
private:
friend class FakeMediaStreamUIProxy;
void ProcessStopRequestFromUI();
void ProcessChangeSourceRequestFromUI(const DesktopMediaID& media_id,
bool captured_surface_control_active);
void ProcessStateChangeFromUI(const DesktopMediaID& media,
blink::mojom::MediaStreamStateChange);
RenderFrameHostDelegate* GetRenderFrameHostDelegate(int render_process_id,
int render_frame_id);
base::WeakPtr<MediaStreamUIProxy> proxy_;
std::unique_ptr<MediaStreamUI> ui_;
bool tests_use_fake_render_frame_hosts_;
const raw_ptr<RenderFrameHostDelegate> test_render_delegate_;
base::WeakPtr<Core> weak_this_;
base::WeakPtrFactory<Core> weak_factory_{this};
// Used for calls supplied to `ui_`. Invalidated every time a new UI is
// created.
base::WeakPtrFactory<Core> weak_factory_for_ui_{this};
};
MediaStreamUIProxy::Core::Core(const base::WeakPtr<MediaStreamUIProxy>& proxy,
RenderFrameHostDelegate* test_render_delegate)
: proxy_(proxy),
tests_use_fake_render_frame_hosts_(test_render_delegate != nullptr),
test_render_delegate_(test_render_delegate) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
weak_this_ = weak_factory_.GetWeakPtr();
}
MediaStreamUIProxy::Core::~Core() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
void MediaStreamUIProxy::Core::RequestAccess(
std::unique_ptr<MediaStreamRequest> request) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RenderFrameHostDelegate* render_delegate = GetRenderFrameHostDelegate(
request->render_process_id, request->render_frame_id);
// Tab may have gone away, or has no delegate from which to request access.
if (!render_delegate) {
ProcessAccessRequestResponse(
request->render_process_id, request->render_frame_id,
blink::mojom::StreamDevicesSet(),
blink::mojom::MediaStreamRequestResult::FAILED_DUE_TO_SHUTDOWN,
std::unique_ptr<MediaStreamUI>());
return;
}
RenderFrameHostImpl* host = RenderFrameHostImpl::FromID(
request->render_process_id, request->render_frame_id);
render_delegate->RequestMediaAccessPermission(
host, *request,
base::BindOnce(&Core::ProcessAccessRequestResponse, weak_this_,
request->render_process_id, request->render_frame_id));
}
void MediaStreamUIProxy::Core::RequestSelectAudioOutput(
std::unique_ptr<SelectAudioOutputRequest> request,
SelectAudioOutputCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
RenderFrameHostDelegate* render_delegate = GetRenderFrameHostDelegate(
request->render_frame_host_id().child_id,
request->render_frame_host_id().frame_routing_id);
if (!render_delegate) {
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(callback),
base::unexpected(
SelectAudioOutputError::kNotSupported)));
return;
}
render_delegate->ProcessSelectAudioOutput(*request, std::move(callback));
}
void MediaStreamUIProxy::Core::OnStarted(
gfx::NativeViewId* window_id,
bool has_source_callback,
const std::string& label,
std::vector<DesktopMediaID> screen_share_ids) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!ui_)
return;
MediaStreamUI::SourceCallback device_change_cb;
if (has_source_callback) {
device_change_cb =
base::BindRepeating(&Core::ProcessChangeSourceRequestFromUI,
weak_factory_for_ui_.GetWeakPtr());
}
*window_id =
ui_->OnStarted(base::BindRepeating(&Core::ProcessStopRequestFromUI,
weak_factory_for_ui_.GetWeakPtr()),
device_change_cb, label, screen_share_ids,
base::BindRepeating(&Core::ProcessStateChangeFromUI,
weak_factory_for_ui_.GetWeakPtr()));
}
void MediaStreamUIProxy::Core::OnDeviceStopped(const std::string& label,
const DesktopMediaID& media_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ui_) {
ui_->OnDeviceStopped(label, media_id);
}
}
void MediaStreamUIProxy::Core::OnDeviceStoppedForSourceChange(
const std::string& label,
const DesktopMediaID& old_media_id,
const DesktopMediaID& new_media_id,
bool captured_surface_control_active) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ui_) {
ui_->OnDeviceStoppedForSourceChange(label, old_media_id, new_media_id,
captured_surface_control_active);
ui_->OnDeviceStopped(label, old_media_id);
}
}
void MediaStreamUIProxy::Core::OnRegionCaptureRectChanged(
const std::optional<gfx::Rect>& region_capture_rec) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ui_) {
ui_->OnRegionCaptureRectChanged(region_capture_rec);
}
}
void MediaStreamUIProxy::Core::SetFocus(const DesktopMediaID& media_id,
bool focus,
bool is_from_microtask,
bool is_from_timer) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ui_) {
ui_->SetFocus(media_id, focus, is_from_microtask, is_from_timer);
}
}
void MediaStreamUIProxy::Core::ProcessAccessRequestResponseForPostTask(
int render_process_id,
int render_frame_id,
blink::mojom::StreamDevicesSetPtr stream_devices_set_ptr,
blink::mojom::MediaStreamRequestResult result,
std::unique_ptr<MediaStreamUI> stream_ui) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(stream_devices_set_ptr);
ProcessAccessRequestResponse(render_process_id, render_frame_id,
*stream_devices_set_ptr, result,
std::move(stream_ui));
}
void MediaStreamUIProxy::Core::ProcessAccessRequestResponse(
int render_process_id,
int render_frame_id,
const blink::mojom::StreamDevicesSet& stream_devices_set,
blink::mojom::MediaStreamRequestResult result,
std::unique_ptr<MediaStreamUI> stream_ui) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK((result != blink::mojom::MediaStreamRequestResult::OK &&
stream_devices_set.stream_devices.empty()) ||
(result == blink::mojom::MediaStreamRequestResult::OK &&
stream_devices_set.stream_devices.size() == 1u));
blink::mojom::StreamDevicesSetPtr filtered_devices_set =
blink::mojom::StreamDevicesSet::New();
blink::mojom::StreamDevices devices;
if (!stream_devices_set.stream_devices.empty()) {
devices = *stream_devices_set.stream_devices[0];
filtered_devices_set->stream_devices.emplace_back(
blink::mojom::StreamDevices::New());
}
auto* host = RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
if (devices.audio_device.has_value()) {
const blink::MediaStreamDevice& audio_device = devices.audio_device.value();
if (audio_device.type !=
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE ||
IsFeatureEnabled(
host, tests_use_fake_render_frame_hosts_,
network::mojom::PermissionsPolicyFeature::kMicrophone)) {
filtered_devices_set->stream_devices[0]->audio_device = audio_device;
}
}
if (devices.video_device.has_value()) {
const blink::MediaStreamDevice& video_device = devices.video_device.value();
if (video_device.type !=
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE ||
IsFeatureEnabled(host, tests_use_fake_render_frame_hosts_,
network::mojom::PermissionsPolicyFeature::kCamera)) {
filtered_devices_set->stream_devices[0]->video_device = video_device;
}
}
if ((filtered_devices_set->stream_devices.empty() ||
(!filtered_devices_set->stream_devices[0]->audio_device.has_value() &&
!filtered_devices_set->stream_devices[0]->video_device.has_value())) &&
result == blink::mojom::MediaStreamRequestResult::OK) {
result = blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED;
filtered_devices_set->stream_devices.clear();
}
if (stream_ui) {
// Callbacks that were supplied to the existing `ui_` are no longer
// applicable. This is important as some implementions (TabSharingUIViews)
// always run the callback when destroyed. However at the point the UI is
// replaced while the screencast is ongoing. Invalidating ensures the
// screencast is not terminated. See crbug.com/1155426 for details.
weak_factory_for_ui_.InvalidateWeakPtrs();
ui_ = std::move(stream_ui);
}
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamUIProxy::ProcessAccessRequestResponse, proxy_,
std::move(filtered_devices_set), result));
}
void MediaStreamUIProxy::Core::ProcessStopRequestFromUI() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamUIProxy::ProcessStopRequestFromUI, proxy_));
}
void MediaStreamUIProxy::Core::ProcessChangeSourceRequestFromUI(
const DesktopMediaID& media_id,
bool captured_surface_control_active) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaStreamUIProxy::ProcessChangeSourceRequestFromUI,
proxy_, media_id, captured_surface_control_active));
}
void MediaStreamUIProxy::Core::ProcessStateChangeFromUI(
const DesktopMediaID& media_id,
blink::mojom::MediaStreamStateChange new_state) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&MediaStreamUIProxy::ProcessStateChangeFromUI,
proxy_, media_id, new_state));
}
RenderFrameHostDelegate* MediaStreamUIProxy::Core::GetRenderFrameHostDelegate(
int render_process_id,
int render_frame_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (test_render_delegate_)
return test_render_delegate_;
RenderFrameHostImpl* host =
RenderFrameHostImpl::FromID(render_process_id, render_frame_id);
return host ? host->delegate() : nullptr;
}
// static
std::unique_ptr<MediaStreamUIProxy> MediaStreamUIProxy::Create() {
return std::unique_ptr<MediaStreamUIProxy>(new MediaStreamUIProxy(nullptr));
}
// static
std::unique_ptr<MediaStreamUIProxy> MediaStreamUIProxy::CreateForTests(
RenderFrameHostDelegate* render_delegate) {
return std::unique_ptr<MediaStreamUIProxy>(
new MediaStreamUIProxy(render_delegate));
}
MediaStreamUIProxy::MediaStreamUIProxy(
RenderFrameHostDelegate* test_render_delegate) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
core_.reset(new Core(weak_factory_.GetWeakPtr(), test_render_delegate));
}
MediaStreamUIProxy::~MediaStreamUIProxy() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
}
void MediaStreamUIProxy::RequestAccess(
std::unique_ptr<MediaStreamRequest> request,
ResponseCallback response_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
response_callback_ = std::move(response_callback);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Core::RequestAccess, core_->GetWeakPtr(),
std::move(request)));
}
void MediaStreamUIProxy::RequestSelectAudioOutput(
std::unique_ptr<SelectAudioOutputRequest> request,
SelectAudioOutputCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&Core::RequestSelectAudioOutput, core_->GetWeakPtr(),
std::move(request), std::move(callback)));
}
void MediaStreamUIProxy::OnStarted(
base::OnceClosure stop_callback,
MediaStreamUI::SourceCallback source_callback,
WindowIdCallback window_id_callback,
const std::string& label,
std::vector<DesktopMediaID> screen_share_ids,
MediaStreamUI::StateChangeCallback state_change_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
stop_callback_ = std::move(stop_callback);
source_callback_ = std::move(source_callback);
state_change_callback_ = std::move(state_change_callback);
// Owned by the PostTaskAndReply callback.
gfx::NativeViewId* window_id = new gfx::NativeViewId(0);
GetUIThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&Core::OnStarted, core_->GetWeakPtr(), window_id,
!!source_callback_, label, screen_share_ids),
base::BindOnce(&MediaStreamUIProxy::OnWindowId,
weak_factory_.GetWeakPtr(), std::move(window_id_callback),
base::Owned(window_id)));
}
void MediaStreamUIProxy::OnDeviceStopped(const std::string& label,
const DesktopMediaID& media_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Core::OnDeviceStopped, core_->GetWeakPtr(),
label, media_id));
}
void MediaStreamUIProxy::OnDeviceStoppedForSourceChange(
const std::string& label,
const DesktopMediaID& old_media_id,
const DesktopMediaID& new_media_id,
bool captured_surface_control_active) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Core::OnDeviceStoppedForSourceChange,
core_->GetWeakPtr(), label, old_media_id,
new_media_id, captured_surface_control_active));
}
void MediaStreamUIProxy::OnRegionCaptureRectChanged(
const std::optional<gfx::Rect>& region_capture_rec) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Core::OnRegionCaptureRectChanged,
core_->GetWeakPtr(), region_capture_rec));
}
void MediaStreamUIProxy::SetFocus(const DesktopMediaID& media_id,
bool focus,
bool is_from_microtask,
bool is_from_timer) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&Core::SetFocus, core_->GetWeakPtr(), media_id,
focus, is_from_microtask, is_from_timer));
}
void MediaStreamUIProxy::ProcessAccessRequestResponse(
blink::mojom::StreamDevicesSetPtr stream_devices_set,
blink::mojom::MediaStreamRequestResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(!response_callback_.is_null());
std::move(response_callback_).Run(*stream_devices_set, result);
}
void MediaStreamUIProxy::ProcessStopRequestFromUI() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Careful when changing the following lines: upstream, this function is
// wrapped into a RepeatingClosure, which allows duplicating it and enabling
// multiple potentital sources to stop the stream; however only the first
// invocation should actually stop the stream.
if (stop_callback_)
std::move(stop_callback_).Run();
}
void MediaStreamUIProxy::ProcessChangeSourceRequestFromUI(
const DesktopMediaID& media_id,
bool captured_surface_control_active) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (source_callback_)
source_callback_.Run(media_id, captured_surface_control_active);
}
void MediaStreamUIProxy::ProcessStateChangeFromUI(
const DesktopMediaID& media_id,
blink::mojom::MediaStreamStateChange new_state) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (state_change_callback_)
state_change_callback_.Run(media_id, new_state);
}
void MediaStreamUIProxy::OnWindowId(WindowIdCallback window_id_callback,
gfx::NativeViewId* window_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!window_id_callback.is_null())
std::move(window_id_callback).Run(*window_id);
}
FakeMediaStreamUIProxy::FakeMediaStreamUIProxy(
bool tests_use_fake_render_frame_hosts)
: MediaStreamUIProxy(nullptr) {
core_->tests_use_fake_render_frame_hosts_ = tests_use_fake_render_frame_hosts;
}
FakeMediaStreamUIProxy::~FakeMediaStreamUIProxy() = default;
void FakeMediaStreamUIProxy::AddAvailableDevices(
const blink::MediaStreamDevices& devices) {
devices_.insert(devices_.end(), devices.begin(), devices.end());
}
void FakeMediaStreamUIProxy::SetMicAccess(bool access) {
mic_access_ = access;
}
void FakeMediaStreamUIProxy::SetCameraAccess(bool access) {
camera_access_ = access;
}
void FakeMediaStreamUIProxy::SetAudioShare(bool audio_share) {
audio_share_ = audio_share;
}
void FakeMediaStreamUIProxy::RequestAccess(
std::unique_ptr<MediaStreamRequest> request,
ResponseCallback response_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
response_callback_ = std::move(response_callback);
if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kUseFakeUIForMediaStream) == "deny") {
// Immediately deny the request.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&MediaStreamUIProxy::Core::ProcessAccessRequestResponseForPostTask,
core_->GetWeakPtr(), request->render_process_id,
request->render_frame_id, blink::mojom::StreamDevicesSet::New(),
blink::mojom::MediaStreamRequestResult::PERMISSION_DENIED,
std::unique_ptr<MediaStreamUI>()));
return;
}
// Use the first capture device of the same media type in the list for the
// fake UI.
blink::mojom::StreamDevicesSetPtr devices_set_to_use =
blink::mojom::StreamDevicesSet::New();
devices_set_to_use->stream_devices.emplace_back(
blink::mojom::StreamDevices::New());
blink::mojom::StreamDevices& devices_to_use =
*devices_set_to_use->stream_devices[0];
for (const blink::MediaStreamDevice& device : devices_) {
if (!devices_to_use.audio_device.has_value() &&
blink::IsAudioInputMediaType(request->audio_type) &&
blink::IsAudioInputMediaType(device.type) &&
(request->requested_audio_device_ids.empty() ||
request->requested_audio_device_ids.front().empty() ||
request->requested_audio_device_ids.front() == device.id)) {
devices_to_use.audio_device = device;
} else if (!devices_to_use.video_device.has_value() &&
blink::IsVideoInputMediaType(request->video_type) &&
blink::IsVideoInputMediaType(device.type) &&
(request->requested_video_device_ids.empty() ||
request->requested_video_device_ids.front().empty() ||
request->requested_video_device_ids.front() == device.id)) {
devices_to_use.video_device = device;
}
}
// Fail the request if a device doesn't exist for the requested type.
if ((request->audio_type != blink::mojom::MediaStreamType::NO_SERVICE &&
!devices_to_use.audio_device.has_value()) ||
(request->video_type != blink::mojom::MediaStreamType::NO_SERVICE &&
!devices_to_use.video_device.has_value())) {
devices_to_use = blink::mojom::StreamDevices();
}
if (!audio_share_) {
devices_to_use.audio_device = std::nullopt;
}
const bool is_devices_empty = !devices_to_use.audio_device.has_value() &&
!devices_to_use.video_device.has_value();
if (is_devices_empty) {
devices_set_to_use->stream_devices.clear();
}
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&MediaStreamUIProxy::Core::ProcessAccessRequestResponseForPostTask,
core_->GetWeakPtr(), request->render_process_id,
request->render_frame_id, std::move(devices_set_to_use),
is_devices_empty ? blink::mojom::MediaStreamRequestResult::NO_HARDWARE
: blink::mojom::MediaStreamRequestResult::OK,
std::unique_ptr<MediaStreamUI>()));
}
void FakeMediaStreamUIProxy::RequestSelectAudioOutput(
std::unique_ptr<SelectAudioOutputRequest> request,
SelectAudioOutputCallback callback) {
std::move(callback).Run(
base::unexpected(content::SelectAudioOutputError::kNoPermission));
}
void FakeMediaStreamUIProxy::OnStarted(
base::OnceClosure stop_callback,
MediaStreamUI::SourceCallback source_callback,
WindowIdCallback window_id_callback,
const std::string& label,
std::vector<DesktopMediaID> screen_share_ids,
MediaStreamUI::StateChangeCallback state_change_callback) {}
void FakeMediaStreamUIProxy::OnDeviceStopped(const std::string& label,
const DesktopMediaID& media_id) {}
void FakeMediaStreamUIProxy::OnDeviceStoppedForSourceChange(
const std::string& label,
const DesktopMediaID& old_media_id,
const DesktopMediaID& new_media_id,
bool captured_surface_control_active) {}
} // namespace content