| 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
|
|
|