Index: content/browser/renderer_host/media/renderer_audio_output_stream_factory_context_impl_unittest.cc |
diff --git a/content/browser/renderer_host/media/renderer_audio_output_stream_factory_context_impl_unittest.cc b/content/browser/renderer_host/media/renderer_audio_output_stream_factory_context_impl_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4a81273b7d168dc542e0543b494062594dbdf772 |
--- /dev/null |
+++ b/content/browser/renderer_host/media/renderer_audio_output_stream_factory_context_impl_unittest.cc |
@@ -0,0 +1,383 @@ |
+// Copyright 2017 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/renderer_audio_output_stream_factory_context_impl.h" |
+ |
+#include <utility> |
+ |
+#include "base/bind.h" |
+#include "base/memory/shared_memory.h" |
+#include "base/memory/shared_memory_handle.h" |
+#include "base/run_loop.h" |
+#include "base/sync_socket.h" |
+#include "cc/base/math_util.h" |
+#include "content/browser/audio_manager_thread.h" |
+#include "content/browser/renderer_host/media/media_stream_manager.h" |
+#include "content/common/media/renderer_audio_output_stream_factory.mojom.h" |
+#include "content/public/browser/browser_thread.h" |
+#include "content/public/test/mock_render_process_host.h" |
+#include "content/public/test/test_browser_context.h" |
+#include "content/public/test/test_browser_thread_bundle.h" |
+#include "media/audio/audio_manager_base.h" |
+#include "media/audio/audio_output_controller.h" |
+#include "media/audio/audio_system_impl.h" |
+#include "media/audio/fake_audio_log_factory.h" |
+#include "media/audio/simple_sources.h" |
+#include "media/base/audio_parameters.h" |
+#include "media/base/media_switches.h" |
+#include "mojo/public/cpp/bindings/binding.h" |
+#include "mojo/public/cpp/system/platform_handle.h" |
+#include "testing/gmock/include/gmock/gmock.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+ |
+namespace content { |
+ |
+namespace { |
+ |
+using testing::_; |
+using testing::StrictMock; |
+using testing::Return; |
+using testing::Test; |
+using AudioOutputStreamFactory = mojom::RendererAudioOutputStreamFactory; |
+using AudioOutputStreamFactoryPtr = |
+ mojo::InterfacePtr<AudioOutputStreamFactory>; |
+using AudioOutputStreamFactoryRequest = |
+ mojo::InterfaceRequest<AudioOutputStreamFactory>; |
+using AudioOutputStream = media::mojom::AudioOutputStream; |
+using AudioOutputStreamPtr = mojo::InterfacePtr<AudioOutputStream>; |
+using AudioOutputStreamRequest = mojo::InterfaceRequest<AudioOutputStream>; |
+using AudioOutputStreamProvider = media::mojom::AudioOutputStreamProvider; |
+using AudioOutputStreamProviderPtr = |
+ mojo::InterfacePtr<AudioOutputStreamProvider>; |
+using AudioOutputStreamProviderRequest = |
+ mojo::InterfaceRequest<AudioOutputStreamProvider>; |
+ |
+const int kRenderProcessId = 42; |
+const int kRenderFrameId = 24; |
+const int kNoSessionId = 0; |
+const float kWaveFrequency = 440.f; |
+const int kChannels = 1; |
+const int kBuffers = 1000; |
+const int kSampleFrequency = 44100; |
+const int kBitsPerSample = 16; |
+const int kSamplesPerBuffer = kSampleFrequency / 100; |
+const char kSalt[] = "salt"; |
+ |
+std::unique_ptr<media::AudioOutputStream::AudioSourceCallback> |
+GetTestAudioSource() { |
+ return base::MakeUnique<media::SineWaveAudioSource>(kChannels, kWaveFrequency, |
+ kSampleFrequency); |
+} |
+ |
+media::AudioParameters GetTestAudioParameters() { |
+ return media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY, |
+ media::CHANNEL_LAYOUT_MONO, kSampleFrequency, |
+ kBitsPerSample, kSamplesPerBuffer); |
+} |
+ |
+void SyncWith(scoped_refptr<base::SingleThreadTaskRunner> task_runner) { |
+ CHECK(task_runner); |
+ CHECK(!task_runner->BelongsToCurrentThread()); |
+ base::WaitableEvent e = {base::WaitableEvent::ResetPolicy::MANUAL, |
+ base::WaitableEvent::InitialState::NOT_SIGNALED}; |
+ task_runner->PostTask(FROM_HERE, base::Bind(&base::WaitableEvent::Signal, |
+ base::Unretained(&e))); |
+ e.Wait(); |
+} |
+ |
+void SyncWithAllThreads() { |
+ DCHECK_CURRENTLY_ON(BrowserThread::UI); |
+ // New tasks might be posted while we are syncing, but in every iteration at |
+ // least one task will be run. 20 iterations should be enough for our code. |
+ for (int i = 0; i < 20; ++i) { |
+ { |
+ base::MessageLoop::ScopedNestableTaskAllower allower( |
+ base::MessageLoop::current()); |
+ base::RunLoop().RunUntilIdle(); |
+ } |
+ SyncWith(BrowserThread::GetTaskRunnerForThread(BrowserThread::IO)); |
+ SyncWith(media::AudioManager::Get()->GetWorkerTaskRunner()); |
+ } |
+} |
+ |
+class MockAudioManager : public media::AudioManagerBase { |
+ public: |
+ MockAudioManager( |
+ scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
+ scoped_refptr<base::SingleThreadTaskRunner> worker_task_runner, |
+ media::AudioLogFactory* audio_log_factory) |
+ : media::AudioManagerBase(task_runner, |
+ worker_task_runner, |
+ audio_log_factory) { |
+ ON_CALL(*this, HasAudioOutputDevices()).WillByDefault(Return(true)); |
+ } |
+ |
+ ~MockAudioManager() override { Shutdown(); } |
+ |
+ MOCK_METHOD0(HasAudioOutputDevices, bool()); |
+ MOCK_METHOD0(HasAudioInputDevices, bool()); |
+ MOCK_METHOD0(GetName, const char*()); |
+ |
+ MOCK_METHOD2(MakeLinearOutputStream, |
+ media::AudioOutputStream*(const media::AudioParameters& params, |
+ const LogCallback& log_callback)); |
+ MOCK_METHOD3(MakeLowLatencyOutputStream, |
+ media::AudioOutputStream*(const media::AudioParameters& params, |
+ const std::string& device_id, |
+ const LogCallback& log_callback)); |
+ MOCK_METHOD3(MakeLinearInputStream, |
+ media::AudioInputStream*(const media::AudioParameters& params, |
+ const std::string& device_id, |
+ const LogCallback& log_callback)); |
+ MOCK_METHOD3(MakeLowLatencyInputStream, |
+ media::AudioInputStream*(const media::AudioParameters& params, |
+ const std::string& device_id, |
+ const LogCallback& log_callback)); |
+ MOCK_METHOD2(GetPreferredOutputStreamParameters, |
+ media::AudioParameters(const std::string& device_id, |
+ const media::AudioParameters& params)); |
+}; |
+ |
+class MockAudioOutputStream : public media::AudioOutputStream, |
+ public base::PlatformThread::Delegate { |
+ public: |
+ explicit MockAudioOutputStream(MockAudioManager* audio_manager) |
+ : done_(base::WaitableEvent::ResetPolicy::MANUAL, |
+ base::WaitableEvent::InitialState::NOT_SIGNALED), |
+ audio_manager_(audio_manager) {} |
+ |
+ ~MockAudioOutputStream() override { |
+ base::PlatformThread::Join(thread_handle_); |
+ } |
+ |
+ void Start(AudioSourceCallback* callback) override { |
+ callback_ = callback; |
+ EXPECT_TRUE(base::PlatformThread::CreateWithPriority( |
+ 0, this, &thread_handle_, base::ThreadPriority::REALTIME_AUDIO)); |
+ } |
+ |
+ void Stop() override { |
+ done_.Wait(); |
+ callback_ = nullptr; |
+ } |
+ |
+ bool Open() override { return true; } |
+ void SetVolume(double volume) override {} |
+ void GetVolume(double* volume) override { *volume = 1; } |
+ void Close() override { |
+ Stop(); |
+ audio_manager_->ReleaseOutputStream(this); |
+ } |
+ |
+ void ThreadMain() override { |
+ std::unique_ptr<media::AudioOutputStream::AudioSourceCallback> |
+ expected_audio = GetTestAudioSource(); |
+ media::AudioParameters params = GetTestAudioParameters(); |
+ std::unique_ptr<media::AudioBus> dest = media::AudioBus::Create(params); |
+ std::unique_ptr<media::AudioBus> expected_buffer = |
+ media::AudioBus::Create(params); |
+ for (int i = 0; i < kBuffers; ++i) { |
+ expected_audio->OnMoreData(base::TimeDelta(), base::TimeTicks::Now(), 0, |
+ expected_buffer.get()); |
+ callback_->OnMoreData(base::TimeDelta(), base::TimeTicks::Now(), 0, |
+ dest.get()); |
+ for (int frame = 0; frame < params.frames_per_buffer(); ++frame) { |
+ // Using EXPECT here causes massive log spam in case of a broken test, |
+ // and ASSERT causes it to hang, so we use CHECK. |
+ CHECK(cc::MathUtil::IsNearlyTheSameForTesting( |
+ expected_buffer->channel(0)[frame], dest->channel(0)[frame])) |
+ << "Got " << dest->channel(0)[frame] << ", expected " |
+ << expected_buffer->channel(0)[frame]; |
+ } |
+ } |
+ done_.Signal(); |
+ } |
+ |
+ private: |
+ base::OnceClosure sync_closure_; |
+ base::PlatformThreadHandle thread_handle_; |
+ base::WaitableEvent done_; |
+ MockAudioManager* audio_manager_; |
+ AudioSourceCallback* callback_; |
+}; |
+ |
+void AuthCallback(base::OnceClosure sync_closure, |
+ media::OutputDeviceStatus* status_out, |
+ media::AudioParameters* params_out, |
+ std::string* id_out, |
+ media::OutputDeviceStatus status, |
+ const media::AudioParameters& params, |
+ const std::string& id) { |
+ *status_out = status; |
+ *params_out = params; |
+ *id_out = id; |
+ std::move(sync_closure).Run(); |
+} |
+ |
+// "Renderer-side" audio client. Provides the signal given by |
+// GetTestAudioSource() from a dedicated thread when given sync socket and |
+// shared memory. |
+// TODO(maxmorin): Replace with an instance of the real client, when it exists. |
+class TestIPCClient : public base::PlatformThread::Delegate { |
+ public: |
+ TestIPCClient() {} |
+ |
+ ~TestIPCClient() override { base::PlatformThread::Join(thread_handle_); } |
+ |
+ // Starts thread, sets up IPC primitives and sends signal on thread. |
+ void Start(mojo::ScopedSharedBufferHandle shared_buffer, |
+ mojo::ScopedHandle socket_handle) { |
+ EXPECT_TRUE(socket_handle.is_valid()); |
+ // Set up socket. |
+ base::PlatformFile fd; |
+ mojo::UnwrapPlatformFile(std::move(socket_handle), &fd); |
+ socket_ = base::MakeUnique<base::CancelableSyncSocket>(fd); |
+ EXPECT_NE(socket_->handle(), base::CancelableSyncSocket::kInvalidHandle); |
+ |
+ // Set up memory. |
+ EXPECT_TRUE(shared_buffer.is_valid()); |
+ size_t memory_length; |
+ base::SharedMemoryHandle shmem_handle; |
+ bool read_only; |
+ EXPECT_EQ( |
+ mojo::UnwrapSharedMemoryHandle(std::move(shared_buffer), &shmem_handle, |
+ &memory_length, &read_only), |
+ MOJO_RESULT_OK); |
+ EXPECT_EQ(memory_length, sizeof(media::AudioOutputBufferParameters) + |
+ media::AudioBus::CalculateMemorySize( |
+ GetTestAudioParameters())); |
+ EXPECT_EQ(read_only, false); |
+ memory_ = base::MakeUnique<base::SharedMemory>(shmem_handle, read_only); |
+ EXPECT_TRUE(memory_->Map(memory_length)); |
+ |
+ EXPECT_TRUE(base::PlatformThread::CreateWithPriority( |
+ 0, this, &thread_handle_, base::ThreadPriority::REALTIME_AUDIO)); |
+ } |
+ |
+ void ThreadMain() override { |
+ std::unique_ptr<media::AudioOutputStream::AudioSourceCallback> |
+ audio_source = GetTestAudioSource(); |
+ |
+ media::AudioOutputBuffer* buffer = |
+ reinterpret_cast<media::AudioOutputBuffer*>(memory_->memory()); |
+ std::unique_ptr<media::AudioBus> output_bus = |
+ media::AudioBus::WrapMemory(GetTestAudioParameters(), buffer->audio); |
+ |
+ // Send s. |
+ for (uint32_t i = 0; i < kBuffers;) { |
+ uint32_t pending_data = 0; |
+ size_t bytes_read = socket_->Receive(&pending_data, sizeof(pending_data)); |
+ // Use check here, since there's a risk of hangs in case of a bug. |
+ PCHECK(sizeof(pending_data) == bytes_read) |
+ << "Tried to read " << sizeof(pending_data) << " bytes but only read " |
+ << bytes_read << " bytes"; |
+ CHECK_EQ(0u, pending_data); |
+ |
+ ++i; |
+ audio_source->OnMoreData(base::TimeDelta(), base::TimeTicks(), 0, |
+ output_bus.get()); |
+ |
+ size_t bytes_written = socket_->Send(&i, sizeof(i)); |
+ PCHECK(sizeof(pending_data) == bytes_written) |
+ << "Tried to write " << sizeof(pending_data) |
+ << " bytes but only wrote " << bytes_written << " bytes"; |
+ } |
+ } |
+ |
+ private: |
+ base::PlatformThreadHandle thread_handle_; |
+ std::unique_ptr<base::CancelableSyncSocket> socket_; |
+ std::unique_ptr<base::SharedMemory> memory_; |
+}; |
+ |
+} // namespace |
+ |
+// TODO(maxmorin): Add test for play, pause and set volume. |
+class RendererAudioOutputStreamFactoryIntegrationTest : public Test { |
+ public: |
+ RendererAudioOutputStreamFactoryIntegrationTest() |
+ : media_stream_manager_(), |
+ thread_bundle_(TestBrowserThreadBundle::Options::REAL_IO_THREAD), |
+ audio_thread_(), |
+ log_factory_(), |
+ audio_manager_(new MockAudioManager(audio_thread_.task_runner(), |
+ audio_thread_.worker_task_runner(), |
+ &log_factory_)), |
+ audio_system_(media::AudioSystemImpl::Create(audio_manager_.get())) { |
+ media_stream_manager_ = |
+ base::MakeUnique<MediaStreamManager>(audio_system_.get()); |
+ } |
+ |
+ void CreateAndBindFactory(AudioOutputStreamFactoryRequest request) { |
+ factory_context_.reset(new RendererAudioOutputStreamFactoryContextImpl( |
+ kRenderProcessId, audio_system_.get(), audio_manager_.get(), |
+ media_stream_manager_.get(), kSalt)); |
+ factory_context_->CreateFactory(kRenderFrameId, std::move(request)); |
+ } |
+ |
+ std::unique_ptr<MediaStreamManager> media_stream_manager_; |
+ TestBrowserThreadBundle thread_bundle_; |
+ AudioManagerThread audio_thread_; |
+ media::FakeAudioLogFactory log_factory_; |
+ media::ScopedAudioManagerPtr audio_manager_; |
+ std::unique_ptr<media::AudioSystem> audio_system_; |
+ std::unique_ptr<RendererAudioOutputStreamFactoryContextImpl, |
+ BrowserThread::DeleteOnIOThread> |
+ factory_context_; |
+}; |
+ |
+TEST_F(RendererAudioOutputStreamFactoryIntegrationTest, StreamIntegrationTest) { |
+ // Sets up the factory on the IO thread and runs client code on the UI thread. |
+ // Send a sine wave from the client and makes sure it's received by the output |
+ // stream. |
+ MockAudioOutputStream* stream = new MockAudioOutputStream( |
+ static_cast<MockAudioManager*>(audio_manager_.get())); |
+ |
+ // Make sure the mock audio manager uses our mock stream. |
+ EXPECT_CALL(*static_cast<MockAudioManager*>(audio_manager_.get()), |
+ MakeLowLatencyOutputStream(_, "", _)) |
+ .WillOnce(Return(stream)); |
+ EXPECT_CALL(*static_cast<MockAudioManager*>(audio_manager_.get()), |
+ GetPreferredOutputStreamParameters(_, _)) |
+ .WillRepeatedly(Return(GetTestAudioParameters())); |
+ |
+ AudioOutputStreamFactoryPtr factory_ptr; |
+ BrowserThread::PostTask( |
+ BrowserThread::IO, FROM_HERE, |
+ base::Bind(&RendererAudioOutputStreamFactoryIntegrationTest:: |
+ CreateAndBindFactory, |
+ base::Unretained(this), |
+ base::Passed(mojo::MakeRequest(&factory_ptr)))); |
+ |
+ AudioOutputStreamProviderPtr provider_ptr; |
+ base::RunLoop loop; |
+ media::OutputDeviceStatus status; |
+ media::AudioParameters params; |
+ std::string id; |
+ factory_ptr->RequestDeviceAuthorization( |
+ mojo::MakeRequest(&provider_ptr), kNoSessionId, "default", |
+ base::Bind(&AuthCallback, base::Passed(loop.QuitWhenIdleClosure()), |
+ base::Unretained(&status), base::Unretained(¶ms), |
+ base::Unretained(&id))); |
+ loop.Run(); |
+ ASSERT_EQ(status, media::OUTPUT_DEVICE_STATUS_OK); |
+ ASSERT_EQ(GetTestAudioParameters().AsHumanReadableString(), |
+ params.AsHumanReadableString()); |
+ ASSERT_TRUE(id.empty()); |
+ |
+ AudioOutputStreamPtr stream_ptr; |
+ { |
+ TestIPCClient client; |
+ provider_ptr->Acquire( |
+ mojo::MakeRequest(&stream_ptr), params, |
+ base::Bind(&TestIPCClient::Start, base::Unretained(&client))); |
+ SyncWithAllThreads(); |
+ stream_ptr->Play(); |
+ SyncWithAllThreads(); |
+ } // Joining client thread. |
+ stream_ptr.reset(); |
+ SyncWithAllThreads(); |
+} |
+ |
+} // namespace content |