Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include <stdint.h> | |
| 6 | |
| 7 #include "base/atomicops.h" | |
| 8 #include "base/synchronization/lock.h" | |
| 9 #include "base/synchronization/waitable_event.h" | |
| 10 #include "base/test/test_timeouts.h" | |
| 11 #include "base/threading/platform_thread.h" | |
| 12 #include "base/threading/thread_checker.h" | |
| 13 #include "content/public/renderer/media_stream_audio_sink.h" | |
| 14 #include "content/renderer/media/media_stream_audio_source.h" | |
| 15 #include "content/renderer/media/media_stream_audio_track.h" | |
| 16 #include "media/base/audio_bus.h" | |
| 17 #include "media/base/audio_parameters.h" | |
| 18 #include "testing/gtest/include/gtest/gtest.h" | |
| 19 #include "third_party/WebKit/public/platform/WebString.h" | |
| 20 #include "third_party/WebKit/public/web/WebHeap.h" | |
| 21 | |
| 22 namespace content { | |
| 23 | |
| 24 namespace { | |
| 25 | |
| 26 constexpr int kSampleRate = 48000; | |
| 27 constexpr int kBufferSize = 480; | |
| 28 | |
| 29 // A simple MediaStreamAudioSource that spawns a real-time audio thread and | |
| 30 // emits audio samples with monotonically-increasing sample values. Includes | |
| 31 // hooks for the unit tests to confirm lifecycle status and to change audio | |
| 32 // format. | |
| 33 class FakeMediaStreamAudioSource | |
| 34 : public MediaStreamAudioSource, | |
| 35 public base::PlatformThread::Delegate { | |
| 36 public: | |
| 37 FakeMediaStreamAudioSource() | |
| 38 : MediaStreamAudioSource(true), stop_event_(true, false), | |
| 39 next_buffer_size_(kBufferSize), sample_count_(0) {} | |
| 40 | |
| 41 ~FakeMediaStreamAudioSource() final { | |
| 42 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 43 EnsureSourceIsStopped(); | |
| 44 } | |
| 45 | |
| 46 bool was_started() const { | |
| 47 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 48 return !thread_.is_null(); | |
| 49 } | |
| 50 | |
| 51 bool was_stopped() const { | |
| 52 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 53 return stop_event_.IsSignaled(); | |
| 54 } | |
| 55 | |
| 56 void SetBufferSize(int new_buffer_size) { | |
| 57 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 58 base::subtle::NoBarrier_Store(&next_buffer_size_, new_buffer_size); | |
| 59 } | |
| 60 | |
| 61 protected: | |
| 62 bool EnsureSourceIsStarted() final { | |
| 63 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 64 if (was_started()) | |
| 65 return true; | |
| 66 if (was_stopped()) | |
| 67 return false; | |
| 68 base::PlatformThread::CreateWithPriority( | |
| 69 0, this, &thread_, base::ThreadPriority::REALTIME_AUDIO); | |
| 70 return true; | |
| 71 } | |
| 72 | |
| 73 void EnsureSourceIsStopped() final { | |
| 74 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 75 if (was_stopped()) | |
| 76 return; | |
| 77 stop_event_.Signal(); | |
| 78 if (was_started()) | |
| 79 base::PlatformThread::Join(thread_); | |
| 80 } | |
| 81 | |
| 82 void ThreadMain() override { | |
| 83 while (!stop_event_.IsSignaled()) { | |
| 84 // If needed, notify of the new format and re-create |audio_bus_|. | |
| 85 const int buffer_size = base::subtle::NoBarrier_Load(&next_buffer_size_); | |
| 86 if (!audio_bus_ || audio_bus_->frames() != buffer_size) { | |
| 87 MediaStreamAudioSource::SetFormat(media::AudioParameters( | |
| 88 media::AudioParameters::AUDIO_PCM_LOW_LATENCY, | |
| 89 media::CHANNEL_LAYOUT_MONO, kSampleRate, 16, buffer_size)); | |
| 90 audio_bus_ = media::AudioBus::Create(1, buffer_size); | |
| 91 } | |
| 92 | |
| 93 // Deliver the next chunk of audio data. Each sample value is its offset | |
| 94 // from the very first sample. | |
| 95 float* const data = audio_bus_->channel(0); | |
| 96 for (int i = 0; i < buffer_size; ++i) | |
| 97 data[i] = ++sample_count_; | |
| 98 MediaStreamAudioSource::DeliverDataToTracks(*audio_bus_, | |
| 99 base::TimeTicks::Now()); | |
| 100 | |
| 101 // Sleep before producing the next chunk of audio. | |
| 102 base::PlatformThread::Sleep(base::TimeDelta::FromMicroseconds( | |
| 103 base::Time::kMicrosecondsPerSecond * buffer_size / kSampleRate)); | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 private: | |
| 108 base::ThreadChecker main_thread_checker_; | |
| 109 | |
| 110 base::PlatformThreadHandle thread_; | |
| 111 mutable base::WaitableEvent stop_event_; | |
| 112 | |
| 113 base::subtle::Atomic32 next_buffer_size_; | |
| 114 std::unique_ptr<media::AudioBus> audio_bus_; | |
| 115 int64_t sample_count_; | |
| 116 | |
| 117 DISALLOW_COPY_AND_ASSIGN(FakeMediaStreamAudioSource); | |
| 118 }; | |
| 119 | |
| 120 // A simple MediaStreamAudioSink that consumes audio and confirms the sample | |
| 121 // values. Includes hooks for the unit tests to monitor the format and flow of | |
| 122 // audio, whether the audio is silent, and the propagation of the "enabled" | |
| 123 // state. | |
| 124 class FakeMediaStreamAudioSink : public MediaStreamAudioSink { | |
| 125 public: | |
| 126 enum EnableState { | |
| 127 NO_ENABLE_NOTIFICATION, | |
| 128 WAS_ENABLED, | |
| 129 WAS_DISABLED | |
| 130 }; | |
| 131 | |
| 132 FakeMediaStreamAudioSink() | |
| 133 : MediaStreamAudioSink(), expected_sample_count_(-1), | |
| 134 num_on_data_calls_(0), audio_is_silent_(true), was_ended_(false), | |
| 135 enable_state_(NO_ENABLE_NOTIFICATION) {} | |
| 136 | |
| 137 ~FakeMediaStreamAudioSink() final { | |
| 138 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 139 } | |
| 140 | |
| 141 media::AudioParameters params() const { | |
| 142 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 143 base::AutoLock auto_lock(params_lock_); | |
| 144 return params_; | |
| 145 } | |
| 146 | |
| 147 int num_on_data_calls() const { | |
| 148 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 149 return base::subtle::NoBarrier_Load(&num_on_data_calls_); | |
| 150 } | |
| 151 | |
| 152 bool is_audio_silent() const { | |
| 153 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 154 return !!base::subtle::NoBarrier_Load(&audio_is_silent_); | |
| 155 } | |
| 156 | |
| 157 bool was_ended() const { | |
| 158 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 159 return was_ended_; | |
| 160 } | |
| 161 | |
| 162 EnableState enable_state() const { | |
| 163 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 164 return enable_state_; | |
| 165 } | |
| 166 | |
| 167 void OnSetFormat(const media::AudioParameters& params) final { | |
| 168 ASSERT_TRUE(params.IsValid()); | |
| 169 base::AutoLock auto_lock(params_lock_); | |
| 170 params_ = params; | |
| 171 } | |
| 172 | |
| 173 void OnData(const media::AudioBus& audio_bus, | |
| 174 base::TimeTicks estimated_capture_time) final { | |
| 175 ASSERT_TRUE(params_.IsValid()); | |
| 176 ASSERT_FALSE(was_ended_); | |
| 177 | |
| 178 ASSERT_EQ(params_.channels(), audio_bus.channels()); | |
| 179 ASSERT_EQ(params_.frames_per_buffer(), audio_bus.frames()); | |
| 180 if (audio_bus.AreFramesZero()) { | |
| 181 base::subtle::NoBarrier_Store(&audio_is_silent_, 1); | |
| 182 expected_sample_count_ = -1; // Reset for when audio comes back. | |
| 183 } else { | |
| 184 base::subtle::NoBarrier_Store(&audio_is_silent_, 0); | |
| 185 const float* const data = audio_bus.channel(0); | |
| 186 if (expected_sample_count_ == -1) | |
| 187 expected_sample_count_ = static_cast<int64_t>(data[0]); | |
| 188 for (int i = 0; i < audio_bus.frames(); ++i) { | |
| 189 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
| |
| 190 ++expected_sample_count_; | |
| 191 } | |
| 192 } | |
| 193 | |
| 194 ASSERT_TRUE(!estimated_capture_time.is_null()); | |
| 195 ASSERT_LT(last_estimated_capture_time_, estimated_capture_time); | |
| 196 last_estimated_capture_time_ = estimated_capture_time; | |
| 197 | |
| 198 base::subtle::NoBarrier_AtomicIncrement(&num_on_data_calls_, 1); | |
| 199 } | |
| 200 | |
| 201 void OnReadyStateChanged( | |
| 202 blink::WebMediaStreamSource::ReadyState state) final { | |
| 203 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 204 if (state == blink::WebMediaStreamSource::ReadyStateEnded) | |
| 205 was_ended_ = true; | |
| 206 } | |
| 207 | |
| 208 void OnEnabledChanged(bool enabled) final { | |
| 209 CHECK(main_thread_checker_.CalledOnValidThread()); | |
| 210 enable_state_ = enabled ? WAS_ENABLED : WAS_DISABLED; | |
| 211 } | |
| 212 | |
| 213 private: | |
| 214 base::ThreadChecker main_thread_checker_; | |
| 215 | |
| 216 mutable base::Lock params_lock_; | |
| 217 media::AudioParameters params_; | |
| 218 int64_t expected_sample_count_; | |
| 219 base::TimeTicks last_estimated_capture_time_; | |
| 220 base::subtle::Atomic32 num_on_data_calls_; | |
| 221 base::subtle::Atomic32 audio_is_silent_; | |
| 222 bool was_ended_; | |
| 223 EnableState enable_state_; | |
| 224 | |
| 225 DISALLOW_COPY_AND_ASSIGN(FakeMediaStreamAudioSink); | |
| 226 }; | |
| 227 | |
| 228 } // namespace | |
| 229 | |
| 230 class MediaStreamAudioTest : public ::testing::Test { | |
|
o1ka
2016/05/04 08:49:23
Very nice testing!
miu
2016/05/04 22:10:09
Thanks! :)
| |
| 231 protected: | |
| 232 void SetUp() override { | |
| 233 blink_audio_source_.initialize(blink::WebString::fromUTF8("audio_id"), | |
| 234 blink::WebMediaStreamSource::TypeAudio, | |
| 235 blink::WebString::fromUTF8("audio_track"), | |
| 236 false /* remote */, true /* readonly */); | |
| 237 blink_audio_track_.initialize(blink_audio_source_.id(), | |
| 238 blink_audio_source_); | |
| 239 } | |
| 240 | |
| 241 void TearDown() override { | |
| 242 blink_audio_track_.reset(); | |
| 243 blink_audio_source_.reset(); | |
| 244 blink::WebHeap::collectAllGarbageForTesting(); | |
| 245 } | |
| 246 | |
| 247 FakeMediaStreamAudioSource* source() const { | |
| 248 return static_cast<FakeMediaStreamAudioSource*>( | |
| 249 MediaStreamAudioSource::From(blink_audio_source_)); | |
| 250 } | |
| 251 | |
| 252 MediaStreamAudioTrack* track() const { | |
| 253 return MediaStreamAudioTrack::From(blink_audio_track_); | |
| 254 } | |
| 255 | |
| 256 blink::WebMediaStreamSource blink_audio_source_; | |
| 257 blink::WebMediaStreamTrack blink_audio_track_; | |
| 258 }; | |
| 259 | |
| 260 // Tests that a simple source-->track-->sink connection and audio data flow | |
| 261 // works. | |
| 262 TEST_F(MediaStreamAudioTest, BasicUsage) { | |
| 263 // Create the source, but it should not be started yet. | |
| 264 ASSERT_FALSE(source()); | |
| 265 blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource()); | |
| 266 ASSERT_TRUE(source()); | |
| 267 EXPECT_FALSE(source()->was_started()); | |
| 268 EXPECT_FALSE(source()->was_stopped()); | |
| 269 | |
| 270 // Connect a track to the source. This should auto-start the source. | |
| 271 ASSERT_FALSE(track()); | |
| 272 EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_)); | |
| 273 ASSERT_TRUE(track()); | |
| 274 EXPECT_TRUE(source()->was_started()); | |
| 275 EXPECT_FALSE(source()->was_stopped()); | |
| 276 | |
| 277 // Connect a sink to the track. This should begin audio flow to the | |
| 278 // sink. Wait and confirm that three OnData() calls were made from the audio | |
| 279 // thread. | |
| 280 FakeMediaStreamAudioSink sink; | |
| 281 EXPECT_FALSE(sink.was_ended()); | |
| 282 track()->AddSink(&sink); | |
| 283 const int start_count = sink.num_on_data_calls(); | |
| 284 while (sink.num_on_data_calls() - start_count < 3) | |
| 285 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 286 | |
| 287 // Check that the audio parameters propagated to the track and sink. | |
| 288 const media::AudioParameters expected_params( | |
| 289 media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO, | |
| 290 kSampleRate, 16, kBufferSize); | |
| 291 EXPECT_TRUE(expected_params.Equals(track()->GetOutputFormat())); | |
| 292 EXPECT_TRUE(expected_params.Equals(sink.params())); | |
| 293 | |
| 294 // Stop the track. Since this was the last track connected to the source, the | |
| 295 // source should automatically stop. In addition, the sink should receive a | |
| 296 // ReadyStateEnded notification. | |
| 297 track()->Stop(); | |
| 298 EXPECT_TRUE(source()->was_started()); | |
| 299 EXPECT_TRUE(source()->was_stopped()); | |
| 300 EXPECT_TRUE(sink.was_ended()); | |
| 301 | |
| 302 track()->RemoveSink(&sink); | |
| 303 } | |
| 304 | |
| 305 // Tests that "ended" tracks can be connected after the source has stopped. | |
| 306 TEST_F(MediaStreamAudioTest, ConnectTrackAfterSourceStopped) { | |
| 307 // Create the source, connect one track, and stop it. This should | |
| 308 // automatically stop the source. | |
| 309 blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource()); | |
| 310 ASSERT_TRUE(source()); | |
| 311 EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_)); | |
| 312 track()->Stop(); | |
| 313 EXPECT_TRUE(source()->was_started()); | |
| 314 EXPECT_TRUE(source()->was_stopped()); | |
| 315 | |
| 316 // Now, connect another track. ConnectToTrack() will return false, but there | |
| 317 // should be a MediaStreamAudioTrack instance created and owned by the | |
| 318 // blink::WebMediaStreamTrack. | |
| 319 blink::WebMediaStreamTrack another_blink_track; | |
| 320 another_blink_track.initialize(blink_audio_source_.id(), blink_audio_source_); | |
| 321 EXPECT_FALSE(MediaStreamAudioTrack::From(another_blink_track)); | |
| 322 EXPECT_FALSE(source()->ConnectToTrack(another_blink_track)); | |
| 323 EXPECT_TRUE(MediaStreamAudioTrack::From(another_blink_track)); | |
| 324 } | |
| 325 | |
| 326 // Tests that a sink is immediately "ended" when connected to a stopped track. | |
| 327 TEST_F(MediaStreamAudioTest, AddSinkToStoppedTrack) { | |
| 328 // Create a track and stop it. Then, when adding a sink, the sink should get | |
| 329 // the ReadyStateEnded notification immediately. | |
| 330 MediaStreamAudioTrack track(true); | |
| 331 track.Stop(); | |
| 332 FakeMediaStreamAudioSink sink; | |
| 333 EXPECT_FALSE(sink.was_ended()); | |
| 334 track.AddSink(&sink); | |
| 335 EXPECT_TRUE(sink.was_ended()); | |
| 336 EXPECT_EQ(0, sink.num_on_data_calls()); | |
| 337 track.RemoveSink(&sink); | |
| 338 } | |
| 339 | |
| 340 // Tests that audio format changes at the source propagate to the track and | |
| 341 // sink. | |
| 342 TEST_F(MediaStreamAudioTest, FormatChangesPropagate) { | |
| 343 // Create a source, connect it to track, and connect the track to a | |
| 344 // sink. | |
| 345 blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource()); | |
| 346 ASSERT_TRUE(source()); | |
| 347 EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_)); | |
| 348 ASSERT_TRUE(track()); | |
| 349 FakeMediaStreamAudioSink sink; | |
| 350 ASSERT_TRUE(!sink.params().IsValid()); | |
| 351 track()->AddSink(&sink); | |
| 352 | |
| 353 // Wait until valid parameters are propagated to the sink, and then confirm | |
| 354 // the parameters are correct at the track and the sink. | |
| 355 while (!sink.params().IsValid()) | |
| 356 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 357 const media::AudioParameters expected_params( | |
| 358 media::AudioParameters::AUDIO_PCM_LOW_LATENCY, media::CHANNEL_LAYOUT_MONO, | |
| 359 kSampleRate, 16, kBufferSize); | |
| 360 EXPECT_TRUE(expected_params.Equals(track()->GetOutputFormat())); | |
| 361 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.
| |
| 362 | |
| 363 // Now, trigger a format change by doubling the buffer size. | |
| 364 source()->SetBufferSize(kBufferSize * 2); | |
| 365 | |
| 366 // Wait until the new buffer size propagates to the sink. | |
| 367 while (sink.params().frames_per_buffer() == kBufferSize) | |
| 368 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 369 EXPECT_EQ(kBufferSize * 2, track()->GetOutputFormat().frames_per_buffer()); | |
| 370 EXPECT_EQ(kBufferSize * 2, sink.params().frames_per_buffer()); | |
| 371 | |
| 372 track()->RemoveSink(&sink); | |
| 373 } | |
| 374 | |
| 375 // Tests that tracks deliver audio when enabled and silent audio when | |
| 376 // disabled. Whenever a track is enabled or disabled, the sink's | |
| 377 // OnEnabledChanged() method should be called. | |
| 378 TEST_F(MediaStreamAudioTest, EnableAndDisableTracks) { | |
| 379 // Create a source and connect it to track. | |
| 380 blink_audio_source_.setExtraData(new FakeMediaStreamAudioSource()); | |
| 381 ASSERT_TRUE(source()); | |
| 382 EXPECT_TRUE(source()->ConnectToTrack(blink_audio_track_)); | |
| 383 ASSERT_TRUE(track()); | |
| 384 | |
| 385 // Connect the track to a sink and expect the sink to be notified that the | |
| 386 // track is enabled. | |
| 387 FakeMediaStreamAudioSink sink; | |
| 388 EXPECT_TRUE(sink.is_audio_silent()); | |
| 389 EXPECT_EQ(FakeMediaStreamAudioSink::NO_ENABLE_NOTIFICATION, | |
| 390 sink.enable_state()); | |
| 391 track()->AddSink(&sink); | |
| 392 EXPECT_EQ(FakeMediaStreamAudioSink::WAS_ENABLED, sink.enable_state()); | |
| 393 | |
| 394 // Wait until non-silent audio reaches the sink. | |
| 395 while (sink.is_audio_silent()) | |
| 396 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 397 | |
| 398 // Now, disable the track and expect the sink to be notified. | |
| 399 track()->SetEnabled(false); | |
| 400 EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED, sink.enable_state()); | |
| 401 | |
| 402 // Wait until silent audio reaches the sink. | |
| 403 while (!sink.is_audio_silent()) | |
| 404 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 405 | |
| 406 // Create a second track and a second sink, but this time the track starts out | |
| 407 // disabled. Expect the sink to be notified at the start that the track is | |
| 408 // disabled. | |
| 409 blink::WebMediaStreamTrack another_blink_track; | |
| 410 another_blink_track.initialize(blink_audio_source_.id(), blink_audio_source_); | |
| 411 EXPECT_TRUE(source()->ConnectToTrack(another_blink_track)); | |
| 412 MediaStreamAudioTrack::From(another_blink_track)->SetEnabled(false); | |
| 413 FakeMediaStreamAudioSink another_sink; | |
| 414 MediaStreamAudioTrack::From(another_blink_track)->AddSink(&another_sink); | |
| 415 EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED, | |
| 416 another_sink.enable_state()); | |
| 417 | |
| 418 // Wait until OnData() is called on the second sink. Expect the audio to be | |
| 419 // silent. | |
| 420 const int start_count = another_sink.num_on_data_calls(); | |
| 421 while (another_sink.num_on_data_calls() == start_count) | |
| 422 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 423 EXPECT_TRUE(another_sink.is_audio_silent()); | |
| 424 | |
| 425 // Now, enable the second track and expect the second sink to be notified. | |
| 426 MediaStreamAudioTrack::From(another_blink_track)->SetEnabled(true); | |
| 427 EXPECT_EQ(FakeMediaStreamAudioSink::WAS_ENABLED, another_sink.enable_state()); | |
| 428 | |
| 429 // Wait until non-silent audio reaches the second sink. | |
| 430 while (another_sink.is_audio_silent()) | |
| 431 base::PlatformThread::Sleep(TestTimeouts::tiny_timeout()); | |
| 432 | |
| 433 // The first track and sink should not have been affected by changing the | |
| 434 // enabled state of the second track and sink. They should still be disabled, | |
| 435 // with silent audio being consumed at the sink. | |
| 436 EXPECT_EQ(FakeMediaStreamAudioSink::WAS_DISABLED, sink.enable_state()); | |
| 437 EXPECT_TRUE(sink.is_audio_silent()); | |
| 438 | |
| 439 MediaStreamAudioTrack::From(another_blink_track)->RemoveSink(&another_sink); | |
| 440 track()->RemoveSink(&sink); | |
| 441 } | |
| 442 | |
| 443 } // namespace content | |
| OLD | NEW |