| // Copyright 2018 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/audio_input_stream_broker.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/sync_socket.h" |
| #include "base/test/bind.h" |
| #include "base/test/mock_callback.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "media/mojo/mojom/audio_input_stream.mojom.h" |
| #include "media/mojo/mojom/audio_processing.mojom.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "services/audio/public/cpp/fake_stream_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::InSequence; |
| using ::testing::Mock; |
| using ::testing::StrictMock; |
| using ::testing::Test; |
| |
| namespace content { |
| |
| namespace { |
| |
| const int kRenderProcessId = 123; |
| const int kRenderFrameId = 234; |
| const uint32_t kShMemCount = 10; |
| const bool kEnableAgc = false; |
| const char kDeviceId[] = "testdeviceid"; |
| const bool kInitiallyMuted = false; |
| |
| media::AudioParameters TestParams() { |
| return media::AudioParameters::UnavailableDeviceParams(); |
| } |
| |
| using MockDeleterCallback = StrictMock< |
| base::MockCallback<base::OnceCallback<void(AudioStreamBroker*)>>>; |
| |
| class MockRendererAudioInputStreamFactoryClient |
| : public blink::mojom::RendererAudioInputStreamFactoryClient { |
| public: |
| MockRendererAudioInputStreamFactoryClient() = default; |
| |
| MockRendererAudioInputStreamFactoryClient( |
| const MockRendererAudioInputStreamFactoryClient&) = delete; |
| MockRendererAudioInputStreamFactoryClient& operator=( |
| const MockRendererAudioInputStreamFactoryClient&) = delete; |
| |
| ~MockRendererAudioInputStreamFactoryClient() override = default; |
| |
| mojo::PendingRemote<blink::mojom::RendererAudioInputStreamFactoryClient> |
| MakeRemote() { |
| return receiver_.BindNewPipeAndPassRemote(); |
| } |
| |
| MOCK_METHOD0(OnStreamCreated, void()); |
| |
| void StreamCreated( |
| mojo::PendingRemote<media::mojom::AudioInputStream> input_stream, |
| mojo::PendingReceiver<media::mojom::AudioInputStreamClient> |
| client_receiver, |
| media::mojom::ReadWriteAudioDataPipePtr data_pipe, |
| bool initially_muted, |
| const std::optional<base::UnguessableToken>& stream_id) override { |
| EXPECT_TRUE(stream_id.has_value()); |
| input_stream_.Bind(std::move(input_stream)); |
| client_receiver_ = std::move(client_receiver); |
| OnStreamCreated(); |
| } |
| |
| void CloseReceiver() { receiver_.reset(); } |
| |
| void setDisconnectHandler( |
| base::OnceCallback<void(uint32_t, const std::string&)> callback) { |
| receiver_.set_disconnect_with_reason_handler(std::move(callback)); |
| } |
| |
| private: |
| mojo::Receiver<blink::mojom::RendererAudioInputStreamFactoryClient> receiver_{ |
| this}; |
| mojo::Remote<media::mojom::AudioInputStream> input_stream_; |
| mojo::PendingReceiver<media::mojom::AudioInputStreamClient> client_receiver_; |
| }; |
| |
| class MockStreamFactory final : public audio::FakeStreamFactory { |
| public: |
| // State of an expected stream creation. |device_id| and |params| are set |
| // ahead of time and verified during request. The other fields are filled in |
| // when the request is received. |
| struct StreamRequestData { |
| StreamRequestData(const std::string& device_id, |
| const media::AudioParameters& params) |
| : device_id(device_id), params(params) {} |
| |
| bool requested = false; |
| mojo::PendingReceiver<media::mojom::AudioInputStream> stream_receiver; |
| mojo::Remote<media::mojom::AudioInputStreamClient> client; |
| mojo::Remote<media::mojom::AudioInputStreamObserver> observer; |
| mojo::Remote<media::mojom::AudioLog> log; |
| const std::string device_id; |
| const media::AudioParameters params; |
| uint32_t shared_memory_count; |
| bool enable_agc; |
| media::mojom::AudioProcessingConfigPtr processing_config; |
| CreateInputStreamCallback created_callback; |
| }; |
| |
| explicit MockStreamFactory(StreamRequestData* ex) |
| : stream_request_data_(ex) {} |
| |
| MockStreamFactory(const MockStreamFactory&) = delete; |
| MockStreamFactory& operator=(const MockStreamFactory&) = delete; |
| |
| ~MockStreamFactory() override = default; |
| |
| private: |
| void CreateInputStream( |
| mojo::PendingReceiver<media::mojom::AudioInputStream> stream_receiver, |
| mojo::PendingRemote<media::mojom::AudioInputStreamClient> client, |
| mojo::PendingRemote<media::mojom::AudioInputStreamObserver> observer, |
| mojo::PendingRemote<media::mojom::AudioLog> log, |
| const std::string& device_id, |
| const media::AudioParameters& params, |
| const base::UnguessableToken& group_id, |
| uint32_t shared_memory_count, |
| bool enable_agc, |
| media::mojom::AudioProcessingConfigPtr processing_config, |
| CreateInputStreamCallback created_callback) override { |
| // No way to cleanly exit the test here in case of failure, so use CHECK. |
| CHECK(stream_request_data_); |
| EXPECT_EQ(stream_request_data_->device_id, device_id); |
| EXPECT_TRUE(stream_request_data_->params.Equals(params)); |
| stream_request_data_->requested = true; |
| stream_request_data_->stream_receiver = std::move(stream_receiver); |
| stream_request_data_->client.Bind(std::move(client)); |
| stream_request_data_->observer.Bind(std::move(observer)); |
| stream_request_data_->log.Bind(std::move(log)); |
| stream_request_data_->shared_memory_count = shared_memory_count; |
| stream_request_data_->enable_agc = enable_agc; |
| stream_request_data_->processing_config = std::move(processing_config); |
| stream_request_data_->created_callback = std::move(created_callback); |
| } |
| |
| const raw_ptr<StreamRequestData> stream_request_data_; |
| }; |
| |
| struct TestEnvironment { |
| explicit TestEnvironment(MockStreamFactory::StreamRequestData* ex) |
| : broker(std::make_unique<AudioInputStreamBroker>( |
| kRenderProcessId, |
| kRenderFrameId, |
| kDeviceId, |
| TestParams(), |
| base::UnguessableToken::Create(), |
| kShMemCount, |
| kEnableAgc, |
| media::mojom::AudioProcessingConfig::New( |
| remote_controls_.BindNewPipeAndPassReceiver(), |
| media::AudioProcessingSettings()), |
| deleter.Get(), |
| renderer_factory_client.MakeRemote())), |
| stream_factory(ex) {} |
| |
| void RunUntilIdle() { task_environment.RunUntilIdle(); } |
| |
| BrowserTaskEnvironment task_environment; |
| MockDeleterCallback deleter; |
| StrictMock<MockRendererAudioInputStreamFactoryClient> renderer_factory_client; |
| mojo::Remote<media::mojom::AudioProcessorControls> remote_controls_; |
| std::unique_ptr<AudioInputStreamBroker> broker; |
| MockStreamFactory stream_factory; |
| mojo::Remote<media::mojom::AudioStreamFactory> factory_ptr{ |
| stream_factory.MakeRemote()}; |
| }; |
| |
| void ExpectRendererFactoryClientDisconnection( |
| TestEnvironment& env, |
| media::mojom::InputStreamErrorCode expected_error) { |
| env.renderer_factory_client.setDisconnectHandler(base::BindLambdaForTesting( |
| [expected_error](uint32_t reason, const std::string& description) { |
| EXPECT_EQ(static_cast<uint32_t>(expected_error), reason); |
| EXPECT_EQ("", description); |
| })); |
| } |
| |
| } // namespace |
| |
| TEST(AudioInputStreamBrokerTest, StoresProcessAndFrameId) { |
| BrowserTaskEnvironment task_environment; |
| MockDeleterCallback deleter; |
| StrictMock<MockRendererAudioInputStreamFactoryClient> renderer_factory_client; |
| |
| AudioInputStreamBroker broker(kRenderProcessId, kRenderFrameId, kDeviceId, |
| TestParams(), base::UnguessableToken::Create(), |
| kShMemCount, kEnableAgc, |
| nullptr /*processing_config*/, deleter.Get(), |
| renderer_factory_client.MakeRemote()); |
| |
| EXPECT_EQ(kRenderProcessId, broker.render_process_id()); |
| EXPECT_EQ(kRenderFrameId, broker.render_frame_id()); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, StreamCreationSuccess_Propagates) { |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| |
| EXPECT_TRUE(stream_request_data.requested); |
| EXPECT_TRUE(stream_request_data.processing_config); |
| EXPECT_TRUE(stream_request_data.processing_config->controls_receiver); |
| |
| // Set up test IPC primitives. |
| const size_t shmem_size = 456; |
| base::SyncSocket socket1, socket2; |
| base::SyncSocket::CreatePair(&socket1, &socket2); |
| std::move(stream_request_data.created_callback) |
| .Run({std::in_place, base::UnsafeSharedMemoryRegion::Create(shmem_size), |
| mojo::PlatformHandle(socket1.Take())}, |
| kInitiallyMuted, base::UnguessableToken::Create()); |
| |
| EXPECT_CALL(env.renderer_factory_client, OnStreamCreated()); |
| |
| env.RunUntilIdle(); |
| |
| Mock::VerifyAndClear(&env.renderer_factory_client); |
| |
| env.broker.reset(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, StreamCreationFailure_CallsDeleter) { |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| |
| EXPECT_TRUE(stream_request_data.requested); |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| |
| std::move(stream_request_data.created_callback) |
| .Run(nullptr, kInitiallyMuted, std::nullopt); |
| |
| env.RunUntilIdle(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, RendererFactoryClientDisconnect_CallsDeleter) { |
| InSequence seq; |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| EXPECT_TRUE(stream_request_data.requested); |
| |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| env.renderer_factory_client.CloseReceiver(); |
| env.RunUntilIdle(); |
| Mock::VerifyAndClear(&env.deleter); |
| |
| env.stream_factory.ResetReceiver(); |
| env.RunUntilIdle(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, ObserverDisconnect_CallsDeleter) { |
| InSequence seq; |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| EXPECT_TRUE(stream_request_data.requested); |
| |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| stream_request_data.observer.reset(); |
| env.RunUntilIdle(); |
| Mock::VerifyAndClear(&env.deleter); |
| |
| env.stream_factory.ResetReceiver(); |
| env.RunUntilIdle(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, ObserverDisconnect_PropagateError) { |
| InSequence seq; |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| EXPECT_TRUE(stream_request_data.requested); |
| |
| ExpectRendererFactoryClientDisconnection( |
| env, media::mojom::InputStreamErrorCode::kUnknown); |
| |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| stream_request_data.observer.ResetWithReason( |
| static_cast<uint32_t>( |
| media::mojom::AudioInputStreamObserver::DisconnectReason::kDefault), |
| std::string()); |
| env.RunUntilIdle(); |
| Mock::VerifyAndClear(&env.deleter); |
| |
| env.stream_factory.ResetReceiver(); |
| env.RunUntilIdle(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, ObserverDisconnect_PropagatePermissionsError) { |
| InSequence seq; |
| MockStreamFactory::StreamRequestData stream_request_data(kDeviceId, |
| TestParams()); |
| TestEnvironment env(&stream_request_data); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.RunUntilIdle(); |
| EXPECT_TRUE(stream_request_data.requested); |
| |
| ExpectRendererFactoryClientDisconnection( |
| env, media::mojom::InputStreamErrorCode::kSystemPermissions); |
| |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| stream_request_data.observer.ResetWithReason( |
| static_cast<uint32_t>(media::mojom::AudioInputStreamObserver:: |
| DisconnectReason::kSystemPermissions), |
| std::string()); |
| env.RunUntilIdle(); |
| Mock::VerifyAndClear(&env.deleter); |
| |
| env.stream_factory.ResetReceiver(); |
| env.RunUntilIdle(); |
| } |
| |
| TEST(AudioInputStreamBrokerTest, |
| FactoryDisconnectDuringConstruction_CallsDeleter) { |
| TestEnvironment env(nullptr); |
| env.broker->CreateStream(env.factory_ptr.get()); |
| env.stream_factory.ResetReceiver(); |
| |
| EXPECT_CALL(env.deleter, Run(env.broker.release())) |
| .WillOnce(testing::DeleteArg<0>()); |
| |
| env.RunUntilIdle(); |
| } |
| |
| } // namespace content |