| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/media/media_devices_util.h" |
| |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/test/bind.h" |
| #include "base/test/test_future.h" |
| #include "content/browser/browser_main_loop.h" |
| #include "content/browser/renderer_host/media/media_devices_manager.h" |
| #include "content/browser/renderer_host/media/media_stream_manager.h" |
| #include "content/browser/renderer_host/media/media_stream_ui_proxy.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/media_device_id.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/shell/browser/shell.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/base/media_switches.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/mediastream/media_devices.mojom-shared.h" |
| #include "third_party/blink/public/mojom/mediastream/media_stream.mojom-shared.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| using ::blink::mojom::MediaDeviceType; |
| using ::blink::mojom::MediaStreamType; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Optional; |
| |
| namespace { |
| |
| // Returns true if the `device_id` corresponds to the system default or |
| // communications device, false otherwise. |
| bool IsSpecialAudioDeviceId(MediaDeviceType device_type, |
| const std::string& device_id) { |
| return (device_type == MediaDeviceType::kMediaAudioInput || |
| device_type == MediaDeviceType::kMediaAudioOutput) && |
| (media::AudioDeviceDescription::IsDefaultDevice(device_id) || |
| media::AudioDeviceDescription::IsCommunicationsDevice(device_id)); |
| } |
| |
| std::optional<MediaStreamType> ToMediaStreamType(MediaDeviceType device_type) { |
| switch (device_type) { |
| case MediaDeviceType::kMediaAudioInput: |
| return MediaStreamType::DEVICE_AUDIO_CAPTURE; |
| case MediaDeviceType::kMediaVideoInput: |
| return MediaStreamType::DEVICE_VIDEO_CAPTURE; |
| default: |
| return std::nullopt; |
| } |
| } |
| |
| void VerifyHMACDeviceID(MediaDeviceType device_type, |
| const std::string& hmac_device_id, |
| const std::string& raw_device_id) { |
| EXPECT_TRUE(IsValidDeviceId(hmac_device_id)); |
| if (IsSpecialAudioDeviceId(device_type, hmac_device_id)) { |
| EXPECT_EQ(raw_device_id, hmac_device_id); |
| } else { |
| EXPECT_NE(raw_device_id, hmac_device_id); |
| } |
| } |
| |
| blink::StreamControls GetAudioStreamControls(std::string hmac_device_id) { |
| blink::StreamControls stream_controls{/*request_audio=*/true, |
| /*request_video=*/false}; |
| stream_controls.audio.device_ids = {hmac_device_id}; |
| return stream_controls; |
| } |
| |
| blink::mojom::StreamSelectionInfoPtr NewSearchBySessionId( |
| base::flat_map<std::string, base::UnguessableToken> session_id_map) { |
| return blink::mojom::StreamSelectionInfo::NewSearchBySessionId( |
| blink::mojom::SearchBySessionId::New(session_id_map)); |
| } |
| |
| } // namespace |
| |
| class MediaDevicesUtilBrowserTest : public ContentBrowserTest { |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); |
| } |
| |
| void SetUpOnMainThread() override { |
| ContentBrowserTest::SetUpOnMainThread(); |
| frame_id_ = shell()->web_contents()->GetPrimaryMainFrame()->GetGlobalId(); |
| device_enumeration_ = EnumerateDevices(); |
| ASSERT_EQ(device_enumeration_.size(), |
| static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes)); |
| for (const auto& typed_enumeration : device_enumeration_) { |
| ASSERT_FALSE(typed_enumeration.empty()); |
| } |
| origin_ = shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetLastCommittedOrigin(); |
| } |
| |
| MediaDeviceEnumeration EnumerateDevices() const { |
| MediaStreamManager* media_stream_manager = |
| BrowserMainLoop::GetInstance()->media_stream_manager(); |
| MediaDeviceEnumeration device_enumeration; |
| base::RunLoop run_loop; |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindLambdaForTesting([&]() { |
| MediaDevicesManager::BoolDeviceTypes types; |
| types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] = true; |
| types[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)] = true; |
| types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)] = true; |
| base::test::TestFuture<const MediaDeviceEnumeration&> future; |
| media_stream_manager->media_devices_manager()->EnumerateDevices( |
| types, base::BindLambdaForTesting( |
| [&](const MediaDeviceEnumeration& enumeration) { |
| device_enumeration = enumeration; |
| run_loop.Quit(); |
| })); |
| })); |
| run_loop.Run(); |
| return device_enumeration; |
| } |
| |
| MediaDeviceSaltAndOrigin GetSaltAndOrigin() { |
| base::test::TestFuture<const MediaDeviceSaltAndOrigin&> future; |
| GetMediaDeviceSaltAndOrigin(frame_id_, future.GetCallback()); |
| return future.Get(); |
| } |
| |
| void GenerateStreams( |
| GlobalRenderFrameHostId render_frame_host_id, |
| const blink::StreamControls& controls, |
| MediaDeviceSaltAndOrigin salt_and_origin, |
| MediaStreamManager::GenerateStreamsCallback generate_stream_cb) { |
| GetIOThreadTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &MediaStreamManager::GenerateStreams, |
| base::Unretained( |
| BrowserMainLoop::GetInstance()->media_stream_manager()), |
| render_frame_host_id, /*requester_id=*/0, /*page_request_id=*/0, |
| controls, salt_and_origin, |
| /*user_gesture=*/false, |
| /*audio_stream_selection_info_ptr=*/ |
| NewSearchBySessionId({}), |
| base::BindPostTaskToCurrentDefault(std::move(generate_stream_cb)), |
| /*device_stopped_cb=*/base::DoNothing(), |
| /*device_changed_cb=*/base::DoNothing(), |
| /*device_request_state_change_cb*/ base::DoNothing(), |
| /*device_capture_configuration_change_cb=*/base::DoNothing(), |
| /*device_capture_handle_change_cb=*/base::DoNothing(), |
| /*zoom_level_change_callback=*/base::DoNothing())); |
| } |
| |
| GlobalRenderFrameHostId frame_id_; |
| url::Origin origin_; |
| MediaDeviceEnumeration device_enumeration_; |
| }; |
| |
| // This test provides coverage for the utilities in |
| // content/public/media_device_id.h |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, TranslateDeviceIdAndBack) { |
| const std::string salt = CreateRandomMediaDeviceIDSalt(); |
| EXPECT_FALSE(salt.empty()); |
| for (int i = 0; i < static_cast<int>(MediaDeviceType::kNumMediaDeviceTypes); |
| ++i) { |
| MediaDeviceType device_type = static_cast<MediaDeviceType>(i); |
| for (const auto& device_info : device_enumeration_[i]) { |
| testing::Message message; |
| message << "Testing device_type = " << device_type |
| << ", raw device ID = " << device_info.device_id; |
| SCOPED_TRACE(message); |
| std::string hmac_device_id = |
| GetHMACForMediaDeviceID(salt, origin_, device_info.device_id); |
| VerifyHMACDeviceID(device_type, hmac_device_id, device_info.device_id); |
| |
| std::optional<MediaStreamType> stream_type = |
| ToMediaStreamType(device_type); |
| EXPECT_EQ(stream_type.has_value(), |
| device_type != MediaDeviceType::kMediaAudioOutput); |
| if (!stream_type.has_value()) { |
| continue; |
| } |
| base::test::TestFuture<const std::optional<std::string>&> future; |
| GetMediaDeviceIDForHMAC(*stream_type, salt, origin_, hmac_device_id, |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| future.GetCallback()); |
| std::optional<std::string> raw_device_id = future.Get(); |
| EXPECT_THAT(raw_device_id, Optional(device_info.device_id)); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| GetMediaDeviceSaltAndOrigin) { |
| MediaDeviceSaltAndOrigin salt_and_origin = GetSaltAndOrigin(); |
| EXPECT_FALSE(salt_and_origin.device_id_salt().empty()); |
| EXPECT_FALSE(salt_and_origin.group_id_salt().empty()); |
| EXPECT_NE(salt_and_origin.device_id_salt(), salt_and_origin.group_id_salt()); |
| EXPECT_EQ( |
| salt_and_origin.origin(), |
| shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| TranslateMediaDeviceInfoArrayWithPermission) { |
| MediaDeviceSaltAndOrigin salt_and_origin = GetSaltAndOrigin(); |
| for (const auto& device_infos : device_enumeration_) { |
| blink::WebMediaDeviceInfoArray web_media_device_infos = |
| TranslateMediaDeviceInfoArray(/*has_permission=*/true, salt_and_origin, |
| device_infos); |
| EXPECT_EQ(web_media_device_infos.size(), device_infos.size()); |
| for (size_t j = 0; j < device_infos.size(); ++j) { |
| EXPECT_EQ(web_media_device_infos[j].device_id, |
| GetHMACForRawMediaDeviceID(salt_and_origin, |
| device_infos[j].device_id)); |
| EXPECT_EQ(web_media_device_infos[j].label, device_infos[j].label); |
| EXPECT_EQ( |
| web_media_device_infos[j].group_id, |
| GetHMACForRawMediaDeviceID(salt_and_origin, device_infos[j].group_id, |
| /*use_group_salt=*/true)); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| TranslateMediaDeviceInfoArrayWithoutPermission) { |
| MediaDeviceSaltAndOrigin salt_and_origin = GetSaltAndOrigin(); |
| for (const auto& device_infos : device_enumeration_) { |
| blink::WebMediaDeviceInfoArray web_media_device_infos = |
| TranslateMediaDeviceInfoArray(/*has_permission=*/false, salt_and_origin, |
| device_infos); |
| EXPECT_EQ(web_media_device_infos.size(), 1u); |
| for (const auto& device_info : web_media_device_infos) { |
| EXPECT_TRUE(device_info.device_id.empty()); |
| EXPECT_TRUE(device_info.label.empty()); |
| EXPECT_TRUE(device_info.group_id.empty()); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, TranslationWithoutSalt) { |
| for (int i = 0; i < static_cast<int>(MediaDeviceType::kNumMediaDeviceTypes); |
| ++i) { |
| SCOPED_TRACE(base::StringPrintf("device_type %d", i)); |
| MediaDeviceType device_type = static_cast<MediaDeviceType>(i); |
| for (const auto& device_info : device_enumeration_[i]) { |
| base::test::TestFuture<const std::string&> future_hmac; |
| GetHMACFromRawDeviceId(frame_id_, device_info.device_id, |
| future_hmac.GetCallback()); |
| std::string hmac_device_id = future_hmac.Get(); |
| VerifyHMACDeviceID(device_type, hmac_device_id, device_info.device_id); |
| |
| base::test::TestFuture<const std::optional<std::string>&> future_raw; |
| GetRawDeviceIdFromHMAC(frame_id_, hmac_device_id, device_type, |
| future_raw.GetCallback()); |
| std::optional<std::string> raw_device_id = future_raw.Get(); |
| EXPECT_THAT(raw_device_id, Optional(device_info.device_id)); |
| } |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| GetRawMediaDeviceIDForHMAC) { |
| const MediaDeviceSaltAndOrigin salt_and_origin = GetSaltAndOrigin(); |
| const std::string existing_raw_device_id = |
| device_enumeration_[0][0].device_id; |
| const std::string existing_hmac_device_id = |
| GetHMACForRawMediaDeviceID(salt_and_origin, existing_raw_device_id); |
| |
| base::test::TestFuture<const std::optional<std::string>&> future; |
| GetIOThreadTaskRunner()->PostTask( |
| FROM_HERE, base::BindOnce(&GetRawDeviceIDForMediaStreamHMAC, |
| MediaStreamType::DEVICE_AUDIO_CAPTURE, |
| salt_and_origin, existing_hmac_device_id, |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| future.GetCallback())); |
| std::optional<std::string> raw_device_id = future.Get(); |
| ASSERT_TRUE(raw_device_id.has_value()); |
| EXPECT_EQ(*raw_device_id, existing_raw_device_id); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, GetRawAudioOutputDeviceID) { |
| const MediaDeviceSaltAndOrigin salt_and_origin = GetSaltAndOrigin(); |
| const std::string existing_raw_device_id = |
| device_enumeration_[static_cast<size_t>( |
| MediaDeviceType::kMediaAudioOutput)][0] |
| .device_id; |
| const std::string existing_hmac_device_id = |
| GetHMACForRawMediaDeviceID(salt_and_origin, existing_raw_device_id); |
| |
| base::test::TestFuture<const std::optional<std::string>&> future; |
| GetIOThreadTaskRunner()->PostTask( |
| FROM_HERE, base::BindOnce(&GetRawDeviceIDForMediaDeviceHMAC, |
| MediaDeviceType::kMediaAudioOutput, |
| salt_and_origin, existing_hmac_device_id, |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| future.GetCallback())); |
| std::optional<std::string> raw_device_id = future.Get(); |
| ASSERT_TRUE(raw_device_id.has_value()); |
| EXPECT_EQ(*raw_device_id, existing_raw_device_id); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| GetRawDeviceIDForNonexistingHMAC) { |
| base::test::TestFuture<const std::optional<std::string>&> future; |
| GetIOThreadTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&GetRawDeviceIDForMediaDeviceHMAC, |
| MediaDeviceType::kMediaAudioOutput, GetSaltAndOrigin(), |
| "nonexisting_hmac_device_id", |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| future.GetCallback())); |
| std::optional<std::string> raw_device_id = future.Get(); |
| EXPECT_FALSE(raw_device_id.has_value()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(MediaDevicesUtilBrowserTest, |
| GetMediaCaptureRawDeviceIdsOpenedForWebContents) { |
| base::test::TestFuture<void> use_fake_ui_factory_for_tests_future; |
| GetIOThreadTaskRunner()->PostTaskAndReply( |
| FROM_HERE, |
| base::BindOnce( |
| &MediaStreamManager::UseFakeUIFactoryForTests, |
| base::Unretained( |
| BrowserMainLoop::GetInstance()->media_stream_manager()), |
| base::BindRepeating([]() { |
| return std::make_unique<FakeMediaStreamUIProxy>( |
| /* tests_use_fake_render_frame_hosts=*/true); |
| }), |
| /*use_for_gum_desktop_capture=*/false, |
| /*captured_tab_id=*/std::nullopt), |
| use_fake_ui_factory_for_tests_future.GetCallback()); |
| ASSERT_TRUE(use_fake_ui_factory_for_tests_future.Wait()); |
| |
| auto audio_devices = device_enumeration_[static_cast<size_t>( |
| MediaDeviceType::kMediaAudioInput)]; |
| const auto kDevice1RawId = audio_devices[0].device_id; |
| const auto kDevice1HMACId = |
| GetHMACForRawMediaDeviceID(GetSaltAndOrigin(), kDevice1RawId); |
| |
| const auto kDevice2RawId = audio_devices[1].device_id; |
| const auto kDevice2HMACId = |
| GetHMACForRawMediaDeviceID(GetSaltAndOrigin(), kDevice2RawId); |
| |
| base::test::TestFuture<blink::mojom::MediaStreamRequestResult, |
| const std::string&, blink::mojom::StreamDevicesSetPtr, |
| bool> |
| streams_generated_future; |
| |
| GenerateStreams(frame_id_, GetAudioStreamControls(kDevice2HMACId), |
| GetSaltAndOrigin(), streams_generated_future.GetCallback()); |
| |
| ASSERT_EQ(std::get<0>(streams_generated_future.Take()), |
| blink::mojom::MediaStreamRequestResult::OK) |
| << "GenerateStreams() call failed"; |
| |
| // Open device 1 on another render frame id. Device 1 shouldn't be |
| // included in the response from `GetRawDeviceIdsOpenedForFrame()`. |
| GenerateStreams({42, 38}, GetAudioStreamControls(kDevice1HMACId), |
| GetSaltAndOrigin(), streams_generated_future.GetCallback()); |
| |
| ASSERT_EQ(std::get<0>(streams_generated_future.Take()), |
| blink::mojom::MediaStreamRequestResult::OK) |
| << "GenerateStreams() call failed"; |
| |
| base::test::TestFuture<std::vector<std::string>> |
| get_raw_device_ids_opened_for_web_contents_future; |
| shell()->web_contents()->GetMediaCaptureRawDeviceIdsOpened( |
| blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, |
| get_raw_device_ids_opened_for_web_contents_future.GetCallback()); |
| EXPECT_THAT(get_raw_device_ids_opened_for_web_contents_future.Get(), |
| ElementsAre(kDevice2RawId)); |
| } |
| |
| } // namespace content |