| Index: content/renderer/media/media_stream_audio_unittest.cc
|
| diff --git a/content/renderer/media/media_stream_audio_unittest.cc b/content/renderer/media/media_stream_audio_unittest.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..060dca37b0706d0276decba38199d7792efaf851
|
| --- /dev/null
|
| +++ b/content/renderer/media/media_stream_audio_unittest.cc
|
| @@ -0,0 +1,450 @@
|
| +// 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 <stdint.h>
|
| +
|
| +#include "base/atomicops.h"
|
| +#include "base/synchronization/lock.h"
|
| +#include "base/synchronization/waitable_event.h"
|
| +#include "base/test/test_timeouts.h"
|
| +#include "base/threading/platform_thread.h"
|
| +#include "base/threading/thread_checker.h"
|
| +#include "content/public/renderer/media_stream_audio_sink.h"
|
| +#include "content/renderer/media/media_stream_audio_source.h"
|
| +#include "content/renderer/media/media_stream_audio_track.h"
|
| +#include "media/base/audio_bus.h"
|
| +#include "media/base/audio_parameters.h"
|
| +#include "testing/gtest/include/gtest/gtest.h"
|
| +#include "third_party/WebKit/public/platform/WebString.h"
|
| +#include "third_party/WebKit/public/web/WebHeap.h"
|
| +
|
| +namespace content {
|
| +
|
| +namespace {
|
| +
|
| +constexpr int kSampleRate = 8000;
|
| +constexpr int kBufferSize = kSampleRate / 100;
|
| +
|
| +// The maximum integer that can be exactly represented by the float data type.
|
| +constexpr int kMaxValueSafelyConvertableToFloat = 1 << 24;
|
| +
|
| +// A simple MediaStreamAudioSource that spawns a real-time audio thread and
|
| +// emits audio samples with monotonically-increasing sample values. Includes
|
| +// hooks for the unit tests to confirm lifecycle status and to change audio
|
| +// format.
|
| +class FakeMediaStreamAudioSource
|
| + : public MediaStreamAudioSource,
|
| + public base::PlatformThread::Delegate {
|
| + public:
|
| + FakeMediaStreamAudioSource()
|
| + : MediaStreamAudioSource(true), stop_event_(true, false),
|
| + next_buffer_size_(kBufferSize), sample_count_(0) {}
|
| +
|
| + ~FakeMediaStreamAudioSource() final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + EnsureSourceIsStopped();
|
| + }
|
| +
|
| + bool was_started() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return !thread_.is_null();
|
| + }
|
| +
|
| + bool was_stopped() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return stop_event_.IsSignaled();
|
| + }
|
| +
|
| + void SetBufferSize(int new_buffer_size) {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + base::subtle::NoBarrier_Store(&next_buffer_size_, new_buffer_size);
|
| + }
|
| +
|
| + protected:
|
| + bool EnsureSourceIsStarted() final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + if (was_started())
|
| + return true;
|
| + if (was_stopped())
|
| + return false;
|
| + base::PlatformThread::CreateWithPriority(
|
| + 0, this, &thread_, base::ThreadPriority::REALTIME_AUDIO);
|
| + return true;
|
| + }
|
| +
|
| + void EnsureSourceIsStopped() final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + if (was_stopped())
|
| + return;
|
| + stop_event_.Signal();
|
| + if (was_started())
|
| + base::PlatformThread::Join(thread_);
|
| + }
|
| +
|
| + void ThreadMain() override {
|
| + while (!stop_event_.IsSignaled()) {
|
| + // If needed, notify of the new format and re-create |audio_bus_|.
|
| + const int buffer_size = base::subtle::NoBarrier_Load(&next_buffer_size_);
|
| + if (!audio_bus_ || audio_bus_->frames() != buffer_size) {
|
| + MediaStreamAudioSource::SetFormat(media::AudioParameters(
|
| + media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
|
| + media::CHANNEL_LAYOUT_MONO, kSampleRate, 16, buffer_size));
|
| + audio_bus_ = media::AudioBus::Create(1, buffer_size);
|
| + }
|
| +
|
| + // Deliver the next chunk of audio data. Each sample value is its offset
|
| + // from the very first sample.
|
| + float* const data = audio_bus_->channel(0);
|
| + for (int i = 0; i < buffer_size; ++i)
|
| + data[i] = ++sample_count_;
|
| + CHECK_LT(sample_count_, kMaxValueSafelyConvertableToFloat);
|
| + MediaStreamAudioSource::DeliverDataToTracks(*audio_bus_,
|
| + base::TimeTicks::Now());
|
| +
|
| + // Sleep before producing the next chunk of audio.
|
| + base::PlatformThread::Sleep(base::TimeDelta::FromMicroseconds(
|
| + base::Time::kMicrosecondsPerSecond * buffer_size / kSampleRate));
|
| + }
|
| + }
|
| +
|
| + private:
|
| + base::ThreadChecker main_thread_checker_;
|
| +
|
| + base::PlatformThreadHandle thread_;
|
| + mutable base::WaitableEvent stop_event_;
|
| +
|
| + base::subtle::Atomic32 next_buffer_size_;
|
| + std::unique_ptr<media::AudioBus> audio_bus_;
|
| + int sample_count_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(FakeMediaStreamAudioSource);
|
| +};
|
| +
|
| +// A simple MediaStreamAudioSink that consumes audio and confirms the sample
|
| +// values. Includes hooks for the unit tests to monitor the format and flow of
|
| +// audio, whether the audio is silent, and the propagation of the "enabled"
|
| +// state.
|
| +class FakeMediaStreamAudioSink : public MediaStreamAudioSink {
|
| + public:
|
| + enum EnableState {
|
| + NO_ENABLE_NOTIFICATION,
|
| + WAS_ENABLED,
|
| + WAS_DISABLED
|
| + };
|
| +
|
| + FakeMediaStreamAudioSink()
|
| + : MediaStreamAudioSink(), expected_sample_count_(-1),
|
| + num_on_data_calls_(0), audio_is_silent_(true), was_ended_(false),
|
| + enable_state_(NO_ENABLE_NOTIFICATION) {}
|
| +
|
| + ~FakeMediaStreamAudioSink() final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + }
|
| +
|
| + media::AudioParameters params() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + base::AutoLock auto_lock(params_lock_);
|
| + return params_;
|
| + }
|
| +
|
| + int num_on_data_calls() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return base::subtle::NoBarrier_Load(&num_on_data_calls_);
|
| + }
|
| +
|
| + bool is_audio_silent() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return !!base::subtle::NoBarrier_Load(&audio_is_silent_);
|
| + }
|
| +
|
| + bool was_ended() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return was_ended_;
|
| + }
|
| +
|
| + EnableState enable_state() const {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + return enable_state_;
|
| + }
|
| +
|
| + void OnSetFormat(const media::AudioParameters& params) final {
|
| + ASSERT_TRUE(params.IsValid());
|
| + base::AutoLock auto_lock(params_lock_);
|
| + params_ = params;
|
| + }
|
| +
|
| + void OnData(const media::AudioBus& audio_bus,
|
| + base::TimeTicks estimated_capture_time) final {
|
| + ASSERT_TRUE(params_.IsValid());
|
| + ASSERT_FALSE(was_ended_);
|
| +
|
| + ASSERT_EQ(params_.channels(), audio_bus.channels());
|
| + ASSERT_EQ(params_.frames_per_buffer(), audio_bus.frames());
|
| + if (audio_bus.AreFramesZero()) {
|
| + base::subtle::NoBarrier_Store(&audio_is_silent_, 1);
|
| + expected_sample_count_ = -1; // Reset for when audio comes back.
|
| + } else {
|
| + base::subtle::NoBarrier_Store(&audio_is_silent_, 0);
|
| + const float* const data = audio_bus.channel(0);
|
| + if (expected_sample_count_ == -1)
|
| + expected_sample_count_ = static_cast<int64_t>(data[0]);
|
| + CHECK_LE(expected_sample_count_ + audio_bus.frames(),
|
| + kMaxValueSafelyConvertableToFloat);
|
| + for (int i = 0; i < audio_bus.frames(); ++i) {
|
| + const float expected_sample_value = expected_sample_count_;
|
| + ASSERT_EQ(expected_sample_value, data[i]);
|
| + ++expected_sample_count_;
|
| + }
|
| + }
|
| +
|
| + ASSERT_TRUE(!estimated_capture_time.is_null());
|
| + ASSERT_LT(last_estimated_capture_time_, estimated_capture_time);
|
| + last_estimated_capture_time_ = estimated_capture_time;
|
| +
|
| + base::subtle::NoBarrier_AtomicIncrement(&num_on_data_calls_, 1);
|
| + }
|
| +
|
| + void OnReadyStateChanged(
|
| + blink::WebMediaStreamSource::ReadyState state) final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + if (state == blink::WebMediaStreamSource::ReadyStateEnded)
|
| + was_ended_ = true;
|
| + }
|
| +
|
| + void OnEnabledChanged(bool enabled) final {
|
| + CHECK(main_thread_checker_.CalledOnValidThread());
|
| + enable_state_ = enabled ? WAS_ENABLED : WAS_DISABLED;
|
| + }
|
| +
|
| + private:
|
| + base::ThreadChecker main_thread_checker_;
|
| +
|
| + mutable base::Lock params_lock_;
|
| + media::AudioParameters params_;
|
| + int expected_sample_count_;
|
| + base::TimeTicks last_estimated_capture_time_;
|
| + base::subtle::Atomic32 num_on_data_calls_;
|
| + base::subtle::Atomic32 audio_is_silent_;
|
| + bool was_ended_;
|
| + EnableState enable_state_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(FakeMediaStreamAudioSink);
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +class MediaStreamAudioTest : public ::testing::Test {
|
| + protected:
|
| + void SetUp() override {
|
| + blink_audio_source_.initialize(blink::WebString::fromUTF8("audio_id"),
|
| + blink::WebMediaStreamSource::TypeAudio,
|
| + blink::WebString::fromUTF8("audio_track"),
|
| + false /* remote */);
|
| + blink_audio_track_.initialize(blink_audio_source_.id(),
|
| + blink_audio_source_);
|
| + }
|
| +
|
| + void TearDown() override {
|
| + blink_audio_track_.reset();
|
| + blink_audio_source_.reset();
|
| + blink::WebHeap::collectAllGarbageForTesting();
|
| + }
|
| +
|
| + FakeMediaStreamAudioSource* source() const {
|
| + return static_cast<FakeMediaStreamAudioSource*>(
|
| + MediaStreamAudioSource::From(blink_audio_source_));
|
| + }
|
| +
|
| + MediaStreamAudioTrack* track() const {
|
| + return MediaStreamAudioTrack::From(blink_audio_track_);
|
| + }
|
| +
|
| + blink::WebMediaStreamSource blink_audio_source_;
|
| + blink::WebMediaStreamTrack blink_audio_track_;
|
| +};
|
| +
|
| +// Tests that a simple source-->track-->sink connection and audio data flow
|
| +// works.
|
| +TEST_F(MediaStreamAudioTest, BasicUsage) {
|
| + // Create the source, but it should not be started yet.
|
| + ASSERT_FALSE(source());
|
| + blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource());
|
| + ASSERT_TRUE(source());
|
| + EXPECT_FALSE(source()->was_started());
|
| + EXPECT_FALSE(source()->was_stopped());
|
| +
|
| + // Connect a track to the source. This should auto-start the source.
|
| + ASSERT_FALSE(track());
|
| + EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_));
|
| + ASSERT_TRUE(track());
|
| + EXPECT_TRUE(source()->was_started());
|
| + EXPECT_FALSE(source()->was_stopped());
|
| +
|
| + // Connect a sink to the track. This should begin audio flow to the
|
| + // sink. Wait and confirm that three OnData() calls were made from the audio
|
| + // thread.
|
| + FakeMediaStreamAudioSink sink;
|
| + EXPECT_FALSE(sink.was_ended());
|
| + track()->AddSink(&sink);
|
| + const int start_count = sink.num_on_data_calls();
|
| + while (sink.num_on_data_calls() - start_count < 3)
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| +
|
| + // Check that the audio parameters propagated to the track and sink.
|
| + const media::AudioParameters expected_params(
|
| + media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO,
|
| + kSampleRate, 16, kBufferSize);
|
| + EXPECT_TRUE(expected_params.Equals(track()->GetOutputFormat()));
|
| + EXPECT_TRUE(expected_params.Equals(sink.params()));
|
| +
|
| + // Stop the track. Since this was the last track connected to the source, the
|
| + // source should automatically stop. In addition, the sink should receive a
|
| + // ReadyStateEnded notification.
|
| + track()->Stop();
|
| + EXPECT_TRUE(source()->was_started());
|
| + EXPECT_TRUE(source()->was_stopped());
|
| + EXPECT_TRUE(sink.was_ended());
|
| +
|
| + track()->RemoveSink(&sink);
|
| +}
|
| +
|
| +// Tests that "ended" tracks can be connected after the source has stopped.
|
| +TEST_F(MediaStreamAudioTest, ConnectTrackAfterSourceStopped) {
|
| + // Create the source, connect one track, and stop it. This should
|
| + // automatically stop the source.
|
| + blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource());
|
| + ASSERT_TRUE(source());
|
| + EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_));
|
| + track()->Stop();
|
| + EXPECT_TRUE(source()->was_started());
|
| + EXPECT_TRUE(source()->was_stopped());
|
| +
|
| + // Now, connect another track. ConnectToTrack() will return false, but there
|
| + // should be a MediaStreamAudioTrack instance created and owned by the
|
| + // blink::WebMediaStreamTrack.
|
| + blink::WebMediaStreamTrack another_blink_track;
|
| + another_blink_track.initialize(blink_audio_source_.id(), blink_audio_source_);
|
| + EXPECT_FALSE(MediaStreamAudioTrack::From(another_blink_track));
|
| + EXPECT_FALSE(source()->ConnectToTrack(another_blink_track));
|
| + EXPECT_TRUE(MediaStreamAudioTrack::From(another_blink_track));
|
| +}
|
| +
|
| +// Tests that a sink is immediately "ended" when connected to a stopped track.
|
| +TEST_F(MediaStreamAudioTest, AddSinkToStoppedTrack) {
|
| + // Create a track and stop it. Then, when adding a sink, the sink should get
|
| + // the ReadyStateEnded notification immediately.
|
| + MediaStreamAudioTrack track(true);
|
| + track.Stop();
|
| + FakeMediaStreamAudioSink sink;
|
| + EXPECT_FALSE(sink.was_ended());
|
| + track.AddSink(&sink);
|
| + EXPECT_TRUE(sink.was_ended());
|
| + EXPECT_EQ(0, sink.num_on_data_calls());
|
| + track.RemoveSink(&sink);
|
| +}
|
| +
|
| +// Tests that audio format changes at the source propagate to the track and
|
| +// sink.
|
| +TEST_F(MediaStreamAudioTest, FormatChangesPropagate) {
|
| + // Create a source, connect it to track, and connect the track to a
|
| + // sink.
|
| + blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource());
|
| + ASSERT_TRUE(source());
|
| + EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_));
|
| + ASSERT_TRUE(track());
|
| + FakeMediaStreamAudioSink sink;
|
| + ASSERT_TRUE(!sink.params().IsValid());
|
| + track()->AddSink(&sink);
|
| +
|
| + // Wait until valid parameters are propagated to the sink, and then confirm
|
| + // the parameters are correct at the track and the sink.
|
| + while (!sink.params().IsValid())
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| + const media::AudioParameters expected_params(
|
| + media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO,
|
| + kSampleRate, 16, kBufferSize);
|
| + EXPECT_TRUE(expected_params.Equals(track()->GetOutputFormat()));
|
| + EXPECT_TRUE(expected_params.Equals(sink.params()));
|
| +
|
| + // Now, trigger a format change by doubling the buffer size.
|
| + source()->SetBufferSize(kBufferSize * 2);
|
| +
|
| + // Wait until the new buffer size propagates to the sink.
|
| + while (sink.params().frames_per_buffer() == kBufferSize)
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| + EXPECT_EQ(kBufferSize * 2, track()->GetOutputFormat().frames_per_buffer());
|
| + EXPECT_EQ(kBufferSize * 2, sink.params().frames_per_buffer());
|
| +
|
| + track()->RemoveSink(&sink);
|
| +}
|
| +
|
| +// Tests that tracks deliver audio when enabled and silent audio when
|
| +// disabled. Whenever a track is enabled or disabled, the sink's
|
| +// OnEnabledChanged() method should be called.
|
| +TEST_F(MediaStreamAudioTest, EnableAndDisableTracks) {
|
| + // Create a source and connect it to track.
|
| + blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource());
|
| + ASSERT_TRUE(source());
|
| + EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_));
|
| + ASSERT_TRUE(track());
|
| +
|
| + // Connect the track to a sink and expect the sink to be notified that the
|
| + // track is enabled.
|
| + FakeMediaStreamAudioSink sink;
|
| + EXPECT_TRUE(sink.is_audio_silent());
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::NO_ENABLE_NOTIFICATION,
|
| + sink.enable_state());
|
| + track()->AddSink(&sink);
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::WAS_ENABLED, sink.enable_state());
|
| +
|
| + // Wait until non-silent audio reaches the sink.
|
| + while (sink.is_audio_silent())
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| +
|
| + // Now, disable the track and expect the sink to be notified.
|
| + track()->SetEnabled(false);
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED, sink.enable_state());
|
| +
|
| + // Wait until silent audio reaches the sink.
|
| + while (!sink.is_audio_silent())
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| +
|
| + // Create a second track and a second sink, but this time the track starts out
|
| + // disabled. Expect the sink to be notified at the start that the track is
|
| + // disabled.
|
| + blink::WebMediaStreamTrack another_blink_track;
|
| + another_blink_track.initialize(blink_audio_source_.id(), blink_audio_source_);
|
| + EXPECT_TRUE(source()->ConnectToTrack(another_blink_track));
|
| + MediaStreamAudioTrack::From(another_blink_track)->SetEnabled(false);
|
| + FakeMediaStreamAudioSink another_sink;
|
| + MediaStreamAudioTrack::From(another_blink_track)->AddSink(&another_sink);
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED,
|
| + another_sink.enable_state());
|
| +
|
| + // Wait until OnData() is called on the second sink. Expect the audio to be
|
| + // silent.
|
| + const int start_count = another_sink.num_on_data_calls();
|
| + while (another_sink.num_on_data_calls() == start_count)
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| + EXPECT_TRUE(another_sink.is_audio_silent());
|
| +
|
| + // Now, enable the second track and expect the second sink to be notified.
|
| + MediaStreamAudioTrack::From(another_blink_track)->SetEnabled(true);
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::WAS_ENABLED, another_sink.enable_state());
|
| +
|
| + // Wait until non-silent audio reaches the second sink.
|
| + while (another_sink.is_audio_silent())
|
| + base::PlatformThread::Sleep(TestTimeouts::tiny_timeout());
|
| +
|
| + // The first track and sink should not have been affected by changing the
|
| + // enabled state of the second track and sink. They should still be disabled,
|
| + // with silent audio being consumed at the sink.
|
| + EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED, sink.enable_state());
|
| + EXPECT_TRUE(sink.is_audio_silent());
|
| +
|
| + MediaStreamAudioTrack::From(another_blink_track)->RemoveSink(&another_sink);
|
| + track()->RemoveSink(&sink);
|
| +}
|
| +
|
| +} // namespace content
|
|
|