Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(227)

Unified Diff: content/renderer/media/media_stream_audio_unittest.cc

Issue 1834323002: MediaStream audio: Refactor 3 separate "glue" implementations into one. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Reworked unit tests around structural changes, and added exhaustive media_stream_audio_unittest.cc. Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..69a243a55d758b4572c504dc865beeca800e97f2
--- /dev/null
+++ b/content/renderer/media/media_stream_audio_unittest.cc
@@ -0,0 +1,443 @@
+// 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 = 48000;
+constexpr int kBufferSize = 480;
+
+// 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_;
+ 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_;
+ int64_t 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]);
+ for (int i = 0; i < audio_bus.frames(); ++i) {
+ ASSERT_EQ(expected_sample_count_, data[i]);
o1ka 2016/05/04 08:49:24 So, from source to sink, you converted int64->floa
miu 2016/05/04 22:10:09 Good point. Actually, 2^24 is the largest exactly
o1ka 2016/05/06 16:53:57 Please put it into the comment for kSampleRate, an
+ ++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_;
+ int64_t 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 {
o1ka 2016/05/04 08:49:23 Very nice testing!
miu 2016/05/04 22:10:09 Thanks! :)
+ protected:
+ void SetUp() override {
+ blink_audio_source_.initialize(blink::WebString::fromUTF8("audio_id"),
+ blink::WebMediaStreamSource::TypeAudio,
+ blink::WebString::fromUTF8("audio_track"),
+ false /* remote */, true /* readonly */);
+ 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()));
o1ka 2016/05/04 08:49:24 What are track()->GetOutputFormat() and sink.param
miu 2016/05/04 22:10:09 That is done on line 350, where the sink's params
o1ka 2016/05/06 16:53:56 Acknowledged.
+
+ // 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

Powered by Google App Engine
This is Rietveld 408576698