blob: b18caff11ab87a39d8966570a2e767cf6f3cd254 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// 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/uvc_control_mac.h"
#include <IOKit/IOCFPlugIn.h>
#include "base/mac/bridging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_ioobject.h"
#include "base/strings/string_number_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace media {
namespace {
const unsigned int kRequestTimeoutInMilliseconds = 1000;
}
// 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;
// Sec. 3.7.2.5 "Processing Unit Descriptor"
typedef struct ProcessingUnitDescriptor {
UInt8 bLength;
UInt8 bDescriptorType;
UInt8 bDescriptorSubType;
UInt8 bUnitID;
UInt8 bSourceID;
UInt16 wMaxMultiplier;
UInt8 bControlSize; // Size of the bmControls field, in bytes.
UInt8 bmControls[]; // A bit set to 1 indicates that the mentioned Control is
// supported for the video stream.
} __attribute__((packed)) ProcessingUnitDescriptor;
// Sec. 3.7.2.3 "Camera Terminal Descriptor"
typedef struct {
UInt8 bLength;
UInt8 bDescriptorType;
UInt8 bDescriptorSubType;
UInt8 bTerminalId;
UInt16 wTerminalType;
UInt8 bAssocTerminal;
UInt8 iTerminal;
UInt16 wObjectiveFocalLengthMin;
UInt16 wObjectiveFocalLengthMax;
UInt16 wOcularFocalLength;
UInt8 bControlSize; // Size of the bmControls field, in bytes.
UInt8 bmControls[]; // A bit set to 1 indicates that the mentioned Control is
// supported for the video stream.
} __attribute__((packed)) CameraTerminalDescriptor;
static constexpr int kPuBrightnessAbsoluteControlBitIndex = 0;
static constexpr int kPuContrastAbsoluteControlBitIndex = 1;
static constexpr int kPuSaturationAbsoluteControlBitIndex = 3;
static constexpr int kPuSharpnessAbsoluteControlBitIndex = 4;
static constexpr int kPuWhiteBalanceTemperatureControlBitIndex = 5;
static constexpr int kPuPowerLineFrequencyControlBitIndex = 10;
static constexpr int kPuWhiteBalanceTemperatureAutoControlBitIndex = 12;
static constexpr int kCtAutoExposureModeControlBitIndex = 1;
static constexpr int kCtExposureTimeAbsoluteControlBitIndex = 3;
static constexpr int kCtFocusAbsoluteControlBitIndex = 5;
static constexpr int kCtZoomAbsoluteControlBitIndex = 9;
static constexpr int kCtPanTiltAbsoluteControlBitIndex = 11;
static constexpr int kCtFocusAutoControlBitIndex = 17;
static const base::flat_map<int, size_t> kProcessingUnitControlBitIndexes = {
{uvc::kPuBrightnessAbsoluteControl, kPuBrightnessAbsoluteControlBitIndex},
{uvc::kPuContrastAbsoluteControl, kPuContrastAbsoluteControlBitIndex},
{uvc::kPuSaturationAbsoluteControl, kPuSaturationAbsoluteControlBitIndex},
{uvc::kPuSharpnessAbsoluteControl, kPuSharpnessAbsoluteControlBitIndex},
{uvc::kPuWhiteBalanceTemperatureControl,
kPuWhiteBalanceTemperatureControlBitIndex},
{uvc::kPuPowerLineFrequencyControl, kPuPowerLineFrequencyControlBitIndex},
{uvc::kPuWhiteBalanceTemperatureAutoControl,
kPuWhiteBalanceTemperatureAutoControlBitIndex},
};
static const base::flat_map<int, size_t> kCameraTerminalControlBitIndexes = {
{uvc::kCtAutoExposureModeControl, kCtAutoExposureModeControlBitIndex},
{uvc::kCtExposureTimeAbsoluteControl,
kCtExposureTimeAbsoluteControlBitIndex},
{uvc::kCtFocusAbsoluteControl, kCtFocusAbsoluteControlBitIndex},
{uvc::kCtZoomAbsoluteControl, kCtZoomAbsoluteControlBitIndex},
{uvc::kCtPanTiltAbsoluteControl, kCtPanTiltAbsoluteControlBitIndex},
{uvc::kCtFocusAutoControl, kCtFocusAutoControlBitIndex},
};
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::NSToCFPtrCast(@(vendor_id)));
CFDictionarySetValue(query_dictionary, CFSTR(kUSBProductName),
base::mac::NSToCFPtrCast(@(product_id)));
kern_return_t kr = IOServiceGetMatchingServices(
kIOMasterPortDefault, query_dictionary.release(), usb_iterator);
if (kr != kIOReturnSuccess) {
VLOG(1) << "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|.
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.
base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> plugin;
SInt32 score; // Unused, but required for IOCreatePlugInInterfaceForService.
kern_return_t kr = IOCreatePlugInInterfaceForService(
usb_device, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID,
plugin.InitializeInto(), &score);
if (kr != kIOReturnSuccess || !plugin) {
VLOG(1) << "IOCreatePlugInInterfaceForService";
return false;
}
// Fetch the Device Interface from the plugin.
HRESULT res = (*plugin)->QueryInterface(
plugin, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID),
reinterpret_cast<LPVOID*>(device_interface));
if (!SUCCEEDED(res) || !*device_interface) {
VLOG(1) << "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.
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.
base::mac::ScopedIOObject<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.InitializeInto());
if (kr != kIOReturnSuccess) {
VLOG(1) << "Could not create an iterator to the device's interfaces.";
return false;
}
// There should be just one interface matching the class-subclass desired.
base::mac::ScopedIOObject<io_service_t> found_interface(
IOIteratorNext(interface_iterator));
if (!found_interface) {
VLOG(1) << "Could not find a Video-AVControl interface in the device.";
return false;
}
// 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) {
VLOG(1) << "IOCreatePlugInInterfaceForService";
return false;
}
return true;
}
template <typename DescriptorType>
std::vector<uint8_t> ExtractControls(IOUSBDescriptorHeader* usb_descriptor) {
auto* descriptor = reinterpret_cast<DescriptorType>(usb_descriptor);
if (descriptor->bControlSize > 0) {
NSData* data = [[NSData alloc] initWithBytes:&descriptor->bmControls[0]
length:descriptor->bControlSize];
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(data.bytes);
return std::vector<uint8_t>(bytes, bytes + data.length);
}
return std::vector<uint8_t>();
}
// Open the video class specific interface in a USB webcam identified by
// |device_model|. Returns interface when it is successfully opened.
static ScopedIOUSBInterfaceInterface OpenVideoClassSpecificControlInterface(
std::string device_model,
int descriptor_subtype,
int& unit_id,
std::vector<uint8_t>& controls) {
if (device_model.length() <= 2 * media::kVidPidSize) {
return ScopedIOUSBInterfaceInterface();
}
std::string vendor_id = device_model.substr(0, media::kVidPidSize);
std::string product_id = device_model.substr(media::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) {
VLOG(1) << "Couldn’t get control interface";
return ScopedIOUSBInterfaceInterface();
}
// Find the device's unit ID presenting type kVcCsInterface and the descriptor
// subtype.
IOUSBDescriptorHeader* descriptor = nullptr;
while ((descriptor = (*control_interface)
->FindNextAssociatedDescriptor(
control_interface.get(), descriptor,
uvc::kVcCsInterface))) {
auto* cs_descriptor =
reinterpret_cast<VcCsInterfaceDescriptor*>(descriptor);
if (cs_descriptor->bDescriptorSubType == descriptor_subtype) {
unit_id = cs_descriptor->bUnitID;
if (descriptor_subtype == uvc::kVcProcessingUnit) {
controls = ExtractControls<ProcessingUnitDescriptor*>(descriptor);
} else if (descriptor_subtype == uvc::kVcInputTerminal) {
controls = ExtractControls<CameraTerminalDescriptor*>(descriptor);
}
break;
}
}
VLOG_IF(1, unit_id == -1) << "This USB device doesn't seem to have a "
<< descriptor_subtype << " descriptor subtype.";
if (unit_id == -1) {
return ScopedIOUSBInterfaceInterface();
}
IOReturn ret = (*control_interface)->USBInterfaceOpen(control_interface);
if (ret != kIOReturnSuccess) {
VLOG(1) << "Unable to open control interface";
// On macOS 12+, VDCAssistant takes ownership of USB devices. So, we should
// not bail out if kIOReturnExclusiveAccess is returned from
// USBInterfaceOpen().
if (!(base::mac::IsAtLeastOS12() && ret == kIOReturnExclusiveAccess)) {
return ScopedIOUSBInterfaceInterface();
}
}
return control_interface;
}
UvcControl::UvcControl(std::string device_model, int descriptor_subtype)
: descriptor_subtype_(descriptor_subtype) {
TRACE_EVENT2(TRACE_DISABLED_BY_DEFAULT("video_and_image_capture"),
"UvcControl::CreateUvcControl", "device_model", device_model,
"descriptor_subtype", descriptor_subtype);
interface_ = OpenVideoClassSpecificControlInterface(
device_model, descriptor_subtype, unit_id_, controls_);
}
UvcControl::~UvcControl() {
if (interface_) {
(*interface_)->USBInterfaceClose(interface_);
}
}
bool UvcControl::IsControlAvailable(int control_selector) const {
if (!controls_.size()) {
return false;
}
size_t bitIndex;
if (descriptor_subtype_ == uvc::kVcProcessingUnit) {
auto it = kProcessingUnitControlBitIndexes.find(control_selector);
if (it == kProcessingUnitControlBitIndexes.end()) {
return false;
}
bitIndex = it->second;
} else if (descriptor_subtype_ == uvc::kVcInputTerminal) {
auto it = kCameraTerminalControlBitIndexes.find(control_selector);
if (it == kCameraTerminalControlBitIndexes.end()) {
return false;
}
bitIndex = it->second;
} else {
return false;
}
UInt8 byteIndex = bitIndex / 8;
if (byteIndex > controls_.size()) {
return false;
}
return ((controls_[byteIndex] & (1 << bitIndex % 8)) != 0);
}
// Create an empty IOUSBDevRequestTO for a USB device to either set or get
// controls.
IOUSBDevRequestTO UvcControl::CreateEmptyCommand(
int request_code,
int endpoint_direction,
int control_selector,
int control_command_size) const {
CHECK(interface_);
CHECK((endpoint_direction == kUSBIn) || (endpoint_direction == kUSBOut));
UInt8 interface_number;
(*interface_)->GetInterfaceNumber(interface_, &interface_number);
IOUSBDevRequestTO command;
memset(&command, 0, sizeof(command));
command.bmRequestType = USBmakebmRequestType(
endpoint_direction, UInt8{kUSBClass}, UInt8{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;
command.noDataTimeout = kRequestTimeoutInMilliseconds;
command.completionTimeout = kRequestTimeoutInMilliseconds;
return command;
}
} // namespace media