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 "media/filters/android_audio_decoder.h" | |
6 | |
7 #include "base/android/build_info.h" | |
8 #include "base/bind.h" | |
9 #include "base/callback_helpers.h" | |
10 #include "base/logging.h" | |
11 #include "base/thread_task_runner_handle.h" | |
12 | |
13 #include "media/base/android/sdk_media_codec_bridge.h" | |
14 #include "media/base/audio_buffer.h" | |
15 #include "media/base/bind_to_current_loop.h" | |
16 | |
17 namespace media { | |
18 | |
19 // Android MediaCodec can only output 16bit PCM audio. | |
20 const int kBytesPerOutputSample = 2; | |
21 | |
22 inline const base::TimeDelta DecodePollDelay() { | |
23 return base::TimeDelta::FromMilliseconds(10); | |
24 } | |
25 | |
26 inline const base::TimeDelta NoWaitTimeOut() { | |
27 return base::TimeDelta::FromMicroseconds(0); | |
28 } | |
29 | |
30 inline const base::TimeDelta IdleTimerTimeOut() { | |
31 return base::TimeDelta::FromSeconds(1); | |
32 } | |
33 | |
34 AndroidAudioDecoder::AndroidAudioDecoder() | |
35 : task_runner_(base::ThreadTaskRunnerHandle::Get()), | |
36 state_(kStateUninitialized), | |
37 channel_count_(0), | |
38 bytes_per_frame_(0), | |
39 cdm_registration_id_(0), | |
40 pending_input_buf_index_(-1), | |
41 weak_factory_(this) { | |
42 DVLOG(1) << __FUNCTION__; | |
43 } | |
44 | |
45 AndroidAudioDecoder::~AndroidAudioDecoder() { | |
46 DVLOG(1) << __FUNCTION__; | |
47 | |
48 if (!set_cdm_ready_cb_.is_null()) | |
49 base::ResetAndReturn(&set_cdm_ready_cb_).Run(CdmReadyCB()); | |
50 if (!cdm_attached_cb_.is_null()) | |
51 base::ResetAndReturn(&cdm_attached_cb_).Run(false); | |
52 if (!init_cb_.is_null()) | |
53 base::ResetAndReturn(&init_cb_).Run(false); | |
54 | |
55 media_codec_.reset(); | |
56 | |
57 // Report kAborted status for pending EOS and all frames in the input queue. | |
58 if (!eos_decode_cb_.is_null()) | |
59 base::ResetAndReturn(&eos_decode_cb_).Run(kAborted); | |
60 | |
61 for (const auto& entry : input_queue_) { | |
62 entry.second.Run(kAborted); | |
63 } | |
64 } | |
65 | |
66 std::string AndroidAudioDecoder::GetDisplayName() const { | |
67 return "AndroidAudioDecoder"; | |
68 } | |
69 | |
70 void AndroidAudioDecoder::Initialize(const AudioDecoderConfig& config, | |
71 const SetCdmReadyCB& set_cdm_ready_cb, | |
72 const InitCB& init_cb, | |
73 const OutputCB& output_cb) { | |
74 DVLOG(1) << __FUNCTION__ << ": " << config.AsHumanReadableString(); | |
75 | |
76 InitCB bound_init_cb = BindToCurrentLoop(init_cb); | |
77 | |
78 // Keep this consistent with AudioCodecBridge. | |
79 const bool is_codec_supported = config.codec() == kCodecVorbis || | |
80 config.codec() == kCodecAAC || | |
81 config.codec() == kCodecOpus; | |
82 if (!is_codec_supported) { | |
83 DVLOG(1) << "Unsuported codec " << GetCodecName(config.codec()); | |
84 bound_init_cb.Run(false); | |
85 return; | |
86 } | |
87 | |
88 config_ = config; | |
89 init_cb_ = bound_init_cb; | |
90 output_cb_ = BindToCurrentLoop(output_cb); | |
91 | |
92 // The following derived parameters are frequently used. | |
93 channel_count_ = ChannelLayoutToChannelCount(config_.channel_layout()); | |
94 bytes_per_frame_ = kBytesPerOutputSample * channel_count_; | |
95 | |
96 if (config.is_encrypted()) { | |
97 // Delay configuration until we get MediaCrypto object. | |
98 set_cdm_ready_cb_ = set_cdm_ready_cb; | |
99 set_cdm_ready_cb_.Run(BindToCurrentLoop( | |
100 base::Bind(&AndroidAudioDecoder::SetCdm, weak_factory_.GetWeakPtr()))); | |
101 SetState(kStateWaitingForCDM); | |
102 return; | |
103 } | |
104 | |
105 const bool success = ConfigureMediaCodec(); | |
106 | |
107 SetState(success ? kStateReady : kStateUninitialized); | |
108 base::ResetAndReturn(&init_cb_).Run(success); | |
109 } | |
110 | |
111 void AndroidAudioDecoder::Decode(const scoped_refptr<DecoderBuffer>& buffer, | |
112 const DecodeCB& decode_cb) { | |
113 DecodeCB bound_decode_cb = BindToCurrentLoop(decode_cb); | |
114 | |
115 if (state_ == kStateError) { | |
116 DVLOG(2) << __FUNCTION__ << " " << buffer->AsShortString() | |
117 << ": Error state, dropping buffer"; | |
118 bound_decode_cb.Run(kDecodeError); | |
119 return; | |
120 } | |
121 | |
122 DVLOG(2) << __FUNCTION__ << " " << buffer->AsShortString(); | |
123 | |
124 input_queue_.push_back(std::make_pair(buffer, bound_decode_cb)); | |
125 | |
126 DoIOTask(); | |
127 } | |
128 | |
129 void AndroidAudioDecoder::Reset(const base::Closure& closure) { | |
130 DVLOG(1) << __FUNCTION__; | |
131 | |
132 io_timer_.Stop(); | |
133 | |
134 if (!eos_decode_cb_.is_null()) | |
135 base::ResetAndReturn(&eos_decode_cb_).Run(kAborted); | |
136 | |
137 // Report kAborted status for all frames in the input queue. | |
138 for (const auto& entry : input_queue_) { | |
139 entry.second.Run(kAborted); | |
140 } | |
141 input_queue_.clear(); | |
142 | |
143 // Flush if we can, otherwise completely recreate and reconfigure the codec. | |
144 // Prior to JB-MR2, flush() had several bugs (b/8125974, b/8347958). | |
145 bool success = false; | |
146 if (state_ != kStateError && state_ != kStateDrained && | |
147 base::android::BuildInfo::GetInstance()->sdk_int() < 18) { | |
148 // media_codec_->Reset() calls MediaCodec.flush(). | |
149 success = (media_codec_->Reset() == MEDIA_CODEC_OK); | |
150 } | |
151 | |
152 if (!success) { | |
153 media_codec_.reset(); | |
154 success = ConfigureMediaCodec(); | |
155 } | |
156 | |
157 SetState(success ? kStateReady : kStateError); | |
158 | |
159 task_runner_->PostTask(FROM_HERE, closure); | |
160 } | |
161 | |
162 void AndroidAudioDecoder::SetCdm(CdmContext* cdm_context, | |
163 const CdmAttachedCB& cdm_attached_cb) { | |
164 DVLOG(1) << __FUNCTION__; | |
165 | |
166 DCHECK(!init_cb_.is_null()); | |
167 DCHECK(!set_cdm_ready_cb_.is_null()); | |
168 set_cdm_ready_cb_.Reset(); | |
169 | |
170 if (!cdm_context || cdm_context->GetCdmId() == CdmContext::kInvalidCdmId) { | |
171 DVLOG(1) << __FUNCTION__ << ": CDM ID not available."; | |
172 cdm_attached_cb.Run(false); | |
173 base::ResetAndReturn(&init_cb_).Run(false); | |
174 SetState(kStateUninitialized); | |
175 return; | |
176 } | |
177 | |
178 if (cdm_) { | |
179 NOTREACHED() << "We do not support resetting CDM."; | |
180 cdm_attached_cb.Run(false); | |
181 base::ResetAndReturn(&init_cb_).Run(false); | |
182 SetState(kStateUninitialized); | |
183 return; | |
184 } | |
185 | |
186 // TODO(timav): get |cdm_| from cdm_context->GetCdmId(). | |
187 | |
188 // #define CDM_IN_ANDROID_AUDIO_DECODE | |
189 #if !defined(CDM_IN_ANDROID_AUDIO_DECODER) | |
190 NOTIMPLEMENTED(); | |
191 cdm_attached_cb.Run(false); | |
192 base::ResetAndReturn(&init_cb_).Run(false); | |
193 SetState(kStateUninitialized); | |
194 #else | |
195 // cdm_ = media::MojoCdmService::GetCdm(cdm_id); | |
196 // DCHECK(cdm_); | |
197 | |
198 // On Android platform the MediaKeys will be its subclass MediaDrmBridge. | |
199 MediaDrmBridge* drm_bridge = static_cast<MediaDrmBridge*>(cdm_.get()); | |
200 | |
201 // Register CDM callbacks. The callbacks registered will be posted back to | |
202 // this thread via BindToCurrentLoop. | |
203 | |
204 // Since |this| holds a reference to the |cdm_|, by the time the CDM is | |
205 // destructed, UnregisterPlayer() must have been called and |this| has been | |
206 // destructed as well. So the |cdm_unset_cb| will never have a chance to be | |
207 // called. | |
208 // TODO(xhwang): Remove |cdm_unset_cb| after it's not used on all platforms. | |
209 cdm_registration_id_ = drm_bridge->RegisterPlayer( | |
210 BindToCurrentLoop(base::Bind(&AndroidAudioDecoder::OnKeyAdded, | |
211 weak_factory_.GetWeakPtr())), | |
212 base::Bind(&base::DoNothing)); | |
213 | |
214 drm_bridge->SetMediaCryptoReadyCB(BindToCurrentLoop(base::Bind( | |
215 &AndroidAudioDecoder::OnMediaCryptoReady, weak_factory_.GetWeakPtr()))); | |
216 | |
217 // Postpone cdm_attached_cb.Run() call till CreateMediaCodec() which we can do | |
218 // after OnMediaCryptoReady(). | |
219 cdm_attached_cb_ = cdm_attached_cb; | |
220 SetState(kStateWaitingForCrypto); | |
221 #endif | |
222 } | |
223 | |
224 void AndroidAudioDecoder::OnMediaCryptoReady( | |
225 media::MediaDrmBridge::JavaObjectPtr media_crypto) { | |
226 DVLOG(1) << __FUNCTION__; | |
227 | |
228 if (!media_crypto) { | |
229 LOG(ERROR) << "MediaCrypto is not available, can't play encrypted stream."; | |
230 base::ResetAndReturn(&cdm_attached_cb_).Run(false); | |
231 base::ResetAndReturn(&init_cb_).Run(false); | |
232 SetState(kStateUninitialized); | |
233 return; | |
234 } | |
235 | |
236 DCHECK(!media_crypto->is_null()); | |
237 | |
238 // We assume this is a part of the initialization process, thus MediaCodec | |
239 // is not created yet. | |
240 DCHECK(!media_codec_); | |
241 | |
242 media_crypto_ = std::move(media_crypto); | |
243 | |
244 // After receiving |media_crypto_| we can configure MediaCodec. | |
245 const bool success = ConfigureMediaCodec(); | |
246 | |
247 SetState(success ? kStateReady : kStateUninitialized); | |
248 | |
249 base::ResetAndReturn(&cdm_attached_cb_).Run(success); | |
250 base::ResetAndReturn(&init_cb_).Run(success); | |
251 } | |
252 | |
253 void AndroidAudioDecoder::OnKeyAdded() { | |
254 DVLOG(1) << __FUNCTION__; | |
255 | |
256 if (state_ == kStateWaitingForKey) | |
257 SetState(kStateReady); | |
258 | |
259 DoIOTask(); | |
260 } | |
261 | |
262 void AndroidAudioDecoder::DoIOTask() { | |
263 if (state_ == kStateError) | |
264 return; | |
265 | |
266 const bool did_input = QueueInput(); | |
267 const bool did_output = DequeueOutput(); | |
268 | |
269 ManageTimer(did_input || did_output); | |
270 } | |
271 | |
272 bool AndroidAudioDecoder::QueueInput() { | |
273 DVLOG(2) << __FUNCTION__; | |
274 | |
275 if (input_queue_.empty()) | |
276 return false; | |
277 | |
278 if (state_ == kStateWaitingForKey) | |
279 return false; | |
280 | |
281 if (state_ == kStateDraining) | |
282 return false; | |
283 | |
284 scoped_refptr<DecoderBuffer> decoder_buffer = input_queue_.front().first; | |
285 const DecodeCB& decode_cb = input_queue_.front().second; | |
286 | |
287 int input_buf_index = pending_input_buf_index_; | |
288 | |
289 // Do not dequeue a new input buffer if we failed with MEDIA_CODEC_NO_KEY. | |
290 // That status does not return the input buffer back to the pool of | |
291 // available input buffers. We have to reuse it in QueueSecureInputBuffer(). | |
292 if (input_buf_index == -1) { | |
293 media::MediaCodecStatus status = | |
294 media_codec_->DequeueInputBuffer(NoWaitTimeOut(), &input_buf_index); | |
295 switch (status) { | |
296 case media::MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER: | |
297 return false; | |
298 case media::MEDIA_CODEC_ERROR: | |
299 DVLOG(1) << __FUNCTION__ << ": MEDIA_CODEC_ERROR"; | |
300 SetState(kStateError); | |
301 | |
302 // Report an error to the pipeline. | |
303 decode_cb.Run(kDecodeError); | |
304 return false; | |
305 case media::MEDIA_CODEC_OK: | |
306 break; | |
307 default: | |
308 NOTREACHED() << "Unknown DequeueInputBuffer status " << status; | |
309 return false; | |
310 } | |
311 } | |
312 | |
313 DCHECK_NE(input_buf_index, -1); | |
314 | |
315 if (decoder_buffer->end_of_stream()) { | |
316 media_codec_->QueueEOS(input_buf_index); | |
317 | |
318 // After queueing EOS we need to flush decoder, i.e. receive this EOS at | |
319 // the output, and then call corresponding decoder_cb. | |
320 eos_decode_cb_ = decode_cb; | |
321 input_queue_.pop_front(); | |
322 SetState(kStateDraining); | |
323 return true; | |
324 } | |
325 | |
326 // Make sure our DecoderBuffer is not EOS, otherwise we can't call any method. | |
327 DCHECK(!decoder_buffer->end_of_stream()); | |
328 | |
329 media::MediaCodecStatus status; | |
330 const DecryptConfig* decrypt_config = decoder_buffer->decrypt_config(); | |
331 if (decrypt_config) { | |
332 // If pending_input_buf_index_ != -1 the input buffer is already filled up, | |
333 // no need to copy it again. | |
334 const uint8_t* memory = | |
335 (pending_input_buf_index_ == -1) ? decoder_buffer->data() : nullptr; | |
336 | |
337 status = media_codec_->QueueSecureInputBuffer( | |
338 input_buf_index, memory, decoder_buffer->data_size(), | |
339 decrypt_config->key_id(), decrypt_config->iv(), | |
340 decrypt_config->subsamples(), decoder_buffer->timestamp()); | |
341 | |
342 DVLOG(2) << __FUNCTION__ | |
343 << ": QueueInputBuffer: pts:" << decoder_buffer->timestamp() | |
344 << " status:" << status; | |
345 } else { | |
346 status = media_codec_->QueueInputBuffer( | |
347 input_buf_index, decoder_buffer->data(), decoder_buffer->data_size(), | |
348 decoder_buffer->timestamp()); | |
349 DVLOG(2) << __FUNCTION__ | |
350 << ": QueueSecureInputBuffer: pts:" << decoder_buffer->timestamp() | |
351 << " status:" << status; | |
352 } | |
353 | |
354 if (status == media::MEDIA_CODEC_NO_KEY) { | |
355 // Keep trying to enqueue the same input buffer. | |
356 // The buffer is owned by us (not the MediaCodec) and is filled with data. | |
357 DVLOG(1) << "QueueSecureInputBuffer failed: MEDIA_CODEC_NO_KEY"; | |
358 pending_input_buf_index_ = input_buf_index; | |
359 SetState(kStateWaitingForKey); | |
360 return false; | |
361 } | |
362 | |
363 pending_input_buf_index_ = -1; | |
364 | |
365 // Although audio_decoder.h says "Once the buffer is decoded the decoder calls | |
366 // |decode_cb|", we call |decode_cb| when the buffer is accepted by | |
367 // MediaCodec, not when it is completely decoded. It seems consistent to what | |
368 // other decoders do. | |
369 decode_cb.Run(kOk); | |
370 input_queue_.pop_front(); | |
371 return true; | |
372 } | |
373 | |
374 bool AndroidAudioDecoder::DequeueOutput() { | |
liberato (no reviews please)
2016/02/01 15:18:06
DequeueOutput looks like it's an improvement over
| |
375 DVLOG(2) << __FUNCTION__; | |
376 | |
377 DCHECK(media_codec_); | |
378 | |
379 MediaCodecStatus status; | |
380 OutputBufferInfo out; | |
liberato (no reviews please)
2016/02/01 15:18:06
OutputBufferInfo: good idea for avda.
| |
381 bool work_done = false; | |
382 do { | |
383 status = media_codec_->DequeueOutputBuffer(NoWaitTimeOut(), &out.buf_index, | |
384 &out.offset, &out.size, &out.pts, | |
385 &out.is_eos, &out.is_key_frame); | |
386 | |
387 switch (status) { | |
388 case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED: | |
389 // Output buffers are replaced in MediaCodecBridge, nothing to do. | |
390 DVLOG(2) << __FUNCTION__ << " MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED"; | |
391 work_done = true; | |
392 break; | |
393 | |
394 case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED: | |
395 DVLOG(2) << __FUNCTION__ << " MEDIA_CODEC_OUTPUT_FORMAT_CHANGED"; | |
396 OnOutputFormatChanged(); | |
397 work_done = true; | |
398 break; | |
399 | |
400 case MEDIA_CODEC_OK: | |
401 // We got the decoded frame. | |
402 if (out.is_eos) { | |
403 media_codec_->ReleaseOutputBuffer(out.buf_index, false); | |
404 | |
405 DCHECK_EQ(state_, kStateDraining); | |
406 DCHECK(!eos_decode_cb_.is_null()); | |
407 | |
408 // Report the end of decoding for EOS DecoderBuffer. | |
409 base::ResetAndReturn(&eos_decode_cb_).Run(kOk); | |
410 | |
411 // media_decoder_job.cc says: once output EOS has occurred, we should | |
412 // not be asked to decode again. | |
413 // Have a separate state for this case. | |
414 SetState(kStateDrained); | |
415 } else { | |
416 // Process the real decoded frame. | |
417 OnDecodedFrame(&out); | |
418 } | |
419 | |
420 work_done = true; | |
421 break; | |
422 | |
423 case MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER: | |
424 // Nothing to do. | |
425 DVLOG(2) << __FUNCTION__ << " MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER"; | |
426 break; | |
427 | |
428 case MEDIA_CODEC_ERROR: | |
429 DVLOG(0) << __FUNCTION__ | |
430 << ": MEDIA_CODEC_ERROR from DequeueOutputBuffer"; | |
431 | |
432 // Next Decode() will report the error to the pipeline. | |
433 SetState(kStateError); | |
434 break; | |
435 | |
436 default: | |
437 NOTREACHED(); | |
438 break; | |
439 } | |
440 } while (status != MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER && | |
441 status != MEDIA_CODEC_ERROR && !out.is_eos); | |
442 | |
443 return work_done; | |
444 } | |
445 | |
446 void AndroidAudioDecoder::ManageTimer(bool did_work) { | |
447 bool should_be_running = true; | |
448 | |
449 base::TimeTicks now = base::TimeTicks::Now(); | |
450 if (!did_work) { | |
451 // Make sure that we have done work recently enough, else stop the timer. | |
452 if (now - most_recent_work_ > IdleTimerTimeOut()) | |
453 should_be_running = false; | |
454 } else { | |
455 most_recent_work_ = now; | |
456 } | |
457 | |
458 if (should_be_running && !io_timer_.IsRunning()) { | |
459 io_timer_.Start(FROM_HERE, DecodePollDelay(), this, | |
460 &AndroidAudioDecoder::DoIOTask); | |
461 } else if (!should_be_running && io_timer_.IsRunning()) { | |
462 io_timer_.Stop(); | |
463 } | |
464 } | |
465 | |
466 void AndroidAudioDecoder::SetState(State new_state) { | |
liberato (no reviews please)
2016/02/01 15:18:06
this is an improvement over avda.
| |
467 DVLOG(1) << __FUNCTION__ << ": " << AsString(state_) << "->" | |
468 << AsString(new_state); | |
469 state_ = new_state; | |
470 } | |
471 | |
472 bool AndroidAudioDecoder::ConfigureMediaCodec() { | |
473 DVLOG(1) << __FUNCTION__; | |
474 | |
475 media_codec_.reset(AudioCodecBridge::Create(config_.codec())); | |
476 if (!media_codec_) { | |
477 DVLOG(0) << __FUNCTION__ << " failed: cannot create AudioCodecBridge"; | |
478 return false; | |
479 } | |
480 | |
481 AudioCodecBridge* audio_codec_bridge = | |
482 static_cast<AudioCodecBridge*>(media_codec_.get()); | |
483 DCHECK(audio_codec_bridge); | |
484 | |
485 jobject media_crypto = media_crypto_ ? media_crypto_->obj() : nullptr; | |
486 | |
487 const bool play_audio = false; // Do not create AudioTrack object. | |
488 if (!audio_codec_bridge->ConfigureAndStart( | |
489 config_.codec(), config_.samples_per_second(), channel_count_, | |
490 &config_.extra_data()[0], config_.extra_data().size(), | |
491 config_.codec_delay(), config_.seek_preroll().InMicroseconds() * 1000, | |
492 play_audio, media_crypto)) { | |
493 DVLOG(0) << __FUNCTION__ << " failed: cannot start audio codec"; | |
494 media_codec_.reset(); | |
495 return false; | |
496 } | |
497 | |
498 return true; | |
499 } | |
500 | |
501 void AndroidAudioDecoder::OnDecodedFrame(const OutputBufferInfo* out) { | |
502 DCHECK(out); | |
503 DCHECK_NE(out->size, 0U); | |
504 DCHECK(media_codec_); | |
505 | |
506 DVLOG(2) << __FUNCTION__ << " pts:" << out->pts; | |
507 | |
508 // Create AudioOutput buffer based on configuration. | |
509 const size_t frame_count = out->size / bytes_per_frame_; | |
510 | |
511 scoped_refptr<AudioBuffer> audio_buffer = AudioBuffer::CreateBuffer( | |
512 kSampleFormatS16, config_.channel_layout(), channel_count_, | |
513 config_.samples_per_second(), frame_count); | |
514 | |
515 // Copy data into AudioBuffer. | |
516 media_codec_->CopyFromOutputBuffer(out->buf_index, out->offset, | |
517 audio_buffer->interleaved_data(), | |
518 audio_buffer->interleaved_data_size()); | |
519 | |
520 // Release MediaCodec output buffer. | |
521 media_codec_->ReleaseOutputBuffer(out->buf_index, false); | |
522 | |
523 // Call the output_cb_. | |
524 output_cb_.Run(audio_buffer); | |
525 } | |
526 | |
527 void AndroidAudioDecoder::OnOutputFormatChanged() { | |
528 DVLOG(0) << __FUNCTION__ << ": not implemented, going to error state"; | |
529 | |
530 SetState(kStateError); | |
531 } | |
532 | |
533 #undef RETURN_STRING | |
534 #define RETURN_STRING(x) \ | |
535 case x: \ | |
536 return #x; | |
537 | |
538 // static | |
539 const char* AndroidAudioDecoder::AsString(State state) { | |
540 switch (state) { | |
541 RETURN_STRING(kStateUninitialized); | |
542 RETURN_STRING(kStateWaitingForCDM); | |
543 RETURN_STRING(kStateWaitingForCrypto); | |
544 RETURN_STRING(kStateReady); | |
545 RETURN_STRING(kStateWaitingForKey); | |
546 RETURN_STRING(kStateDraining); | |
547 RETURN_STRING(kStateDrained); | |
548 RETURN_STRING(kStateError); | |
549 } | |
550 return nullptr; // crash early | |
551 } | |
552 | |
553 #undef RETURN_STRING | |
554 | |
555 } // namespace media | |
OLD | NEW |