Index: content/browser/renderer_host/media/audio_output_service_context_impl_unittest.cc |
diff --git a/content/browser/renderer_host/media/audio_output_service_context_impl_unittest.cc b/content/browser/renderer_host/media/audio_output_service_context_impl_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c195db5103b2dd5e22e92a500a6eb40ae3b211bf |
--- /dev/null |
+++ b/content/browser/renderer_host/media/audio_output_service_context_impl_unittest.cc |
@@ -0,0 +1,355 @@ |
+// Copyright 2016 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_output_service_context_impl.h" |
+ |
+#include <cmath> |
+#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/audio_output.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/fake_audio_log_factory.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 Signal = std::vector<std::unique_ptr<const media::AudioBus>>; |
+using AudioOutputService = mojom::RendererAudioOutputService; |
+using AudioOutputServicePtr = mojo::InterfacePtr<AudioOutputService>; |
+using AudioOutputServiceRequest = mojo::InterfaceRequest<AudioOutputService>; |
+using AudioOutput = mojom::AudioOutput; |
+using AudioOutputPtr = mojo::InterfacePtr<AudioOutput>; |
+using AudioOutputRequest = mojo::InterfaceRequest<AudioOutput>; |
+ |
+const int kRenderProcessId = 42; |
+const int kRenderFrameId = 24; |
+const char kSecurityOrigin[] = "http://localhost"; |
+const char kSalt[] = "salt"; |
+const double pi = std::acos(-1); |
+const media::AudioParameters params( |
+ media::AudioParameters::AUDIO_PCM_LOW_LATENCY, |
+ media::CHANNEL_LAYOUT_MONO, |
+ 44100 /*sample frequency*/, |
+ 16 /*bits per sample*/, |
+ 441 /*10 ms buffers*/); |
+ |
+Signal MakeSineWave() { |
+ Signal s; |
+ s.reserve(1000); |
+ int64_t t = 0; |
+ for (int i = 0; i < 1000; i++) { |
+ std::unique_ptr<media::AudioBus> bus = media::AudioBus::Create(params); |
+ float* channel = bus->channel(0); |
+ int frames = params.frames_per_buffer(); |
+ for (int j = 0; j < frames; ++j) { |
+ ++t; |
+ channel[j] = std::sin(t * params.GetMicrosecondsPerFrame() / 1000000.0 * |
+ 440 * 2 * pi); |
+ } |
+ s.push_back(std::move(bus)); |
+ } |
+ return s; |
+} |
+ |
+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) {} |
+ |
+ ~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) |
+ : 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 {} |
+ |
+ bool Open() override { return true; } |
+ void SetVolume(double volume) override {} |
+ void GetVolume(double* volume) override { *volume = 1; } |
+ void Close() override { audio_manager_->ReleaseOutputStream(this); } |
+ |
+ void ThreadMain() override { |
+ Signal expected = MakeSineWave(); |
+ std::unique_ptr<media::AudioBus> dest = media::AudioBus::Create(params); |
+ for (const auto& bus : expected) { |
+ 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(bus->channel(0)[frame], |
+ dest->channel(0)[frame])) |
+ << "Got " << dest->channel(0)[frame] << ", expected " |
+ << bus->channel(0)[frame]; |
+ } |
+ } |
+ } |
+ |
+ private: |
+ base::OnceClosure sync_closure_; |
+ base::PlatformThreadHandle thread_handle_; |
+ MockAudioManager* audio_manager_; |
+ AudioSourceCallback* callback_; |
+}; |
+ |
+void AuthCallback2(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. Sends a signal from a dedicated thread. |
+// TODO(maxmorin): Replace with an instance of the real client, when it exists. |
+class TestClient : public base::PlatformThread::Delegate { |
+ public: |
+ TestClient() {} |
+ |
+ ~TestClient() override { base::PlatformThread::Join(thread_handle_); } |
+ |
+ // Starts thread, sets up IPC primitives and sends signal on thread. |
+ void Start(Signal s, |
+ mojo::ScopedSharedBufferHandle shared_buffer, |
+ mojo::ScopedHandle socket_handle) { |
+ s_ = std::move(s); |
+ |
+ 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(params)); |
+ 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 { |
+ media::AudioOutputBuffer* buffer = |
+ reinterpret_cast<media::AudioOutputBuffer*>(memory_->memory()); |
+ std::unique_ptr<media::AudioBus> output_bus = |
+ media::AudioBus::WrapMemory(params, buffer->audio); |
+ |
+ // Send s. |
+ uint32_t buffer_index = 0; |
+ for (const auto& bus : s_) { |
+ uint32_t pending_data = 0; |
+ size_t bytes_read = socket_->Receive(&pending_data, sizeof(pending_data)); |
+ EXPECT_EQ(sizeof(pending_data), bytes_read); |
+ EXPECT_EQ(0u, pending_data); |
+ |
+ ++buffer_index; |
+ |
+ bus->CopyTo(output_bus.get()); |
+ |
+ size_t bytes_written = socket_->Send(&buffer_index, sizeof(buffer_index)); |
+ EXPECT_EQ(sizeof(buffer_index), bytes_written); |
+ } |
+ } |
+ |
+ private: |
+ base::PlatformThreadHandle thread_handle_; |
+ Signal s_; |
+ std::unique_ptr<base::CancelableSyncSocket> socket_; |
+ std::unique_ptr<base::SharedMemory> memory_; |
+}; |
+ |
+} // namespace |
+ |
+class AudioOutputServiceIntegrationTest : public Test { |
+ public: |
+ AudioOutputServiceIntegrationTest() |
+ : media_stream_manager_(), |
+ thread_bundle_(TestBrowserThreadBundle::Options::REAL_IO_THREAD), |
+ audio_thread_(), |
+ log_factory_(), |
+ audio_manager_( |
+ new StrictMock<MockAudioManager>(audio_thread_.task_runner(), |
+ audio_thread_.worker_task_runner(), |
+ &log_factory_)) { |
+ media_stream_manager_ = |
+ base::MakeUnique<MediaStreamManager>(audio_manager_.get()); |
+ } |
+ |
+ ~AudioOutputServiceIntegrationTest() override { SyncWithAllThreads(); } |
+ |
+ void CreateAndBindService(AudioOutputServiceRequest request) { |
+ service_context_.reset(new AudioOutputServiceContextImpl( |
+ kRenderProcessId, audio_manager_.get(), media_stream_manager_.get(), |
+ kSalt)); |
+ service_context_->CreateService(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<AudioOutputServiceContextImpl, |
+ BrowserThread::DeleteOnIOThread> |
+ service_context_; |
+}; |
+ |
+TEST_F(AudioOutputServiceIntegrationTest, StreamIntegrationTest) { |
+ // Sets up the service 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(params)); |
+ |
+ AudioOutputServicePtr service_ptr; |
+ BrowserThread::PostTask( |
+ BrowserThread::IO, FROM_HERE, |
+ base::Bind(&AudioOutputServiceIntegrationTest::CreateAndBindService, |
+ base::Unretained(this), |
+ base::Passed(mojo::MakeRequest(&service_ptr)))); |
+ |
+ AudioOutputPtr output_ptr; |
+ base::RunLoop loop; |
+ media::OutputDeviceStatus status; |
+ media::AudioParameters params2; |
+ std::string id; |
+ service_ptr->RequestDeviceAuthorization( |
+ mojo::MakeRequest<AudioOutput>(&output_ptr), /*session_id*/ 0, "default", |
+ url::Origin(GURL(kSecurityOrigin)), |
+ base::Bind(&AuthCallback2, base::Passed(loop.QuitWhenIdleClosure()), |
+ base::Unretained(&status), base::Unretained(¶ms2), |
+ base::Unretained(&id))); |
+ loop.Run(); |
+ ASSERT_EQ(status, media::OUTPUT_DEVICE_STATUS_OK); |
+ ASSERT_EQ(params.AsHumanReadableString(), params2.AsHumanReadableString()); |
+ ASSERT_TRUE(id.empty()); |
+ |
+ { |
+ TestClient client; |
+ output_ptr->Start(params, |
+ base::Bind(&TestClient::Start, base::Unretained(&client), |
+ base::Passed(MakeSineWave()))); |
+ output_ptr->Play(); |
+ SyncWithAllThreads(); |
+ } // Joining client thread. |
+ output_ptr.reset(); |
+ SyncWithAllThreads(); // Joins stream thread. |
+} |
+ |
+} // namespace content |