OLD | NEW |
---|---|
(Empty) | |
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 | |
3 // found in the LICENSE file. | |
4 | |
5 #include "media/base/android/media_codec_decoder.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/bind_helpers.h" | |
9 #include "base/callback_helpers.h" | |
10 #include "base/logging.h" | |
11 #include "media/base/android/media_codec_bridge.h" | |
12 | |
13 namespace media { | |
14 | |
15 namespace { | |
16 | |
17 // Stop requesting new data in the kPrefetching state when the queue size | |
18 // reaches this limit. | |
19 const int kPrefetchLimit = 8; | |
20 | |
21 // Request new data in the kRunning state if the queue size is less than this. | |
22 const int kPlaybackLowLimit = 4; | |
23 | |
24 // Posting delay of the next frame processing, in milliseconds | |
25 const int kNextFrameDelay = 2; | |
26 | |
27 // Timeout for dequeuing an input buffer from MediaCodec in milliseconds. | |
28 const int kInputBufferTimeout = 20; | |
29 | |
30 // Timeout for dequeuing an output buffer from MediaCodec in milliseconds. | |
31 const int kOutputBufferTimeout = 20; | |
32 } | |
33 | |
34 MediaCodecDecoder::MediaCodecDecoder( | |
35 const scoped_refptr<base::SingleThreadTaskRunner>& media_task_runner, | |
36 const base::Closure& request_data_cb, | |
37 const base::Closure& starvation_cb, | |
38 const base::Closure& stop_done_cb, | |
39 const base::Closure& error_cb, | |
40 const char* decoder_thread_name) | |
41 : media_task_runner_(media_task_runner), | |
42 decoder_thread_(decoder_thread_name), | |
43 request_data_cb_(request_data_cb), | |
44 starvation_cb_(starvation_cb), | |
45 stop_done_cb_(stop_done_cb), | |
46 error_cb_(error_cb), | |
47 state_(kStopped), | |
48 eos_enqueued_(false), | |
49 completed_(false), | |
50 last_frame_posted_(false), | |
51 weak_factory_(this) { | |
52 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
53 | |
54 DVLOG(1) << "Decoder::Decoder() " << decoder_thread_name; | |
55 | |
56 weak_this_ = weak_factory_.GetWeakPtr(); | |
57 | |
58 internal_error_cb_ = base::Bind(&MediaCodecDecoder::OnCodecError, weak_this_); | |
59 } | |
60 | |
61 MediaCodecDecoder::~MediaCodecDecoder() { | |
62 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
63 | |
64 DVLOG(1) << "Decoder::~Decoder()"; | |
65 | |
66 // NB: ReleaseDecoderResources() is virtual | |
67 ReleaseDecoderResources(); | |
68 } | |
69 | |
70 void MediaCodecDecoder::ReleaseDecoderResources() { | |
71 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
72 | |
73 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
74 | |
75 decoder_thread_.Stop(); // synchronous | |
76 state_ = kStopped; | |
77 media_codec_bridge_.reset(); | |
78 } | |
79 | |
80 void MediaCodecDecoder::Flush() { | |
81 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
82 | |
83 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
84 | |
85 DCHECK_EQ(GetState(), kStopped); | |
86 | |
87 eos_enqueued_ = false; | |
88 completed_ = false; | |
89 au_queue_.Flush(); | |
90 | |
91 if (media_codec_bridge_) { | |
92 // MediaCodecBridge::Reset() performs MediaCodecBridge.flush() | |
93 MediaCodecStatus flush_status = media_codec_bridge_->Reset(); | |
94 if (flush_status != MEDIA_CODEC_OK) { | |
95 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
96 << "MediaCodecBridge::Reset() failed"; | |
97 media_task_runner_->PostTask(FROM_HERE, internal_error_cb_); | |
98 } | |
99 } | |
100 } | |
101 | |
102 void MediaCodecDecoder::ReleaseMediaCodec() { | |
103 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
104 | |
105 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
106 | |
107 media_codec_bridge_.reset(); | |
108 } | |
109 | |
110 bool MediaCodecDecoder::IsPrefetchingOrPlaying() const { | |
111 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
112 | |
113 base::AutoLock lock(state_lock_); | |
114 return state_ == kPrefetching || state_ == kRunning; | |
115 } | |
116 | |
117 bool MediaCodecDecoder::IsStopped() const { | |
118 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
119 | |
120 return GetState() == kStopped; | |
121 } | |
122 | |
123 bool MediaCodecDecoder::IsCompleted() const { | |
124 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
125 | |
126 return completed_; | |
127 } | |
128 | |
129 base::android::ScopedJavaLocalRef<jobject> MediaCodecDecoder::GetMediaCrypto() { | |
130 base::android::ScopedJavaLocalRef<jobject> media_crypto; | |
131 | |
132 // drm_bridge_ is not implemented | |
qinmin
2015/06/18 00:24:40
add a TODO
Tima Vaisburd
2015/06/18 19:34:04
Done.
| |
133 // if (drm_bridge_) | |
134 // media_crypto = drm_bridge_->GetMediaCrypto(); | |
135 return media_crypto; | |
136 } | |
137 | |
138 void MediaCodecDecoder::Prefetch(const base::Closure& prefetch_done_cb) { | |
139 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
140 | |
141 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
142 | |
143 DCHECK(GetState() == kStopped); | |
144 | |
145 prefetch_done_cb_ = prefetch_done_cb; | |
146 | |
147 SetState(kPrefetching); | |
148 PrefetchNextChunk(); | |
149 } | |
150 | |
151 MediaCodecDecoder::ConfigStatus MediaCodecDecoder::Configure() { | |
152 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
153 | |
154 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
155 | |
156 if (GetState() == kError) { | |
157 DVLOG(0) << class_name() << "::" << __FUNCTION__ << ": wrong state kError"; | |
158 return CONFIG_FAILURE; | |
159 } | |
160 | |
161 // Here I assume that OnDemuxerConfigsAvailable won't come | |
162 // in the middle of demuxer data. | |
163 | |
164 if (media_codec_bridge_) { | |
165 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
166 << ": reconfiguration is not required, ignoring"; | |
167 return CONFIG_OK; | |
168 } | |
169 | |
170 return ConfigureInternal(); | |
171 } | |
172 | |
173 bool MediaCodecDecoder::Start(base::TimeDelta current_time) { | |
174 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
175 | |
176 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
177 << " current_time:" << current_time; | |
178 | |
179 DecoderState state = GetState(); | |
180 if (state == kRunning) { | |
181 DVLOG(1) << class_name() << "::" << __FUNCTION__ << ": already started"; | |
182 return true; // already started | |
183 } | |
184 | |
185 if (state != kPrefetched) { | |
186 DVLOG(0) << class_name() << "::" << __FUNCTION__ << ": wrong state " | |
187 << AsString(state) << " ignoring"; | |
188 return false; | |
189 } | |
190 | |
191 if (!media_codec_bridge_) { | |
192 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
193 << ": not configured, ignoring"; | |
194 return false; | |
195 } | |
196 | |
197 DCHECK(!decoder_thread_.IsRunning()); | |
198 | |
199 // We only synchronize video stream. | |
200 // When audio is present, the |current_time| is audio time. | |
201 SynchronizePTSWithTime(current_time); | |
202 | |
203 last_frame_posted_ = false; | |
204 | |
205 // Start the decoder thread | |
206 if (!decoder_thread_.Start()) { | |
207 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
208 << ": cannot start decoder thread"; | |
209 return false; | |
210 } | |
211 | |
212 SetState(kRunning); | |
213 | |
214 decoder_thread_.task_runner()->PostTask( | |
215 FROM_HERE, | |
216 base::Bind(&MediaCodecDecoder::ProcessNextFrame, base::Unretained(this))); | |
217 | |
218 return true; | |
219 } | |
220 | |
221 void MediaCodecDecoder::SyncStop() { | |
222 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
223 | |
224 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
225 | |
226 if (GetState() == kError) { | |
227 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
228 << ": wrong state kError, ignoring"; | |
229 return; | |
230 } | |
231 | |
232 // After this method returns, decoder thread will not be running. | |
233 | |
234 decoder_thread_.Stop(); // synchronous | |
235 state_ = kStopped; | |
236 | |
237 // Shall we move |delayed_buffers_| from VideoDecoder to Decoder class? | |
238 ReleaseDelayedBuffers(); | |
239 } | |
240 | |
241 void MediaCodecDecoder::RequestToStop() { | |
242 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
243 | |
244 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
245 | |
246 DecoderState state = GetState(); | |
247 switch (state) { | |
248 case kError: | |
249 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
250 << ": wrong state kError, ignoring"; | |
251 break; | |
252 case kRunning: | |
253 SetState(kStopping); | |
254 break; | |
255 case kStopping: | |
256 break; // ignore | |
257 case kStopped: | |
258 case kPrefetching: | |
259 case kPrefetched: | |
260 // There is nothing to wait for, we can sent nofigication right away. | |
261 DCHECK(!decoder_thread_.IsRunning()); | |
262 SetState(kStopped); | |
263 media_task_runner_->PostTask(FROM_HERE, stop_done_cb_); | |
264 break; | |
265 default: | |
266 NOTREACHED(); | |
267 break; | |
268 } | |
269 } | |
270 | |
271 void MediaCodecDecoder::OnLastFrameRendered(bool completed) { | |
272 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
273 | |
274 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
275 << " completed:" << completed; | |
276 | |
277 decoder_thread_.Stop(); // synchronous | |
278 state_ = kStopped; | |
279 completed_ = completed; | |
280 | |
281 media_task_runner_->PostTask(FROM_HERE, stop_done_cb_); | |
282 } | |
283 | |
284 void MediaCodecDecoder::OnDemuxerDataAvailable(const DemuxerData& data) { | |
285 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
286 | |
287 DVLOG(2) << class_name() << "::" << __FUNCTION__ | |
288 << " #AUs:" << data.access_units.size() | |
289 << " #Configs:" << data.demuxer_configs.size(); | |
290 #if !defined(NDEBUG) | |
291 for (const auto& unit : data.access_units) | |
292 DVLOG(2) << class_name() << "::" << __FUNCTION__ << " au: " << unit; | |
293 #endif | |
294 | |
295 au_queue_.PushBack(data); | |
296 | |
297 if (state_ == kPrefetching) | |
298 PrefetchNextChunk(); | |
299 } | |
300 | |
301 void MediaCodecDecoder::CheckLastFrame(bool eos_encountered, | |
302 bool has_delayed_tasks) { | |
303 DCHECK(decoder_thread_.task_runner()->BelongsToCurrentThread()); | |
304 | |
305 bool last_frame_when_stopping = GetState() == kStopping && !has_delayed_tasks; | |
306 | |
307 if (last_frame_when_stopping || eos_encountered) { | |
308 media_task_runner_->PostTask( | |
309 FROM_HERE, base::Bind(&MediaCodecDecoder::OnLastFrameRendered, | |
310 weak_this_, eos_encountered)); | |
311 last_frame_posted_ = true; | |
312 } | |
313 } | |
314 | |
315 void MediaCodecDecoder::OnCodecError() { | |
316 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
317 | |
318 SetState(kError); | |
319 error_cb_.Run(); | |
320 } | |
321 | |
322 void MediaCodecDecoder::PrefetchNextChunk() { | |
323 DCHECK(media_task_runner_->BelongsToCurrentThread()); | |
324 | |
325 DVLOG(1) << class_name() << "::" << __FUNCTION__; | |
326 | |
327 AccessUnitQueue::Info au_info = au_queue_.GetInfo(); | |
328 | |
329 if (eos_enqueued_ || au_info.length >= kPrefetchLimit || au_info.has_eos) { | |
330 // We are done prefetching | |
331 SetState(kPrefetched); | |
332 DVLOG(1) << class_name() << "::" << __FUNCTION__ << " posting PrefetchDone"; | |
333 media_task_runner_->PostTask(FROM_HERE, | |
334 base::ResetAndReturn(&prefetch_done_cb_)); | |
335 return; | |
336 } | |
337 | |
338 request_data_cb_.Run(); | |
339 } | |
340 | |
341 void MediaCodecDecoder::ProcessNextFrame() { | |
342 DCHECK(decoder_thread_.task_runner()->BelongsToCurrentThread()); | |
343 | |
344 DVLOG(2) << class_name() << "::" << __FUNCTION__; | |
345 | |
346 DecoderState state = GetState(); | |
347 | |
348 if (state != kRunning && state != kStopping) { | |
349 DVLOG(1) << class_name() << "::" << __FUNCTION__ << ": not running"; | |
350 return; | |
351 } | |
352 | |
353 if (state == kStopping) { | |
354 if (NumDelayedRenderTasks() == 0 && !last_frame_posted_) { | |
355 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
356 << ": kStopping, posting OnLastFrameRendered"; | |
357 media_task_runner_->PostTask( | |
358 FROM_HERE, base::Bind(&MediaCodecDecoder::OnLastFrameRendered, | |
359 weak_this_, false)); | |
360 last_frame_posted_ = true; | |
361 } | |
362 | |
363 // We can stop processing, the |au_queue_| and MediaCodec queues can freeze. | |
364 // We only need to let finish the delayed rendering tasks. | |
365 return; | |
366 } | |
367 | |
368 DCHECK(state == kRunning); | |
369 | |
370 // Keep the number pending video frames low, ideally maintaining | |
371 // the same audio and video duration after stop request | |
372 | |
373 if (NumDelayedRenderTasks() <= 1) { | |
qinmin
2015/06/18 00:24:40
make this a one line statement:
if (NumDelayedRend
Tima Vaisburd
2015/06/18 19:34:04
I put the NumDelayedRenderTasks() condition inside
| |
374 if (!EnqueueInputBuffer()) | |
375 return; | |
376 } | |
377 | |
378 bool eos_encountered = false; | |
379 if (!DepleteOutputBufferQueue(&eos_encountered)) | |
380 return; | |
381 | |
382 if (eos_encountered) { | |
383 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
384 << " EOS dequeued, stopping frame processing"; | |
385 return; | |
386 } | |
387 | |
388 // We need a small delay if we want to stop this thread by | |
389 // decoder_thread_.Stop() reliably. | |
390 // The decoder thread message loop processes all pending | |
391 // (but not delayed) tasks before it can quit; without a delay | |
392 // the message loop might be forever processing the pendng tasks. | |
393 decoder_thread_.task_runner()->PostDelayedTask( | |
394 FROM_HERE, | |
395 base::Bind(&MediaCodecDecoder::ProcessNextFrame, base::Unretained(this)), | |
396 base::TimeDelta::FromMilliseconds(kNextFrameDelay)); | |
397 } | |
398 | |
399 // Returns false if we should stop decoding process. Right now | |
400 // it happens if we got MediaCodec error or detected starvation. | |
401 bool MediaCodecDecoder::EnqueueInputBuffer() { | |
402 DCHECK(decoder_thread_.task_runner()->BelongsToCurrentThread()); | |
403 | |
404 DVLOG(2) << class_name() << "::" << __FUNCTION__; | |
405 | |
406 if (eos_enqueued_) { | |
407 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
408 << ": eos_enqueued, returning"; | |
409 return true; // Nothing to do | |
410 } | |
411 | |
412 // Get the next frame from the queue and the queue info | |
413 | |
414 AccessUnitQueue::Info au_info = au_queue_.GetInfo(); | |
415 | |
416 // Request the data from Demuxer | |
417 if (au_info.length <= kPlaybackLowLimit && !au_info.has_eos) | |
418 media_task_runner_->PostTask(FROM_HERE, request_data_cb_); | |
419 | |
420 // Get the next frame from the queue | |
421 | |
422 if (!au_info.length) { | |
423 // Report starvation and return, Start() will be called again later. | |
424 DVLOG(1) << class_name() << "::" << __FUNCTION__ << ": starvation detected"; | |
425 media_task_runner_->PostTask(FROM_HERE, starvation_cb_); | |
426 return false; | |
427 } | |
428 | |
429 if (au_info.configs) { | |
430 DVLOG(1) << class_name() << "::" << __FUNCTION__ | |
431 << ": received new configs, not implemented"; | |
432 // post an error for now? | |
433 media_task_runner_->PostTask(FROM_HERE, internal_error_cb_); | |
434 return false; | |
435 } | |
436 | |
437 // Dequeue input buffer | |
438 | |
439 base::TimeDelta timeout = | |
440 base::TimeDelta::FromMilliseconds(kInputBufferTimeout); | |
441 int index = -1; | |
442 MediaCodecStatus status = | |
443 media_codec_bridge_->DequeueInputBuffer(timeout, &index); | |
444 | |
445 DVLOG(2) << class_name() << ":: DequeueInputBuffer index:" << index; | |
446 | |
447 switch (status) { | |
448 case MEDIA_CODEC_ERROR: | |
449 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
450 << ": MEDIA_CODEC_ERROR DequeueInputBuffer failed"; | |
451 media_task_runner_->PostTask(FROM_HERE, internal_error_cb_); | |
452 return false; | |
453 | |
454 case MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER: | |
455 return true; | |
456 | |
457 default: | |
458 break; | |
459 } | |
460 | |
461 // We got the buffer | |
462 DCHECK_EQ(status, MEDIA_CODEC_OK); | |
463 DCHECK_GE(index, 0); | |
464 | |
465 const AccessUnit* unit = au_info.front_unit; | |
466 DCHECK(unit); | |
467 | |
468 if (unit->is_end_of_stream) { | |
469 DVLOG(1) << class_name() << "::" << __FUNCTION__ << ": QueueEOS"; | |
470 media_codec_bridge_->QueueEOS(index); | |
471 eos_enqueued_ = true; | |
472 return true; | |
473 } | |
474 | |
475 DVLOG(2) << class_name() << ":: QueueInputBuffer pts:" << unit->timestamp; | |
476 | |
477 status = media_codec_bridge_->QueueInputBuffer( | |
478 index, &unit->data[0], unit->data.size(), unit->timestamp); | |
479 | |
480 if (status == MEDIA_CODEC_ERROR) { | |
481 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
482 << ": MEDIA_CODEC_ERROR: QueueInputBuffer failed"; | |
483 media_task_runner_->PostTask(FROM_HERE, internal_error_cb_); | |
484 return false; | |
485 } | |
486 | |
487 // Have successfully queued input buffer, go to next access unit. | |
488 au_queue_.Advance(); | |
489 return true; | |
490 } | |
491 | |
492 // Returns false if there was MediaCodec error. | |
493 bool MediaCodecDecoder::DepleteOutputBufferQueue(bool* eos_encountered) { | |
494 DCHECK(decoder_thread_.task_runner()->BelongsToCurrentThread()); | |
495 | |
496 DVLOG(2) << class_name() << "::" << __FUNCTION__; | |
497 | |
498 int buffer_index = 0; | |
499 size_t offset = 0; | |
500 size_t size = 0; | |
501 base::TimeDelta pts; | |
502 MediaCodecStatus status; | |
503 | |
504 base::TimeDelta timeout = | |
505 base::TimeDelta::FromMilliseconds(kOutputBufferTimeout); | |
506 | |
507 // Extract all output buffers that are available. | |
508 // Usually there will be only one, but sometimes it is preceeded by | |
509 // MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED or MEDIA_CODEC_OUTPUT_FORMAT_CHANGED. | |
510 do { | |
511 status = media_codec_bridge_->DequeueOutputBuffer( | |
512 timeout, &buffer_index, &offset, &size, &pts, eos_encountered, nullptr); | |
513 | |
514 // Reset the timeout to 0 for the subsequent DequeueOutputBuffer() calls | |
515 // to quickly break the loop after we got all currently available buffers. | |
516 timeout = base::TimeDelta::FromMilliseconds(0); | |
517 | |
518 switch (status) { | |
519 case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED: | |
520 // Output buffers are replaced in MediaCodecBridge, nothing to do. | |
521 break; | |
522 | |
523 case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED: | |
524 DVLOG(2) << class_name() << "::" << __FUNCTION__ | |
525 << " MEDIA_CODEC_OUTPUT_FORMAT_CHANGED"; | |
526 OnOutputFormatChanged(); | |
527 break; | |
528 | |
529 case MEDIA_CODEC_OK: | |
530 // We got the decoded frame | |
531 Render(buffer_index, size, true, pts, *eos_encountered); | |
532 break; | |
533 | |
534 case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER: | |
535 // Nothing to do. | |
536 break; | |
537 | |
538 case MEDIA_CODEC_ERROR: | |
539 DVLOG(0) << class_name() << "::" << __FUNCTION__ | |
540 << ": MEDIA_CODEC_ERROR from DequeueOutputBuffer"; | |
541 media_task_runner_->PostTask(FROM_HERE, internal_error_cb_); | |
542 break; | |
543 | |
544 default: | |
545 NOTREACHED(); | |
546 break; | |
547 } | |
548 | |
549 } while (status != MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER && | |
550 status != MEDIA_CODEC_ERROR && !*eos_encountered); | |
551 | |
552 return status != MEDIA_CODEC_ERROR; | |
553 } | |
554 | |
555 MediaCodecDecoder::DecoderState MediaCodecDecoder::GetState() const { | |
556 base::AutoLock lock(state_lock_); | |
557 return state_; | |
558 } | |
559 | |
560 void MediaCodecDecoder::SetState(DecoderState state) { | |
561 DVLOG(1) << class_name() << "::" << __FUNCTION__ << " " << state; | |
562 | |
563 base::AutoLock lock(state_lock_); | |
564 state_ = state; | |
565 } | |
566 | |
567 #undef RETURN_STRING | |
568 #define RETURN_STRING(x) \ | |
569 case x: \ | |
570 return #x; | |
571 | |
572 const char* MediaCodecDecoder::AsString(DecoderState state) { | |
573 switch (state) { | |
574 RETURN_STRING(kStopped); | |
575 RETURN_STRING(kPrefetching); | |
576 RETURN_STRING(kPrefetched); | |
577 RETURN_STRING(kRunning); | |
578 RETURN_STRING(kStopping); | |
579 RETURN_STRING(kError); | |
580 default: | |
581 return "Unknown DecoderState"; | |
582 } | |
583 } | |
584 | |
585 #undef RETURN_STRING | |
586 | |
587 } // namespace media | |
OLD | NEW |