OLD | NEW |
| (Empty) |
1 // Copyright 2014 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 "media/filters/renderer_impl.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/callback.h" | |
9 #include "base/callback_helpers.h" | |
10 #include "base/compiler_specific.h" | |
11 #include "base/location.h" | |
12 #include "base/single_thread_task_runner.h" | |
13 #include "media/base/audio_renderer.h" | |
14 #include "media/base/bind_to_current_loop.h" | |
15 #include "media/base/demuxer_stream_provider.h" | |
16 #include "media/base/time_source.h" | |
17 #include "media/base/video_renderer.h" | |
18 #include "media/base/wall_clock_time_source.h" | |
19 | |
20 namespace media { | |
21 | |
22 RendererImpl::RendererImpl( | |
23 const scoped_refptr<base::SingleThreadTaskRunner>& task_runner, | |
24 scoped_ptr<AudioRenderer> audio_renderer, | |
25 scoped_ptr<VideoRenderer> video_renderer) | |
26 : state_(STATE_UNINITIALIZED), | |
27 task_runner_(task_runner), | |
28 audio_renderer_(audio_renderer.Pass()), | |
29 video_renderer_(video_renderer.Pass()), | |
30 time_source_(NULL), | |
31 time_ticking_(false), | |
32 audio_buffering_state_(BUFFERING_HAVE_NOTHING), | |
33 video_buffering_state_(BUFFERING_HAVE_NOTHING), | |
34 audio_ended_(false), | |
35 video_ended_(false), | |
36 cdm_context_(nullptr), | |
37 underflow_disabled_for_testing_(false), | |
38 clockless_video_playback_enabled_for_testing_(false), | |
39 weak_factory_(this) { | |
40 weak_this_ = weak_factory_.GetWeakPtr(); | |
41 DVLOG(1) << __FUNCTION__; | |
42 } | |
43 | |
44 RendererImpl::~RendererImpl() { | |
45 DVLOG(1) << __FUNCTION__; | |
46 DCHECK(task_runner_->BelongsToCurrentThread()); | |
47 | |
48 // Tear down in opposite order of construction as |video_renderer_| can still | |
49 // need |time_source_| (which can be |audio_renderer_|) to be alive. | |
50 video_renderer_.reset(); | |
51 audio_renderer_.reset(); | |
52 | |
53 if (!init_cb_.is_null()) | |
54 base::ResetAndReturn(&init_cb_).Run(PIPELINE_ERROR_ABORT); | |
55 else if (!flush_cb_.is_null()) | |
56 base::ResetAndReturn(&flush_cb_).Run(); | |
57 } | |
58 | |
59 void RendererImpl::Initialize(DemuxerStreamProvider* demuxer_stream_provider, | |
60 const PipelineStatusCB& init_cb, | |
61 const StatisticsCB& statistics_cb, | |
62 const BufferingStateCB& buffering_state_cb, | |
63 const PaintCB& paint_cb, | |
64 const base::Closure& ended_cb, | |
65 const PipelineStatusCB& error_cb) { | |
66 DVLOG(1) << __FUNCTION__; | |
67 DCHECK(task_runner_->BelongsToCurrentThread()); | |
68 DCHECK_EQ(state_, STATE_UNINITIALIZED); | |
69 DCHECK(!init_cb.is_null()); | |
70 DCHECK(!statistics_cb.is_null()); | |
71 DCHECK(!buffering_state_cb.is_null()); | |
72 DCHECK(!paint_cb.is_null()); | |
73 DCHECK(!ended_cb.is_null()); | |
74 DCHECK(!error_cb.is_null()); | |
75 DCHECK(demuxer_stream_provider->GetStream(DemuxerStream::AUDIO) || | |
76 demuxer_stream_provider->GetStream(DemuxerStream::VIDEO)); | |
77 | |
78 demuxer_stream_provider_ = demuxer_stream_provider; | |
79 statistics_cb_ = statistics_cb; | |
80 buffering_state_cb_ = buffering_state_cb; | |
81 paint_cb_ = paint_cb; | |
82 ended_cb_ = ended_cb; | |
83 error_cb_ = error_cb; | |
84 init_cb_ = init_cb; | |
85 | |
86 state_ = STATE_INITIALIZING; | |
87 InitializeAudioRenderer(); | |
88 } | |
89 | |
90 void RendererImpl::SetCdm(CdmContext* cdm_context, | |
91 const CdmAttachedCB& cdm_attached_cb) { | |
92 DVLOG(1) << __FUNCTION__; | |
93 DCHECK(task_runner_->BelongsToCurrentThread()); | |
94 DCHECK(cdm_context); | |
95 | |
96 if (cdm_context_) { | |
97 DVLOG(1) << "Switching CDM not supported."; | |
98 cdm_attached_cb.Run(false); | |
99 return; | |
100 } | |
101 | |
102 cdm_context_ = cdm_context; | |
103 | |
104 if (decryptor_ready_cb_.is_null()) { | |
105 cdm_attached_cb.Run(true); | |
106 return; | |
107 } | |
108 | |
109 base::ResetAndReturn(&decryptor_ready_cb_) | |
110 .Run(cdm_context->GetDecryptor(), cdm_attached_cb); | |
111 } | |
112 | |
113 void RendererImpl::Flush(const base::Closure& flush_cb) { | |
114 DVLOG(1) << __FUNCTION__; | |
115 DCHECK(task_runner_->BelongsToCurrentThread()); | |
116 DCHECK(flush_cb_.is_null()); | |
117 | |
118 if (state_ != STATE_PLAYING) { | |
119 DCHECK_EQ(state_, STATE_ERROR); | |
120 return; | |
121 } | |
122 | |
123 flush_cb_ = flush_cb; | |
124 state_ = STATE_FLUSHING; | |
125 | |
126 if (time_ticking_) | |
127 PausePlayback(); | |
128 | |
129 FlushAudioRenderer(); | |
130 } | |
131 | |
132 void RendererImpl::StartPlayingFrom(base::TimeDelta time) { | |
133 DVLOG(1) << __FUNCTION__; | |
134 DCHECK(task_runner_->BelongsToCurrentThread()); | |
135 | |
136 if (state_ != STATE_PLAYING) { | |
137 DCHECK_EQ(state_, STATE_ERROR); | |
138 return; | |
139 } | |
140 | |
141 time_source_->SetMediaTime(time); | |
142 | |
143 if (audio_renderer_) | |
144 audio_renderer_->StartPlaying(); | |
145 if (video_renderer_) | |
146 video_renderer_->StartPlayingFrom(time); | |
147 } | |
148 | |
149 void RendererImpl::SetPlaybackRate(float playback_rate) { | |
150 DVLOG(1) << __FUNCTION__ << "(" << playback_rate << ")"; | |
151 DCHECK(task_runner_->BelongsToCurrentThread()); | |
152 | |
153 // Playback rate changes are only carried out while playing. | |
154 if (state_ != STATE_PLAYING) | |
155 return; | |
156 | |
157 time_source_->SetPlaybackRate(playback_rate); | |
158 } | |
159 | |
160 void RendererImpl::SetVolume(float volume) { | |
161 DVLOG(1) << __FUNCTION__; | |
162 DCHECK(task_runner_->BelongsToCurrentThread()); | |
163 | |
164 if (audio_renderer_) | |
165 audio_renderer_->SetVolume(volume); | |
166 } | |
167 | |
168 base::TimeDelta RendererImpl::GetMediaTime() { | |
169 // No BelongsToCurrentThread() checking because this can be called from other | |
170 // threads. | |
171 return time_source_->CurrentMediaTime(); | |
172 } | |
173 | |
174 bool RendererImpl::HasAudio() { | |
175 DCHECK(task_runner_->BelongsToCurrentThread()); | |
176 return audio_renderer_ != NULL; | |
177 } | |
178 | |
179 bool RendererImpl::HasVideo() { | |
180 DCHECK(task_runner_->BelongsToCurrentThread()); | |
181 return video_renderer_ != NULL; | |
182 } | |
183 | |
184 void RendererImpl::DisableUnderflowForTesting() { | |
185 DVLOG(1) << __FUNCTION__; | |
186 DCHECK(task_runner_->BelongsToCurrentThread()); | |
187 DCHECK_EQ(state_, STATE_UNINITIALIZED); | |
188 | |
189 underflow_disabled_for_testing_ = true; | |
190 } | |
191 | |
192 void RendererImpl::EnableClocklessVideoPlaybackForTesting() { | |
193 DVLOG(1) << __FUNCTION__; | |
194 DCHECK(task_runner_->BelongsToCurrentThread()); | |
195 DCHECK_EQ(state_, STATE_UNINITIALIZED); | |
196 DCHECK(underflow_disabled_for_testing_) | |
197 << "Underflow must be disabled for clockless video playback"; | |
198 | |
199 clockless_video_playback_enabled_for_testing_ = true; | |
200 } | |
201 | |
202 base::TimeDelta RendererImpl::GetMediaTimeForSyncingVideo() { | |
203 // No BelongsToCurrentThread() checking because this can be called from other | |
204 // threads. | |
205 // | |
206 // TODO(scherkus): Currently called from VideoRendererImpl's internal thread, | |
207 // which should go away at some point http://crbug.com/110814 | |
208 if (clockless_video_playback_enabled_for_testing_) | |
209 return base::TimeDelta::Max(); | |
210 | |
211 return time_source_->CurrentMediaTimeForSyncingVideo(); | |
212 } | |
213 | |
214 void RendererImpl::SetDecryptorReadyCallback( | |
215 const DecryptorReadyCB& decryptor_ready_cb) { | |
216 // Cancels the previous decryptor request. | |
217 if (decryptor_ready_cb.is_null()) { | |
218 if (!decryptor_ready_cb_.is_null()) { | |
219 base::ResetAndReturn(&decryptor_ready_cb_) | |
220 .Run(nullptr, base::Bind(IgnoreCdmAttached)); | |
221 } | |
222 return; | |
223 } | |
224 | |
225 // We initialize audio and video decoders in sequence. | |
226 DCHECK(decryptor_ready_cb_.is_null()); | |
227 | |
228 if (cdm_context_) { | |
229 decryptor_ready_cb.Run(cdm_context_->GetDecryptor(), | |
230 base::Bind(IgnoreCdmAttached)); | |
231 return; | |
232 } | |
233 | |
234 decryptor_ready_cb_ = decryptor_ready_cb; | |
235 } | |
236 | |
237 void RendererImpl::InitializeAudioRenderer() { | |
238 DVLOG(1) << __FUNCTION__; | |
239 DCHECK(task_runner_->BelongsToCurrentThread()); | |
240 DCHECK_EQ(state_, STATE_INITIALIZING); | |
241 DCHECK(!init_cb_.is_null()); | |
242 | |
243 PipelineStatusCB done_cb = | |
244 base::Bind(&RendererImpl::OnAudioRendererInitializeDone, weak_this_); | |
245 | |
246 if (!demuxer_stream_provider_->GetStream(DemuxerStream::AUDIO)) { | |
247 audio_renderer_.reset(); | |
248 task_runner_->PostTask(FROM_HERE, base::Bind(done_cb, PIPELINE_OK)); | |
249 return; | |
250 } | |
251 | |
252 // Note: After the initialization of a renderer, error events from it may | |
253 // happen at any time and all future calls must guard against STATE_ERROR. | |
254 audio_renderer_->Initialize( | |
255 demuxer_stream_provider_->GetStream(DemuxerStream::AUDIO), done_cb, | |
256 base::Bind(&RendererImpl::SetDecryptorReadyCallback, weak_this_), | |
257 base::Bind(&RendererImpl::OnUpdateStatistics, weak_this_), | |
258 base::Bind(&RendererImpl::OnBufferingStateChanged, weak_this_, | |
259 &audio_buffering_state_), | |
260 base::Bind(&RendererImpl::OnAudioRendererEnded, weak_this_), | |
261 base::Bind(&RendererImpl::OnError, weak_this_)); | |
262 } | |
263 | |
264 void RendererImpl::OnAudioRendererInitializeDone(PipelineStatus status) { | |
265 DVLOG(1) << __FUNCTION__ << ": " << status; | |
266 DCHECK(task_runner_->BelongsToCurrentThread()); | |
267 | |
268 // OnError() may be fired at any time by the renderers, even if they thought | |
269 // they initialized successfully (due to delayed output device setup). | |
270 if (state_ != STATE_INITIALIZING) { | |
271 DCHECK(init_cb_.is_null()); | |
272 audio_renderer_.reset(); | |
273 return; | |
274 } | |
275 | |
276 if (status != PIPELINE_OK) { | |
277 base::ResetAndReturn(&init_cb_).Run(status); | |
278 return; | |
279 } | |
280 | |
281 DCHECK(!init_cb_.is_null()); | |
282 InitializeVideoRenderer(); | |
283 } | |
284 | |
285 void RendererImpl::InitializeVideoRenderer() { | |
286 DVLOG(1) << __FUNCTION__; | |
287 DCHECK(task_runner_->BelongsToCurrentThread()); | |
288 DCHECK_EQ(state_, STATE_INITIALIZING); | |
289 DCHECK(!init_cb_.is_null()); | |
290 | |
291 PipelineStatusCB done_cb = | |
292 base::Bind(&RendererImpl::OnVideoRendererInitializeDone, weak_this_); | |
293 | |
294 if (!demuxer_stream_provider_->GetStream(DemuxerStream::VIDEO)) { | |
295 video_renderer_.reset(); | |
296 task_runner_->PostTask(FROM_HERE, base::Bind(done_cb, PIPELINE_OK)); | |
297 return; | |
298 } | |
299 | |
300 video_renderer_->Initialize( | |
301 demuxer_stream_provider_->GetStream(DemuxerStream::VIDEO), done_cb, | |
302 base::Bind(&RendererImpl::SetDecryptorReadyCallback, weak_this_), | |
303 base::Bind(&RendererImpl::OnUpdateStatistics, weak_this_), | |
304 base::Bind(&RendererImpl::OnBufferingStateChanged, weak_this_, | |
305 &video_buffering_state_), | |
306 base::ResetAndReturn(&paint_cb_), | |
307 base::Bind(&RendererImpl::OnVideoRendererEnded, weak_this_), | |
308 base::Bind(&RendererImpl::OnError, weak_this_), | |
309 base::Bind(&RendererImpl::GetMediaTimeForSyncingVideo, | |
310 base::Unretained(this))); | |
311 } | |
312 | |
313 void RendererImpl::OnVideoRendererInitializeDone(PipelineStatus status) { | |
314 DVLOG(1) << __FUNCTION__ << ": " << status; | |
315 DCHECK(task_runner_->BelongsToCurrentThread()); | |
316 | |
317 // OnError() may be fired at any time by the renderers, even if they thought | |
318 // they initialized successfully (due to delayed output device setup). | |
319 if (state_ != STATE_INITIALIZING) { | |
320 DCHECK(init_cb_.is_null()); | |
321 audio_renderer_.reset(); | |
322 video_renderer_.reset(); | |
323 return; | |
324 } | |
325 | |
326 DCHECK(!init_cb_.is_null()); | |
327 | |
328 if (status != PIPELINE_OK) { | |
329 base::ResetAndReturn(&init_cb_).Run(status); | |
330 return; | |
331 } | |
332 | |
333 if (audio_renderer_) { | |
334 time_source_ = audio_renderer_->GetTimeSource(); | |
335 } else { | |
336 wall_clock_time_source_.reset(new WallClockTimeSource()); | |
337 time_source_ = wall_clock_time_source_.get(); | |
338 } | |
339 | |
340 state_ = STATE_PLAYING; | |
341 DCHECK(time_source_); | |
342 DCHECK(audio_renderer_ || video_renderer_); | |
343 base::ResetAndReturn(&init_cb_).Run(PIPELINE_OK); | |
344 } | |
345 | |
346 void RendererImpl::FlushAudioRenderer() { | |
347 DVLOG(1) << __FUNCTION__; | |
348 DCHECK(task_runner_->BelongsToCurrentThread()); | |
349 DCHECK_EQ(state_, STATE_FLUSHING); | |
350 DCHECK(!flush_cb_.is_null()); | |
351 | |
352 if (!audio_renderer_) { | |
353 OnAudioRendererFlushDone(); | |
354 return; | |
355 } | |
356 | |
357 audio_renderer_->Flush( | |
358 base::Bind(&RendererImpl::OnAudioRendererFlushDone, weak_this_)); | |
359 } | |
360 | |
361 void RendererImpl::OnAudioRendererFlushDone() { | |
362 DVLOG(1) << __FUNCTION__; | |
363 DCHECK(task_runner_->BelongsToCurrentThread()); | |
364 | |
365 if (state_ == STATE_ERROR) { | |
366 DCHECK(flush_cb_.is_null()); | |
367 return; | |
368 } | |
369 | |
370 DCHECK_EQ(state_, STATE_FLUSHING); | |
371 DCHECK(!flush_cb_.is_null()); | |
372 | |
373 DCHECK_EQ(audio_buffering_state_, BUFFERING_HAVE_NOTHING); | |
374 audio_ended_ = false; | |
375 FlushVideoRenderer(); | |
376 } | |
377 | |
378 void RendererImpl::FlushVideoRenderer() { | |
379 DVLOG(1) << __FUNCTION__; | |
380 DCHECK(task_runner_->BelongsToCurrentThread()); | |
381 DCHECK_EQ(state_, STATE_FLUSHING); | |
382 DCHECK(!flush_cb_.is_null()); | |
383 | |
384 if (!video_renderer_) { | |
385 OnVideoRendererFlushDone(); | |
386 return; | |
387 } | |
388 | |
389 video_renderer_->Flush( | |
390 base::Bind(&RendererImpl::OnVideoRendererFlushDone, weak_this_)); | |
391 } | |
392 | |
393 void RendererImpl::OnVideoRendererFlushDone() { | |
394 DVLOG(1) << __FUNCTION__; | |
395 DCHECK(task_runner_->BelongsToCurrentThread()); | |
396 | |
397 if (state_ == STATE_ERROR) { | |
398 DCHECK(flush_cb_.is_null()); | |
399 return; | |
400 } | |
401 | |
402 DCHECK_EQ(state_, STATE_FLUSHING); | |
403 DCHECK(!flush_cb_.is_null()); | |
404 | |
405 DCHECK_EQ(video_buffering_state_, BUFFERING_HAVE_NOTHING); | |
406 video_ended_ = false; | |
407 state_ = STATE_PLAYING; | |
408 base::ResetAndReturn(&flush_cb_).Run(); | |
409 } | |
410 | |
411 void RendererImpl::OnUpdateStatistics(const PipelineStatistics& stats) { | |
412 DCHECK(task_runner_->BelongsToCurrentThread()); | |
413 statistics_cb_.Run(stats); | |
414 } | |
415 | |
416 void RendererImpl::OnBufferingStateChanged(BufferingState* buffering_state, | |
417 BufferingState new_buffering_state) { | |
418 DVLOG(1) << __FUNCTION__ << "(" << *buffering_state << ", " | |
419 << new_buffering_state << ") " | |
420 << (buffering_state == &audio_buffering_state_ ? "audio" : "video"); | |
421 DCHECK(task_runner_->BelongsToCurrentThread()); | |
422 bool was_waiting_for_enough_data = WaitingForEnoughData(); | |
423 | |
424 *buffering_state = new_buffering_state; | |
425 | |
426 // Disable underflow by ignoring updates that renderers have ran out of data. | |
427 if (state_ == STATE_PLAYING && underflow_disabled_for_testing_ && | |
428 time_ticking_) { | |
429 DVLOG(1) << "Update ignored because underflow is disabled for testing."; | |
430 return; | |
431 } | |
432 | |
433 // Renderer underflowed. | |
434 if (!was_waiting_for_enough_data && WaitingForEnoughData()) { | |
435 PausePlayback(); | |
436 | |
437 // TODO(scherkus): Fire BUFFERING_HAVE_NOTHING callback to alert clients of | |
438 // underflow state http://crbug.com/144683 | |
439 return; | |
440 } | |
441 | |
442 // Renderer prerolled. | |
443 if (was_waiting_for_enough_data && !WaitingForEnoughData()) { | |
444 StartPlayback(); | |
445 buffering_state_cb_.Run(BUFFERING_HAVE_ENOUGH); | |
446 return; | |
447 } | |
448 } | |
449 | |
450 bool RendererImpl::WaitingForEnoughData() const { | |
451 DCHECK(task_runner_->BelongsToCurrentThread()); | |
452 if (state_ != STATE_PLAYING) | |
453 return false; | |
454 if (audio_renderer_ && audio_buffering_state_ != BUFFERING_HAVE_ENOUGH) | |
455 return true; | |
456 if (video_renderer_ && video_buffering_state_ != BUFFERING_HAVE_ENOUGH) | |
457 return true; | |
458 return false; | |
459 } | |
460 | |
461 void RendererImpl::PausePlayback() { | |
462 DVLOG(1) << __FUNCTION__; | |
463 DCHECK(task_runner_->BelongsToCurrentThread()); | |
464 DCHECK(time_ticking_); | |
465 switch (state_) { | |
466 case STATE_PLAYING: | |
467 DCHECK(PlaybackHasEnded() || WaitingForEnoughData()) | |
468 << "Playback should only pause due to ending or underflowing"; | |
469 break; | |
470 | |
471 case STATE_FLUSHING: | |
472 // It's OK to pause playback when flushing. | |
473 break; | |
474 | |
475 case STATE_UNINITIALIZED: | |
476 case STATE_INITIALIZING: | |
477 NOTREACHED() << "Invalid state: " << state_; | |
478 break; | |
479 | |
480 case STATE_ERROR: | |
481 // An error state may occur at any time. | |
482 break; | |
483 } | |
484 | |
485 time_ticking_ = false; | |
486 time_source_->StopTicking(); | |
487 } | |
488 | |
489 void RendererImpl::StartPlayback() { | |
490 DVLOG(1) << __FUNCTION__; | |
491 DCHECK(task_runner_->BelongsToCurrentThread()); | |
492 DCHECK_EQ(state_, STATE_PLAYING); | |
493 DCHECK(!time_ticking_); | |
494 DCHECK(!WaitingForEnoughData()); | |
495 | |
496 time_ticking_ = true; | |
497 time_source_->StartTicking(); | |
498 } | |
499 | |
500 void RendererImpl::OnAudioRendererEnded() { | |
501 DVLOG(1) << __FUNCTION__; | |
502 DCHECK(task_runner_->BelongsToCurrentThread()); | |
503 | |
504 if (state_ != STATE_PLAYING) | |
505 return; | |
506 | |
507 DCHECK(!audio_ended_); | |
508 audio_ended_ = true; | |
509 | |
510 RunEndedCallbackIfNeeded(); | |
511 } | |
512 | |
513 void RendererImpl::OnVideoRendererEnded() { | |
514 DVLOG(1) << __FUNCTION__; | |
515 DCHECK(task_runner_->BelongsToCurrentThread()); | |
516 | |
517 if (state_ != STATE_PLAYING) | |
518 return; | |
519 | |
520 DCHECK(!video_ended_); | |
521 video_ended_ = true; | |
522 | |
523 RunEndedCallbackIfNeeded(); | |
524 } | |
525 | |
526 bool RendererImpl::PlaybackHasEnded() const { | |
527 DVLOG(1) << __FUNCTION__; | |
528 DCHECK(task_runner_->BelongsToCurrentThread()); | |
529 | |
530 if (audio_renderer_ && !audio_ended_) | |
531 return false; | |
532 | |
533 if (video_renderer_ && !video_ended_) | |
534 return false; | |
535 | |
536 return true; | |
537 } | |
538 | |
539 void RendererImpl::RunEndedCallbackIfNeeded() { | |
540 DVLOG(1) << __FUNCTION__; | |
541 DCHECK(task_runner_->BelongsToCurrentThread()); | |
542 | |
543 if (!PlaybackHasEnded()) | |
544 return; | |
545 | |
546 if (time_ticking_) | |
547 PausePlayback(); | |
548 | |
549 ended_cb_.Run(); | |
550 } | |
551 | |
552 void RendererImpl::OnError(PipelineStatus error) { | |
553 DVLOG(1) << __FUNCTION__ << "(" << error << ")"; | |
554 DCHECK(task_runner_->BelongsToCurrentThread()); | |
555 DCHECK_NE(PIPELINE_OK, error) << "PIPELINE_OK isn't an error!"; | |
556 | |
557 // An error has already been delivered. | |
558 if (state_ == STATE_ERROR) | |
559 return; | |
560 | |
561 const State old_state = state_; | |
562 state_ = STATE_ERROR; | |
563 | |
564 if (old_state == STATE_INITIALIZING) { | |
565 base::ResetAndReturn(&init_cb_).Run(error); | |
566 return; | |
567 } | |
568 | |
569 // After OnError() returns, the pipeline may destroy |this|. | |
570 base::ResetAndReturn(&error_cb_).Run(error); | |
571 | |
572 if (!flush_cb_.is_null()) | |
573 base::ResetAndReturn(&flush_cb_).Run(); | |
574 } | |
575 | |
576 } // namespace media | |
OLD | NEW |