| // Copyright 2020 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 "media/fuchsia/audio/fuchsia_audio_output_device.h" |
| |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/test/task_environment.h" |
| #include "base/threading/thread.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "media/base/audio_renderer_sink.h" |
| #include "media/base/audio_timestamp_helper.h" |
| #include "media/fuchsia/audio/fake_audio_consumer.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace media { |
| |
| constexpr int kSampleRate = 44100; |
| constexpr ChannelLayout kChannelLayout = CHANNEL_LAYOUT_STEREO; |
| constexpr int kNumChannels = 2; |
| constexpr uint64_t kTestSessionId = 42; |
| constexpr base::TimeDelta kPeriod = base::Milliseconds(10); |
| constexpr int kFramesPerPeriod = 441; |
| |
| class TestRenderer : public AudioRendererSink::RenderCallback { |
| public: |
| TestRenderer() = default; |
| ~TestRenderer() override = default; |
| |
| // AudioRendererSink::Renderer interface. |
| int Render(base::TimeDelta delay, |
| base::TimeTicks delay_timestamp, |
| int prior_frames_skipped, |
| AudioBus* dest) override { |
| EXPECT_EQ(dest->channels(), kNumChannels); |
| frames_skipped_ += prior_frames_skipped; |
| frames_rendered_ += dest->frames(); |
| |
| EXPECT_GT(delay, base::TimeDelta()); |
| auto presentation_time = delay_timestamp + delay; |
| EXPECT_GT(presentation_time, last_presentation_time_); |
| last_presentation_time_ = presentation_time; |
| |
| return dest->frames(); |
| } |
| void OnRenderError() override { num_render_errors_++; } |
| |
| int frames_rendered() const { return frames_rendered_; } |
| void reset_frames_rendered() { frames_rendered_ = 0; } |
| |
| int frames_skipped() const { return frames_skipped_; } |
| int num_render_errors() const { return num_render_errors_; } |
| |
| base::TimeTicks last_presentation_time() const { |
| return last_presentation_time_; |
| } |
| |
| private: |
| int frames_rendered_ = 0; |
| int frames_skipped_ = 0; |
| int num_render_errors_ = 0; |
| base::TimeTicks last_presentation_time_; |
| }; |
| |
| class FuchsiaAudioOutputDeviceTest : public testing::Test { |
| public: |
| FuchsiaAudioOutputDeviceTest() { |
| fidl::InterfaceHandle<fuchsia::media::AudioConsumer> audio_consumer; |
| fake_audio_consumer_ = std::make_unique<FakeAudioConsumer>( |
| kTestSessionId, audio_consumer.NewRequest()); |
| |
| output_device_ = FuchsiaAudioOutputDevice::Create( |
| std::move(audio_consumer), base::ThreadTaskRunnerHandle::Get()); |
| } |
| |
| ~FuchsiaAudioOutputDeviceTest() override { output_device_->Stop(); } |
| |
| protected: |
| void Initialize() { |
| output_device_->Initialize( |
| AudioParameters(AudioParameters::AUDIO_PCM_LOW_LATENCY, kChannelLayout, |
| kSampleRate, kFramesPerPeriod), |
| &renderer_); |
| |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(renderer_.frames_rendered(), 0); |
| } |
| |
| void InitializeAndStart() { |
| Initialize(); |
| |
| // As soon as Start() is processed FuchsiaAudioOutputDevice is expected to |
| // start rendering some samples. |
| output_device_->Start(); |
| task_environment_.RunUntilIdle(); |
| EXPECT_GT(renderer_.frames_rendered(), 0); |
| } |
| |
| void CallPumpSamples() { |
| output_device_->PumpSamples(base::TimeTicks::Now() + |
| base::Milliseconds(200)); |
| } |
| |
| void ValidatePresentationTime() { |
| // Verify that the current renderer lead time is in the |
| // [min_lead_time, min_lead_time + 30ms] range. 30ms is chosen to allow |
| // FuchsiaAudioOutputDevice to pre-render slightely ahead of the target |
| // time, while keeping latency reasonably low. |
| auto lead_time = |
| renderer_.last_presentation_time() - base::TimeTicks::Now(); |
| EXPECT_GT(lead_time, FakeAudioConsumer::kMinLeadTime); |
| EXPECT_LT(lead_time, |
| FakeAudioConsumer::kMinLeadTime + base::Milliseconds(30)); |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::SingleThreadTaskEnvironment::MainThreadType::IO, |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| std::unique_ptr<FakeAudioConsumer> fake_audio_consumer_; |
| TestRenderer renderer_; |
| scoped_refptr<FuchsiaAudioOutputDevice> output_device_; |
| }; |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, Start) { |
| Initialize(); |
| |
| // Verify that playback doesn't start before Start(). |
| task_environment_.FastForwardBy(base::Seconds(2)); |
| EXPECT_EQ(renderer_.frames_rendered(), 0); |
| |
| // Rendering should start after Start(). |
| output_device_->Start(); |
| task_environment_.RunUntilIdle(); |
| EXPECT_GT(renderer_.frames_rendered(), 0); |
| |
| ValidatePresentationTime(); |
| } |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, StartAndPlay) { |
| InitializeAndStart(); |
| |
| renderer_.reset_frames_rendered(); |
| |
| // Try advancing time and verify that FuchsiaAudioOutputDevice keeps calling |
| // Render(). |
| for (int i = 0; i < 3; ++i) { |
| task_environment_.FastForwardBy(kPeriod); |
| EXPECT_EQ(renderer_.frames_rendered(), kFramesPerPeriod); |
| EXPECT_EQ(renderer_.frames_skipped(), 0); |
| renderer_.reset_frames_rendered(); |
| } |
| } |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, Pause) { |
| InitializeAndStart(); |
| |
| renderer_.reset_frames_rendered(); |
| |
| // Advancing time and verify that FuchsiaAudioOutputDevice keeps calling |
| // Render(). |
| task_environment_.FastForwardBy(kPeriod); |
| EXPECT_EQ(renderer_.frames_rendered(), kFramesPerPeriod); |
| EXPECT_EQ(renderer_.frames_skipped(), 0); |
| renderer_.reset_frames_rendered(); |
| |
| // Render() should not be called while paused. |
| output_device_->Pause(); |
| task_environment_.FastForwardBy(base::Seconds(10)); |
| EXPECT_EQ(renderer_.frames_rendered(), 0); |
| |
| // Unpause the stream and verify that Render() is being called now. |
| output_device_->Play(); |
| task_environment_.FastForwardBy(kPeriod); |
| EXPECT_GT(renderer_.frames_rendered(), 0); |
| EXPECT_EQ(renderer_.frames_skipped(), 0); |
| } |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, Underflow) { |
| InitializeAndStart(); |
| |
| renderer_.reset_frames_rendered(); |
| |
| // Missing the timer once should not cause any issues. Timer tasks can't |
| // always run at the exact scheduled time. FuchsiaAudioOutputDevice should |
| // be resilient to small delays. |
| task_environment_.AdvanceClock(kPeriod * 2); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(renderer_.frames_rendered(), kFramesPerPeriod * 2); |
| EXPECT_EQ(renderer_.frames_skipped(), 0); |
| renderer_.reset_frames_rendered(); |
| |
| // Advance time by 100ms, causing some frames to be skipped. |
| task_environment_.AdvanceClock(kPeriod * 10); |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(renderer_.frames_rendered(), kFramesPerPeriod * 3); |
| EXPECT_EQ(renderer_.frames_skipped(), kFramesPerPeriod * 7); |
| renderer_.reset_frames_rendered(); |
| |
| ValidatePresentationTime(); |
| } |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, Error) { |
| InitializeAndStart(); |
| |
| renderer_.reset_frames_rendered(); |
| |
| fake_audio_consumer_.reset(); |
| task_environment_.RunUntilIdle(); |
| |
| EXPECT_EQ(renderer_.num_render_errors(), 1); |
| EXPECT_EQ(renderer_.frames_rendered(), 0); |
| } |
| |
| TEST_F(FuchsiaAudioOutputDeviceTest, Stop) { |
| InitializeAndStart(); |
| |
| renderer_.reset_frames_rendered(); |
| |
| // Call Stop() and then PumpSamples() immediately after that. The callback |
| // should not be called. |
| output_device_->Stop(); |
| CallPumpSamples(); |
| EXPECT_EQ(renderer_.frames_rendered(), 0); |
| } |
| |
| } // namespace media |