| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chromecast/media/audio/cast_audio_output_stream.h" | 5 #include "chromecast/media/audio/cast_audio_output_stream.h" |
| 6 | 6 |
| 7 #include "base/bind.h" | 7 #include "base/bind.h" |
| 8 #include "base/callback_helpers.h" |
| 8 #include "base/synchronization/waitable_event.h" | 9 #include "base/synchronization/waitable_event.h" |
| 9 #include "base/threading/thread_checker.h" | 10 #include "base/threading/thread_checker.h" |
| 10 #include "chromecast/base/metrics/cast_metrics_helper.h" | 11 #include "chromecast/base/metrics/cast_metrics_helper.h" |
| 11 #include "chromecast/base/task_runner_impl.h" | 12 #include "chromecast/base/task_runner_impl.h" |
| 12 #include "chromecast/media/audio/cast_audio_manager.h" | 13 #include "chromecast/media/audio/cast_audio_manager.h" |
| 13 #include "chromecast/media/base/media_message_loop.h" | 14 #include "chromecast/media/base/media_message_loop.h" |
| 14 #include "chromecast/media/cma/base/cast_decoder_buffer_impl.h" | 15 #include "chromecast/media/cma/base/cast_decoder_buffer_impl.h" |
| 15 #include "chromecast/media/cma/base/decoder_buffer_adapter.h" | 16 #include "chromecast/media/cma/base/decoder_buffer_adapter.h" |
| 16 #include "chromecast/media/cma/pipeline/frame_status_cb_impl.h" | |
| 17 #include "chromecast/public/media/audio_pipeline_device.h" | |
| 18 #include "chromecast/public/media/decoder_config.h" | 17 #include "chromecast/public/media/decoder_config.h" |
| 19 #include "chromecast/public/media/decrypt_context.h" | 18 #include "chromecast/public/media/decrypt_context.h" |
| 20 #include "chromecast/public/media/media_clock_device.h" | |
| 21 #include "chromecast/public/media/media_pipeline_backend.h" | 19 #include "chromecast/public/media/media_pipeline_backend.h" |
| 22 #include "chromecast/public/media/media_pipeline_device_params.h" | 20 #include "chromecast/public/media/media_pipeline_device_params.h" |
| 23 #include "media/base/bind_to_current_loop.h" | 21 #include "media/base/bind_to_current_loop.h" |
| 24 #include "media/base/decoder_buffer.h" | 22 #include "media/base/decoder_buffer.h" |
| 25 | 23 |
| 26 namespace chromecast { | 24 namespace chromecast { |
| 27 namespace media { | 25 namespace media { |
| 26 namespace { |
| 28 | 27 |
| 29 namespace { | 28 MediaPipelineBackend::AudioDecoder* InitializeBackend( |
| 30 bool InitClockDevice(MediaClockDevice* clock_device) { | 29 const ::media::AudioParameters& audio_params, |
| 31 DCHECK(clock_device); | 30 MediaPipelineBackend* backend, |
| 32 DCHECK_EQ(clock_device->GetState(), MediaClockDevice::kStateUninitialized); | 31 MediaPipelineBackend::Delegate* delegate) { |
| 32 DCHECK(backend); |
| 33 DCHECK(delegate); |
| 33 | 34 |
| 34 if (!clock_device->SetState(media::MediaClockDevice::kStateIdle)) | 35 MediaPipelineBackend::AudioDecoder* decoder = backend->CreateAudioDecoder(); |
| 35 return false; | 36 if (!decoder) |
| 36 | 37 return nullptr; |
| 37 if (!clock_device->ResetTimeline(0)) | |
| 38 return false; | |
| 39 | |
| 40 if (!clock_device->SetRate(1.0)) | |
| 41 return false; | |
| 42 | |
| 43 return true; | |
| 44 } | |
| 45 | |
| 46 bool InitAudioDevice(const ::media::AudioParameters& audio_params, | |
| 47 AudioPipelineDevice* audio_device) { | |
| 48 DCHECK(audio_device); | |
| 49 DCHECK_EQ(audio_device->GetState(), AudioPipelineDevice::kStateUninitialized); | |
| 50 | 38 |
| 51 AudioConfig audio_config; | 39 AudioConfig audio_config; |
| 52 audio_config.codec = kCodecPCM; | 40 audio_config.codec = kCodecPCM; |
| 53 audio_config.sample_format = kSampleFormatS16; | 41 audio_config.sample_format = kSampleFormatS16; |
| 54 audio_config.bytes_per_channel = audio_params.bits_per_sample() / 8; | 42 audio_config.bytes_per_channel = audio_params.bits_per_sample() / 8; |
| 55 audio_config.channel_number = audio_params.channels(); | 43 audio_config.channel_number = audio_params.channels(); |
| 56 audio_config.samples_per_second = audio_params.sample_rate(); | 44 audio_config.samples_per_second = audio_params.sample_rate(); |
| 57 audio_config.extra_data = nullptr; | 45 audio_config.extra_data = nullptr; |
| 58 audio_config.extra_data_size = 0; | 46 audio_config.extra_data_size = 0; |
| 59 audio_config.is_encrypted = false; | 47 audio_config.is_encrypted = false; |
| 60 if (!audio_device->SetConfig(audio_config)) | |
| 61 return false; | |
| 62 | 48 |
| 63 if (!audio_device->SetState(AudioPipelineDevice::kStateIdle)) | 49 if (!decoder->SetConfig(audio_config)) |
| 64 return false; | 50 return nullptr; |
| 65 | 51 |
| 66 return true; | 52 if (!backend->Initialize(delegate)) |
| 53 return nullptr; |
| 54 |
| 55 return decoder; |
| 67 } | 56 } |
| 57 |
| 68 } // namespace | 58 } // namespace |
| 69 | 59 |
| 70 // Backend represents a MediaPipelineBackend adapter that runs on cast | 60 // Backend represents a MediaPipelineBackend adapter that runs on cast |
| 71 // media thread (media::MediaMessageLoop::GetTaskRunner). | 61 // media thread (media::MediaMessageLoop::GetTaskRunner). |
| 72 // It can be created and destroyed on any thread, but all other member functions | 62 // It can be created and destroyed on any thread, but all other member functions |
| 73 // must be called on a single thread. | 63 // must be called on a single thread. |
| 74 class CastAudioOutputStream::Backend { | 64 class CastAudioOutputStream::Backend : public MediaPipelineBackend::Delegate { |
| 75 public: | 65 public: |
| 76 typedef base::Callback<void(bool)> PushFrameCompletionCallback; | 66 typedef base::Callback<void(bool)> PushBufferCompletionCallback; |
| 77 | 67 |
| 78 Backend(const ::media::AudioParameters& audio_params) | 68 Backend(const ::media::AudioParameters& audio_params) |
| 79 : audio_params_(audio_params) { | 69 : audio_params_(audio_params), |
| 70 decoder_(nullptr), |
| 71 first_start_(true), |
| 72 error_(false) { |
| 80 thread_checker_.DetachFromThread(); | 73 thread_checker_.DetachFromThread(); |
| 81 } | 74 } |
| 82 ~Backend() {} | 75 ~Backend() override {} |
| 83 | 76 |
| 84 void Open(CastAudioManager* audio_manager, | 77 void Open(CastAudioManager* audio_manager, |
| 85 bool* success, | 78 bool* success, |
| 86 base::WaitableEvent* completion_event) { | 79 base::WaitableEvent* completion_event) { |
| 87 DCHECK(thread_checker_.CalledOnValidThread()); | 80 DCHECK(thread_checker_.CalledOnValidThread()); |
| 88 DCHECK(backend_ == nullptr); | 81 DCHECK(backend_ == nullptr); |
| 82 DCHECK(audio_manager); |
| 83 DCHECK(success); |
| 84 DCHECK(completion_event); |
| 89 | 85 |
| 90 backend_task_runner_.reset(new TaskRunnerImpl()); | 86 backend_task_runner_.reset(new TaskRunnerImpl()); |
| 91 MediaPipelineDeviceParams device_params( | 87 MediaPipelineDeviceParams device_params( |
| 92 MediaPipelineDeviceParams::kModeIgnorePts, backend_task_runner_.get()); | 88 MediaPipelineDeviceParams::kModeIgnorePts, backend_task_runner_.get()); |
| 93 | 89 backend_ = audio_manager->CreateMediaPipelineBackend(device_params); |
| 94 scoped_ptr<MediaPipelineBackend> pipeline_backend = | 90 if (backend_) |
| 95 audio_manager->CreateMediaPipelineBackend(device_params); | 91 decoder_ = InitializeBackend(audio_params_, backend_.get(), this); |
| 96 if (pipeline_backend && InitClockDevice(pipeline_backend->GetClock()) && | 92 *success = decoder_ != nullptr; |
| 97 InitAudioDevice(audio_params_, pipeline_backend->GetAudio())) { | |
| 98 backend_ = pipeline_backend.Pass(); | |
| 99 } | |
| 100 *success = backend_ != nullptr; | |
| 101 completion_event->Signal(); | 93 completion_event->Signal(); |
| 102 } | 94 } |
| 103 | 95 |
| 104 void Close() { | 96 void Close() { |
| 105 DCHECK(thread_checker_.CalledOnValidThread()); | 97 DCHECK(thread_checker_.CalledOnValidThread()); |
| 106 | 98 |
| 107 if (backend_) { | 99 if (backend_) |
| 108 backend_->GetClock()->SetState(MediaClockDevice::kStateIdle); | 100 backend_->Stop(); |
| 109 backend_->GetAudio()->SetState(AudioPipelineDevice::kStateIdle); | |
| 110 } | |
| 111 backend_.reset(); | 101 backend_.reset(); |
| 112 backend_task_runner_.reset(); | 102 backend_task_runner_.reset(); |
| 113 } | 103 } |
| 114 | 104 |
| 115 void Start() { | 105 void Start() { |
| 116 DCHECK(thread_checker_.CalledOnValidThread()); | 106 DCHECK(thread_checker_.CalledOnValidThread()); |
| 107 DCHECK(backend_); |
| 117 | 108 |
| 118 MediaClockDevice* clock_device = backend_->GetClock(); | 109 if (first_start_) |
| 119 clock_device->SetState(MediaClockDevice::kStateRunning); | 110 backend_->Start(0); |
| 120 clock_device->SetRate(1.0f); | 111 else |
| 121 | 112 backend_->Resume(); |
| 122 AudioPipelineDevice* audio_device = backend_->GetAudio(); | |
| 123 audio_device->SetState(AudioPipelineDevice::kStateRunning); | |
| 124 } | 113 } |
| 125 | 114 |
| 126 void Stop() { | 115 void Stop() { |
| 127 DCHECK(thread_checker_.CalledOnValidThread()); | 116 DCHECK(thread_checker_.CalledOnValidThread()); |
| 117 DCHECK(backend_); |
| 128 | 118 |
| 129 MediaClockDevice* clock_device = backend_->GetClock(); | 119 backend_->Pause(); |
| 130 clock_device->SetRate(0.0f); | 120 first_start_ = false; |
| 131 } | 121 } |
| 132 | 122 |
| 133 void PushFrame(scoped_refptr<media::DecoderBufferBase> decoder_buffer, | 123 void PushBuffer(scoped_refptr<media::DecoderBufferBase> decoder_buffer, |
| 134 const PushFrameCompletionCallback& completion_cb) { | 124 const PushBufferCompletionCallback& completion_cb) { |
| 135 DCHECK(thread_checker_.CalledOnValidThread()); | 125 DCHECK(thread_checker_.CalledOnValidThread()); |
| 126 DCHECK(decoder_); |
| 127 DCHECK(completion_cb_.is_null()); |
| 128 if (error_) { |
| 129 completion_cb.Run(false); |
| 130 return; |
| 131 } |
| 136 | 132 |
| 137 AudioPipelineDevice* audio_device = backend_->GetAudio(); | 133 if (backend_buffer_) |
| 138 MediaComponentDevice::FrameStatus status = | 134 backend_buffer_->set_buffer(decoder_buffer); |
| 139 audio_device->PushFrame(nullptr, // decrypt_context | 135 else |
| 140 new CastDecoderBufferImpl(decoder_buffer), | 136 backend_buffer_.reset(new CastDecoderBufferImpl(decoder_buffer)); |
| 141 new media::FrameStatusCBImpl(base::Bind( | |
| 142 &Backend::OnPushFrameStatus, | |
| 143 base::Unretained(this), completion_cb))); | |
| 144 | 137 |
| 145 if (status != MediaComponentDevice::kFramePending) | 138 MediaPipelineBackend::BufferStatus status = |
| 146 OnPushFrameStatus(completion_cb, status); | 139 decoder_->PushBuffer(nullptr, backend_buffer_.get()); |
| 140 completion_cb_ = completion_cb; |
| 141 if (status != MediaPipelineBackend::kBufferPending) |
| 142 OnPushBufferComplete(decoder_, status); |
| 147 } | 143 } |
| 148 | 144 |
| 149 void SetVolume(double volume) { | 145 void SetVolume(double volume) { |
| 150 DCHECK(thread_checker_.CalledOnValidThread()); | 146 DCHECK(thread_checker_.CalledOnValidThread()); |
| 147 DCHECK(decoder_); |
| 148 decoder_->SetVolume(volume); |
| 149 } |
| 151 | 150 |
| 152 AudioPipelineDevice* audio_device = backend_->GetAudio(); | 151 // MediaPipelineBackend::Delegate implementation |
| 153 audio_device->SetStreamVolumeMultiplier(volume); | 152 void OnVideoResolutionChanged(MediaPipelineBackend::VideoDecoder* decoder, |
| 153 const Size& size) override {} |
| 154 |
| 155 void OnPushBufferComplete( |
| 156 MediaPipelineBackend::Decoder* decoder, |
| 157 MediaPipelineBackend::BufferStatus status) override { |
| 158 DCHECK(thread_checker_.CalledOnValidThread()); |
| 159 DCHECK_NE(status, MediaPipelineBackend::kBufferPending); |
| 160 |
| 161 base::ResetAndReturn(&completion_cb_) |
| 162 .Run(status == MediaPipelineBackend::kBufferSuccess); |
| 163 } |
| 164 |
| 165 void OnEndOfStream(MediaPipelineBackend::Decoder* decoder) override {} |
| 166 |
| 167 void OnDecoderError(MediaPipelineBackend::Decoder* decoder) override { |
| 168 error_ = true; |
| 169 if (!completion_cb_.is_null()) |
| 170 OnPushBufferComplete(decoder_, MediaPipelineBackend::kBufferFailed); |
| 154 } | 171 } |
| 155 | 172 |
| 156 private: | 173 private: |
| 157 void OnPushFrameStatus(const PushFrameCompletionCallback& completion_cb, | |
| 158 MediaComponentDevice::FrameStatus status) { | |
| 159 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 160 DCHECK_NE(status, MediaComponentDevice::kFramePending); | |
| 161 | |
| 162 completion_cb.Run(status == MediaComponentDevice::kFrameSuccess); | |
| 163 } | |
| 164 | |
| 165 const ::media::AudioParameters audio_params_; | 174 const ::media::AudioParameters audio_params_; |
| 166 scoped_ptr<MediaPipelineBackend> backend_; | 175 scoped_ptr<MediaPipelineBackend> backend_; |
| 167 scoped_ptr<TaskRunnerImpl> backend_task_runner_; | 176 scoped_ptr<TaskRunnerImpl> backend_task_runner_; |
| 177 MediaPipelineBackend::AudioDecoder* decoder_; |
| 178 PushBufferCompletionCallback completion_cb_; |
| 179 bool first_start_; |
| 180 bool error_; |
| 181 scoped_ptr<CastDecoderBufferImpl> backend_buffer_; |
| 168 base::ThreadChecker thread_checker_; | 182 base::ThreadChecker thread_checker_; |
| 169 DISALLOW_COPY_AND_ASSIGN(Backend); | 183 DISALLOW_COPY_AND_ASSIGN(Backend); |
| 170 }; | 184 }; |
| 171 | 185 |
| 172 // CastAudioOutputStream runs on audio thread (AudioManager::GetTaskRunner). | 186 // CastAudioOutputStream runs on audio thread (AudioManager::GetTaskRunner). |
| 173 CastAudioOutputStream::CastAudioOutputStream( | 187 CastAudioOutputStream::CastAudioOutputStream( |
| 174 const ::media::AudioParameters& audio_params, | 188 const ::media::AudioParameters& audio_params, |
| 175 CastAudioManager* audio_manager) | 189 CastAudioManager* audio_manager) |
| 176 : audio_params_(audio_params), | 190 : audio_params_(audio_params), |
| 177 audio_manager_(audio_manager), | 191 audio_manager_(audio_manager), |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 241 DCHECK(audio_task_runner_->BelongsToCurrentThread()); | 255 DCHECK(audio_task_runner_->BelongsToCurrentThread()); |
| 242 DCHECK(source_callback); | 256 DCHECK(source_callback); |
| 243 | 257 |
| 244 source_callback_ = source_callback; | 258 source_callback_ = source_callback; |
| 245 backend_task_runner_->PostTask( | 259 backend_task_runner_->PostTask( |
| 246 FROM_HERE, base::Bind(&Backend::Start, base::Unretained(backend_.get()))); | 260 FROM_HERE, base::Bind(&Backend::Start, base::Unretained(backend_.get()))); |
| 247 | 261 |
| 248 next_push_time_ = base::TimeTicks::Now(); | 262 next_push_time_ = base::TimeTicks::Now(); |
| 249 if (!backend_busy_) { | 263 if (!backend_busy_) { |
| 250 audio_task_runner_->PostTask(FROM_HERE, | 264 audio_task_runner_->PostTask(FROM_HERE, |
| 251 base::Bind(&CastAudioOutputStream::PushFrame, | 265 base::Bind(&CastAudioOutputStream::PushBuffer, |
| 252 weak_factory_.GetWeakPtr())); | 266 weak_factory_.GetWeakPtr())); |
| 253 } | 267 } |
| 254 | 268 |
| 255 metrics::CastMetricsHelper::GetInstance()->LogTimeToFirstAudio(); | 269 metrics::CastMetricsHelper::GetInstance()->LogTimeToFirstAudio(); |
| 256 } | 270 } |
| 257 | 271 |
| 258 void CastAudioOutputStream::Stop() { | 272 void CastAudioOutputStream::Stop() { |
| 259 DCHECK(audio_task_runner_->BelongsToCurrentThread()); | 273 DCHECK(audio_task_runner_->BelongsToCurrentThread()); |
| 260 | 274 |
| 261 source_callback_ = nullptr; | 275 source_callback_ = nullptr; |
| (...skipping 18 matching lines...) Expand all Loading... |
| 280 | 294 |
| 281 void CastAudioOutputStream::OnClosed() { | 295 void CastAudioOutputStream::OnClosed() { |
| 282 DCHECK(audio_task_runner_->BelongsToCurrentThread()); | 296 DCHECK(audio_task_runner_->BelongsToCurrentThread()); |
| 283 | 297 |
| 284 VLOG(1) << __FUNCTION__ << " : " << this; | 298 VLOG(1) << __FUNCTION__ << " : " << this; |
| 285 // Signal to the manager that we're closed and can be removed. | 299 // Signal to the manager that we're closed and can be removed. |
| 286 // This should be the last call in the function as it deletes "this". | 300 // This should be the last call in the function as it deletes "this". |
| 287 audio_manager_->ReleaseOutputStream(this); | 301 audio_manager_->ReleaseOutputStream(this); |
| 288 } | 302 } |
| 289 | 303 |
| 290 void CastAudioOutputStream::PushFrame() { | 304 void CastAudioOutputStream::PushBuffer() { |
| 291 DCHECK(audio_task_runner_->BelongsToCurrentThread()); | 305 DCHECK(audio_task_runner_->BelongsToCurrentThread()); |
| 292 DCHECK(!backend_busy_); | 306 DCHECK(!backend_busy_); |
| 293 | 307 |
| 294 if (!source_callback_) | 308 if (!source_callback_) |
| 295 return; | 309 return; |
| 296 | 310 |
| 297 uint32_t bytes_delay = 0; | 311 uint32_t bytes_delay = 0; |
| 298 int frame_count = source_callback_->OnMoreData(audio_bus_.get(), bytes_delay); | 312 int frame_count = source_callback_->OnMoreData(audio_bus_.get(), bytes_delay); |
| 299 VLOG(3) << "frames_filled=" << frame_count << " with latency=" << bytes_delay; | 313 VLOG(3) << "frames_filled=" << frame_count << " with latency=" << bytes_delay; |
| 300 | 314 |
| 301 DCHECK_EQ(frame_count, audio_bus_->frames()); | 315 DCHECK_EQ(frame_count, audio_bus_->frames()); |
| 302 DCHECK_EQ(static_cast<int>(decoder_buffer_->data_size()), | 316 DCHECK_EQ(static_cast<int>(decoder_buffer_->data_size()), |
| 303 frame_count * audio_params_.GetBytesPerFrame()); | 317 frame_count * audio_params_.GetBytesPerFrame()); |
| 304 audio_bus_->ToInterleaved(frame_count, audio_params_.bits_per_sample() / 8, | 318 audio_bus_->ToInterleaved(frame_count, audio_params_.bits_per_sample() / 8, |
| 305 decoder_buffer_->writable_data()); | 319 decoder_buffer_->writable_data()); |
| 306 | 320 |
| 307 auto completion_cb = ::media::BindToCurrentLoop(base::Bind( | 321 auto completion_cb = ::media::BindToCurrentLoop( |
| 308 &CastAudioOutputStream::OnPushFrameComplete, weak_factory_.GetWeakPtr())); | 322 base::Bind(&CastAudioOutputStream::OnPushBufferComplete, |
| 309 backend_task_runner_->PostTask( | 323 weak_factory_.GetWeakPtr())); |
| 310 FROM_HERE, | 324 backend_task_runner_->PostTask(FROM_HERE, |
| 311 base::Bind(&Backend::PushFrame, base::Unretained(backend_.get()), | 325 base::Bind(&Backend::PushBuffer, |
| 312 decoder_buffer_, completion_cb)); | 326 base::Unretained(backend_.get()), |
| 327 decoder_buffer_, |
| 328 completion_cb)); |
| 313 backend_busy_ = true; | 329 backend_busy_ = true; |
| 314 } | 330 } |
| 315 | 331 |
| 316 void CastAudioOutputStream::OnPushFrameComplete(bool success) { | 332 void CastAudioOutputStream::OnPushBufferComplete(bool success) { |
| 317 DCHECK(audio_task_runner_->BelongsToCurrentThread()); | 333 DCHECK(audio_task_runner_->BelongsToCurrentThread()); |
| 318 | 334 |
| 319 backend_busy_ = false; | 335 backend_busy_ = false; |
| 320 if (!source_callback_) | 336 if (!source_callback_) |
| 321 return; | 337 return; |
| 322 | 338 |
| 323 if (!success) { | 339 if (!success) { |
| 324 source_callback_->OnError(this); | 340 source_callback_->OnError(this); |
| 325 return; | 341 return; |
| 326 } | 342 } |
| 327 | 343 |
| 328 // Schedule next push frame. | 344 // Schedule next push buffer. |
| 329 // Need to account for time spent in pulling and pushing frame as well | 345 // Need to account for time spent in pulling and pushing buffer as well |
| 330 // as the imprecision of PostDelayedTask(). | 346 // as the imprecision of PostDelayedTask(). |
| 331 const base::TimeTicks now = base::TimeTicks::Now(); | 347 const base::TimeTicks now = base::TimeTicks::Now(); |
| 332 base::TimeDelta delay = next_push_time_ + buffer_duration_ - now; | 348 base::TimeDelta delay = next_push_time_ + buffer_duration_ - now; |
| 333 delay = std::max(delay, base::TimeDelta()); | 349 delay = std::max(delay, base::TimeDelta()); |
| 334 next_push_time_ = now + delay; | 350 next_push_time_ = now + delay; |
| 335 | 351 |
| 336 audio_task_runner_->PostDelayedTask( | 352 audio_task_runner_->PostDelayedTask( |
| 337 FROM_HERE, | 353 FROM_HERE, |
| 338 base::Bind(&CastAudioOutputStream::PushFrame, weak_factory_.GetWeakPtr()), | 354 base::Bind(&CastAudioOutputStream::PushBuffer, |
| 355 weak_factory_.GetWeakPtr()), |
| 339 delay); | 356 delay); |
| 340 } | 357 } |
| 341 | 358 |
| 342 } // namespace media | 359 } // namespace media |
| 343 } // namespace chromecast | 360 } // namespace chromecast |
| OLD | NEW |