1 // Copyright (c) 2012 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/gpu_video_decoder.h" 6 7 #include <algorithm> 8 9 #include "base/bind.h" 10 #include "base/callback_helpers.h" 11 #include "base/cpu.h" 12 #include "base/message_loop/message_loop.h" 13 #include "base/stl_util.h" 14 #include "base/task_runner_util.h" 15 #include "media/base/bind_to_loop.h" 16 #include "media/base/decoder_buffer.h" 17 #include "media/base/pipeline.h" 18 #include "media/base/pipeline_status.h" 19 #include "media/base/video_decoder_config.h" 20 #include "media/filters/gpu_video_decoder_factories.h" 21 22 namespace media { 23 24 // Maximum number of concurrent VDA::Decode() operations GVD will maintain. 25 // Higher values allow better pipelining in the GPU, but also require more 26 // resources. 27 enum { kMaxInFlightDecodes = 4 }; 28 29 // Size of shared-memory segments we allocate. Since we reuse them we let them 30 // be on the beefy side. 31 static const size_t kSharedMemorySegmentBytes = 100 << 10; 32 33 GpuVideoDecoder::SHMBuffer::SHMBuffer(base::SharedMemory* m, size_t s) 34 : shm(m), size(s) { 35 } 36 37 GpuVideoDecoder::SHMBuffer::~SHMBuffer() {} 38 39 GpuVideoDecoder::BufferPair::BufferPair( 40 SHMBuffer* s, const scoped_refptr<DecoderBuffer>& b) 41 : shm_buffer(s), buffer(b) { 42 } 43 44 GpuVideoDecoder::BufferPair::~BufferPair() {} 45 46 GpuVideoDecoder::BufferData::BufferData( 47 int32 bbid, base::TimeDelta ts, const gfx::Rect& vr, const gfx::Size& ns) 48 : bitstream_buffer_id(bbid), timestamp(ts), visible_rect(vr), 49 natural_size(ns) { 50 } 51 52 GpuVideoDecoder::BufferData::~BufferData() {} 53 54 GpuVideoDecoder::GpuVideoDecoder( 55 const scoped_refptr<GpuVideoDecoderFactories>& factories) 56 : needs_bitstream_conversion_(false), 57 gvd_loop_proxy_(factories->GetMessageLoop()), 58 weak_factory_(this), 59 factories_(factories), 60 state_(kNormal), 61 decoder_texture_target_(0), 62 next_picture_buffer_id_(0), 63 next_bitstream_buffer_id_(0), 64 available_pictures_(0) { 65 DCHECK(factories_.get()); 66 } 67 68 void GpuVideoDecoder::Reset(const base::Closure& closure) { 69 DVLOG(3) << "Reset()"; 70 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 71 72 if (state_ == kDrainingDecoder && !factories_->IsAborted()) { 73 gvd_loop_proxy_->PostTask(FROM_HERE, base::Bind( 74 &GpuVideoDecoder::Reset, weak_this_, closure)); 75 // NOTE: if we're deferring Reset() until a Flush() completes, return 76 // queued pictures to the VDA so they can be used to finish that Flush(). 77 if (pending_decode_cb_.is_null()) 78 ready_video_frames_.clear(); 79 return; 80 } 81 82 // Throw away any already-decoded, not-yet-delivered frames. 83 ready_video_frames_.clear(); 84 85 if (!vda_) { 86 gvd_loop_proxy_->PostTask(FROM_HERE, closure); 87 return; 88 } 89 90 if (!pending_decode_cb_.is_null()) 91 EnqueueFrameAndTriggerFrameDelivery(VideoFrame::CreateEmptyFrame()); 92 93 DCHECK(pending_reset_cb_.is_null()); 94 pending_reset_cb_ = BindToCurrentLoop(closure); 95 96 vda_->Reset(); 97 } 98 99 void GpuVideoDecoder::Stop(const base::Closure& closure) { 100 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 101 if (vda_) 102 DestroyVDA(); 103 if (!pending_decode_cb_.is_null()) 104 EnqueueFrameAndTriggerFrameDelivery(VideoFrame::CreateEmptyFrame()); 105 if (!pending_reset_cb_.is_null()) 106 base::ResetAndReturn(&pending_reset_cb_).Run(); 107 BindToCurrentLoop(closure).Run(); 108 } 109 110 static bool IsCodedSizeSupported(const gfx::Size& coded_size) { 111 // Only non-Windows, Ivy Bridge+ platforms can support more than 1920x1080. 112 // We test against 1088 to account for 16x16 macroblocks. 113 if (coded_size.width() <= 1920 && coded_size.height() <= 1088) 114 return true; 115 116 base::CPU cpu; 117 bool hw_large_video_support = 118 (cpu.vendor_name() == "GenuineIntel") && cpu.model() >= 58; 119 bool os_large_video_support = true; 120 #if defined(OS_WIN) 121 os_large_video_support = false; 122 #endif 123 return os_large_video_support && hw_large_video_support; 124 } 125 126 void GpuVideoDecoder::Initialize(const VideoDecoderConfig& config, 127 const PipelineStatusCB& orig_status_cb) { 128 DVLOG(3) << "Initialize()"; 129 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 130 DCHECK(config.IsValidConfig()); 131 DCHECK(!config.is_encrypted()); 132 133 weak_this_ = weak_factory_.GetWeakPtr(); 134 135 PipelineStatusCB status_cb = CreateUMAReportingPipelineCB( 136 "Media.GpuVideoDecoderInitializeStatus", 137 BindToCurrentLoop(orig_status_cb)); 138 139 bool previously_initialized = config_.IsValidConfig(); 140 #if !defined(OS_CHROMEOS) 141 if (previously_initialized) { 142 // TODO(xhwang): Make GpuVideoDecoder reinitializable. 143 // See http://crbug.com/233608 144 DVLOG(1) << "GpuVideoDecoder reinitialization not supported."; 145 status_cb.Run(DECODER_ERROR_NOT_SUPPORTED); 146 return; 147 } 148 #endif 149 DVLOG(1) << "(Re)initializing GVD with config: " 150 << config.AsHumanReadableString(); 151 152 // TODO(posciak): destroy and create a new VDA on codec/profile change 153 // (http://crbug.com/260224). 154 if (previously_initialized && (config_.profile() != config.profile())) { 155 DVLOG(1) << "Codec or profile changed, cannot reinitialize."; 156 status_cb.Run(DECODER_ERROR_NOT_SUPPORTED); 157 return; 158 } 159 160 if (!IsCodedSizeSupported(config.coded_size())) { 161 status_cb.Run(DECODER_ERROR_NOT_SUPPORTED); 162 return; 163 } 164 165 config_ = config; 166 needs_bitstream_conversion_ = (config.codec() == kCodecH264); 167 168 if (previously_initialized) { 169 // Reinitialization with a different config (but same codec and profile). 170 // VDA should handle it by detecting this in-stream by itself, 171 // no need to notify it. 172 status_cb.Run(PIPELINE_OK); 173 return; 174 } 175 176 vda_.reset(factories_->CreateVideoDecodeAccelerator(config.profile(), this)); 177 if (!vda_) { 178 status_cb.Run(DECODER_ERROR_NOT_SUPPORTED); 179 return; 180 } 181 182 DVLOG(3) << "GpuVideoDecoder::Initialize() succeeded."; 183 status_cb.Run(PIPELINE_OK); 184 } 185 186 void GpuVideoDecoder::DestroyTextures() { 187 std::map<int32, PictureBuffer>::iterator it; 188 189 for (it = assigned_picture_buffers_.begin(); 190 it != assigned_picture_buffers_.end(); ++it) { 191 factories_->DeleteTexture(it->second.texture_id()); 192 } 193 assigned_picture_buffers_.clear(); 194 195 for (it = dismissed_picture_buffers_.begin(); 196 it != dismissed_picture_buffers_.end(); ++it) { 197 factories_->DeleteTexture(it->second.texture_id()); 198 } 199 dismissed_picture_buffers_.clear(); 200 } 201 202 void GpuVideoDecoder::DestroyVDA() { 203 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 204 205 if (vda_) 206 vda_.release()->Destroy(); 207 208 DestroyTextures(); 209 } 210 211 void GpuVideoDecoder::Decode(const scoped_refptr<DecoderBuffer>& buffer, 212 const DecodeCB& decode_cb) { 213 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 214 DCHECK(pending_reset_cb_.is_null()); 215 DCHECK(pending_decode_cb_.is_null()); 216 217 pending_decode_cb_ = BindToCurrentLoop(decode_cb); 218 219 if (state_ == kError || !vda_) { 220 base::ResetAndReturn(&pending_decode_cb_).Run(kDecodeError, NULL); 221 return; 222 } 223 224 switch (state_) { 225 case kDecoderDrained: 226 if (!ready_video_frames_.empty()) { 227 EnqueueFrameAndTriggerFrameDelivery(NULL); 228 return; 229 } 230 state_ = kNormal; 231 // Fall-through. 232 case kNormal: 233 break; 234 case kDrainingDecoder: 235 DCHECK(buffer->end_of_stream()); 236 // Do nothing. Will be satisfied either by a PictureReady or 237 // NotifyFlushDone below. 238 return; 239 case kError: 240 NOTREACHED(); 241 return; 242 } 243 244 if (buffer->end_of_stream()) { 245 if (state_ == kNormal) { 246 state_ = kDrainingDecoder; 247 vda_->Flush(); 248 } 249 return; 250 } 251 252 size_t size = buffer->data_size(); 253 SHMBuffer* shm_buffer = GetSHM(size); 254 if (!shm_buffer) { 255 base::ResetAndReturn(&pending_decode_cb_).Run(kDecodeError, NULL); 256 return; 257 } 258 259 memcpy(shm_buffer->shm->memory(), buffer->data(), size); 260 BitstreamBuffer bitstream_buffer( 261 next_bitstream_buffer_id_, shm_buffer->shm->handle(), size); 262 // Mask against 30 bits, to avoid (undefined) wraparound on signed integer. 263 next_bitstream_buffer_id_ = (next_bitstream_buffer_id_ + 1) & 0x3FFFFFFF; 264 bool inserted = bitstream_buffers_in_decoder_.insert(std::make_pair( 265 bitstream_buffer.id(), BufferPair(shm_buffer, buffer))).second; 266 DCHECK(inserted); 267 RecordBufferData(bitstream_buffer, *buffer.get()); 268 269 vda_->Decode(bitstream_buffer); 270 271 if (!ready_video_frames_.empty()) { 272 EnqueueFrameAndTriggerFrameDelivery(NULL); 273 return; 274 } 275 276 if (CanMoreDecodeWorkBeDone()) 277 base::ResetAndReturn(&pending_decode_cb_).Run(kNotEnoughData, NULL); 278 } 279 280 bool GpuVideoDecoder::CanMoreDecodeWorkBeDone() { 281 return bitstream_buffers_in_decoder_.size() < kMaxInFlightDecodes; 282 } 283 284 void GpuVideoDecoder::RecordBufferData(const BitstreamBuffer& bitstream_buffer, 285 const DecoderBuffer& buffer) { 286 input_buffer_data_.push_front(BufferData(bitstream_buffer.id(), 287 buffer.timestamp(), 288 config_.visible_rect(), 289 config_.natural_size())); 290 // Why this value? Because why not. avformat.h:MAX_REORDER_DELAY is 16, but 291 // that's too small for some pathological B-frame test videos. The cost of 292 // using too-high a value is low (192 bits per extra slot). 293 static const size_t kMaxInputBufferDataSize = 128; 294 // Pop from the back of the list, because that's the oldest and least likely 295 // to be useful in the future data. 296 if (input_buffer_data_.size() > kMaxInputBufferDataSize) 297 input_buffer_data_.pop_back(); 298 } 299 300 void GpuVideoDecoder::GetBufferData(int32 id, base::TimeDelta* timestamp, 301 gfx::Rect* visible_rect, 302 gfx::Size* natural_size) { 303 for (std::list<BufferData>::const_iterator it = 304 input_buffer_data_.begin(); it != input_buffer_data_.end(); 305 ++it) { 306 if (it->bitstream_buffer_id != id) 307 continue; 308 *timestamp = it->timestamp; 309 *visible_rect = it->visible_rect; 310 *natural_size = it->natural_size; 311 return; 312 } 313 NOTREACHED() << "Missing bitstreambuffer id: " << id; 314 } 315 316 bool GpuVideoDecoder::HasAlpha() const { 317 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 318 return true; 319 } 320 321 bool GpuVideoDecoder::NeedsBitstreamConversion() const { 322 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 323 return needs_bitstream_conversion_; 324 } 325 326 bool GpuVideoDecoder::CanReadWithoutStalling() const { 327 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 328 return available_pictures_ > 0 || !ready_video_frames_.empty(); 329 } 330 331 void GpuVideoDecoder::NotifyInitializeDone() { 332 NOTREACHED() << "GpuVideoDecodeAcceleratorHost::Initialize is synchronous!"; 333 } 334 335 void GpuVideoDecoder::ProvidePictureBuffers(uint32 count, 336 const gfx::Size& size, 337 uint32 texture_target) { 338 DVLOG(3) << "ProvidePictureBuffers(" << count << ", " 339 << size.width() << "x" << size.height() << ")"; 340 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 341 342 std::vector<uint32> texture_ids; 343 std::vector<gpu::Mailbox> texture_mailboxes; 344 decoder_texture_target_ = texture_target; 345 // Discards the sync point returned here since PictureReady will imply that 346 // the produce has already happened, and the texture is ready for use. 347 if (!factories_->CreateTextures(count, 348 size, 349 &texture_ids, 350 &texture_mailboxes, 351 decoder_texture_target_)) { 352 NotifyError(VideoDecodeAccelerator::PLATFORM_FAILURE); 353 return; 354 } 355 DCHECK_EQ(count, texture_ids.size()); 356 DCHECK_EQ(count, texture_mailboxes.size()); 357 358 if (!vda_) 359 return; 360 361 std::vector<PictureBuffer> picture_buffers; 362 for (size_t i = 0; i < texture_ids.size(); ++i) { 363 picture_buffers.push_back(PictureBuffer( 364 next_picture_buffer_id_++, size, texture_ids[i], texture_mailboxes[i])); 365 bool inserted = assigned_picture_buffers_.insert(std::make_pair( 366 picture_buffers.back().id(), picture_buffers.back())).second; 367 DCHECK(inserted); 368 } 369 370 available_pictures_ += count; 371 372 vda_->AssignPictureBuffers(picture_buffers); 373 } 374 375 void GpuVideoDecoder::DismissPictureBuffer(int32 id) { 376 DVLOG(3) << "DismissPictureBuffer(" << id << ")"; 377 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 378 379 std::map<int32, PictureBuffer>::iterator it = 380 assigned_picture_buffers_.find(id); 381 if (it == assigned_picture_buffers_.end()) { 382 NOTREACHED() << "Missing picture buffer: " << id; 383 return; 384 } 385 386 PictureBuffer buffer_to_dismiss = it->second; 387 assigned_picture_buffers_.erase(it); 388 389 std::set<int32>::iterator at_display_it = 390 picture_buffers_at_display_.find(id); 391 392 if (at_display_it == picture_buffers_at_display_.end()) { 393 // We can delete the texture immediately as it's not being displayed. 394 factories_->DeleteTexture(buffer_to_dismiss.texture_id()); 395 CHECK_GT(available_pictures_, 0); 396 --available_pictures_; 397 } else { 398 // Texture in display. Postpone deletion until after it's returned to us. 399 bool inserted = dismissed_picture_buffers_.insert(std::make_pair( 400 id, buffer_to_dismiss)).second; 401 DCHECK(inserted); 402 } 403 } 404 405 void GpuVideoDecoder::PictureReady(const media::Picture& picture) { 406 DVLOG(3) << "PictureReady()"; 407 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 408 409 std::map<int32, PictureBuffer>::iterator it = 410 assigned_picture_buffers_.find(picture.picture_buffer_id()); 411 if (it == assigned_picture_buffers_.end()) { 412 NOTREACHED() << "Missing picture buffer: " << picture.picture_buffer_id(); 413 NotifyError(VideoDecodeAccelerator::PLATFORM_FAILURE); 414 return; 415 } 416 const PictureBuffer& pb = it->second; 417 418 // Update frame's timestamp. 419 base::TimeDelta timestamp; 420 gfx::Rect visible_rect; 421 gfx::Size natural_size; 422 GetBufferData(picture.bitstream_buffer_id(), ×tamp, &visible_rect, 423 &natural_size); 424 DCHECK(decoder_texture_target_); 425 426 scoped_refptr<VideoFrame> frame(VideoFrame::WrapNativeTexture( 427 new VideoFrame::MailboxHolder( 428 pb.texture_mailbox(), 429 0, // sync_point 430 BindToCurrentLoop(base::Bind(&GpuVideoDecoder::ReusePictureBuffer, 431 weak_this_, 432 picture.picture_buffer_id()))), 433 decoder_texture_target_, 434 pb.size(), 435 visible_rect, 436 natural_size, 437 timestamp, 438 base::Bind(&GpuVideoDecoderFactories::ReadPixels, 439 factories_, 440 pb.texture_id(), 441 decoder_texture_target_, 442 gfx::Size(visible_rect.width(), visible_rect.height())), 443 base::Closure())); 444 CHECK_GT(available_pictures_, 0); 445 --available_pictures_; 446 bool inserted = 447 picture_buffers_at_display_.insert(picture.picture_buffer_id()).second; 448 DCHECK(inserted); 449 450 EnqueueFrameAndTriggerFrameDelivery(frame); 451 } 452 453 void GpuVideoDecoder::EnqueueFrameAndTriggerFrameDelivery( 454 const scoped_refptr<VideoFrame>& frame) { 455 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 456 457 // During a pending vda->Reset(), we don't accumulate frames. Drop it on the 458 // floor and return. 459 if (!pending_reset_cb_.is_null()) 460 return; 461 462 if (frame.get()) 463 ready_video_frames_.push_back(frame); 464 else 465 DCHECK(!ready_video_frames_.empty()); 466 467 if (pending_decode_cb_.is_null()) 468 return; 469 470 base::ResetAndReturn(&pending_decode_cb_) 471 .Run(kOk, ready_video_frames_.front()); 472 ready_video_frames_.pop_front(); 473 } 474 475 void GpuVideoDecoder::ReusePictureBuffer(int64 picture_buffer_id, 476 uint32 sync_point) { 477 DVLOG(3) << "ReusePictureBuffer(" << picture_buffer_id << ")"; 478 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 479 480 if (!vda_) 481 return; 482 483 CHECK(!picture_buffers_at_display_.empty()); 484 485 size_t num_erased = picture_buffers_at_display_.erase(picture_buffer_id); 486 DCHECK(num_erased); 487 488 std::map<int32, PictureBuffer>::iterator it = 489 assigned_picture_buffers_.find(picture_buffer_id); 490 491 if (it == assigned_picture_buffers_.end()) { 492 // This picture was dismissed while in display, so we postponed deletion. 493 it = dismissed_picture_buffers_.find(picture_buffer_id); 494 DCHECK(it != dismissed_picture_buffers_.end()); 495 factories_->DeleteTexture(it->second.texture_id()); 496 dismissed_picture_buffers_.erase(it); 497 return; 498 } 499 500 factories_->WaitSyncPoint(sync_point); 501 ++available_pictures_; 502 503 vda_->ReusePictureBuffer(picture_buffer_id); 504 } 505 506 GpuVideoDecoder::SHMBuffer* GpuVideoDecoder::GetSHM(size_t min_size) { 507 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 508 if (available_shm_segments_.empty() || 509 available_shm_segments_.back()->size < min_size) { 510 size_t size_to_allocate = std::max(min_size, kSharedMemorySegmentBytes); 511 base::SharedMemory* shm = factories_->CreateSharedMemory(size_to_allocate); 512 // CreateSharedMemory() can return NULL during Shutdown. 513 if (!shm) 514 return NULL; 515 return new SHMBuffer(shm, size_to_allocate); 516 } 517 SHMBuffer* ret = available_shm_segments_.back(); 518 available_shm_segments_.pop_back(); 519 return ret; 520 } 521 522 void GpuVideoDecoder::PutSHM(SHMBuffer* shm_buffer) { 523 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 524 available_shm_segments_.push_back(shm_buffer); 525 } 526 527 void GpuVideoDecoder::NotifyEndOfBitstreamBuffer(int32 id) { 528 DVLOG(3) << "NotifyEndOfBitstreamBuffer(" << id << ")"; 529 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 530 531 std::map<int32, BufferPair>::iterator it = 532 bitstream_buffers_in_decoder_.find(id); 533 if (it == bitstream_buffers_in_decoder_.end()) { 534 NotifyError(VideoDecodeAccelerator::PLATFORM_FAILURE); 535 NOTREACHED() << "Missing bitstream buffer: " << id; 536 return; 537 } 538 539 PutSHM(it->second.shm_buffer); 540 bitstream_buffers_in_decoder_.erase(it); 541 542 if (pending_reset_cb_.is_null() && state_ != kDrainingDecoder && 543 CanMoreDecodeWorkBeDone() && !pending_decode_cb_.is_null()) { 544 base::ResetAndReturn(&pending_decode_cb_).Run(kNotEnoughData, NULL); 545 } 546 } 547 548 GpuVideoDecoder::~GpuVideoDecoder() { 549 DCHECK(!vda_.get()); // Stop should have been already called. 550 DCHECK(pending_decode_cb_.is_null()); 551 for (size_t i = 0; i < available_shm_segments_.size(); ++i) { 552 available_shm_segments_[i]->shm->Close(); 553 delete available_shm_segments_[i]; 554 } 555 available_shm_segments_.clear(); 556 for (std::map<int32, BufferPair>::iterator it = 557 bitstream_buffers_in_decoder_.begin(); 558 it != bitstream_buffers_in_decoder_.end(); ++it) { 559 it->second.shm_buffer->shm->Close(); 560 } 561 bitstream_buffers_in_decoder_.clear(); 562 563 DestroyTextures(); 564 } 565 566 void GpuVideoDecoder::NotifyFlushDone() { 567 DVLOG(3) << "NotifyFlushDone()"; 568 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 569 DCHECK_EQ(state_, kDrainingDecoder); 570 state_ = kDecoderDrained; 571 EnqueueFrameAndTriggerFrameDelivery(VideoFrame::CreateEmptyFrame()); 572 } 573 574 void GpuVideoDecoder::NotifyResetDone() { 575 DVLOG(3) << "NotifyResetDone()"; 576 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 577 DCHECK(ready_video_frames_.empty()); 578 579 // This needs to happen after the Reset() on vda_ is done to ensure pictures 580 // delivered during the reset can find their time data. 581 input_buffer_data_.clear(); 582 583 if (!pending_reset_cb_.is_null()) 584 base::ResetAndReturn(&pending_reset_cb_).Run(); 585 586 if (!pending_decode_cb_.is_null()) 587 EnqueueFrameAndTriggerFrameDelivery(VideoFrame::CreateEmptyFrame()); 588 } 589 590 void GpuVideoDecoder::NotifyError(media::VideoDecodeAccelerator::Error error) { 591 DCHECK(gvd_loop_proxy_->BelongsToCurrentThread()); 592 if (!vda_) 593 return; 594 595 DLOG(ERROR) << "VDA Error: " << error; 596 DestroyVDA(); 597 598 state_ = kError; 599 600 if (!pending_decode_cb_.is_null()) { 601 base::ResetAndReturn(&pending_decode_cb_).Run(kDecodeError, NULL); 602 return; 603 } 604 } 605 606 } // namespace media 607