blob: 98293cd3ed671ec5625a3c76d75ad4e08e1be3a0 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/mediastream/media_devices.h"
#include <memory>
#include <utility>
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "media/base/media_permission.h"
#include "media/base/output_device_info.h"
#include "media/base/video_types.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video/video_capture_device_descriptor.h"
#include "media/capture/video_capture_types.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/mojom/media/capture_handle_config.mojom-blink.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-blink.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_boolean_string.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_output_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_capture_handle_config.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_crop_target.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_double_range.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_long_range.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_device_info.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_device_kind.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_capabilities.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_restriction_target.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_boolean_mediatrackconstraints.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_user_media_stream_constraints.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/events/event_listener.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/modules/mediastream/crop_target.h"
#include "third_party/blink/renderer/modules/mediastream/input_device_info.h"
#include "third_party/blink/renderer/modules/mediastream/media_device_info.h"
#include "third_party/blink/renderer/modules/mediastream/media_permission_testing_platform.h"
#include "third_party/blink/renderer/modules/mediastream/restriction_target.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "ui/gfx/geometry/mojom/geometry.mojom.h"
namespace blink {
using ::blink::mojom::blink::MediaDeviceInfoPtr;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::StrictMock;
using MediaDeviceType = ::blink::mojom::MediaDeviceType;
namespace {
constexpr char kInvalidSinkId[] = "invalid_sink_id";
constexpr char kValidSinkId[] = "valid_sink_id";
String MaxLengthCaptureHandle() {
String maxHandle = "0123456789abcdef"; // 16 characters.
while (maxHandle.length() < 1024) {
maxHandle = maxHandle + maxHandle;
}
CHECK_EQ(maxHandle.length(), 1024u) << "Malformed test.";
return maxHandle;
}
class MockMediaDevicesDispatcherHost final
: public mojom::blink::MediaDevicesDispatcherHost {
public:
MockMediaDevicesDispatcherHost()
: enumeration_({
// clang-format off
{
{"fake_audio_input_1", "Fake Audio Input 1", "common_group_1"},
{"fake_audio_input_2", "Fake Audio Input 2", "common_group_2"},
{"fake_audio_input_3", "Fake Audio Input 3", "audio_input_group"},
}, {
{"fake_video_input_1", "Fake Video Input 1", "common_group_1",
media::VideoCaptureControlSupport(),
blink::mojom::FacingMode::kNone,
media::CameraAvailability::kAvailable},
{"fake_video_input_2", "Fake Video Input 2", "video_input_group",
media::VideoCaptureControlSupport(),
blink::mojom::FacingMode::kUser, std::nullopt},
{"fake_video_input_3", "Fake Video Input 3", "video_input_group 2",
media::VideoCaptureControlSupport(),
blink::mojom::FacingMode::kUser,
media::CameraAvailability::
kUnavailableExclusivelyUsedByOtherApplication},
},
{
{"fake_audio_output_1", "Fake Audio Output 1", "common_group_1"},
{"fake_audio_putput_2", "Fake Audio Output 2", "common_group_2"},
}
// clang-format on
}) {
mojom::blink::VideoInputDeviceCapabilitiesPtr video_capabilities =
mojom::blink::VideoInputDeviceCapabilities::New();
video_capabilities->device_id = String(enumeration_[1][0].device_id);
video_capabilities->group_id = String(enumeration_[1][0].group_id);
video_capabilities->facing_mode =
enumeration_[1][0].video_facing; // mojom::blink::FacingMode::kNone;
video_capabilities->formats.push_back(media::VideoCaptureFormat(
gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
video_capabilities->availability =
static_cast<media::mojom::CameraAvailability>(
*enumeration_[1][0].availability);
video_input_capabilities_.push_back(std::move(video_capabilities));
video_capabilities = mojom::blink::VideoInputDeviceCapabilities::New();
video_capabilities->device_id = String(enumeration_[1][1].device_id);
video_capabilities->group_id = String(enumeration_[1][1].group_id);
video_capabilities->formats.push_back(media::VideoCaptureFormat(
gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
video_capabilities->facing_mode = enumeration_[1][1].video_facing;
media::VideoCaptureFormat format;
video_input_capabilities_.push_back(std::move(video_capabilities));
video_capabilities = mojom::blink::VideoInputDeviceCapabilities::New();
video_capabilities->device_id = String(enumeration_[1][2].device_id);
video_capabilities->group_id = String(enumeration_[1][2].group_id);
video_capabilities->formats.push_back(media::VideoCaptureFormat(
gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
video_capabilities->formats.push_back(
media::VideoCaptureFormat(gfx::Size(1920, 1080), 60.0,
media::VideoPixelFormat::PIXEL_FORMAT_I420));
video_capabilities->facing_mode = enumeration_[1][2].video_facing;
video_capabilities->availability =
static_cast<media::mojom::CameraAvailability>(
*enumeration_[1][2].availability);
video_input_capabilities_.push_back(std::move(video_capabilities));
mojom::blink::AudioInputDeviceCapabilitiesPtr audio_capabilities =
mojom::blink::AudioInputDeviceCapabilities::New();
audio_capabilities->device_id = String(enumeration_[0][0].device_id);
audio_capabilities->group_id = String(enumeration_[0][0].group_id);
audio_capabilities->parameters =
media::AudioParameters::UnavailableDeviceParams();
audio_capabilities->is_valid = true;
audio_input_capabilities_.push_back(std::move(audio_capabilities));
audio_capabilities = mojom::blink::AudioInputDeviceCapabilities::New();
audio_capabilities->device_id = String(enumeration_[0][1].device_id);
audio_capabilities->group_id = String(enumeration_[0][1].group_id);
audio_capabilities->parameters =
media::AudioParameters::UnavailableDeviceParams();
audio_capabilities->is_valid = true;
audio_input_capabilities_.push_back(std::move(audio_capabilities));
audio_capabilities = mojom::blink::AudioInputDeviceCapabilities::New();
audio_capabilities->device_id = String(enumeration_[0][2].device_id);
audio_capabilities->group_id = String(enumeration_[0][2].group_id);
audio_capabilities->parameters =
media::AudioParameters::UnavailableDeviceParams();
audio_capabilities->parameters.set_effects(
media::AudioParameters::PlatformEffectsMask::ECHO_CANCELLER);
audio_capabilities->is_valid = true;
audio_input_capabilities_.push_back(std::move(audio_capabilities));
}
~MockMediaDevicesDispatcherHost() override {
EXPECT_FALSE(expected_capture_handle_config_);
}
void EnumerateDevices(bool request_audio_input,
bool request_video_input,
bool request_audio_output,
bool request_video_input_capabilities,
bool request_audio_input_capabilities,
EnumerateDevicesCallback callback) override {
Vector<Vector<WebMediaDeviceInfo>> enumeration(static_cast<size_t>(
blink::mojom::blink::MediaDeviceType::kNumMediaDeviceTypes));
Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>
video_input_capabilities;
Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>
audio_input_capabilities;
if (request_audio_input) {
wtf_size_t index = static_cast<wtf_size_t>(
blink::mojom::blink::MediaDeviceType::kMediaAudioInput);
enumeration[index] = enumeration_[index];
if (request_audio_input_capabilities) {
for (const auto& c : audio_input_capabilities_) {
mojom::blink::AudioInputDeviceCapabilitiesPtr capabilities =
mojom::blink::AudioInputDeviceCapabilities::New();
*capabilities = *c;
audio_input_capabilities.push_back(std::move(capabilities));
}
}
}
if (request_video_input) {
wtf_size_t index = static_cast<wtf_size_t>(
blink::mojom::blink::MediaDeviceType::kMediaVideoInput);
enumeration[index] = enumeration_[index];
if (request_video_input_capabilities) {
for (const auto& c : video_input_capabilities_) {
mojom::blink::VideoInputDeviceCapabilitiesPtr capabilities =
mojom::blink::VideoInputDeviceCapabilities::New();
*capabilities = *c;
video_input_capabilities.push_back(std::move(capabilities));
}
}
}
if (request_audio_output) {
wtf_size_t index = static_cast<wtf_size_t>(
blink::mojom::blink::MediaDeviceType::kMediaAudioOutput);
enumeration[index] = enumeration_[index];
}
std::move(callback).Run(std::move(enumeration),
std::move(video_input_capabilities),
std::move(audio_input_capabilities));
}
void SelectAudioOutput(
const String& device_id,
SelectAudioOutputCallback select_audio_output_callback) override {
mojom::blink::SelectAudioOutputResultPtr result =
mojom::blink::SelectAudioOutputResult::New();
if (device_id == "test_device_id") {
result->status = blink::mojom::AudioOutputStatus::kSuccess;
result->device_info.device_id = "test_device_id";
result->device_info.label = "Test Speaker";
result->device_info.group_id = "test_group_id";
} else {
result->status = blink::mojom::AudioOutputStatus::kNoPermission;
}
std::move(select_audio_output_callback).Run(std::move(result));
}
void GetVideoInputCapabilities(GetVideoInputCapabilitiesCallback) override {
NOTREACHED();
}
void GetAllVideoInputDeviceFormats(
const String&,
GetAllVideoInputDeviceFormatsCallback) override {
NOTREACHED();
}
void GetAvailableVideoInputDeviceFormats(
const String&,
GetAvailableVideoInputDeviceFormatsCallback) override {
NOTREACHED();
}
void GetAudioInputCapabilities(GetAudioInputCapabilitiesCallback) override {
NOTREACHED();
}
void AddMediaDevicesListener(
bool subscribe_audio_input,
bool subscribe_video_input,
bool subscribe_audio_output,
mojo::PendingRemote<mojom::blink::MediaDevicesListener> listener)
override {
listener_.Bind(std::move(listener));
}
void SetCaptureHandleConfig(
mojom::blink::CaptureHandleConfigPtr config) override {
CHECK(config);
auto expected_config = std::move(expected_capture_handle_config_);
expected_capture_handle_config_ = nullptr;
CHECK(expected_config);
// TODO(crbug.com/1208868): Define CaptureHandleConfig traits that compare
// |permitted_origins| using SecurityOrigin::IsSameOriginWith(), thereby
// allowing this block to be replaced by a single EXPECT_EQ. (This problem
// only manifests in Blink.)
EXPECT_EQ(config->expose_origin, expected_config->expose_origin);
EXPECT_EQ(config->capture_handle, expected_config->capture_handle);
EXPECT_EQ(config->all_origins_permitted,
expected_config->all_origins_permitted);
CHECK_EQ(config->permitted_origins.size(),
expected_config->permitted_origins.size());
for (wtf_size_t i = 0; i < config->permitted_origins.size(); ++i) {
EXPECT_TRUE(config->permitted_origins[i]->IsSameOriginWith(
expected_config->permitted_origins[i].get()));
}
}
void SetPreferredSinkId(const String& sink_id,
SetPreferredSinkIdCallback callback) override {
if (sink_id == kValidSinkId) {
std::move(callback).Run(
static_cast<media::mojom::blink::OutputDeviceStatus>(
output_device_status_));
} else {
std::move(callback).Run(
static_cast<media::mojom::blink::OutputDeviceStatus>(
media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_FOUND));
}
}
void CloseFocusWindowOfOpportunity(const String& label) override {}
void ProduceSubCaptureTargetId(
SubCaptureTarget::Type type,
ProduceSubCaptureTargetIdCallback callback) override {
auto it = next_ids_.find(type);
if (it == next_ids_.end()) {
GTEST_FAIL();
}
std::vector<String>& queue = it->second;
CHECK(!queue.empty());
String next_id = queue.front();
queue.erase(queue.begin());
std::move(callback).Run(std::move(next_id));
}
void SetNextId(SubCaptureTarget::Type type, String next_id) {
std::vector<String>& queue = next_ids_[type];
queue.push_back(std::move(next_id));
}
void SetOutputDeviceStatus(media::OutputDeviceStatus status) {
output_device_status_ = status;
}
void ExpectSetCaptureHandleConfig(
mojom::blink::CaptureHandleConfigPtr config) {
CHECK(config);
CHECK(!expected_capture_handle_config_) << "Unfulfilled expectation.";
expected_capture_handle_config_ = std::move(config);
}
mojom::blink::CaptureHandleConfigPtr expected_capture_handle_config() {
return std::move(expected_capture_handle_config_);
}
mojo::PendingRemote<mojom::blink::MediaDevicesDispatcherHost>
CreatePendingRemoteAndBind() {
mojo::PendingRemote<mojom::blink::MediaDevicesDispatcherHost> remote;
receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
return remote;
}
void CloseBinding() { receiver_.reset(); }
mojo::Remote<mojom::blink::MediaDevicesListener>& listener() {
return listener_;
}
const Vector<Vector<WebMediaDeviceInfo>>& enumeration() const {
return enumeration_;
}
void NotifyDeviceChanges() {
listener()->OnDevicesChanged(MediaDeviceType::kMediaAudioInput,
enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaAudioInput)]);
listener()->OnDevicesChanged(MediaDeviceType::kMediaVideoInput,
enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaVideoInput)]);
listener()->OnDevicesChanged(MediaDeviceType::kMediaAudioOutput,
enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaAudioOutput)]);
}
Vector<WebMediaDeviceInfo>& AudioInputDevices() {
return enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaAudioInput)];
}
Vector<WebMediaDeviceInfo>& VideoInputDevices() {
return enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaVideoInput)];
}
Vector<WebMediaDeviceInfo>& AudioOutputDevices() {
return enumeration_[static_cast<wtf_size_t>(
MediaDeviceType::kMediaAudioOutput)];
}
const Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>&
VideoInputCapabilities() {
return video_input_capabilities_;
}
const Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>&
AudioInputCapabilities() {
return audio_input_capabilities_;
}
private:
mojo::Remote<mojom::blink::MediaDevicesListener> listener_;
mojo::Receiver<mojom::blink::MediaDevicesDispatcherHost> receiver_{this};
mojom::blink::CaptureHandleConfigPtr expected_capture_handle_config_;
std::map<SubCaptureTarget::Type, std::vector<String>> next_ids_;
media::OutputDeviceStatus output_device_status_ =
media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_OK;
Vector<Vector<WebMediaDeviceInfo>> enumeration_{static_cast<size_t>(
blink::mojom::blink::MediaDeviceType::kNumMediaDeviceTypes)};
Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>
video_input_capabilities_;
Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>
audio_input_capabilities_;
};
class MockDeviceChangeEventListener : public NativeEventListener {
public:
MOCK_METHOD(void, Invoke, (ExecutionContext*, Event*));
};
V8MediaDeviceKind::Enum ToEnum(MediaDeviceType type) {
switch (type) {
case MediaDeviceType::kMediaAudioInput:
return V8MediaDeviceKind::Enum::kAudioinput;
case blink::MediaDeviceType::kMediaVideoInput:
return V8MediaDeviceKind::Enum::kVideoinput;
case blink::MediaDeviceType::kMediaAudioOutput:
return V8MediaDeviceKind::Enum::kAudiooutput;
case blink::MediaDeviceType::kNumMediaDeviceTypes:
break;
}
NOTREACHED();
}
void VerifyFacingMode(const Vector<String>& js_facing_mode,
blink::mojom::FacingMode cpp_facing_mode) {
switch (cpp_facing_mode) {
case blink::mojom::FacingMode::kNone:
EXPECT_TRUE(js_facing_mode.empty());
break;
case blink::mojom::FacingMode::kUser:
EXPECT_THAT(js_facing_mode, ElementsAre("user"));
break;
case blink::mojom::FacingMode::kEnvironment:
EXPECT_THAT(js_facing_mode, ElementsAre("environment"));
break;
case blink::mojom::FacingMode::kLeft:
EXPECT_THAT(js_facing_mode, ElementsAre("left"));
break;
case blink::mojom::FacingMode::kRight:
EXPECT_THAT(js_facing_mode, ElementsAre("right"));
break;
}
}
void VerifyDeviceInfo(const MediaDeviceInfo* device,
const WebMediaDeviceInfo& expected,
MediaDeviceType type) {
EXPECT_EQ(device->deviceId(), String(expected.device_id));
EXPECT_EQ(device->groupId(), String(expected.group_id));
EXPECT_EQ(device->label(), String(expected.label));
EXPECT_EQ(device->kind(), ToEnum(type));
}
void VerifyVideoInputCapabilities(
const MediaDeviceInfo* device,
const WebMediaDeviceInfo& expected_device_info,
const mojom::blink::VideoInputDeviceCapabilitiesPtr&
expected_capabilities) {
CHECK_EQ(device->kind(), V8MediaDeviceKind::Enum::kVideoinput);
const InputDeviceInfo* info = static_cast<const InputDeviceInfo*>(device);
MediaTrackCapabilities* capabilities = info->getCapabilities();
EXPECT_EQ(capabilities->hasFacingMode(), expected_device_info.IsAvailable());
if (capabilities->hasFacingMode()) {
VerifyFacingMode(capabilities->facingMode(),
expected_device_info.video_facing);
}
EXPECT_EQ(capabilities->hasDeviceId(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasGroupId(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasWidth(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasHeight(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasAspectRatio(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasFrameRate(), expected_device_info.IsAvailable());
if (expected_device_info.IsAvailable()) {
int max_expected_width = 0;
int max_expected_height = 0;
float max_expected_frame_rate = 0.0;
for (const auto& format : expected_capabilities->formats) {
max_expected_width =
std::max(max_expected_width, format.frame_size.width());
max_expected_height =
std::max(max_expected_height, format.frame_size.height());
max_expected_frame_rate =
std::max(max_expected_frame_rate, format.frame_rate);
}
EXPECT_EQ(capabilities->deviceId().Utf8(), expected_device_info.device_id);
EXPECT_EQ(capabilities->groupId().Utf8(), expected_device_info.group_id);
EXPECT_EQ(capabilities->width()->min(), 1);
EXPECT_EQ(capabilities->width()->max(), max_expected_width);
EXPECT_EQ(capabilities->height()->min(), 1);
EXPECT_EQ(capabilities->height()->max(), max_expected_height);
EXPECT_EQ(capabilities->aspectRatio()->min(), 1.0 / max_expected_height);
EXPECT_EQ(capabilities->aspectRatio()->max(), max_expected_width);
EXPECT_EQ(capabilities->frameRate()->min(), 1.0);
EXPECT_EQ(capabilities->frameRate()->max(), max_expected_frame_rate);
}
}
EchoCancellationMode ToEchoCancellationMode(
const V8UnionBooleanOrString* value) {
if (value->IsBoolean()) {
return value->GetAsBoolean() ? EchoCancellationMode::kBrowserDecides
: EchoCancellationMode::kDisabled;
}
CHECK(value->IsString());
if (value->GetAsString() == "remote-only") {
return EchoCancellationMode::kRemoteOnly;
}
CHECK_EQ(value->GetAsString(), "all");
return EchoCancellationMode::kAll;
}
void VerifyAudioInputCapabilities(
const MediaDeviceInfo* device,
const WebMediaDeviceInfo& expected_device_info,
const mojom::blink::AudioInputDeviceCapabilitiesPtr&
expected_capabilities) {
CHECK_EQ(device->kind(), V8MediaDeviceKind::Enum::kAudioinput);
const InputDeviceInfo* info = static_cast<const InputDeviceInfo*>(device);
MediaTrackCapabilities* capabilities = info->getCapabilities();
EXPECT_EQ(capabilities->hasDeviceId(), expected_device_info.IsAvailable());
EXPECT_EQ(capabilities->hasGroupId(), expected_device_info.IsAvailable());
if (expected_device_info.IsAvailable()) {
EXPECT_EQ(capabilities->deviceId().Utf8(), expected_device_info.device_id);
EXPECT_EQ(capabilities->groupId().Utf8(), expected_device_info.group_id);
Vector<EchoCancellationMode> echo_cancellation;
for (auto value : capabilities->echoCancellation()) {
echo_cancellation.push_back(ToEchoCancellationMode(value));
}
EXPECT_TRUE(base::Contains(echo_cancellation,
EchoCancellationMode::kBrowserDecides));
EXPECT_TRUE(
base::Contains(echo_cancellation, EchoCancellationMode::kDisabled));
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
EXPECT_TRUE(
base::Contains(echo_cancellation, EchoCancellationMode::kRemoteOnly));
#endif
int effects = expected_capabilities->parameters.effects();
// On some platforms, capabilities are not queried because it is costly.
// In this case, device parameters are unknown. See crbug.com/40945999
if (!base::FeatureList::IsEnabled(
kEnumerateDevicesRequestAudioCapabilities)) {
effects = media::AudioParameters::PlatformEffectsMask::NO_EFFECTS;
}
if (EchoCanceller::IsSystemWideAecAvailable(effects)) {
EXPECT_TRUE(
base::Contains(echo_cancellation, EchoCancellationMode::kAll));
}
}
}
SubCaptureTarget* ToSubCaptureTarget(const blink::ScriptValue& value) {
if (CropTarget* crop_target =
V8CropTarget::ToWrappable(value.GetIsolate(), value.V8Value())) {
return crop_target;
}
if (RestrictionTarget* restriction_target = V8RestrictionTarget::ToWrappable(
value.GetIsolate(), value.V8Value())) {
return restriction_target;
}
NOTREACHED();
}
bool ProduceSubCaptureTargetAndGetPromise(V8TestingScope& scope,
SubCaptureTarget::Type type,
MediaDevices* media_devices,
Element* element) {
switch (type) {
case SubCaptureTarget::Type::kCropTarget:
return !media_devices
->ProduceCropTarget(scope.GetScriptState(), element,
scope.GetExceptionState())
.IsEmpty();
case SubCaptureTarget::Type::kRestrictionTarget:
return !media_devices
->ProduceRestrictionTarget(scope.GetScriptState(), element,
scope.GetExceptionState())
.IsEmpty();
}
}
void ProduceSubCaptureTargetAndGetTester(
V8TestingScope& scope,
SubCaptureTarget::Type type,
MediaDevices* media_devices,
Element* element,
std::optional<ScriptPromiseTester>& tester) {
switch (type) {
case SubCaptureTarget::Type::kCropTarget:
tester.emplace(
scope.GetScriptState(),
media_devices->ProduceCropTarget(scope.GetScriptState(), element,
scope.GetExceptionState()));
return;
case SubCaptureTarget::Type::kRestrictionTarget:
tester.emplace(
scope.GetScriptState(),
media_devices->ProduceRestrictionTarget(
scope.GetScriptState(), element, scope.GetExceptionState()));
return;
}
}
class MockMediaPermission : public media::MediaPermission {
public:
MockMediaPermission() = default;
void HasPermission(Type type,
PermissionStatusCB permission_status_cb) override {
bool has_permission = false;
if (type == Type::kAudioCapture) {
has_permission = has_microphone_permission_;
} else if (type == Type::kVideoCapture) {
has_permission = has_camera_permission_;
}
std::move(permission_status_cb).Run(has_permission);
}
void RequestPermission(Type type,
PermissionStatusCB permission_status_cb) override {}
bool IsEncryptedMediaEnabled() override { return false; }
#if BUILDFLAG(IS_WIN)
void IsHardwareSecureDecryptionAllowed(
IsHardwareSecureDecryptionAllowedCB cb) override {}
#endif // BUILDFLAG(IS_WIN)
void SetCameraPermission(bool has_permission) {
has_camera_permission_ = has_permission;
}
void SetMicrophonePermission(bool has_permission) {
has_microphone_permission_ = has_permission;
}
private:
bool has_camera_permission_ = true;
bool has_microphone_permission_ = true;
};
} // namespace
class MediaDevicesTest : public PageTestBase {
public:
MediaDevicesTest()
: platform_(std::make_unique<MockMediaPermission>()),
dispatcher_host_(std::make_unique<MockMediaDevicesDispatcherHost>()) {}
MediaDevices* GetMediaDevices(LocalDOMWindow& window) {
if (!media_devices_) {
media_devices_ = MakeGarbageCollected<MediaDevices>(*window.navigator());
media_devices_->SetDispatcherHostForTesting(
dispatcher_host_->CreatePendingRemoteAndBind());
}
return media_devices_;
}
void CloseBinding() { dispatcher_host_->CloseBinding(); }
void OnListenerConnectionError() { listener_connection_error_ = true; }
bool listener_connection_error() const { return listener_connection_error_; }
ScopedTestingPlatformSupport<MediaPermissionTestingPlatform,
std::unique_ptr<media::MediaPermission>>&
platform() {
return platform_;
}
MockMediaDevicesDispatcherHost& dispatcher_host() {
DCHECK(dispatcher_host_);
return *dispatcher_host_;
}
void AddDeviceChangeListener(EventListener* event_listener) {
GetMediaDevices(*GetDocument().domWindow())
->addEventListener(event_type_names::kDevicechange, event_listener);
platform()->RunUntilIdle();
}
void RemoveDeviceChangeListener(EventListener* event_listener) {
GetMediaDevices(*GetDocument().domWindow())
->removeEventListener(event_type_names::kDevicechange, event_listener,
/*use_capture=*/false);
platform()->RunUntilIdle();
}
void NotifyDeviceChanges() {
dispatcher_host().NotifyDeviceChanges();
platform()->RunUntilIdle();
}
void ExpectEnumerateDevicesHistogramReport(
EnumerateDevicesResult expected_result) {
histogram_tester_.ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.Result", 1);
histogram_tester_.ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.Result", expected_result, 1);
histogram_tester_.ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.Latency", 1);
}
DOMException* CallAndValidateSetPreferredSinkId(const char* sink_id,
bool expect_fulfilled) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ScriptPromiseTester tester(
scope.GetScriptState(),
media_devices->setPreferredSinkId(scope.GetScriptState(), sink_id,
scope.GetExceptionState()));
tester.WaitUntilSettled();
if (expect_fulfilled) {
EXPECT_TRUE(tester.IsFulfilled());
} else {
EXPECT_TRUE(tester.IsRejected());
}
return V8DOMException::ToWrappable(scope.GetIsolate(),
tester.Value().V8Value());
}
void SetCameraPermission(bool has_permission) {
static_cast<MockMediaPermission*>(
platform()->GetWebRTCMediaPermission(nullptr))
->SetCameraPermission(has_permission);
}
void SetMicrophonePermission(bool has_permission) {
static_cast<MockMediaPermission*>(
platform()->GetWebRTCMediaPermission(nullptr))
->SetMicrophonePermission(has_permission);
}
base::HistogramTester& histogram_tester() { return histogram_tester_; }
private:
ScopedTestingPlatformSupport<MediaPermissionTestingPlatform,
std::unique_ptr<media::MediaPermission>>
platform_;
std::unique_ptr<MockMediaDevicesDispatcherHost> dispatcher_host_;
bool listener_connection_error_ = false;
Persistent<MediaDevices> media_devices_;
base::HistogramTester histogram_tester_;
};
TEST_F(MediaDevicesTest, GetUserMediaCanBeCalled) {
V8TestingScope scope;
UserMediaStreamConstraints* constraints =
UserMediaStreamConstraints::Create();
auto promise = GetMediaDevices(scope.GetWindow())
->getUserMedia(scope.GetScriptState(), constraints,
scope.GetExceptionState());
// We return the created promise before it was resolved/rejected.
ASSERT_FALSE(promise.IsEmpty());
// We expect a type error because the given constraints are empty.
EXPECT_EQ(scope.GetExceptionState().Code(),
ToExceptionCode(ESErrorType::kTypeError));
VLOG(1) << "Exception message is" << scope.GetExceptionState().Message();
}
TEST_F(MediaDevicesTest, EnumerateDevices) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ScriptPromiseTester tester(
scope.GetScriptState(),
media_devices->enumerateDevices(scope.GetScriptState(),
scope.GetExceptionState()));
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
auto device_infos = NativeValueTraits<IDLArray<MediaDeviceInfo>>::NativeValue(
scope.GetIsolate(), tester.Value().V8Value(), scope.GetExceptionState());
ASSERT_FALSE(scope.GetExceptionState().HadException());
ExpectEnumerateDevicesHistogramReport(EnumerateDevicesResult::kOk);
const auto& video_input_capabilities =
dispatcher_host().VideoInputCapabilities();
const auto& audio_input_capabilities =
dispatcher_host().AudioInputCapabilities();
for (wtf_size_t i = 0, result_index = 0, video_input_index = 0,
audio_input_index = 0;
i < static_cast<wtf_size_t>(MediaDeviceType::kNumMediaDeviceTypes);
++i) {
for (const auto& expected_device_info :
dispatcher_host().enumeration()[i]) {
testing::Message message;
message << "Verifying result index " << result_index;
SCOPED_TRACE(message);
VerifyDeviceInfo(device_infos[result_index], expected_device_info,
static_cast<MediaDeviceType>(i));
if (i == static_cast<wtf_size_t>(MediaDeviceType::kMediaVideoInput)) {
VerifyVideoInputCapabilities(
device_infos[result_index], expected_device_info,
video_input_capabilities[video_input_index]);
video_input_index++;
} else if (i ==
static_cast<wtf_size_t>(MediaDeviceType::kMediaAudioInput)) {
VerifyAudioInputCapabilities(
device_infos[result_index], expected_device_info,
audio_input_capabilities[audio_input_index]);
audio_input_index++;
}
result_index++;
}
}
}
TEST_F(MediaDevicesTest, EnumerateDevicesAfterConnectionError) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
// Simulate a connection error by closing the binding.
CloseBinding();
platform()->RunUntilIdle();
ScriptPromiseTester tester(
scope.GetScriptState(),
media_devices->enumerateDevices(scope.GetScriptState(),
scope.GetExceptionState()));
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsRejected());
ExpectEnumerateDevicesHistogramReport(
EnumerateDevicesResult::kErrorMediaDevicesDispatcherHostDisconnected);
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigAfterConnectionError) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
// Simulate a connection error by closing the binding.
CloseBinding();
platform()->RunUntilIdle();
// Note: SetCaptureHandleConfigEmpty proves the following is a valid call.
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
}
TEST_F(MediaDevicesTest, ObserveDeviceChangeEvent) {
EXPECT_FALSE(dispatcher_host().listener());
// Subscribe to the devicechange event.
StrictMock<MockDeviceChangeEventListener>* event_listener =
MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
AddDeviceChangeListener(event_listener);
EXPECT_TRUE(dispatcher_host().listener());
dispatcher_host().listener().set_disconnect_handler(
BindOnce(&MediaDevicesTest::OnListenerConnectionError, Unretained(this)));
// Send a device change notification from the dispatcher host. The event is
// not fired because devices did not actually change.
NotifyDeviceChanges();
// Adding a new device fires the event.
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioInputDevices().push_back(WebMediaDeviceInfo(
"new_fake_audio_input_device", "new_fake_label", "new_fake_group"));
NotifyDeviceChanges();
// Renaming a device ID fires the event.
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id";
NotifyDeviceChanges();
// Renaming a group ID fires the event.
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioOutputDevices().begin()->group_id = "new_group_id";
NotifyDeviceChanges();
// Renaming a label fires the event.
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioOutputDevices().begin()->label = "new_label";
NotifyDeviceChanges();
// Changing availability fires the event.
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().VideoInputDevices().begin()->availability =
media::CameraAvailability::kUnavailableExclusivelyUsedByOtherApplication;
NotifyDeviceChanges();
// Changing facing mode does not file the event.
EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
dispatcher_host().VideoInputDevices().begin()->video_facing =
blink::mojom::FacingMode::kLeft;
NotifyDeviceChanges();
// Unsubscribe.
RemoveDeviceChangeListener(event_listener);
EXPECT_TRUE(listener_connection_error());
// Sending a device change notification after unsubscribe does not fire the
// event.
dispatcher_host().AudioInputDevices().push_back(WebMediaDeviceInfo(
"yet_another_input_device", "yet_another_label", "yet_another_group"));
NotifyDeviceChanges();
}
TEST_F(MediaDevicesTest, RemoveDeviceFiresDeviceChange) {
StrictMock<MockDeviceChangeEventListener>* event_listener =
MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
AddDeviceChangeListener(event_listener);
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().VideoInputDevices().EraseAt(0);
NotifyDeviceChanges();
}
TEST_F(MediaDevicesTest, RenameDeviceIDFiresDeviceChange) {
StrictMock<MockDeviceChangeEventListener>* event_listener =
MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
AddDeviceChangeListener(event_listener);
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioOutputDevices().begin()->device_id = "new_device_id";
NotifyDeviceChanges();
}
TEST_F(MediaDevicesTest, RenameLabelFiresDeviceChange) {
StrictMock<MockDeviceChangeEventListener>* event_listener =
MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
AddDeviceChangeListener(event_listener);
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioOutputDevices().begin()->label = "new_label";
NotifyDeviceChanges();
}
TEST_F(MediaDevicesTest, ObserveDeviceChangeEventPermissions) {
StrictMock<MockDeviceChangeEventListener>* event_listener =
MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
AddDeviceChangeListener(event_listener);
SetCameraPermission(false);
SetMicrophonePermission(true);
EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id";
NotifyDeviceChanges();
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id";
NotifyDeviceChanges();
SetCameraPermission(true);
SetMicrophonePermission(false);
EXPECT_CALL(*event_listener, Invoke(_, _));
dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id_2";
NotifyDeviceChanges();
EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id_2";
NotifyDeviceChanges();
SetCameraPermission(false);
SetMicrophonePermission(false);
EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id_3";
NotifyDeviceChanges();
dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id_3";
NotifyDeviceChanges();
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigEmpty) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = false;
expected_config->capture_handle = "";
expected_config->all_origins_permitted = false;
expected_config->permitted_origins = {};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigWithExposeOrigin) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setExposeOrigin(true);
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = true;
expected_config->capture_handle = "";
expected_config->all_origins_permitted = false;
expected_config->permitted_origins = {};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithHandle) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setHandle("0xabcdef0123456789");
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = false;
expected_config->capture_handle = "0xabcdef0123456789";
expected_config->all_origins_permitted = false;
expected_config->permitted_origins = {};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithMaxHandle) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
const String maxHandle = MaxLengthCaptureHandle();
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setHandle(maxHandle);
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = false;
expected_config->capture_handle = maxHandle;
expected_config->all_origins_permitted = false;
expected_config->permitted_origins = {};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest,
SetCaptureHandleConfigCaptureWithOverMaxHandleRejected) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setHandle(MaxLengthCaptureHandle() + "a"); // Over max length.
// Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
ASSERT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ(scope.GetExceptionState().Code(),
ToExceptionCode(ESErrorType::kTypeError));
}
TEST_F(MediaDevicesTest,
SetCaptureHandleConfigCaptureWithPermittedOriginsWildcard) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setPermittedOrigins({"*"});
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = false;
expected_config->capture_handle = "";
expected_config->all_origins_permitted = true;
expected_config->permitted_origins = {};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithPermittedOrigins) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setPermittedOrigins(
{"https://chromium.org", "ftp://chromium.org:1234"});
// Expected output.
auto expected_config = mojom::blink::CaptureHandleConfig::New();
expected_config->expose_origin = false;
expected_config->capture_handle = "";
expected_config->all_origins_permitted = false;
expected_config->permitted_origins = {
SecurityOrigin::CreateFromString("https://chromium.org"),
SecurityOrigin::CreateFromString("ftp://chromium.org:1234")};
dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
TEST_F(MediaDevicesTest,
SetCaptureHandleConfigCaptureWithWildcardAndSomethingElseRejected) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setPermittedOrigins({"*", "https://chromium.org"});
// Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
ASSERT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ(scope.GetExceptionState().Code(),
ToExceptionCode(DOMExceptionCode::kNotSupportedError));
}
TEST_F(MediaDevicesTest,
SetCaptureHandleConfigCaptureWithMalformedOriginRejected) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
CaptureHandleConfig* input_config =
MakeGarbageCollected<CaptureHandleConfig>();
input_config->setPermittedOrigins(
{"https://chromium.org:99999"}); // Invalid.
// Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.
media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
scope.GetExceptionState());
platform()->RunUntilIdle();
ASSERT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ(scope.GetExceptionState().Code(),
ToExceptionCode(DOMExceptionCode::kNotSupportedError));
}
TEST_F(MediaDevicesTest, SetPreferredSinkIdWithValidId) {
CallAndValidateSetPreferredSinkId(kValidSinkId, /*expect_fulfilled=*/true);
}
TEST_F(MediaDevicesTest, SetPreferredSinkIdWithInvalidId) {
DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
kInvalidSinkId, /*expect_fulfilled=*/false);
EXPECT_EQ(dom_exception->code(),
static_cast<uint16_t>(DOMExceptionCode::kNotFoundError));
}
TEST_F(MediaDevicesTest, SetPreferredSinkIAuthorizationDenied) {
dispatcher_host().SetOutputDeviceStatus(
media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED);
DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
kValidSinkId, /*expect_fulfilled=*/false);
EXPECT_EQ(dom_exception->name(), "NotAllowedError");
}
TEST_F(MediaDevicesTest, SetPreferredSinkTimeout) {
dispatcher_host().SetOutputDeviceStatus(
media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT);
DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
kValidSinkId, /*expect_fulfilled=*/false);
EXPECT_EQ(dom_exception->code(),
static_cast<uint16_t>(DOMExceptionCode::kTimeoutError));
}
// Regression test for crbug.com/403348706. This ensures that device change
// events, queued before the LocalFrame's ExecutionContext was destroyed,
// resolve without crashing the renderer.
TEST_F(MediaDevicesTest,
DeviceChangeEventsDoNotCrashWhenExecutionContextDestroyed) {
// Simulate resolution of a `MaybeFireDeviceChangeEvent()` task.
MediaDevices* media_devices = GetMediaDevices(*GetDocument().domWindow());
media_devices->MaybeFireDeviceChangeEvent(true);
// Navigate the local frame's document, this will replace and destroy the
// frame's document and dom window, and consequently the observed
// ExecutionContext.
Document& initial_document = GetDocument();
LocalDOMWindow* initial_dom_window = GetDocument().domWindow();
NavigateTo(KURL("https://example.com"));
EXPECT_NE(GetDocument(), initial_document);
EXPECT_NE(GetDocument().domWindow(), initial_dom_window);
// Simulate the resolution of a `MaybeFireDeviceChangeEvent()` task, queued
// before the observed context was destroyed. This should resolve without
// crashing.
media_devices->MaybeFireDeviceChangeEvent(true);
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
// This test logically belongs to the ProduceSubCaptureTargetTest suite,
// but does not require parameterization.
TEST_F(MediaDevicesTest, DistinctIdsForDistinctTypes) {
ScopedElementCaptureForTest scoped_element_capture(true);
V8TestingScope scope;
MediaDevices* const media_devices =
GetMediaDevices(*GetDocument().domWindow());
ASSERT_TRUE(media_devices);
dispatcher_host().SetNextId(SubCaptureTarget::Type::kCropTarget,
String("983bf2ff-7410-416c-808a-78421cbd8fdc"));
dispatcher_host().SetNextId(SubCaptureTarget::Type::kRestrictionTarget,
String("70db842e-5326-42c1-86b2-e3b2f74e97d2"));
SetBodyContent(R"HTML(
<div id='test-div'></div>
)HTML");
Document& document = GetDocument();
Element* const div = document.getElementById(AtomicString("test-div"));
const auto first_promise = media_devices->ProduceCropTarget(
scope.GetScriptState(), div, scope.GetExceptionState());
ScriptPromiseTester first_tester(scope.GetScriptState(), first_promise);
first_tester.WaitUntilSettled();
EXPECT_TRUE(first_tester.IsFulfilled());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// The second call to |produceSubCaptureTargetId|, given the different type,
// should return a different ID.
const auto second_promise = media_devices->ProduceRestrictionTarget(
scope.GetScriptState(), div, scope.GetExceptionState());
ScriptPromiseTester second_tester(scope.GetScriptState(), second_promise);
second_tester.WaitUntilSettled();
EXPECT_TRUE(second_tester.IsFulfilled());
EXPECT_FALSE(scope.GetExceptionState().HadException());
const String first_result = ToSubCaptureTarget(first_tester.Value())->GetId();
ASSERT_FALSE(first_result.empty());
const String second_result =
ToSubCaptureTarget(second_tester.Value())->GetId();
ASSERT_FALSE(second_result.empty());
EXPECT_NE(first_result, second_result);
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
TEST_F(MediaDevicesTest, MetricsFailedEnumerateDevicesThenGetUserMedia) {
{
V8TestingScope scope;
MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/false);
media_devices->ReportSuccessfulGetUserMedia();
histogram_tester().ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::kFailedEnumerateDevicesFirst,
1);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kFailedEnumerateDevicesThenGetUserMedia,
1);
}
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
EnumerateDevicesFirstStateOnContextDestroyed::kFailed, 1);
}
TEST_F(MediaDevicesTest, MetricsSuccessfulEnumerateDevicesThenGetUserMedia) {
{
V8TestingScope scope;
MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/true);
media_devices->ReportSuccessfulGetUserMedia();
histogram_tester().ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kSuccessfulEnumerateDevicesFirst,
1);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kSuccessfulEnumerateDevicesThenGetUserMedia,
1);
}
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
EnumerateDevicesFirstStateOnContextDestroyed::
kSuccessfulFollowedByGetUserMedia,
1);
}
TEST_F(MediaDevicesTest, MetricsGetUserMediaThenSuccessfulEnumerateDevices) {
{
V8TestingScope scope;
MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
media_devices->ReportSuccessfulGetUserMedia();
media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/true);
histogram_tester().ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kGetUserMediaThenSuccessfulEnumerateDevices,
1);
}
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
EnumerateDevicesFirstStateOnContextDestroyed::
kSuccessfulAfterGetUserMedia,
1);
}
TEST_F(MediaDevicesTest, MetricsGetUserMediaThenFailedEnumerateDevices) {
{
V8TestingScope scope;
MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
media_devices->ReportSuccessfulGetUserMedia();
media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/false);
histogram_tester().ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
histogram_tester().ExpectBucketCount(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kGetUserMediaThenFailedEnumerateDevices,
1);
}
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
EnumerateDevicesFirstStateOnContextDestroyed::kFailed, 1);
}
TEST_F(MediaDevicesTest, MetricsEnumerateDevicesOnly) {
{
V8TestingScope scope;
ScriptPromiseTester(scope.GetScriptState(),
GetMediaDevices(scope.GetWindow())
->enumerateDevices(scope.GetScriptState(),
scope.GetExceptionState()))
.WaitUntilSettled();
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::
kSuccessfulEnumerateDevicesFirst,
1);
}
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
EnumerateDevicesFirstStateOnContextDestroyed::
kSuccessfulNeverGetUserMedia,
1);
}
TEST_F(MediaDevicesTest, MetricsGetUserMediaOnly) {
{
V8TestingScope scope;
MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
// A full getUserMedia() call cannot be mocked in this test, so just use the
// report function.
media_devices->ReportSuccessfulGetUserMedia();
histogram_tester().ExpectUniqueSample(
"Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
}
histogram_tester().ExpectTotalCount(
"Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed", 0);
}
class ProduceSubCaptureTargetTest
: public MediaDevicesTest,
public testing::WithParamInterface<
std::pair<SubCaptureTarget::Type, bool>> {
public:
ProduceSubCaptureTargetTest()
: type_(std::get<0>(GetParam())),
scoped_element_capture_(std::get<1>(GetParam())) {}
~ProduceSubCaptureTargetTest() override = default;
const SubCaptureTarget::Type type_;
ScopedElementCaptureForTest scoped_element_capture_;
};
INSTANTIATE_TEST_SUITE_P(
_,
ProduceSubCaptureTargetTest,
::testing::Values(std::make_pair(SubCaptureTarget::Type::kCropTarget,
/* Element Capture enabled: */ false),
std::make_pair(SubCaptureTarget::Type::kCropTarget,
/* Element Capture enabled: */ true),
std::make_pair(SubCaptureTarget::Type::kRestrictionTarget,
/* Element Capture enabled: */ true)));
TEST_P(ProduceSubCaptureTargetTest, IdWithValidElement) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ASSERT_TRUE(media_devices);
SetBodyContent(R"HTML(
<div id='test-div'></div>
<iframe id='test-iframe' src="about:blank"></iframe>
<p id='test-p'>
<var id='test-var'>e</var> equals mc<sup id='test-sup'>2</sup>, or is
<wbr id='test-wbr'>it mc<sub id='test-sub'>2</sub>?
<u id='test-u'>probz</u>.
</p>
<select id='test-select'></select>
<svg id='test-svg' width="400" height="110">
<rect id='test-rect' width="300" height="100"/>
</svg>
<math id='test-math' xmlns='http://www.w3.org/1998/Math/MathML'>
</math>
)HTML");
Document& document = GetDocument();
static const std::vector<const char*> kElementIds{
"test-div", "test-iframe", "test-p", "test-var",
"test-sup", "test-wbr", "test-sub", "test-u",
"test-select", "test-svg", "test-rect", "test-math"};
for (const char* id : kElementIds) {
Element* const element = document.getElementById(AtomicString(id));
dispatcher_host().SetNextId(
type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));
std::optional<ScriptPromiseTester> tester;
ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, element,
tester);
ASSERT_TRUE(tester);
tester->WaitUntilSettled();
EXPECT_TRUE(tester->IsFulfilled())
<< "Failed promise for element id=" << id;
EXPECT_FALSE(scope.GetExceptionState().HadException());
}
}
TEST_P(ProduceSubCaptureTargetTest, IdRejectedIfDifferentWindow) {
V8TestingScope scope;
// Intentionally sets up a MediaDevices object in a different window.
auto* media_devices = GetMediaDevices(scope.GetWindow());
ASSERT_TRUE(media_devices);
SetBodyContent(R"HTML(
<div id='test-div'></div>
<iframe id='test-iframe' src="about:blank" />
)HTML");
Document& document = GetDocument();
Element* const div = document.getElementById(AtomicString("test-div"));
bool got_promise =
ProduceSubCaptureTargetAndGetPromise(scope, type_, media_devices, div);
platform()->RunUntilIdle();
EXPECT_FALSE(got_promise);
EXPECT_TRUE(scope.GetExceptionState().HadException());
EXPECT_EQ(scope.GetExceptionState().CodeAs<DOMExceptionCode>(),
DOMExceptionCode::kNotSupportedError);
EXPECT_EQ(
scope.GetExceptionState().Message(),
String("The Element and the MediaDevices object must be same-window."));
}
TEST_P(ProduceSubCaptureTargetTest, DuplicateId) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ASSERT_TRUE(media_devices);
// This ID should be used for the single ID produced.
dispatcher_host().SetNextId(type_,
String("983bf2ff-7410-416c-808a-78421cbd8fdc"));
// This ID should never be encountered.
dispatcher_host().SetNextId(type_,
String("70db842e-5326-42c1-86b2-e3b2f74e97d2"));
SetBodyContent(R"HTML(
<div id='test-div'></div>
)HTML");
Document& document = GetDocument();
Element* const div = document.getElementById(AtomicString("test-div"));
std::optional<ScriptPromiseTester> first_tester;
ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div,
first_tester);
ASSERT_TRUE(first_tester);
first_tester->WaitUntilSettled();
EXPECT_TRUE(first_tester->IsFulfilled());
EXPECT_FALSE(scope.GetExceptionState().HadException());
// The second call to |produceSubCaptureTargetId| should return the same ID.
std::optional<ScriptPromiseTester> second_tester;
ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div,
second_tester);
ASSERT_TRUE(second_tester);
second_tester->WaitUntilSettled();
EXPECT_TRUE(second_tester->IsFulfilled());
EXPECT_FALSE(scope.GetExceptionState().HadException());
const String first_result =
ToSubCaptureTarget(first_tester->Value())->GetId();
ASSERT_FALSE(first_result.empty());
const String second_result =
ToSubCaptureTarget(second_tester->Value())->GetId();
ASSERT_FALSE(second_result.empty());
EXPECT_EQ(first_result, second_result);
}
TEST_P(ProduceSubCaptureTargetTest, CorrectTokenClassInstantiated) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ASSERT_TRUE(media_devices);
SetBodyContent(R"HTML(
<div id='test-div'></div>
)HTML");
Document& document = GetDocument();
Element* const div = document.getElementById(AtomicString("test-div"));
dispatcher_host().SetNextId(
type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));
std::optional<ScriptPromiseTester> tester;
ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div, tester);
ASSERT_TRUE(tester);
tester->WaitUntilSettled();
ASSERT_TRUE(tester->IsFulfilled());
ASSERT_FALSE(scope.GetExceptionState().HadException());
// Type instantiated if and only if it's the expected type.
const blink::ScriptValue value = tester->Value();
EXPECT_EQ(!!V8CropTarget::ToWrappable(value.GetIsolate(), value.V8Value()),
type_ == SubCaptureTarget::Type::kCropTarget);
EXPECT_EQ(
!!V8RestrictionTarget::ToWrappable(value.GetIsolate(), value.V8Value()),
type_ == SubCaptureTarget::Type::kRestrictionTarget);
}
TEST_P(ProduceSubCaptureTargetTest, IdStringFormat) {
V8TestingScope scope;
auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
ASSERT_TRUE(media_devices);
SetBodyContent(R"HTML(
<div id='test-div'></div>
)HTML");
Document& document = GetDocument();
Element* const div = document.getElementById(AtomicString("test-div"));
dispatcher_host().SetNextId(
type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));
std::optional<ScriptPromiseTester> tester;
ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div, tester);
ASSERT_TRUE(tester);
tester->WaitUntilSettled();
EXPECT_TRUE(tester->IsFulfilled());
EXPECT_FALSE(scope.GetExceptionState().HadException());
const SubCaptureTarget* const target = ToSubCaptureTarget(tester->Value());
const String& id = target->GetId();
EXPECT_TRUE(id.ContainsOnlyASCIIOrEmpty());
EXPECT_TRUE(base::Uuid::ParseLowercase(id.Ascii()).is_valid());
}
// TODO(crbug.com/1418194): Add tests after MediaDevicesDispatcherHost
// has been updated.
} // namespace blink