blob: 16b98ddcca6a8cba25f9d87082742eed6ccebad2 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/webrtc/media_stream_devices_controller.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "components/webrtc/media_stream_devices_util.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_permission_controller.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#if BUILDFLAG(IS_ANDROID)
#include "content/public/browser/render_widget_host_view.h"
#include "ui/android/window_android.h"
#endif
using testing::_;
namespace {
blink::MediaStreamDevices CreateFakeDevices(
blink::mojom::MediaStreamType type) {
blink::MediaStreamDevices devices;
devices.reserve(3);
for (size_t i = 0; i < devices.capacity(); ++i) {
devices.emplace_back(type, "id_" + base::NumberToString(i),
"name " + base::NumberToString(i));
}
return devices;
}
class FakeEnumerator : public webrtc::MediaStreamDeviceEnumeratorImpl {
public:
FakeEnumerator()
: audio_capture_devices_(CreateFakeDevices(
blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)),
video_capture_devices_(CreateFakeDevices(
blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)) {}
const blink::MediaStreamDevices& GetAudioCaptureDevices() const override {
return audio_capture_devices_;
}
const blink::MediaStreamDevices& GetVideoCaptureDevices() const override {
return video_capture_devices_;
}
const std::optional<blink::MediaStreamDevice>
GetPreferredAudioDeviceForBrowserContext(
content::BrowserContext* context,
const std::vector<std::string>& eligible_device_ids) const override {
auto devices = GetAudioCaptureDevices();
if (!eligible_device_ids.empty()) {
devices = webrtc::FilterMediaDevices(devices, eligible_device_ids);
}
if (devices.empty()) {
return std::nullopt;
}
if (!eligible_device_ids.empty()) {
return devices.back();
} else {
return devices.front();
}
}
const std::optional<blink::MediaStreamDevice>
GetPreferredVideoDeviceForBrowserContext(
content::BrowserContext* context,
const std::vector<std::string>& eligible_device_ids) const override {
auto devices = GetVideoCaptureDevices();
if (!eligible_device_ids.empty()) {
devices = webrtc::FilterMediaDevices(devices, eligible_device_ids);
}
if (devices.empty()) {
return std::nullopt;
}
if (!eligible_device_ids.empty()) {
return devices.back();
} else {
return devices.front();
}
}
private:
const blink::MediaStreamDevices audio_capture_devices_;
const blink::MediaStreamDevices video_capture_devices_;
};
class MockPermissionController : public content::MockPermissionController {
public:
MOCK_METHOD(
void,
RequestPermissionsFromCurrentDocument,
(content::RenderFrameHost * render_frame_host,
content::PermissionRequestDescription request_description,
base::OnceCallback<
void(const std::vector<blink::mojom::PermissionStatus>&)> callback));
};
std::vector<std::string> GetIds(const blink::MediaStreamDevices& devices,
size_t start_index) {
CHECK_LT(start_index, devices.size());
std::vector<std::string> device_ids;
std::transform(devices.begin() + start_index, devices.end(),
std::back_inserter(device_ids),
[](const auto& device) { return device.id; });
return device_ids;
}
} // namespace
class MediaStreamDevicesControllerTest : public testing::Test {
void SetUp() override { InitializeWebContents(); }
void InitializeWebContents() {
browser_context_.SetPermissionControllerForTesting(
std::make_unique<MockPermissionController>());
web_contents_ =
test_web_contents_factory_.CreateWebContents(&browser_context_);
render_frame_host_ = web_contents_->GetPrimaryMainFrame();
render_frame_host_id_ = render_frame_host_->GetGlobalId();
content::OverrideLastCommittedOrigin(render_frame_host_, origin_);
#if BUILDFLAG(IS_ANDROID)
// Create a scoped window so that
// WebContents::GetNativeView()->GetWindowAndroid() does not return
// null.
window_ = ui::WindowAndroid::CreateForTesting();
window_.get()->get()->AddChild(web_contents_->GetNativeView());
web_contents_->GetRenderWidgetHostView()->Show();
#endif
}
protected:
std::tuple<blink::mojom::MediaStreamRequestResult,
blink::mojom::StreamDevicesPtr>
MakeRequest(blink::MediaStreamRequestType request_type,
const std::vector<std::string>& requested_audio_device_ids,
const std::vector<std::string>& requested_video_device_ids,
bool request_audio,
bool request_video) {
auto* mock_permission_controller = static_cast<MockPermissionController*>(
browser_context_.GetPermissionController());
ON_CALL(*mock_permission_controller, GetPermissionResultForCurrentDocument)
.WillByDefault([](blink::PermissionType permission,
content::RenderFrameHost* render_frame_host) {
return content::PermissionResult{
content::PermissionStatus::GRANTED,
content::PermissionStatusSource::UNSPECIFIED,
};
});
std::vector<blink::PermissionType> expected_permissions;
if (request_audio) {
expected_permissions.push_back(blink::PermissionType::AUDIO_CAPTURE);
}
if (request_video) {
expected_permissions.push_back(blink::PermissionType::VIDEO_CAPTURE);
}
content::PermissionRequestDescription expected_description{
std::move(expected_permissions), false};
expected_description.requested_audio_capture_device_ids =
requested_audio_device_ids;
expected_description.requested_video_capture_device_ids =
requested_video_device_ids;
EXPECT_CALL(*mock_permission_controller,
RequestPermissionsFromCurrentDocument(render_frame_host_.get(),
expected_description, _))
.WillOnce(
[](auto*, auto,
base::OnceCallback<void(
const std::vector<content::PermissionStatus>&)> callback) {
std::move(callback).Run({content::PermissionStatus::GRANTED,
content::PermissionStatus::GRANTED});
});
base::test::TestFuture<blink::mojom::MediaStreamRequestResult,
blink::mojom::StreamDevicesPtr>
result_future;
webrtc::MediaStreamDevicesController::RequestPermissions(
content::MediaStreamRequest{
/*render_process_id=*/render_frame_host_id_.child_id,
/*render_frame_id=*/render_frame_host_id_.frame_routing_id,
/*page_request_id=*/0,
/*url_origin=*/origin_,
/*user_gesture=*/false,
/*request_type=*/
request_type,
/*requested_audio_device_ids=*/requested_audio_device_ids,
/*requested_video_device_ids=*/requested_video_device_ids,
request_audio ? blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE
: blink::mojom::MediaStreamType::NO_SERVICE,
request_video ? blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE
: blink::mojom::MediaStreamType::NO_SERVICE,
/*disable_local_echo=*/false,
/*request_pan_tilt_zoom_permission=*/false,
/*captured_surface_control_active=*/false,
},
&enumerator_,
base::BindLambdaForTesting(
[&](const blink::mojom::StreamDevicesSet& stream_devices_set,
blink::mojom::MediaStreamRequestResult result,
bool blocked_by_permissions_policy,
ContentSetting audio_setting, ContentSetting video_setting) {
CHECK_EQ(stream_devices_set.stream_devices.size(), 1u);
result_future.SetValue(
result, stream_devices_set.stream_devices[0]->Clone());
}));
return result_future.Take();
}
content::BrowserTaskEnvironment task_environment_;
FakeEnumerator enumerator_;
content::TestBrowserContext browser_context_;
content::TestWebContentsFactory test_web_contents_factory_;
raw_ptr<content::WebContents> web_contents_;
raw_ptr<content::RenderFrameHost> render_frame_host_;
content::GlobalRenderFrameHostId render_frame_host_id_;
const url::Origin origin_ = url::Origin::Create(GURL{"https://stuff.com"});
#if BUILDFLAG(IS_ANDROID)
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window_;
#endif
};
TEST_F(MediaStreamDevicesControllerTest,
RequestPermissions_MEDIA_GENERATE_STREAM) {
// Request all but the first device to exercise filtering.
const auto kRequestedAudioCaptureDeviceIds =
GetIds(enumerator_.GetAudioCaptureDevices(), /*start_index=*/1);
const auto kRequestedVideoCaptureDeviceIds =
GetIds(enumerator_.GetVideoCaptureDevices(), /*start_index=*/1);
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_GENERATE_STREAM,
kRequestedAudioCaptureDeviceIds, kRequestedVideoCaptureDeviceIds,
/*request_audio=*/true, /*request_video=*/true);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->audio_device.has_value());
EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
enumerator_.GetAudioCaptureDevices().back()));
ASSERT_TRUE(stream_devices->video_device.has_value());
EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
enumerator_.GetVideoCaptureDevices().back()));
}
TEST_F(MediaStreamDevicesControllerTest,
RequestPermissions_MEDIA_DEVICE_ACCESS) {
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_DEVICE_ACCESS,
/*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
/*request_audio=*/true, /*request_video=*/false);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->audio_device.has_value());
EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
enumerator_.GetAudioCaptureDevices().front()));
ASSERT_FALSE(stream_devices->video_device.has_value());
}
TEST_F(
MediaStreamDevicesControllerTest,
RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Audio_UsesDefualtWhenNoRequestedDevices) {
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
/*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
/*request_audio=*/true, /*request_video=*/false);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->audio_device.has_value());
EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
enumerator_.GetAudioCaptureDevices().front()));
ASSERT_FALSE(stream_devices->video_device.has_value());
}
TEST_F(MediaStreamDevicesControllerTest,
RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Audio_UsesRequestedDevices) {
// Request all but the first device to exercise filtering.
const auto kRequestedAudioCaptureDeviceIds =
GetIds(enumerator_.GetAudioCaptureDevices(), /*start_index=*/1);
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
kRequestedAudioCaptureDeviceIds, /*requested_video_device_ids=*/{},
/*request_audio=*/true, /*request_video=*/false);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->audio_device.has_value());
EXPECT_TRUE(stream_devices->audio_device->IsSameDevice(
enumerator_.GetAudioCaptureDevices().back()));
ASSERT_FALSE(stream_devices->video_device.has_value());
}
TEST_F(
MediaStreamDevicesControllerTest,
RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Video_UsesDefualtWhenNoRequestedDevices) {
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
/*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
/*request_audio=*/false, /*request_video=*/true);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->video_device.has_value());
EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
enumerator_.GetVideoCaptureDevices().front()));
ASSERT_FALSE(stream_devices->audio_device.has_value());
}
TEST_F(MediaStreamDevicesControllerTest,
RequestPermissions_OPEN_DEVICE_PEPPER_ONLY_Video_UsesRequestedDevices) {
// Request all but the first device to exercise filtering.
const auto kRequestedVideoCaptureDeviceIds =
GetIds(enumerator_.GetVideoCaptureDevices(), /*start_index=*/1);
auto [result, stream_devices] = MakeRequest(
blink::MediaStreamRequestType::MEDIA_OPEN_DEVICE_PEPPER_ONLY,
/*requested_audio_device_ids=*/{},
/*requested_video_device_ids=*/kRequestedVideoCaptureDeviceIds,
/*request_audio=*/false, /*request_video=*/true);
ASSERT_EQ(result, blink::mojom::MediaStreamRequestResult::OK);
ASSERT_TRUE(stream_devices->video_device.has_value());
EXPECT_TRUE(stream_devices->video_device->IsSameDevice(
enumerator_.GetVideoCaptureDevices().back()));
ASSERT_FALSE(stream_devices->audio_device.has_value());
}