Chromium Code Reviews| 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..a60c0350dfade9f2bead2c620cdbe06f3988f426 |
| --- /dev/null |
| +++ b/content/browser/renderer_host/media/renderer_audio_output_stream_factory_context_impl_unittest.cc |
| @@ -0,0 +1,396 @@ |
| +// 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 <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/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/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 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 char kSecurityOrigin[] = "http://localhost"; |
| +const int kSampleFrequency = 44100; |
| +const int kBitsPerSample = 16; |
| +const int kSamplesPerBuffer = kSampleFrequency / 100; |
| +const char kSalt[] = "salt"; |
| +const double pi = std::acos(-1); |
| + |
| +media::AudioParameters GetTestAudioParameters() { |
| + return media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY, |
| + media::CHANNEL_LAYOUT_MONO, kSampleFrequency, |
| + kBitsPerSample, kSamplesPerBuffer); |
| +} |
| + |
| +Signal MakeSineWave() { |
|
DaleCurtis
2017/03/24 17:47:12
See https://cs.chromium.org/chromium/src/media/aud
Max Morin
2017/03/27 14:40:51
Oh, nice. I should've looked for this :).
|
| + Signal s; |
| + media::AudioParameters params = GetTestAudioParameters(); |
| + const float frequency = 440.0; |
| + const int n_frames = 1000; |
| + s.reserve(n_frames); |
| + int64_t t = 0; |
| + for (int i = 0; i < n_frames; 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 * |
| + frequency * 2.0 * 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) { |
| + 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 { |
| + Signal expected = MakeSineWave(); |
| + media::AudioParameters params = GetTestAudioParameters(); |
| + 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]; |
| + } |
| + } |
| + 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 a signal 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(Signal s, |
| + mojo::ScopedSharedBufferHandle shared_buffer, |
| + mojo::ScopedHandle socket_handle) { |
| + signal_to_provide_ = 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( |
| + 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 { |
| + media::AudioOutputBuffer* buffer = |
| + reinterpret_cast<media::AudioOutputBuffer*>(memory_->memory()); |
| + std::unique_ptr<media::AudioBus> output_bus = |
| + media::AudioBus::WrapMemory(GetTestAudioParameters(), buffer->audio); |
| + |
| + // Send s. |
| + uint32_t buffer_index = 0; |
| + for (const auto& bus : signal_to_provide_) { |
| + 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); |
| + |
| + ++buffer_index; |
| + |
| + bus->CopyTo(output_bus.get()); |
| + |
| + size_t bytes_written = socket_->Send(&buffer_index, sizeof(buffer_index)); |
| + PCHECK(sizeof(pending_data) == bytes_written) |
| + << "Tried to write " << sizeof(pending_data) |
| + << " bytes but only wrote " << bytes_written << " bytes"; |
| + } |
| + } |
| + |
| + private: |
| + base::PlatformThreadHandle thread_handle_; |
| + Signal signal_to_provide_; |
| + 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(), 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", |
| + url::Origin(GURL(kSecurityOrigin)), |
| + 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), |
| + base::Passed(MakeSineWave()))); |
| + SyncWithAllThreads(); |
| + stream_ptr->Play(); |
| + SyncWithAllThreads(); |
| + } // Joining client thread. |
| + stream_ptr.reset(); |
| + SyncWithAllThreads(); |
| +} |
| + |
| +} // namespace content |