OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 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 "media/capture/video/mac/video_capture_device_mac.h" | 5 #include "media/capture/video/mac/video_capture_device_mac.h" |
6 | 6 |
7 #include <IOKit/IOCFPlugIn.h> | 7 #include <IOKit/IOCFPlugIn.h> |
8 #include <IOKit/usb/IOUSBLib.h> | 8 #include <IOKit/usb/IOUSBLib.h> |
9 #include <IOKit/usb/USBSpec.h> | 9 #include <IOKit/usb/USBSpec.h> |
10 #include <stddef.h> | 10 #include <stddef.h> |
11 #include <stdint.h> | 11 #include <stdint.h> |
12 | 12 |
13 #include <limits> | 13 #include <limits> |
14 #include <utility> | 14 #include <utility> |
15 | 15 |
16 #include "base/bind.h" | 16 #include "base/bind.h" |
17 #include "base/location.h" | 17 #include "base/location.h" |
18 #include "base/logging.h" | 18 #include "base/logging.h" |
19 #include "base/mac/scoped_ioobject.h" | 19 #include "base/mac/scoped_ioobject.h" |
20 #include "base/mac/scoped_ioplugininterface.h" | 20 #include "base/mac/scoped_ioplugininterface.h" |
21 #include "base/macros.h" | 21 #include "base/macros.h" |
22 #include "base/single_thread_task_runner.h" | 22 #include "base/single_thread_task_runner.h" |
23 #include "base/strings/string_number_conversions.h" | 23 #include "base/strings/string_number_conversions.h" |
24 #include "base/thread_task_runner_handle.h" | 24 #include "base/thread_task_runner_handle.h" |
25 #include "base/time/time.h" | 25 #include "base/time/time.h" |
26 #import "media/base/mac/avfoundation_glue.h" | 26 #import "media/base/mac/avfoundation_glue.h" |
27 #include "media/base/timestamp_constants.h" | 27 #include "media/base/timestamp_constants.h" |
28 #import "media/capture/video/mac/platform_video_capturing_mac.h" | 28 #import "media/capture/video/mac/platform_video_capturing_mac.h" |
29 #import "media/capture/video/mac/video_capture_device_avfoundation_mac.h" | 29 #import "media/capture/video/mac/video_capture_device_avfoundation_mac.h" |
30 #import "media/capture/video/mac/video_capture_device_qtkit_mac.h" | |
31 #include "ui/gfx/geometry/size.h" | 30 #include "ui/gfx/geometry/size.h" |
32 | 31 |
33 @implementation DeviceNameAndTransportType | 32 @implementation DeviceNameAndTransportType |
34 | 33 |
35 - (id)initWithName:(NSString*)deviceName transportType:(int32_t)transportType { | 34 - (id)initWithName:(NSString*)deviceName transportType:(int32_t)transportType { |
36 if (self = [super init]) { | 35 if (self = [super init]) { |
37 deviceName_.reset([deviceName copy]); | 36 deviceName_.reset([deviceName copy]); |
38 transportType_ = transportType; | 37 transportType_ = transportType; |
39 } | 38 } |
40 return self; | 39 return self; |
(...skipping 11 matching lines...) Expand all Loading... |
52 | 51 |
53 namespace media { | 52 namespace media { |
54 | 53 |
55 // Mac specific limits for minimum and maximum frame rate. | 54 // Mac specific limits for minimum and maximum frame rate. |
56 const float kMinFrameRate = 1.0f; | 55 const float kMinFrameRate = 1.0f; |
57 const float kMaxFrameRate = 30.0f; | 56 const float kMaxFrameRate = 30.0f; |
58 | 57 |
59 // In device identifiers, the USB VID and PID are stored in 4 bytes each. | 58 // In device identifiers, the USB VID and PID are stored in 4 bytes each. |
60 const size_t kVidPidSize = 4; | 59 const size_t kVidPidSize = 4; |
61 | 60 |
62 const struct Resolution { | |
63 const int width; | |
64 const int height; | |
65 } kQVGA = {320, 240}, kVGA = {640, 480}, kHD = {1280, 720}; | |
66 | |
67 const struct Resolution* const kWellSupportedResolutions[] = { | |
68 &kQVGA, | |
69 &kVGA, | |
70 &kHD, | |
71 }; | |
72 | |
73 // Rescaling the image to fix the pixel aspect ratio runs the risk of making | |
74 // the aspect ratio worse, if QTKit selects a new source mode with a different | |
75 // shape. This constant ensures that we don't take this risk if the current | |
76 // aspect ratio is tolerable. | |
77 const float kMaxPixelAspectRatio = 1.15; | |
78 | |
79 // The following constants are extracted from the specification "Universal | 61 // The following constants are extracted from the specification "Universal |
80 // Serial Bus Device Class Definition for Video Devices", Rev. 1.1 June 1, 2005. | 62 // Serial Bus Device Class Definition for Video Devices", Rev. 1.1 June 1, 2005. |
81 // http://www.usb.org/developers/devclass_docs/USB_Video_Class_1_1.zip | 63 // http://www.usb.org/developers/devclass_docs/USB_Video_Class_1_1.zip |
82 // CS_INTERFACE: Sec. A.4 "Video Class-Specific Descriptor Types". | 64 // CS_INTERFACE: Sec. A.4 "Video Class-Specific Descriptor Types". |
83 const int kVcCsInterface = 0x24; | 65 const int kVcCsInterface = 0x24; |
84 // VC_PROCESSING_UNIT: Sec. A.5 "Video Class-Specific VC Interface Descriptor | 66 // VC_PROCESSING_UNIT: Sec. A.5 "Video Class-Specific VC Interface Descriptor |
85 // Subtypes". | 67 // Subtypes". |
86 const int kVcProcessingUnit = 0x5; | 68 const int kVcProcessingUnit = 0x5; |
87 // SET_CUR: Sec. A.8 "Video Class-Specific Request Codes". | 69 // SET_CUR: Sec. A.8 "Video Class-Specific Request Codes". |
88 const int kVcRequestCodeSetCur = 0x1; | 70 const int kVcRequestCodeSetCur = 0x1; |
89 // PU_POWER_LINE_FREQUENCY_CONTROL: Sec. A.9.5 "Processing Unit Control | 71 // PU_POWER_LINE_FREQUENCY_CONTROL: Sec. A.9.5 "Processing Unit Control |
90 // Selectors". | 72 // Selectors". |
91 const int kPuPowerLineFrequencyControl = 0x5; | 73 const int kPuPowerLineFrequencyControl = 0x5; |
92 // Sec. 4.2.2.3.5 Power Line Frequency Control. | 74 // Sec. 4.2.2.3.5 Power Line Frequency Control. |
93 const int k50Hz = 1; | 75 const int k50Hz = 1; |
94 const int k60Hz = 2; | 76 const int k60Hz = 2; |
95 const int kPuPowerLineFrequencyControlCommandSize = 1; | 77 const int kPuPowerLineFrequencyControlCommandSize = 1; |
96 | 78 |
97 // Addition to the IOUSB family of structures, with subtype and unit ID. | 79 // Addition to the IOUSB family of structures, with subtype and unit ID. |
98 typedef struct IOUSBInterfaceDescriptor { | 80 typedef struct IOUSBInterfaceDescriptor { |
99 IOUSBDescriptorHeader header; | 81 IOUSBDescriptorHeader header; |
100 UInt8 bDescriptorSubType; | 82 UInt8 bDescriptorSubType; |
101 UInt8 bUnitID; | 83 UInt8 bUnitID; |
102 } IOUSBInterfaceDescriptor; | 84 } IOUSBInterfaceDescriptor; |
103 | 85 |
104 static void GetBestMatchSupportedResolution(gfx::Size* resolution) { | |
105 int min_diff = std::numeric_limits<int32_t>::max(); | |
106 const int desired_area = resolution->GetArea(); | |
107 for (size_t i = 0; i < arraysize(kWellSupportedResolutions); ++i) { | |
108 const int area = kWellSupportedResolutions[i]->width * | |
109 kWellSupportedResolutions[i]->height; | |
110 const int diff = std::abs(desired_area - area); | |
111 if (diff < min_diff) { | |
112 min_diff = diff; | |
113 resolution->SetSize(kWellSupportedResolutions[i]->width, | |
114 kWellSupportedResolutions[i]->height); | |
115 } | |
116 } | |
117 } | |
118 | |
119 // Tries to create a user-side device interface for a given USB device. Returns | 86 // Tries to create a user-side device interface for a given USB device. Returns |
120 // true if interface was found and passes it back in |device_interface|. The | 87 // true if interface was found and passes it back in |device_interface|. The |
121 // caller should release |device_interface|. | 88 // caller should release |device_interface|. |
122 static bool FindDeviceInterfaceInUsbDevice( | 89 static bool FindDeviceInterfaceInUsbDevice( |
123 const int vendor_id, | 90 const int vendor_id, |
124 const int product_id, | 91 const int product_id, |
125 const io_service_t usb_device, | 92 const io_service_t usb_device, |
126 IOUSBDeviceInterface*** device_interface) { | 93 IOUSBDeviceInterface*** device_interface) { |
127 // Create a plugin, i.e. a user-side controller to manipulate USB device. | 94 // Create a plugin, i.e. a user-side controller to manipulate USB device. |
128 IOCFPlugInInterface** plugin; | 95 IOCFPlugInInterface** plugin; |
(...skipping 211 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
340 const size_t vid_location = unique_id_.size() - 2 * kVidPidSize; | 307 const size_t vid_location = unique_id_.size() - 2 * kVidPidSize; |
341 std::string id_vendor = unique_id_.substr(vid_location, kVidPidSize); | 308 std::string id_vendor = unique_id_.substr(vid_location, kVidPidSize); |
342 const size_t pid_location = unique_id_.size() - kVidPidSize; | 309 const size_t pid_location = unique_id_.size() - kVidPidSize; |
343 std::string id_product = unique_id_.substr(pid_location, kVidPidSize); | 310 std::string id_product = unique_id_.substr(pid_location, kVidPidSize); |
344 | 311 |
345 return id_vendor + ":" + id_product; | 312 return id_vendor + ":" + id_product; |
346 } | 313 } |
347 | 314 |
348 VideoCaptureDeviceMac::VideoCaptureDeviceMac(const Name& device_name) | 315 VideoCaptureDeviceMac::VideoCaptureDeviceMac(const Name& device_name) |
349 : device_name_(device_name), | 316 : device_name_(device_name), |
350 tried_to_square_pixels_(false), | |
351 task_runner_(base::ThreadTaskRunnerHandle::Get()), | 317 task_runner_(base::ThreadTaskRunnerHandle::Get()), |
352 state_(kNotInitialized), | 318 state_(kNotInitialized), |
353 capture_device_(nil), | 319 capture_device_(nil), |
354 first_timestamp_(media::kNoTimestamp()), | 320 first_timestamp_(media::kNoTimestamp()), |
355 weak_factory_(this) { | 321 weak_factory_(this) { |
356 // Avoid reconfiguring AVFoundation or blacklisted devices. | |
357 final_resolution_selected_ = AVFoundationGlue::IsAVFoundationSupported() || | |
358 device_name.is_blacklisted(); | |
359 } | 322 } |
360 | 323 |
361 VideoCaptureDeviceMac::~VideoCaptureDeviceMac() { | 324 VideoCaptureDeviceMac::~VideoCaptureDeviceMac() { |
362 DCHECK(task_runner_->BelongsToCurrentThread()); | 325 DCHECK(task_runner_->BelongsToCurrentThread()); |
363 [capture_device_ release]; | 326 [capture_device_ release]; |
364 } | 327 } |
365 | 328 |
366 void VideoCaptureDeviceMac::AllocateAndStart( | 329 void VideoCaptureDeviceMac::AllocateAndStart( |
367 const VideoCaptureParams& params, | 330 const VideoCaptureParams& params, |
368 scoped_ptr<VideoCaptureDevice::Client> client) { | 331 scoped_ptr<VideoCaptureDevice::Client> client) { |
369 DCHECK(task_runner_->BelongsToCurrentThread()); | 332 DCHECK(task_runner_->BelongsToCurrentThread()); |
370 if (state_ != kIdle) { | 333 if (state_ != kIdle) { |
371 return; | 334 return; |
372 } | 335 } |
373 | 336 |
374 // QTKit API can scale captured frame to any size requested, which would lead | |
375 // to undesired aspect ratio changes. Try to open the camera with a known | |
376 // supported format and let the client crop/pad the captured frames. | |
377 gfx::Size resolution = params.requested_format.frame_size; | |
378 if (!AVFoundationGlue::IsAVFoundationSupported()) | |
379 GetBestMatchSupportedResolution(&resolution); | |
380 | |
381 client_ = std::move(client); | 337 client_ = std::move(client); |
382 if (device_name_.capture_api_type() == Name::AVFOUNDATION) | 338 if (device_name_.capture_api_type() == Name::AVFOUNDATION) |
383 LogMessage("Using AVFoundation for device: " + device_name_.name()); | 339 LogMessage("Using AVFoundation for device: " + device_name_.name()); |
384 else | 340 |
385 LogMessage("Using QTKit for device: " + device_name_.name()); | |
386 NSString* deviceId = | 341 NSString* deviceId = |
387 [NSString stringWithUTF8String:device_name_.id().c_str()]; | 342 [NSString stringWithUTF8String:device_name_.id().c_str()]; |
388 | 343 |
389 [capture_device_ setFrameReceiver:this]; | 344 [capture_device_ setFrameReceiver:this]; |
390 | 345 |
391 if (![capture_device_ setCaptureDevice:deviceId]) { | 346 if (![capture_device_ setCaptureDevice:deviceId]) { |
392 SetErrorState(FROM_HERE, "Could not open capture device."); | 347 SetErrorState(FROM_HERE, "Could not open capture device."); |
393 return; | 348 return; |
394 } | 349 } |
395 | 350 |
396 capture_format_.frame_size = resolution; | 351 capture_format_.frame_size = params.requested_format.frame_size; |
397 capture_format_.frame_rate = | 352 capture_format_.frame_rate = |
398 std::max(kMinFrameRate, | 353 std::max(kMinFrameRate, |
399 std::min(params.requested_format.frame_rate, kMaxFrameRate)); | 354 std::min(params.requested_format.frame_rate, kMaxFrameRate)); |
400 // Leave the pixel format selection to AVFoundation/QTKit. The pixel format | 355 // Leave the pixel format selection to AVFoundation. The pixel format |
401 // will be passed to |ReceiveFrame|. | 356 // will be passed to |ReceiveFrame|. |
402 capture_format_.pixel_format = PIXEL_FORMAT_UNKNOWN; | 357 capture_format_.pixel_format = PIXEL_FORMAT_UNKNOWN; |
403 | 358 |
404 // QTKit: Set the capture resolution only if this is VGA or smaller, otherwise | |
405 // leave it unconfigured and start capturing: QTKit will produce frames at the | |
406 // native resolution, allowing us to identify cameras whose native resolution | |
407 // is too low for HD. This additional information comes at a cost in startup | |
408 // latency, because the webcam will need to be reopened if its default | |
409 // resolution is not HD or VGA. | |
410 // AVfoundation is configured for all resolutions. | |
411 if (AVFoundationGlue::IsAVFoundationSupported() || | |
412 resolution.width() <= kVGA.width || resolution.height() <= kVGA.height) { | |
413 if (!UpdateCaptureResolution()) | 359 if (!UpdateCaptureResolution()) |
414 return; | 360 return; |
415 } | |
416 | 361 |
417 // Try setting the power line frequency removal (anti-flicker). The built-in | 362 // Try setting the power line frequency removal (anti-flicker). The built-in |
418 // cameras are normally suspended so the configuration must happen right | 363 // cameras are normally suspended so the configuration must happen right |
419 // before starting capture and during configuration. | 364 // before starting capture and during configuration. |
420 const std::string& device_model = device_name_.GetModel(); | 365 const std::string& device_model = device_name_.GetModel(); |
421 if (device_model.length() > 2 * kVidPidSize) { | 366 if (device_model.length() > 2 * kVidPidSize) { |
422 std::string vendor_id = device_model.substr(0, kVidPidSize); | 367 std::string vendor_id = device_model.substr(0, kVidPidSize); |
423 std::string model_id = device_model.substr(kVidPidSize + 1); | 368 std::string model_id = device_model.substr(kVidPidSize + 1); |
424 int vendor_id_as_int, model_id_as_int; | 369 int vendor_id_as_int, model_id_as_int; |
425 if (base::HexStringToInt(base::StringPiece(vendor_id), &vendor_id_as_int) && | 370 if (base::HexStringToInt(base::StringPiece(vendor_id), &vendor_id_as_int) && |
(...skipping 12 matching lines...) Expand all Loading... |
438 } | 383 } |
439 | 384 |
440 void VideoCaptureDeviceMac::StopAndDeAllocate() { | 385 void VideoCaptureDeviceMac::StopAndDeAllocate() { |
441 DCHECK(task_runner_->BelongsToCurrentThread()); | 386 DCHECK(task_runner_->BelongsToCurrentThread()); |
442 DCHECK(state_ == kCapturing || state_ == kError) << state_; | 387 DCHECK(state_ == kCapturing || state_ == kError) << state_; |
443 | 388 |
444 [capture_device_ setCaptureDevice:nil]; | 389 [capture_device_ setCaptureDevice:nil]; |
445 [capture_device_ setFrameReceiver:nil]; | 390 [capture_device_ setFrameReceiver:nil]; |
446 client_.reset(); | 391 client_.reset(); |
447 state_ = kIdle; | 392 state_ = kIdle; |
448 tried_to_square_pixels_ = false; | |
449 } | 393 } |
450 | 394 |
451 bool VideoCaptureDeviceMac::Init( | 395 bool VideoCaptureDeviceMac::Init( |
452 VideoCaptureDevice::Name::CaptureApiType capture_api_type) { | 396 VideoCaptureDevice::Name::CaptureApiType capture_api_type) { |
453 DCHECK(task_runner_->BelongsToCurrentThread()); | 397 DCHECK(task_runner_->BelongsToCurrentThread()); |
454 DCHECK_EQ(state_, kNotInitialized); | 398 DCHECK_EQ(state_, kNotInitialized); |
455 | 399 |
456 if (capture_api_type == Name::AVFOUNDATION) { | 400 if (capture_api_type == Name::AVFOUNDATION) { |
457 capture_device_ = | 401 capture_device_ = |
458 [[VideoCaptureDeviceAVFoundation alloc] initWithFrameReceiver:this]; | 402 [[VideoCaptureDeviceAVFoundation alloc] initWithFrameReceiver:this]; |
459 } else { | |
460 capture_device_ = | |
461 [[VideoCaptureDeviceQTKit alloc] initWithFrameReceiver:this]; | |
462 } | 403 } |
463 | 404 |
464 if (!capture_device_) | 405 if (!capture_device_) |
465 return false; | 406 return false; |
466 | 407 |
467 state_ = kIdle; | 408 state_ = kIdle; |
468 return true; | 409 return true; |
469 } | 410 } |
470 | 411 |
471 void VideoCaptureDeviceMac::ReceiveFrame(const uint8_t* video_frame, | 412 void VideoCaptureDeviceMac::ReceiveFrame(const uint8_t* video_frame, |
472 int video_frame_length, | 413 int video_frame_length, |
473 const VideoCaptureFormat& frame_format, | 414 const VideoCaptureFormat& frame_format, |
474 int aspect_numerator, | 415 int aspect_numerator, |
475 int aspect_denominator, | 416 int aspect_denominator, |
476 base::TimeDelta timestamp) { | 417 base::TimeDelta timestamp) { |
477 // This method is safe to call from a device capture thread, i.e. any thread | 418 // This method is safe to call from a device capture thread, i.e. any thread |
478 // controlled by QTKit/AVFoundation. | 419 // controlled by AVFoundation. |
479 if (!final_resolution_selected_) { | 420 if (capture_format_.frame_size != frame_format.frame_size) { |
480 DCHECK(!AVFoundationGlue::IsAVFoundationSupported()); | |
481 if (capture_format_.frame_size.width() > kVGA.width || | |
482 capture_format_.frame_size.height() > kVGA.height) { | |
483 // We are requesting HD. Make sure that the picture is good, otherwise | |
484 // drop down to VGA. | |
485 bool change_to_vga = false; | |
486 if (frame_format.frame_size.width() < | |
487 capture_format_.frame_size.width() || | |
488 frame_format.frame_size.height() < | |
489 capture_format_.frame_size.height()) { | |
490 // These are the default capture settings, not yet configured to match | |
491 // |capture_format_|. | |
492 DCHECK(frame_format.frame_rate == 0); | |
493 DVLOG(1) << "Switching to VGA because the default resolution is " | |
494 << frame_format.frame_size.ToString(); | |
495 change_to_vga = true; | |
496 } | |
497 | |
498 if (capture_format_.frame_size == frame_format.frame_size && | |
499 aspect_numerator != aspect_denominator) { | |
500 DVLOG(1) << "Switching to VGA because HD has nonsquare pixel " | |
501 << "aspect ratio " << aspect_numerator << ":" | |
502 << aspect_denominator; | |
503 change_to_vga = true; | |
504 } | |
505 | |
506 if (change_to_vga) | |
507 capture_format_.frame_size.SetSize(kVGA.width, kVGA.height); | |
508 } | |
509 | |
510 if (capture_format_.frame_size == frame_format.frame_size && | |
511 !tried_to_square_pixels_ && | |
512 (aspect_numerator > kMaxPixelAspectRatio * aspect_denominator || | |
513 aspect_denominator > kMaxPixelAspectRatio * aspect_numerator)) { | |
514 // The requested size results in non-square PAR. Shrink the frame to 1:1 | |
515 // PAR (assuming QTKit selects the same input mode, which is not | |
516 // guaranteed). | |
517 int new_width = capture_format_.frame_size.width(); | |
518 int new_height = capture_format_.frame_size.height(); | |
519 if (aspect_numerator < aspect_denominator) | |
520 new_width = (new_width * aspect_numerator) / aspect_denominator; | |
521 else | |
522 new_height = (new_height * aspect_denominator) / aspect_numerator; | |
523 capture_format_.frame_size.SetSize(new_width, new_height); | |
524 tried_to_square_pixels_ = true; | |
525 } | |
526 | |
527 if (capture_format_.frame_size == frame_format.frame_size) { | |
528 final_resolution_selected_ = true; | |
529 } else { | |
530 UpdateCaptureResolution(); | |
531 // Let the resolution update sink through QTKit and wait for next frame. | |
532 return; | |
533 } | |
534 } | |
535 | |
536 // QTKit capture source can change resolution if someone else reconfigures the | |
537 // camera, and that is fine: http://crbug.com/353620. In AVFoundation, this | |
538 // should not happen, it should resize internally. | |
539 if (!AVFoundationGlue::IsAVFoundationSupported()) { | |
540 capture_format_.frame_size = frame_format.frame_size; | |
541 } else if (capture_format_.frame_size != frame_format.frame_size) { | |
542 ReceiveError(FROM_HERE, | 421 ReceiveError(FROM_HERE, |
543 "Captured resolution " + frame_format.frame_size.ToString() + | 422 "Captured resolution " + frame_format.frame_size.ToString() + |
544 ", and expected " + capture_format_.frame_size.ToString()); | 423 ", and expected " + capture_format_.frame_size.ToString()); |
545 return; | 424 return; |
546 } | 425 } |
547 | 426 |
548 base::TimeTicks aligned_timestamp; | 427 base::TimeTicks aligned_timestamp; |
549 if (timestamp == media::kNoTimestamp()) { | 428 if (timestamp == media::kNoTimestamp()) { |
550 aligned_timestamp = base::TimeTicks::Now(); | 429 aligned_timestamp = base::TimeTicks::Now(); |
551 } else { | 430 } else { |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
585 if (![capture_device_ setCaptureHeight:capture_format_.frame_size.height() | 464 if (![capture_device_ setCaptureHeight:capture_format_.frame_size.height() |
586 width:capture_format_.frame_size.width() | 465 width:capture_format_.frame_size.width() |
587 frameRate:capture_format_.frame_rate]) { | 466 frameRate:capture_format_.frame_rate]) { |
588 ReceiveError(FROM_HERE, "Could not configure capture device."); | 467 ReceiveError(FROM_HERE, "Could not configure capture device."); |
589 return false; | 468 return false; |
590 } | 469 } |
591 return true; | 470 return true; |
592 } | 471 } |
593 | 472 |
594 } // namespace media | 473 } // namespace media |
OLD | NEW |