| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/capture/video/mac/video_capture_device_mac.h" |
| |
| #include <IOKit/IOCFPlugIn.h> |
| #include <IOKit/usb/IOUSBLib.h> |
| #include <IOKit/usb/USBSpec.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <limits> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #include "base/mac/scoped_ioobject.h" |
| #include "base/mac/scoped_ioplugininterface.h" |
| #include "base/macros.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "media/base/timestamp_constants.h" |
| #include "media/capture/mojom/image_capture_types.h" |
| #import "media/capture/video/mac/video_capture_device_avfoundation_mac.h" |
| #include "media/capture/video/mac/video_capture_device_avfoundation_utils_mac.h" |
| #include "ui/gfx/geometry/size.h" |
| |
| using ScopedIOUSBInterfaceInterface = |
| base::mac::ScopedIOPluginInterface<IOUSBInterfaceInterface220>; |
| |
| @implementation DeviceNameAndTransportType |
| |
| - (instancetype)initWithName:(NSString*)deviceName |
| transportType:(int32_t)transportType { |
| if (self = [super init]) { |
| _deviceName.reset([deviceName copy]); |
| _transportType = transportType; |
| } |
| return self; |
| } |
| |
| - (NSString*)deviceName { |
| return _deviceName; |
| } |
| |
| - (int32_t)transportType { |
| return _transportType; |
| } |
| |
| @end // @implementation DeviceNameAndTransportType |
| |
| namespace media { |
| |
| // Mac specific limits for minimum and maximum frame rate. |
| const float kMinFrameRate = 1.0f; |
| const float kMaxFrameRate = 30.0f; |
| |
| // In device identifiers, the USB VID and PID are stored in 4 bytes each. |
| const size_t kVidPidSize = 4; |
| |
| // The following constants are extracted from the specification "Universal |
| // Serial Bus Device Class Definition for Video Devices", Rev. 1.1 June 1, 2005. |
| // http://www.usb.org/developers/devclass_docs/USB_Video_Class_1_1.zip |
| // Sec. A.4 "Video Class-Specific Descriptor Types". |
| const int kVcCsInterface = 0x24; // CS_INTERFACE |
| // Sec. A.5 "Video Class-Specific VC Interface Descriptor Subtypes". |
| const int kVcInputTerminal = 0x2; // VC_INPUT_TERMINAL |
| const int kVcProcessingUnit = 0x5; // VC_PROCESSING_UNIT |
| // Sec. A.8 "Video Class-Specific Request Codes". |
| const int kVcRequestCodeSetCur = 0x1; // SET_CUR |
| const int kVcRequestCodeGetCur = 0x81; // GET_CUR |
| const int kVcRequestCodeGetMin = 0x82; // GET_MIN |
| const int kVcRequestCodeGetMax = 0x83; // GET_MAX |
| const int kVcRequestCodeGetRes = 0x84; // GET_RES |
| // Sec. A.9.4. "Camera Terminal Control Selectors". |
| const int kCtZoomAbsoluteControl = 0x0b; // CT_ZOOM_ABSOLUTE_CONTROL |
| const int kCtPanTiltAbsoluteControl = 0x0d; // CT_PANTILT_ABSOLUTE_CONTROL |
| // Sec. A.9.5 "Processing Unit Control Selectors". |
| const int kPuPowerLineFrequencyControl = |
| 0x5; // PU_POWER_LINE_FREQUENCY_CONTROL |
| // Sec. 4.2.2.3.5 "Power Line Frequency Control". |
| const int k50Hz = 1; |
| const int k60Hz = 2; |
| const int kPuPowerLineFrequencyControlCommandSize = 1; |
| // Sec. 4.2.2.1.11 "Zoom (Absolute) Control". |
| const int kCtZoomAbsoluteControlCommandSize = 2; |
| // Sec. 4.2.2.1.13 "PanTilt (Absolute) Control". |
| const int kCtPanTiltAbsoluteControlCommandSize = 8; |
| |
| // Addition to the IOUSB family of structures, with subtype and unit ID. |
| // Sec. 3.7.2 "Class-Specific VC Interface Descriptor" |
| typedef struct VcCsInterfaceDescriptor { |
| IOUSBDescriptorHeader header; |
| UInt8 bDescriptorSubType; |
| UInt8 bUnitID; |
| } VcCsInterfaceDescriptor; |
| |
| static bool FindDeviceWithVendorAndProductIds(int vendor_id, |
| int product_id, |
| io_iterator_t* usb_iterator) { |
| // Compose a search dictionary with vendor and product ID. |
| base::ScopedCFTypeRef<CFMutableDictionaryRef> query_dictionary( |
| IOServiceMatching(kIOUSBDeviceClassName)); |
| CFDictionarySetValue(query_dictionary, CFSTR(kUSBVendorName), |
| base::mac::NSToCFCast(@(vendor_id))); |
| CFDictionarySetValue(query_dictionary, CFSTR(kUSBProductName), |
| base::mac::NSToCFCast(@(product_id))); |
| |
| kern_return_t kr = IOServiceGetMatchingServices( |
| kIOMasterPortDefault, query_dictionary.release(), usb_iterator); |
| if (kr != kIOReturnSuccess) { |
| DLOG(ERROR) << "No devices found with specified Vendor and Product ID."; |
| return false; |
| } |
| return true; |
| } |
| |
| // Tries to create a user-side device interface for a given USB device. Returns |
| // true if interface was found and passes it back in |device_interface|. The |
| // caller should release |device_interface|. |
| static bool FindDeviceInterfaceInUsbDevice( |
| const int vendor_id, |
| const int product_id, |
| const io_service_t usb_device, |
| IOUSBDeviceInterface*** device_interface) { |
| // Create a plugin, i.e. a user-side controller to manipulate USB device. |
| IOCFPlugInInterface** plugin; |
| SInt32 score; // Unused, but required for IOCreatePlugInInterfaceForService. |
| kern_return_t kr = IOCreatePlugInInterfaceForService( |
| usb_device, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin, |
| &score); |
| if (kr != kIOReturnSuccess || !plugin) { |
| DLOG(ERROR) << "IOCreatePlugInInterfaceForService"; |
| return false; |
| } |
| base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> plugin_ref(plugin); |
| |
| // Fetch the Device Interface from the plugin. |
| HRESULT res = (*plugin)->QueryInterface( |
| plugin, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), |
| reinterpret_cast<LPVOID*>(device_interface)); |
| if (!SUCCEEDED(res) || !*device_interface) { |
| DLOG(ERROR) << "QueryInterface, couldn't create interface to USB"; |
| return false; |
| } |
| return true; |
| } |
| |
| // Tries to find a Video Control type interface inside a general USB device |
| // interface |device_interface|, and returns it in |video_control_interface| if |
| // found. The returned interface must be released in the caller. |
| static bool FindVideoControlInterfaceInDeviceInterface( |
| IOUSBDeviceInterface** device_interface, |
| IOCFPlugInInterface*** video_control_interface) { |
| // Create an iterator to the list of Video-AVControl interfaces of the device, |
| // then get the first interface in the list. |
| io_iterator_t interface_iterator; |
| IOUSBFindInterfaceRequest interface_request = { |
| .bInterfaceClass = kUSBVideoInterfaceClass, |
| .bInterfaceSubClass = kUSBVideoControlSubClass, |
| .bInterfaceProtocol = kIOUSBFindInterfaceDontCare, |
| .bAlternateSetting = kIOUSBFindInterfaceDontCare}; |
| kern_return_t kr = |
| (*device_interface) |
| ->CreateInterfaceIterator(device_interface, &interface_request, |
| &interface_iterator); |
| if (kr != kIOReturnSuccess) { |
| DLOG(ERROR) << "Could not create an iterator to the device's interfaces."; |
| return false; |
| } |
| base::mac::ScopedIOObject<io_iterator_t> iterator_ref(interface_iterator); |
| |
| // There should be just one interface matching the class-subclass desired. |
| io_service_t found_interface; |
| found_interface = IOIteratorNext(interface_iterator); |
| if (!found_interface) { |
| DLOG(ERROR) << "Could not find a Video-AVControl interface in the device."; |
| return false; |
| } |
| base::mac::ScopedIOObject<io_service_t> found_interface_ref(found_interface); |
| |
| // Create a user side controller (i.e. a "plugin") for the found interface. |
| SInt32 score; |
| kr = IOCreatePlugInInterfaceForService( |
| found_interface, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, |
| video_control_interface, &score); |
| if (kr != kIOReturnSuccess || !*video_control_interface) { |
| DLOG(ERROR) << "IOCreatePlugInInterfaceForService"; |
| return false; |
| } |
| return true; |
| } |
| |
| // Creates a control interface for |plugin_interface| and produces a command to |
| // set the appropriate Power Line frequency for flicker removal. |
| static void SetAntiFlickerInVideoControlInterface( |
| IOCFPlugInInterface** plugin_interface, |
| const PowerLineFrequency frequency) { |
| // Create, the control interface for the found plugin, and release |
| // the intermediate plugin. |
| ScopedIOUSBInterfaceInterface control_interface; |
| HRESULT res = |
| (*plugin_interface) |
| ->QueryInterface( |
| plugin_interface, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID), |
| reinterpret_cast<LPVOID*>(control_interface.InitializeInto())); |
| if (!SUCCEEDED(res) || !control_interface) { |
| DLOG(ERROR) << "Couldn’t create control interface"; |
| return; |
| } |
| |
| // Find the device's unit ID presenting type 0x24 (kVcCsInterface) and |
| // subtype 0x5 (kVcProcessingUnit). Inside this unit is where we find the |
| // power line frequency removal setting, and this id is device dependent. |
| int real_unit_id = -1; |
| IOUSBDescriptorHeader* descriptor = NULL; |
| while ((descriptor = |
| (*control_interface) |
| ->FindNextAssociatedDescriptor(control_interface.get(), |
| descriptor, kVcCsInterface))) { |
| auto* cs_descriptor = |
| reinterpret_cast<VcCsInterfaceDescriptor*>(descriptor); |
| if (cs_descriptor->bDescriptorSubType == kVcProcessingUnit) { |
| real_unit_id = cs_descriptor->bUnitID; |
| break; |
| } |
| } |
| DVLOG_IF(1, real_unit_id == -1) |
| << "This USB device doesn't seem to have a " |
| << " VC_PROCESSING_UNIT, anti-flicker not available"; |
| if (real_unit_id == -1) |
| return; |
| |
| if ((*control_interface)->USBInterfaceOpen(control_interface) != |
| kIOReturnSuccess) { |
| DLOG(ERROR) << "Unable to open control interface"; |
| return; |
| } |
| |
| // Create the control request and launch it to the device's control interface. |
| // Note how the wIndex needs the interface number OR'ed in the lowest bits. |
| IOUSBDevRequest command; |
| command.bmRequestType = |
| USBmakebmRequestType(kUSBOut, kUSBClass, kUSBInterface); |
| command.bRequest = kVcRequestCodeSetCur; |
| UInt8 interface_number; |
| (*control_interface) |
| ->GetInterfaceNumber(control_interface, &interface_number); |
| command.wIndex = (real_unit_id << 8) | interface_number; |
| const int selector = kPuPowerLineFrequencyControl; |
| command.wValue = (selector << 8); |
| command.wLength = kPuPowerLineFrequencyControlCommandSize; |
| command.wLenDone = 0; |
| int power_line_flag_value = |
| (frequency == PowerLineFrequency::FREQUENCY_50HZ) ? k50Hz : k60Hz; |
| command.pData = &power_line_flag_value; |
| |
| IOReturn ret = |
| (*control_interface)->ControlRequest(control_interface, 0, &command); |
| DLOG_IF(ERROR, ret != kIOReturnSuccess) << "Anti-flicker control request" |
| << " failed (0x" << std::hex << ret |
| << "), unit id: " << real_unit_id; |
| DVLOG_IF(1, ret == kIOReturnSuccess) << "Anti-flicker set to " |
| << static_cast<int>(frequency) << "Hz"; |
| |
| (*control_interface)->USBInterfaceClose(control_interface); |
| } |
| |
| // Sets the flicker removal in a USB webcam identified by |vendor_id| and |
| // |product_id|, if available. The process includes first finding all USB |
| // devices matching the specified |vendor_id| and |product_id|; for each |
| // matching device, a device interface, and inside it a video control interface |
| // are created. The latter is used to a send a power frequency setting command. |
| static void SetAntiFlickerInUsbDevice(const int vendor_id, |
| const int product_id, |
| const PowerLineFrequency frequency) { |
| if (frequency == PowerLineFrequency::FREQUENCY_DEFAULT) |
| return; |
| DVLOG(1) << "Setting Power Line Frequency to " << static_cast<int>(frequency) |
| << " Hz, device " << std::hex << vendor_id << "-" << product_id; |
| |
| base::mac::ScopedIOObject<io_iterator_t> usb_iterator; |
| if (!FindDeviceWithVendorAndProductIds(vendor_id, product_id, |
| usb_iterator.InitializeInto())) { |
| return; |
| } |
| |
| while (io_service_t usb_device = IOIteratorNext(usb_iterator)) { |
| base::mac::ScopedIOObject<io_service_t> usb_device_ref(usb_device); |
| |
| IOUSBDeviceInterface** device_interface = NULL; |
| if (!FindDeviceInterfaceInUsbDevice(vendor_id, product_id, usb_device, |
| &device_interface)) { |
| return; |
| } |
| base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface> |
| device_interface_ref(device_interface); |
| |
| IOCFPlugInInterface** video_control_interface = NULL; |
| if (!FindVideoControlInterfaceInDeviceInterface(device_interface, |
| &video_control_interface)) { |
| return; |
| } |
| base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> |
| plugin_interface_ref(video_control_interface); |
| |
| SetAntiFlickerInVideoControlInterface(video_control_interface, frequency); |
| } |
| } |
| |
| // Create an empty IOUSBDevRequest for a USB device to either control or get |
| // information from pan, tilt, and zoom controls. |
| static IOUSBDevRequest CreateEmptyPanTiltZoomRequest( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| int request_code, |
| int endpoint_direction, |
| int control_selector, |
| int control_command_size) { |
| DCHECK((endpoint_direction == kUSBIn) || (endpoint_direction == kUSBOut)); |
| UInt8 interface_number; |
| (*control_interface) |
| ->GetInterfaceNumber(control_interface, &interface_number); |
| IOUSBDevRequest command; |
| command.bmRequestType = |
| USBmakebmRequestType(endpoint_direction, kUSBClass, kUSBInterface); |
| command.bRequest = request_code; |
| command.wIndex = (unit_id << 8) | interface_number; |
| command.wValue = (control_selector << 8); |
| command.wLength = control_command_size; |
| command.wLenDone = 0; |
| return command; |
| } |
| |
| // Send USB request to get information about pan and tilt controls. |
| // Returns true if the request is successful. To send the request, the interface |
| // must be opened already. |
| static bool SendPanTiltControlRequest( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| int request_code, |
| int* pan_result, |
| int* tilt_result) { |
| IOUSBDevRequest command = CreateEmptyPanTiltZoomRequest( |
| control_interface, unit_id, request_code, kUSBIn, |
| kCtPanTiltAbsoluteControl, kCtPanTiltAbsoluteControlCommandSize); |
| int32_t data[2]; |
| command.pData = &data; |
| |
| IOReturn ret = |
| (*control_interface)->ControlRequest(control_interface, 0, &command); |
| DLOG_IF(ERROR, ret != kIOReturnSuccess) |
| << "Control pan tilt request" |
| << " failed (0x" << std::hex << ret << "), unit id: " << unit_id; |
| if (ret != kIOReturnSuccess) |
| return false; |
| |
| *pan_result = USBToHostLong(data[0]); |
| *tilt_result = USBToHostLong(data[1]); |
| return true; |
| } |
| |
| // Send USB request to get information about zoom control. |
| // Returns true if the request is successful. To send the request, the interface |
| // must be opened already. |
| static bool SendZoomControlRequest( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| int request_code, |
| int* result) { |
| IOUSBDevRequest command = CreateEmptyPanTiltZoomRequest( |
| control_interface, unit_id, request_code, kUSBIn, kCtZoomAbsoluteControl, |
| kCtZoomAbsoluteControlCommandSize); |
| uint16_t data; |
| command.pData = &data; |
| |
| IOReturn ret = |
| (*control_interface)->ControlRequest(control_interface, 0, &command); |
| DLOG_IF(ERROR, ret != kIOReturnSuccess) |
| << "Control zoom request" |
| << " failed (0x" << std::hex << ret << "), unit id: " << unit_id; |
| if (ret != kIOReturnSuccess) |
| return false; |
| |
| *result = USBToHostLong(data); |
| return true; |
| } |
| |
| // Retrieves the control range and current value of pan and tilt controls. |
| // The interface must be opened already. |
| static void GetPanTiltControlRangeAndCurrent( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| mojom::Range* pan_range, |
| mojom::Range* tilt_range) { |
| int pan_max, pan_min, pan_step, pan_current; |
| int tilt_max, tilt_min, tilt_step, tilt_current; |
| if (!SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetMax, &pan_max, &tilt_max) || |
| !SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetMin, &pan_min, &tilt_min) || |
| !SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetRes, &pan_step, &tilt_step) || |
| !SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetCur, &pan_current, |
| &tilt_current)) { |
| return; |
| } |
| pan_range->max = pan_max; |
| pan_range->min = pan_min; |
| pan_range->step = pan_step; |
| pan_range->current = pan_current; |
| tilt_range->max = tilt_max; |
| tilt_range->min = tilt_min; |
| tilt_range->step = tilt_step; |
| tilt_range->current = tilt_current; |
| } |
| |
| // Retrieves the control range and current value of a zoom control. The |
| // interface must be opened already. |
| static void GetZoomControlRangeAndCurrent( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| mojom::Range* zoom_range) { |
| int max, min, step, current; |
| if (!SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetMax, |
| &max) || |
| !SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetMin, |
| &min) || |
| !SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetRes, |
| &step) || |
| !SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetCur, |
| ¤t)) { |
| return; |
| } |
| zoom_range->max = max; |
| zoom_range->min = min; |
| zoom_range->step = step; |
| zoom_range->current = current; |
| } |
| |
| // Set pan and tilt values for a USB camera device. The interface must be opened |
| // already. |
| static void SetPanTiltInUsbDevice( |
| IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| absl::optional<int> pan, |
| absl::optional<int> tilt) { |
| if (!pan.has_value() && !tilt.has_value()) |
| return; |
| |
| int pan_current, tilt_current; |
| if ((!pan.has_value() || !tilt.has_value()) && |
| !SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetCur, &pan_current, |
| &tilt_current)) { |
| return; |
| } |
| |
| uint32_t pan_tilt_data[2]; |
| pan_tilt_data[0] = |
| CFSwapInt32HostToLittle((uint32_t)pan.value_or(pan_current)); |
| pan_tilt_data[1] = |
| CFSwapInt32HostToLittle((uint32_t)tilt.value_or(tilt_current)); |
| |
| IOUSBDevRequest command = CreateEmptyPanTiltZoomRequest( |
| control_interface, unit_id, kVcRequestCodeSetCur, kUSBOut, |
| kCtPanTiltAbsoluteControl, kCtPanTiltAbsoluteControlCommandSize); |
| command.pData = pan_tilt_data; |
| |
| IOReturn ret = |
| (*control_interface)->ControlRequest(control_interface, 0, &command); |
| DLOG_IF(ERROR, ret != kIOReturnSuccess) |
| << "Control request" |
| << " failed (0x" << std::hex << ret << "), unit id: " << unit_id |
| << " pan value: " << pan.value_or(pan_current) |
| << " tilt value: " << tilt.value_or(tilt_current); |
| DVLOG_IF(1, ret == kIOReturnSuccess) |
| << "Setting pan value to " << pan.value_or(pan_current) |
| << " and tilt value to " << tilt.value_or(tilt_current); |
| } |
| |
| // Set zoom value for a USB camera device. The interface must be opened already. |
| static void SetZoomInUsbDevice(IOUSBInterfaceInterface220** control_interface, |
| int unit_id, |
| int zoom) { |
| IOUSBDevRequest command = CreateEmptyPanTiltZoomRequest( |
| control_interface, unit_id, kVcRequestCodeSetCur, kUSBOut, |
| kCtZoomAbsoluteControl, kCtZoomAbsoluteControlCommandSize); |
| command.pData = &zoom; |
| |
| IOReturn ret = |
| (*control_interface)->ControlRequest(control_interface, 0, &command); |
| DLOG_IF(ERROR, ret != kIOReturnSuccess) |
| << "Control request" |
| << " failed (0x" << std::hex << ret << "), unit id: " << unit_id |
| << " zoom value: " << zoom; |
| DVLOG_IF(1, ret == kIOReturnSuccess) << "Setting zoom value to " << zoom; |
| } |
| |
| // Open the pan, tilt, zoom interface in a USB webcam identified by |
| // |device_model|. Returns interface when it is succcessfully opened. You have |
| // to close the interface manually when you're done. |
| static ScopedIOUSBInterfaceInterface OpenPanTiltZoomControlInterface( |
| std::string device_model, |
| int* unit_id) { |
| if (device_model.length() <= 2 * kVidPidSize) { |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| std::string vendor_id = device_model.substr(0, kVidPidSize); |
| std::string product_id = device_model.substr(kVidPidSize + 1); |
| int vendor_id_as_int, product_id_as_int; |
| if (!base::HexStringToInt(vendor_id, &vendor_id_as_int) || |
| !base::HexStringToInt(product_id, &product_id_as_int)) { |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| |
| base::mac::ScopedIOObject<io_iterator_t> usb_iterator; |
| if (!FindDeviceWithVendorAndProductIds(vendor_id_as_int, product_id_as_int, |
| usb_iterator.InitializeInto())) { |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| |
| base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> |
| video_control_interface; |
| |
| while (io_service_t usb_device = IOIteratorNext(usb_iterator)) { |
| base::mac::ScopedIOObject<io_service_t> usb_device_ref(usb_device); |
| base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface> device_interface; |
| |
| if (!FindDeviceInterfaceInUsbDevice(vendor_id_as_int, product_id_as_int, |
| usb_device, |
| device_interface.InitializeInto())) { |
| continue; |
| } |
| |
| if (FindVideoControlInterfaceInDeviceInterface( |
| device_interface, video_control_interface.InitializeInto())) { |
| break; |
| } |
| } |
| |
| if (video_control_interface == nullptr) { |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| |
| // Create the control interface for the found plugin, and release |
| // the intermediate plugin. |
| ScopedIOUSBInterfaceInterface control_interface; |
| HRESULT res = |
| (*video_control_interface) |
| ->QueryInterface( |
| video_control_interface, |
| CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID220), |
| reinterpret_cast<LPVOID*>(control_interface.InitializeInto())); |
| if (!SUCCEEDED(res) || !control_interface) { |
| DLOG(ERROR) << "Couldn’t get control interface"; |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| |
| // Find the device's unit ID presenting type 0x24 (kVcCsInterface) and |
| // subtype 0x2 (kVcInputTerminal). Inside this unit is where we find the |
| // settings for pan, tilt, and zoom, and this id is device dependent. |
| IOUSBDescriptorHeader* descriptor = nullptr; |
| while ((descriptor = |
| (*control_interface) |
| ->FindNextAssociatedDescriptor(control_interface.get(), |
| descriptor, kVcCsInterface))) { |
| auto* cs_descriptor = |
| reinterpret_cast<VcCsInterfaceDescriptor*>(descriptor); |
| if (cs_descriptor->bDescriptorSubType == kVcInputTerminal) { |
| *unit_id = cs_descriptor->bUnitID; |
| break; |
| } |
| } |
| |
| DVLOG_IF(1, *unit_id == -1) |
| << "This USB device doesn't seem to have a " |
| << " VC_INPUT_TERMINAL. Pan, tilt, zoom are not available."; |
| if (*unit_id == -1) |
| return ScopedIOUSBInterfaceInterface(); |
| |
| if ((*control_interface)->USBInterfaceOpen(control_interface) != |
| kIOReturnSuccess) { |
| DLOG(ERROR) << "Unable to open control interface"; |
| return ScopedIOUSBInterfaceInterface(); |
| } |
| |
| return control_interface; |
| } |
| |
| VideoCaptureDeviceMac::VideoCaptureDeviceMac( |
| const VideoCaptureDeviceDescriptor& device_descriptor) |
| : device_descriptor_(device_descriptor), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| state_(kNotInitialized), |
| capture_device_(nil), |
| weak_factory_(this) {} |
| |
| VideoCaptureDeviceMac::~VideoCaptureDeviceMac() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| } |
| |
| void VideoCaptureDeviceMac::AllocateAndStart( |
| const VideoCaptureParams& params, |
| std::unique_ptr<VideoCaptureDevice::Client> client) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (state_ != kIdle) { |
| return; |
| } |
| |
| client_ = std::move(client); |
| if (device_descriptor_.capture_api == VideoCaptureApi::MACOSX_AVFOUNDATION) |
| LogMessage("Using AVFoundation for device: " + |
| device_descriptor_.display_name()); |
| |
| NSString* deviceId = base::SysUTF8ToNSString(device_descriptor_.device_id); |
| |
| [capture_device_ setFrameReceiver:this]; |
| |
| NSString* errorMessage = nil; |
| if (![capture_device_ setCaptureDevice:deviceId errorMessage:&errorMessage]) { |
| SetErrorState(VideoCaptureError::kMacSetCaptureDeviceFailed, FROM_HERE, |
| base::SysNSStringToUTF8(errorMessage)); |
| return; |
| } |
| |
| capture_format_.frame_size = params.requested_format.frame_size; |
| capture_format_.frame_rate = |
| std::max(kMinFrameRate, |
| std::min(params.requested_format.frame_rate, kMaxFrameRate)); |
| // Leave the pixel format selection to AVFoundation. The pixel format |
| // will be passed to |ReceiveFrame|. |
| capture_format_.pixel_format = PIXEL_FORMAT_UNKNOWN; |
| |
| if (!UpdateCaptureResolution()) |
| return; |
| |
| // Try setting the power line frequency removal (anti-flicker). The built-in |
| // cameras are normally suspended so the configuration must happen right |
| // before starting capture and during configuration. |
| const std::string device_model = GetDeviceModelId( |
| device_descriptor_.device_id, device_descriptor_.capture_api, |
| device_descriptor_.transport_type); |
| if (device_model.length() > 2 * kVidPidSize) { |
| std::string vendor_id = device_model.substr(0, kVidPidSize); |
| std::string product_id = device_model.substr(kVidPidSize + 1); |
| int vendor_id_as_int, product_id_as_int; |
| if (base::HexStringToInt(vendor_id, &vendor_id_as_int) && |
| base::HexStringToInt(product_id, &product_id_as_int)) { |
| SetAntiFlickerInUsbDevice(vendor_id_as_int, product_id_as_int, |
| GetPowerLineFrequency(params)); |
| } |
| } |
| |
| if (![capture_device_ startCapture]) { |
| SetErrorState(VideoCaptureError::kMacCouldNotStartCaptureDevice, FROM_HERE, |
| "Could not start capture device."); |
| return; |
| } |
| |
| client_->OnStarted(); |
| state_ = kCapturing; |
| } |
| |
| void VideoCaptureDeviceMac::StopAndDeAllocate() { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(state_ == kCapturing || state_ == kError) << state_; |
| |
| NSString* errorMessage = nil; |
| if (![capture_device_ setCaptureDevice:nil errorMessage:&errorMessage]) |
| LogMessage(base::SysNSStringToUTF8(errorMessage)); |
| |
| [capture_device_ setFrameReceiver:nil]; |
| client_.reset(); |
| state_ = kIdle; |
| } |
| |
| void VideoCaptureDeviceMac::TakePhoto(TakePhotoCallback callback) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK(state_ == kCapturing) << state_; |
| |
| if (photo_callback_) // Only one picture can be in flight at a time. |
| return; |
| |
| photo_callback_ = std::move(callback); |
| [capture_device_ takePhoto]; |
| } |
| |
| void VideoCaptureDeviceMac::GetPhotoState(GetPhotoStateCallback callback) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| |
| auto photo_state = mojo::CreateEmptyPhotoState(); |
| |
| photo_state->height = mojom::Range::New( |
| capture_format_.frame_size.height(), capture_format_.frame_size.height(), |
| capture_format_.frame_size.height(), 0 /* step */); |
| photo_state->width = mojom::Range::New( |
| capture_format_.frame_size.width(), capture_format_.frame_size.width(), |
| capture_format_.frame_size.width(), 0 /* step */); |
| |
| const std::string device_model = GetDeviceModelId( |
| device_descriptor_.device_id, device_descriptor_.capture_api, |
| device_descriptor_.transport_type); |
| int unit_id = -1; |
| ScopedIOUSBInterfaceInterface control_interface( |
| OpenPanTiltZoomControlInterface(device_model, &unit_id)); |
| if (control_interface) { |
| GetPanTiltControlRangeAndCurrent(control_interface, unit_id, |
| photo_state->pan.get(), |
| photo_state->tilt.get()); |
| GetZoomControlRangeAndCurrent(control_interface, unit_id, |
| photo_state->zoom.get()); |
| (*control_interface)->USBInterfaceClose(control_interface); |
| } |
| |
| std::move(callback).Run(std::move(photo_state)); |
| } |
| |
| void VideoCaptureDeviceMac::SetPhotoOptions(mojom::PhotoSettingsPtr settings, |
| SetPhotoOptionsCallback callback) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| // Drop |callback| and return if there are any unsupported |settings|. |
| // TODO(mcasas): centralise checks elsewhere, https://crbug.com/724285. |
| if ((settings->has_width && |
| settings->width != capture_format_.frame_size.width()) || |
| (settings->has_height && |
| settings->height != capture_format_.frame_size.height()) || |
| settings->has_fill_light_mode || settings->has_red_eye_reduction) { |
| return; |
| } |
| |
| if (settings->has_pan || settings->has_tilt || settings->has_zoom) { |
| const std::string device_model = GetDeviceModelId( |
| device_descriptor_.device_id, device_descriptor_.capture_api, |
| device_descriptor_.transport_type); |
| int unit_id = -1; |
| ScopedIOUSBInterfaceInterface control_interface( |
| OpenPanTiltZoomControlInterface(device_model, &unit_id)); |
| if (!control_interface) |
| return; |
| |
| if (settings->has_pan || settings->has_tilt) { |
| SetPanTiltInUsbDevice( |
| control_interface, unit_id, |
| settings->has_pan ? absl::make_optional(settings->pan) |
| : absl::nullopt, |
| settings->has_tilt ? absl::make_optional(settings->tilt) |
| : absl::nullopt); |
| } |
| if (settings->has_zoom) { |
| SetZoomInUsbDevice(control_interface, unit_id, settings->zoom); |
| } |
| (*control_interface)->USBInterfaceClose(control_interface); |
| } |
| |
| std::move(callback).Run(true); |
| } |
| |
| void VideoCaptureDeviceMac::OnUtilizationReport( |
| int frame_feedback_id, |
| media::VideoCaptureFeedback feedback) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (!capture_device_) |
| return; |
| [capture_device_ setScaledResolutions:std::move(feedback.mapped_sizes)]; |
| } |
| |
| bool VideoCaptureDeviceMac::Init(VideoCaptureApi capture_api_type) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| DCHECK_EQ(state_, kNotInitialized); |
| |
| if (capture_api_type != VideoCaptureApi::MACOSX_AVFOUNDATION) |
| return false; |
| |
| capture_device_.reset( |
| [[VideoCaptureDeviceAVFoundation alloc] initWithFrameReceiver:this]); |
| |
| if (!capture_device_) |
| return false; |
| |
| state_ = kIdle; |
| return true; |
| } |
| |
| void VideoCaptureDeviceMac::ReceiveFrame(const uint8_t* video_frame, |
| int video_frame_length, |
| const VideoCaptureFormat& frame_format, |
| const gfx::ColorSpace color_space, |
| int aspect_numerator, |
| int aspect_denominator, |
| base::TimeDelta timestamp) { |
| if (capture_format_.frame_size != frame_format.frame_size) { |
| ReceiveError(VideoCaptureError::kMacReceivedFrameWithUnexpectedResolution, |
| FROM_HERE, |
| "Captured resolution " + frame_format.frame_size.ToString() + |
| ", and expected " + capture_format_.frame_size.ToString()); |
| return; |
| } |
| |
| client_->OnIncomingCapturedData(video_frame, video_frame_length, frame_format, |
| color_space, 0 /* clockwise_rotation */, |
| false /* flip_y */, base::TimeTicks::Now(), |
| timestamp); |
| } |
| |
| void VideoCaptureDeviceMac::ReceiveExternalGpuMemoryBufferFrame( |
| CapturedExternalVideoBuffer frame, |
| std::vector<CapturedExternalVideoBuffer> scaled_frames, |
| base::TimeDelta timestamp) { |
| if (capture_format_.frame_size != frame.format.frame_size) { |
| ReceiveError(VideoCaptureError::kMacReceivedFrameWithUnexpectedResolution, |
| FROM_HERE, |
| "Captured resolution " + frame.format.frame_size.ToString() + |
| ", and expected " + capture_format_.frame_size.ToString()); |
| return; |
| } |
| client_->OnIncomingCapturedExternalBuffer(std::move(frame), |
| std::move(scaled_frames), |
| base::TimeTicks::Now(), timestamp); |
| } |
| |
| void VideoCaptureDeviceMac::OnPhotoTaken(const uint8_t* image_data, |
| size_t image_length, |
| const std::string& mime_type) { |
| DCHECK(photo_callback_); |
| if (!image_data || !image_length) { |
| OnPhotoError(); |
| return; |
| } |
| |
| mojom::BlobPtr blob = mojom::Blob::New(); |
| blob->data.assign(image_data, image_data + image_length); |
| blob->mime_type = mime_type; |
| std::move(photo_callback_).Run(std::move(blob)); |
| } |
| |
| void VideoCaptureDeviceMac::OnPhotoError() { |
| DLOG(ERROR) << __func__ << " error taking picture"; |
| photo_callback_.Reset(); |
| } |
| |
| void VideoCaptureDeviceMac::ReceiveError(VideoCaptureError error, |
| const base::Location& from_here, |
| const std::string& reason) { |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VideoCaptureDeviceMac::SetErrorState, |
| weak_factory_.GetWeakPtr(), error, from_here, reason)); |
| } |
| |
| void VideoCaptureDeviceMac::LogMessage(const std::string& message) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| if (client_) |
| client_->OnLog(message); |
| } |
| |
| // static |
| std::string VideoCaptureDeviceMac::GetDeviceModelId( |
| const std::string& device_id, |
| VideoCaptureApi capture_api, |
| VideoCaptureTransportType transport_type) { |
| // Skip the AVFoundation's not USB nor built-in devices. |
| if (capture_api == VideoCaptureApi::MACOSX_AVFOUNDATION && |
| transport_type != VideoCaptureTransportType::MACOSX_USB_OR_BUILT_IN) |
| return ""; |
| if (capture_api == VideoCaptureApi::MACOSX_DECKLINK) |
| return ""; |
| // Both PID and VID are 4 characters. |
| if (device_id.size() < 2 * kVidPidSize) |
| return ""; |
| |
| // The last characters of device id is a concatenation of VID and then PID. |
| const size_t vid_location = device_id.size() - 2 * kVidPidSize; |
| std::string id_vendor = device_id.substr(vid_location, kVidPidSize); |
| const size_t pid_location = device_id.size() - kVidPidSize; |
| std::string id_product = device_id.substr(pid_location, kVidPidSize); |
| |
| return id_vendor + ":" + id_product; |
| } |
| |
| // Check if the video capture device supports pan, tilt and zoom controls. |
| // static |
| VideoCaptureControlSupport VideoCaptureDeviceMac::GetControlSupport( |
| const std::string& device_model) { |
| VideoCaptureControlSupport control_support; |
| |
| int unit_id = -1; |
| ScopedIOUSBInterfaceInterface control_interface( |
| OpenPanTiltZoomControlInterface(device_model, &unit_id)); |
| if (!control_interface) |
| return control_support; |
| |
| int zoom_max, zoom_min = 0; |
| if (SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetMax, |
| &zoom_max) && |
| SendZoomControlRequest(control_interface, unit_id, kVcRequestCodeGetMin, |
| &zoom_min) && |
| zoom_min < zoom_max) { |
| control_support.zoom = true; |
| } |
| int pan_max, pan_min = 0; |
| int tilt_max, tilt_min = 0; |
| if (SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetMax, &pan_max, &tilt_max) && |
| SendPanTiltControlRequest(control_interface, unit_id, |
| kVcRequestCodeGetMin, &pan_min, &tilt_min)) { |
| if (pan_min < pan_max) |
| control_support.pan = true; |
| if (tilt_min < tilt_max) |
| control_support.tilt = true; |
| } |
| |
| (*control_interface)->USBInterfaceClose(control_interface); |
| return control_support; |
| } |
| |
| void VideoCaptureDeviceMac::SetErrorState(VideoCaptureError error, |
| const base::Location& from_here, |
| const std::string& reason) { |
| DCHECK(task_runner_->BelongsToCurrentThread()); |
| state_ = kError; |
| client_->OnError(error, from_here, reason); |
| } |
| |
| bool VideoCaptureDeviceMac::UpdateCaptureResolution() { |
| if (![capture_device_ setCaptureHeight:capture_format_.frame_size.height() |
| width:capture_format_.frame_size.width() |
| frameRate:capture_format_.frame_rate]) { |
| ReceiveError(VideoCaptureError::kMacUpdateCaptureResolutionFailed, |
| FROM_HERE, "Could not configure capture device."); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace media |