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 |