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