| // 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/single_thread_task_runner.h" |
| #include "base/sync_socket.h" |
| #include "base/task/post_task.h" |
| #include "cc/base/math_util.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_task_traits.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 "content/renderer/media/audio/mojo_audio_output_ipc.h" |
| #include "media/audio/audio_manager_base.h" |
| #include "media/audio/audio_output_controller.h" |
| #include "media/audio/audio_output_device.h" |
| #include "media/audio/audio_system_impl.h" |
| #include "media/audio/audio_thread_impl.h" |
| #include "media/audio/mock_audio_manager.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; |
| |
| const int kRenderProcessId = 42; |
| const int kRenderFrameId = 24; |
| const float kWaveFrequency = 440.f; |
| const int kChannels = 1; |
| const int kBuffers = 100; |
| const int kSampleFrequency = 8000; |
| const int kSamplesPerBuffer = kSampleFrequency / 10; |
| |
| std::unique_ptr<media::AudioOutputStream::AudioSourceCallback> |
| GetTestAudioSource() { |
| return std::make_unique<media::SineWaveAudioSource>(kChannels, kWaveFrequency, |
| kSampleFrequency); |
| } |
| |
| media::AudioParameters GetTestAudioParameters() { |
| return media::AudioParameters(media::AudioParameters::AUDIO_PCM_LOW_LATENCY, |
| media::CHANNEL_LAYOUT_MONO, kSampleFrequency, |
| 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::BindOnce(&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::RunLoop(base::RunLoop::Type::kNestableTasksAllowed).RunUntilIdle(); |
| SyncWith(base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO})); |
| SyncWith(media::AudioManager::Get()->GetWorkerTaskRunner()); |
| } |
| } |
| |
| class MockAudioOutputStream : public media::AudioOutputStream, |
| public base::PlatformThread::Delegate { |
| public: |
| MockAudioOutputStream() |
| : done_(base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED) {} |
| |
| ~MockAudioOutputStream() override { |
| base::PlatformThread::Join(thread_handle_); |
| } |
| |
| void Wait() { done_.Wait(); } |
| |
| 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(); |
| delete 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::IsFloatNearlyTheSame( |
| 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_; |
| AudioSourceCallback* callback_; |
| }; |
| |
| class TestRenderCallback : public media::AudioRendererSink::RenderCallback { |
| public: |
| TestRenderCallback() : source_(GetTestAudioSource()) {} |
| |
| ~TestRenderCallback() override {} |
| |
| int Render(base::TimeDelta delay, |
| base::TimeTicks delay_timestamp, |
| int prior_frames_skipped, |
| media::AudioBus* dest) override { |
| return source_->OnMoreData(delay, delay_timestamp, prior_frames_skipped, |
| dest); |
| } |
| |
| MOCK_METHOD0(OnRenderError, void()); |
| |
| private: |
| std::unique_ptr<media::AudioOutputStream::AudioSourceCallback> source_; |
| }; |
| |
| } // 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_manager_(std::make_unique<media::AudioThreadImpl>()), |
| audio_system_(&audio_manager_) { |
| media_stream_manager_ = std::make_unique<MediaStreamManager>( |
| &audio_system_, audio_manager_.GetTaskRunner()); |
| } |
| |
| ~RendererAudioOutputStreamFactoryIntegrationTest() override { |
| audio_manager_.Shutdown(); |
| } |
| |
| UniqueAudioOutputStreamFactoryPtr CreateAndBindFactory( |
| mojom::RendererAudioOutputStreamFactoryRequest request) { |
| factory_context_.reset(new RendererAudioOutputStreamFactoryContextImpl( |
| kRenderProcessId, &audio_system_, &audio_manager_, |
| media_stream_manager_.get())); |
| return RenderFrameAudioOutputStreamFactoryHandle::CreateFactory( |
| factory_context_.get(), kRenderFrameId, std::move(request)); |
| } |
| |
| std::unique_ptr<MediaStreamManager> media_stream_manager_; |
| TestBrowserThreadBundle thread_bundle_; |
| media::MockAudioManager audio_manager_; |
| media::AudioSystemImpl audio_system_; |
| std::unique_ptr<RendererAudioOutputStreamFactoryContextImpl, |
| BrowserThread::DeleteOnIOThread> |
| factory_context_; |
| }; |
| |
| // It's flaky on the buildbot, http://crbug.com/761214. |
| 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(); |
| |
| // Make sure the mock audio manager uses our mock stream. |
| bool create_stream_called = false; |
| audio_manager_.SetMakeOutputStreamCB(base::BindRepeating( |
| [](bool* create_stream_called, media::AudioOutputStream* stream, |
| const media::AudioParameters& params, |
| const std::string& name) -> media::AudioOutputStream* { |
| DCHECK(!*create_stream_called); |
| DCHECK(stream); |
| DCHECK_EQ(name, "default"); |
| DCHECK_EQ(params.AsHumanReadableString(), |
| GetTestAudioParameters().AsHumanReadableString()); |
| *create_stream_called = true; |
| return stream; |
| }, |
| &create_stream_called, stream)); |
| |
| mojom::RendererAudioOutputStreamFactoryPtr stream_factory; |
| auto factory_handle = |
| CreateAndBindFactory(mojo::MakeRequest(&stream_factory)); |
| |
| base::Thread renderer_side_ipc_thread("Renderer IPC thread"); |
| ASSERT_TRUE(renderer_side_ipc_thread.Start()); |
| auto renderer_ipc_task_runner = renderer_side_ipc_thread.task_runner(); |
| |
| // Bind |stream_factory| to |renderer_ipc_task_runner|. |
| mojom::RendererAudioOutputStreamFactory* factory_ptr; |
| renderer_ipc_task_runner->PostTask( |
| FROM_HERE, base::BindOnce( |
| [](mojom::RendererAudioOutputStreamFactoryPtr* factory, |
| mojom::RendererAudioOutputStreamFactoryPtrInfo info, |
| mojom::RendererAudioOutputStreamFactory** ptr) { |
| factory->Bind(std::move(info)); |
| *ptr = factory->get(); |
| }, |
| base::Unretained(&stream_factory), |
| stream_factory.PassInterface(), &factory_ptr)); |
| // Wait for factory_ptr to be set. |
| SyncWith(renderer_ipc_task_runner); |
| |
| auto renderer_side_ipc = std::make_unique<MojoAudioOutputIPC>( |
| base::BindRepeating( |
| [](mojom::RendererAudioOutputStreamFactory* factory_ptr) { |
| return factory_ptr; |
| }, |
| factory_ptr), |
| renderer_ipc_task_runner); |
| |
| auto device = base::MakeRefCounted<media::AudioOutputDevice>( |
| std::move(renderer_side_ipc), renderer_ipc_task_runner, |
| media::AudioSinkParameters(), base::TimeDelta()); |
| |
| StrictMock<TestRenderCallback> source; |
| |
| device->Initialize(GetTestAudioParameters(), &source); |
| device->Start(); |
| device->Play(); |
| // Wait for stream to start. |
| SyncWithAllThreads(); |
| // Wait for stream to finish. Verifies data. |
| stream->Wait(); |
| device->Stop(); |
| |
| // |stream_factory| must be destroyed on the correct thread. |
| renderer_ipc_task_runner->PostTask( |
| FROM_HERE, |
| base::BindOnce([](mojom::RendererAudioOutputStreamFactoryPtr) {}, |
| std::move(stream_factory))); |
| SyncWith(renderer_ipc_task_runner); |
| // Wait for any clean-up tasks. |
| SyncWithAllThreads(); |
| } |
| |
| } // namespace content |