blob: 83d4d0a18d17c5049ec1c4f69c200a993bc58ac8 [file] [log] [blame]
// 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,
&current)) {
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