| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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/audio_renderer_host.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/run_loop.h" |
| #include "base/sync_socket.h" |
| #include "content/browser/media/capture/audio_mirroring_manager.h" |
| #include "content/browser/media/media_internals.h" |
| #include "content/browser/renderer_host/media/audio_input_device_manager.h" |
| #include "content/browser/renderer_host/media/media_stream_manager.h" |
| #include "content/common/media/audio_messages.h" |
| #include "content/public/browser/media_device_id.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "ipc/ipc_message_utils.h" |
| #include "media/audio/audio_system_impl.h" |
| #include "media/audio/fake_audio_log_factory.h" |
| #include "media/audio/fake_audio_manager.h" |
| #include "media/audio/test_audio_thread.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/media_switches.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::_; |
| using ::testing::Assign; |
| using ::testing::AtLeast; |
| using ::testing::DoAll; |
| using ::testing::NotNull; |
| |
| namespace content { |
| |
| namespace { |
| const int kStreamId = 50; |
| const char kSecurityOrigin[] = "http://localhost"; |
| const char kDefaultDeviceId[] = ""; |
| const char kSalt[] = "salt"; |
| const char kBadDeviceId[] = |
| "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad1"; |
| const char kInvalidDeviceId[] = "invalid-device-id"; |
| |
| class MockAudioMirroringManager : public AudioMirroringManager { |
| public: |
| MockAudioMirroringManager() {} |
| virtual ~MockAudioMirroringManager() {} |
| |
| MOCK_METHOD3(AddDiverter, |
| void(int render_process_id, |
| int render_frame_id, |
| Diverter* diverter)); |
| MOCK_METHOD1(RemoveDiverter, void(Diverter* diverter)); |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockAudioMirroringManager); |
| }; |
| |
| class FakeAudioManagerWithAssociations : public media::FakeAudioManager { |
| public: |
| explicit FakeAudioManagerWithAssociations(media::AudioLogFactory* factory) |
| : FakeAudioManager(base::MakeUnique<media::TestAudioThread>(), factory) {} |
| |
| void CreateDeviceAssociation(const std::string& input_device_id, |
| const std::string& output_device_id) { |
| // We shouldn't accidentally add hashed ids, since the audio manager |
| // works with raw ids. |
| EXPECT_FALSE(IsValidDeviceId(input_device_id)); |
| EXPECT_FALSE(IsValidDeviceId(output_device_id)); |
| |
| associations_[input_device_id] = output_device_id; |
| } |
| |
| std::string GetAssociatedOutputDeviceID( |
| const std::string& input_id) override { |
| auto it = associations_.find(input_id); |
| return it == associations_.end() ? "" : it->second; |
| } |
| |
| private: |
| std::map<std::string, std::string> associations_; |
| }; |
| |
| } // namespace |
| |
| class MockAudioRendererHost : public AudioRendererHost { |
| public: |
| MockAudioRendererHost(base::RunLoop* auth_run_loop, |
| int render_process_id, |
| media::AudioManager* audio_manager, |
| media::AudioSystem* audio_system, |
| AudioMirroringManager* mirroring_manager, |
| MediaStreamManager* media_stream_manager, |
| const std::string& salt) |
| : AudioRendererHost(render_process_id, |
| audio_manager, |
| audio_system, |
| mirroring_manager, |
| media_stream_manager, |
| salt), |
| shared_memory_length_(0), |
| auth_run_loop_(auth_run_loop) { |
| } |
| |
| // A list of mock methods. |
| MOCK_METHOD4(OnDeviceAuthorized, |
| void(int stream_id, |
| media::OutputDeviceStatus device_status, |
| const media::AudioParameters& output_params, |
| const std::string& matched_device_id)); |
| MOCK_METHOD2(WasNotifiedOfCreation, void(int stream_id, int length)); |
| MOCK_METHOD1(WasNotifiedOfError, void(int stream_id)); |
| |
| void ShutdownForBadMessage() override { bad_msg_count++; } |
| |
| int bad_msg_count = 0; |
| |
| private: |
| virtual ~MockAudioRendererHost() { |
| // Make sure all audio streams have been deleted. |
| EXPECT_TRUE(delegates_.empty()); |
| } |
| |
| // This method is used to dispatch IPC messages to the renderer. We intercept |
| // these messages here and dispatch to our mock methods to verify the |
| // conversation between this object and the renderer. |
| // Note: this means that file descriptors won't be duplicated, |
| // leading to double-close errors from SyncSocket. |
| // See crbug.com/647659. |
| bool Send(IPC::Message* message) override { |
| CHECK(message); |
| |
| // In this method we dispatch the messages to the according handlers as if |
| // we are the renderer. |
| bool handled = true; |
| IPC_BEGIN_MESSAGE_MAP(MockAudioRendererHost, *message) |
| IPC_MESSAGE_HANDLER(AudioMsg_NotifyDeviceAuthorized, |
| OnNotifyDeviceAuthorized) |
| IPC_MESSAGE_HANDLER(AudioMsg_NotifyStreamCreated, |
| OnNotifyStreamCreated) |
| IPC_MESSAGE_HANDLER(AudioMsg_NotifyStreamError, OnNotifyStreamError) |
| IPC_MESSAGE_UNHANDLED(handled = false) |
| IPC_END_MESSAGE_MAP() |
| EXPECT_TRUE(handled); |
| |
| delete message; |
| return true; |
| } |
| |
| void OnNotifyDeviceAuthorized(int stream_id, |
| media::OutputDeviceStatus device_status, |
| const media::AudioParameters& output_params, |
| const std::string& matched_device_id) { |
| // Make sure we didn't leak a raw device id. |
| EXPECT_TRUE(IsValidDeviceId(matched_device_id)); |
| |
| OnDeviceAuthorized(stream_id, device_status, output_params, |
| matched_device_id); |
| auth_run_loop_->Quit(); |
| } |
| |
| void OnNotifyStreamCreated( |
| int stream_id, |
| base::SharedMemoryHandle handle, |
| base::SyncSocket::TransitDescriptor socket_descriptor, |
| uint32_t length) { |
| // Maps the shared memory. |
| shared_memory_.reset(new base::SharedMemory(handle, false)); |
| CHECK(shared_memory_->Map(length)); |
| CHECK(shared_memory_->memory()); |
| shared_memory_length_ = length; |
| |
| // Create the SyncSocket using the handle. |
| base::SyncSocket::Handle sync_socket_handle = |
| base::SyncSocket::UnwrapHandle(socket_descriptor); |
| sync_socket_.reset(new base::SyncSocket(sync_socket_handle)); |
| |
| // And then delegate the call to the mock method. |
| WasNotifiedOfCreation(stream_id, length); |
| } |
| |
| void OnNotifyStreamError(int stream_id) { WasNotifiedOfError(stream_id); } |
| |
| std::unique_ptr<base::SharedMemory> shared_memory_; |
| std::unique_ptr<base::SyncSocket> sync_socket_; |
| uint32_t shared_memory_length_; |
| base::RunLoop* auth_run_loop_; // Used to wait for authorization. |
| |
| DISALLOW_COPY_AND_ASSIGN(MockAudioRendererHost); |
| }; |
| |
| class AudioRendererHostTest : public RenderViewHostTestHarness { |
| public: |
| AudioRendererHostTest() {} |
| ~AudioRendererHostTest() override {} |
| |
| void SetUp() override { |
| base::CommandLine::ForCurrentProcess()->AppendSwitch( |
| switches::kUseFakeDeviceForMediaStream); |
| // MediaStreamManager depends on legacy TestBrowserThreadBundle behavior. |
| // TODO: Remove once MediaStreamManager is ported to TaskScheduler. |
| DisableScopedTaskEnvironment(); |
| |
| RenderViewHostTestHarness::SetUp(); |
| audio_manager_ = |
| base::MakeUnique<FakeAudioManagerWithAssociations>(&log_factory_); |
| audio_system_ = media::AudioSystemImpl::Create(audio_manager_.get()); |
| media_stream_manager_ = |
| base::MakeUnique<MediaStreamManager>(audio_system_.get()); |
| auth_run_loop_ = base::MakeUnique<base::RunLoop>(); |
| host_ = base::MakeRefCounted<MockAudioRendererHost>( |
| auth_run_loop_.get(), process()->GetID(), audio_manager_.get(), |
| audio_system_.get(), &mirroring_manager_, media_stream_manager_.get(), |
| kSalt); |
| |
| // Simulate IPC channel connected. |
| host_->set_peer_process_for_testing(base::Process::Current()); |
| |
| NavigateAndCommit(GURL(kSecurityOrigin)); |
| } |
| |
| void TearDown() override { |
| // Simulate closing the IPC channel and give the audio thread time to close |
| // the underlying streams. |
| host_->OnChannelClosing(); |
| |
| // Release the reference to the mock object. The object will be destructed |
| // on message_loop_. |
| host_ = nullptr; |
| |
| // Note: Shutdown is usually called after the IO thread is stopped, but in |
| // this case it is not possible as we won't have a message loop at that |
| // point. |
| audio_manager_->Shutdown(); |
| RenderViewHostTestHarness::TearDown(); |
| } |
| |
| protected: |
| void OverrideDevicePermissions(bool has_permissions) { |
| host_->OverrideDevicePermissionsForTesting(has_permissions); |
| } |
| |
| std::string GetNondefaultIdExpectedToPassPermissionsCheck() { |
| std::string nondefault_id; |
| |
| MediaDevicesManager::BoolDeviceTypes devices_to_enumerate; |
| devices_to_enumerate[MEDIA_DEVICE_TYPE_AUDIO_OUTPUT] = true; |
| media_stream_manager_->media_devices_manager()->EnumerateDevices( |
| devices_to_enumerate, |
| base::Bind( |
| [](std::string* out, const MediaDeviceEnumeration& result) { |
| // Index 0 is default, so use 1. Always exists because we use |
| // fake devices. |
| CHECK(result[MediaDeviceType::MEDIA_DEVICE_TYPE_AUDIO_OUTPUT] |
| .size() > 1) |
| << "Expected to have a nondefault device."; |
| *out = result[MediaDeviceType::MEDIA_DEVICE_TYPE_AUDIO_OUTPUT][1] |
| .device_id; |
| }, |
| base::Unretained(&nondefault_id))); |
| |
| // Make sure nondefault_id is set before returning. |
| base::RunLoop().RunUntilIdle(); |
| |
| return nondefault_id; |
| } |
| |
| std::string GetNondefaultInputId() { |
| std::string nondefault_id; |
| |
| MediaDevicesManager::BoolDeviceTypes devices_to_enumerate; |
| devices_to_enumerate[MEDIA_DEVICE_TYPE_AUDIO_INPUT] = true; |
| media_stream_manager_->media_devices_manager()->EnumerateDevices( |
| devices_to_enumerate, |
| base::Bind( |
| // Index 0 is default, so use 1. Always exists because we use |
| // fake devices. |
| [](std::string* out, const MediaDeviceEnumeration& result) { |
| CHECK(result[MediaDeviceType::MEDIA_DEVICE_TYPE_AUDIO_INPUT] |
| .size() > 1) |
| << "Expected to have a nondefault device."; |
| *out = result[MediaDeviceType::MEDIA_DEVICE_TYPE_AUDIO_INPUT][1] |
| .device_id; |
| }, |
| base::Unretained(&nondefault_id))); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| return nondefault_id; |
| } |
| |
| void Create() { |
| Create(kDefaultDeviceId, url::Origin(GURL(kSecurityOrigin)), true, true); |
| } |
| |
| void Create(const std::string& device_id, |
| const url::Origin& security_origin, |
| bool wait_for_auth, |
| bool expect_onauthorized) { |
| media::OutputDeviceStatus expected_device_status = |
| device_id == kDefaultDeviceId || |
| device_id == |
| MediaStreamManager::GetHMACForMediaDeviceID( |
| kSalt, url::Origin(GURL(kSecurityOrigin)), |
| GetNondefaultIdExpectedToPassPermissionsCheck()) |
| ? media::OUTPUT_DEVICE_STATUS_OK |
| : device_id == kBadDeviceId |
| ? media::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED |
| : media::OUTPUT_DEVICE_STATUS_ERROR_NOT_FOUND; |
| |
| if (expect_onauthorized) |
| EXPECT_CALL(*host_.get(), |
| OnDeviceAuthorized(kStreamId, expected_device_status, _, _)); |
| |
| if (expected_device_status == media::OUTPUT_DEVICE_STATUS_OK) { |
| EXPECT_CALL(*host_.get(), WasNotifiedOfCreation(kStreamId, _)); |
| EXPECT_CALL(mirroring_manager_, |
| AddDiverter(process()->GetID(), main_rfh()->GetRoutingID(), |
| NotNull())) |
| .RetiresOnSaturation(); |
| } |
| |
| // Send a create stream message to the audio output stream and wait until |
| // we receive the created message. |
| media::AudioParameters params( |
| media::AudioParameters::AUDIO_FAKE, media::CHANNEL_LAYOUT_STEREO, |
| media::AudioParameters::kAudioCDSampleRate, 16, |
| media::AudioParameters::kAudioCDSampleRate / 10); |
| int session_id = 0; |
| |
| host_->OnRequestDeviceAuthorization(kStreamId, main_rfh()->GetRoutingID(), |
| session_id, device_id, security_origin); |
| if (wait_for_auth) |
| auth_run_loop_->Run(); |
| |
| if (!wait_for_auth || |
| expected_device_status == media::OUTPUT_DEVICE_STATUS_OK) |
| host_->OnCreateStream(kStreamId, main_rfh()->GetRoutingID(), params); |
| |
| if (expected_device_status == media::OUTPUT_DEVICE_STATUS_OK) |
| // At some point in the future, a corresponding RemoveDiverter() call must |
| // be made. |
| EXPECT_CALL(mirroring_manager_, RemoveDiverter(NotNull())) |
| .RetiresOnSaturation(); |
| SyncWithAudioThread(); |
| } |
| |
| void CreateWithoutWaitingForAuth(const std::string& device_id) { |
| Create(device_id, url::Origin(GURL(kSecurityOrigin)), false, false); |
| } |
| |
| void CreateWithInvalidRenderFrameId() { |
| // When creating a stream with an invalid render frame ID, the host will |
| // reply with a stream error message. |
| EXPECT_CALL(*host_, WasNotifiedOfError(kStreamId)); |
| |
| // However, validation does not block stream creation, so these method calls |
| // might be made: |
| EXPECT_CALL(*host_, WasNotifiedOfCreation(kStreamId, _)).Times(AtLeast(0)); |
| EXPECT_CALL(mirroring_manager_, AddDiverter(_, _, _)).Times(AtLeast(0)); |
| EXPECT_CALL(mirroring_manager_, RemoveDiverter(_)).Times(AtLeast(0)); |
| |
| // Provide a seemingly-valid render frame ID; and it should be rejected when |
| // AudioRendererHost calls ValidateRenderFrameId(). |
| const int kInvalidRenderFrameId = main_rfh()->GetRoutingID() + 1; |
| const media::AudioParameters params( |
| media::AudioParameters::AUDIO_FAKE, media::CHANNEL_LAYOUT_STEREO, |
| media::AudioParameters::kAudioCDSampleRate, 16, |
| media::AudioParameters::kAudioCDSampleRate / 10); |
| host_->OnCreateStream(kStreamId, kInvalidRenderFrameId, params); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void CreateUnifiedStream(const url::Origin& security_origin) { |
| std::string output_id = GetNondefaultIdExpectedToPassPermissionsCheck(); |
| std::string input_id = GetNondefaultInputId(); |
| std::string hashed_output_id = MediaStreamManager::GetHMACForMediaDeviceID( |
| kSalt, url::Origin(GURL(kSecurityOrigin)), output_id); |
| // Set up association between input and output so that the output |
| // device gets selected when using session id: |
| audio_manager_->CreateDeviceAssociation(input_id, output_id); |
| int session_id = media_stream_manager_->audio_input_device_manager()->Open( |
| MediaStreamDevice(MEDIA_DEVICE_AUDIO_CAPTURE, input_id, |
| "Fake input device")); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Send a create stream message to the audio output stream and wait until |
| // we receive the created message. |
| media::AudioParameters params( |
| media::AudioParameters::AUDIO_FAKE, media::CHANNEL_LAYOUT_STEREO, |
| media::AudioParameters::kAudioCDSampleRate, 16, |
| media::AudioParameters::kAudioCDSampleRate / 10); |
| |
| EXPECT_CALL(*host_.get(), |
| OnDeviceAuthorized(kStreamId, media::OUTPUT_DEVICE_STATUS_OK, _, |
| hashed_output_id)) |
| .Times(1); |
| EXPECT_CALL(*host_.get(), WasNotifiedOfCreation(kStreamId, _)); |
| EXPECT_CALL( |
| mirroring_manager_, |
| AddDiverter(process()->GetID(), main_rfh()->GetRoutingID(), NotNull())) |
| .RetiresOnSaturation(); |
| EXPECT_CALL(mirroring_manager_, RemoveDiverter(NotNull())) |
| .RetiresOnSaturation(); |
| |
| host_->OnRequestDeviceAuthorization( |
| kStreamId, main_rfh()->GetRoutingID(), session_id, |
| /*device id*/ std::string(), security_origin); |
| |
| auth_run_loop_->Run(); |
| |
| host_->OnCreateStream(kStreamId, main_rfh()->GetRoutingID(), params); |
| |
| SyncWithAudioThread(); |
| } |
| |
| void Close() { |
| // Send a message to AudioRendererHost to tell it we want to close the |
| // stream. |
| host_->OnCloseStream(kStreamId); |
| SyncWithAudioThread(); |
| } |
| |
| void Play() { |
| host_->OnPlayStream(kStreamId); |
| SyncWithAudioThread(); |
| } |
| |
| void Pause() { |
| host_->OnPauseStream(kStreamId); |
| SyncWithAudioThread(); |
| } |
| |
| void SetVolume(double volume) { |
| host_->OnSetVolume(kStreamId, volume); |
| SyncWithAudioThread(); |
| } |
| |
| void SimulateError() { |
| EXPECT_EQ(1u, host_->delegates_.size()) |
| << "Calls Create() before calling this method"; |
| |
| // Expect an error signal sent through IPC. |
| EXPECT_CALL(*host_.get(), WasNotifiedOfError(kStreamId)); |
| |
| // Simulate an error sent from the audio device. |
| host_->OnStreamError(kStreamId); |
| SyncWithAudioThread(); |
| |
| // Expect the audio stream record is removed. |
| EXPECT_EQ(0u, host_->delegates_.size()); |
| } |
| |
| // SyncWithAudioThread() waits until all pending tasks on the audio thread |
| // are executed while also processing pending task in message_loop_ on the |
| // current thread. It is used to synchronize with the audio thread when we are |
| // closing an audio stream. |
| void SyncWithAudioThread() { |
| base::RunLoop().RunUntilIdle(); |
| |
| base::RunLoop run_loop; |
| audio_manager_->GetTaskRunner()->PostTask( |
| FROM_HERE, media::BindToCurrentLoop(run_loop.QuitClosure())); |
| run_loop.Run(); |
| } |
| |
| void AssertBadMsgReported() { |
| // Bad messages can be reported either directly to the RPH or through the |
| // ARH, so we check both of them. |
| EXPECT_EQ(process()->bad_msg_count() + host_->bad_msg_count, 1); |
| } |
| |
| private: |
| media::FakeAudioLogFactory log_factory_; |
| std::unique_ptr<FakeAudioManagerWithAssociations> audio_manager_; |
| std::unique_ptr<media::AudioSystem> audio_system_; |
| std::unique_ptr<MediaStreamManager> media_stream_manager_; |
| MockAudioMirroringManager mirroring_manager_; |
| std::unique_ptr<base::RunLoop> auth_run_loop_; |
| scoped_refptr<MockAudioRendererHost> host_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AudioRendererHostTest); |
| }; |
| |
| TEST_F(AudioRendererHostTest, CreateAndClose) { |
| Create(); |
| Close(); |
| } |
| |
| // Simulate the case where a stream is not properly closed. |
| TEST_F(AudioRendererHostTest, CreateAndShutdown) { |
| Create(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreatePlayAndClose) { |
| Create(); |
| Play(); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreatePlayPauseAndClose) { |
| Create(); |
| Play(); |
| Pause(); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, SetVolume) { |
| Create(); |
| SetVolume(0.5); |
| Play(); |
| Pause(); |
| Close(); |
| } |
| |
| // Simulate the case where a stream is not properly closed. |
| TEST_F(AudioRendererHostTest, CreatePlayAndShutdown) { |
| Create(); |
| Play(); |
| } |
| |
| // Simulate the case where a stream is not properly closed. |
| TEST_F(AudioRendererHostTest, CreatePlayPauseAndShutdown) { |
| Create(); |
| Play(); |
| Pause(); |
| } |
| |
| TEST_F(AudioRendererHostTest, SimulateError) { |
| Create(); |
| Play(); |
| SimulateError(); |
| } |
| |
| // Simulate the case when an error is generated on the browser process, |
| // the audio device is closed but the render process try to close the |
| // audio stream again. |
| TEST_F(AudioRendererHostTest, SimulateErrorAndClose) { |
| Create(); |
| Play(); |
| SimulateError(); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateUnifiedStreamAndClose) { |
| CreateUnifiedStream(url::Origin(GURL(kSecurityOrigin))); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateUnauthorizedDevice) { |
| Create(kBadDeviceId, url::Origin(GURL(kSecurityOrigin)), true, true); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateAuthorizedDevice) { |
| OverrideDevicePermissions(true); |
| std::string id = GetNondefaultIdExpectedToPassPermissionsCheck(); |
| std::string hashed_id = MediaStreamManager::GetHMACForMediaDeviceID( |
| kSalt, url::Origin(GURL(kSecurityOrigin)), id); |
| Create(hashed_id, url::Origin(GURL(kSecurityOrigin)), true, true); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateDeviceWithAuthorizationPendingIsError) { |
| CreateWithoutWaitingForAuth(kBadDeviceId); |
| Close(); |
| AssertBadMsgReported(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateInvalidDevice) { |
| Create(kInvalidDeviceId, url::Origin(GURL(kSecurityOrigin)), true, true); |
| Close(); |
| } |
| |
| TEST_F(AudioRendererHostTest, CreateFailsForInvalidRenderFrame) { |
| CreateWithInvalidRenderFrameId(); |
| Close(); |
| } |
| |
| // TODO(hclam): Add tests for data conversation in low latency mode. |
| |
| } // namespace content |