blob: b13496c8058bb3602e52b9fded4f9d42aa69a824 [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/video/capture/mac/video_capture_device_mac.h"
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USBSpec.h>
#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/message_loop/message_loop_proxy.h"
#include "base/mac/scoped_ioobject.h"
#include "base/mac/scoped_ioplugininterface.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#import "media/video/capture/mac/avfoundation_glue.h"
#import "media/video/capture/mac/platform_video_capturing_mac.h"
#import "media/video/capture/mac/video_capture_device_avfoundation_mac.h"
#import "media/video/capture/mac/video_capture_device_qtkit_mac.h"
@implementation DeviceNameAndTransportType
- (id)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 {
const int kMinFrameRate = 1;
const int kMaxFrameRate = 30;
// In device identifiers, the USB VID and PID are stored in 4 bytes each.
const size_t kVidPidSize = 4;
const struct Resolution {
const int width;
const int height;
} kQVGA = { 320, 240 },
kVGA = { 640, 480 },
kHD = { 1280, 720 };
const struct Resolution* const kWellSupportedResolutions[] = {
&kQVGA,
&kVGA,
&kHD,
};
// Rescaling the image to fix the pixel aspect ratio runs the risk of making
// the aspect ratio worse, if QTKit selects a new source mode with a different
// shape. This constant ensures that we don't take this risk if the current
// aspect ratio is tolerable.
const float kMaxPixelAspectRatio = 1.15;
// 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
// CS_INTERFACE: Sec. A.4 "Video Class-Specific Descriptor Types".
const int kVcCsInterface = 0x24;
// VC_PROCESSING_UNIT: Sec. A.5 "Video Class-Specific VC Interface Descriptor
// Subtypes".
const int kVcProcessingUnit = 0x5;
// SET_CUR: Sec. A.8 "Video Class-Specific Request Codes".
const int kVcRequestCodeSetCur = 0x1;
// PU_POWER_LINE_FREQUENCY_CONTROL: Sec. A.9.5 "Processing Unit Control
// Selectors".
const int kPuPowerLineFrequencyControl = 0x5;
// Sec. 4.2.2.3.5 Power Line Frequency Control.
const int k50Hz = 1;
const int k60Hz = 2;
const int kPuPowerLineFrequencyControlCommandSize = 1;
// Addition to the IOUSB family of structures, with subtype and unit ID.
typedef struct IOUSBInterfaceDescriptor {
IOUSBDescriptorHeader header;
UInt8 bDescriptorSubType;
UInt8 bUnitID;
} IOUSBInterfaceDescriptor;
// TODO(ronghuawu): Replace this with CapabilityList::GetBestMatchedCapability.
void GetBestMatchSupportedResolution(int* width, int* height) {
int min_diff = kint32max;
int matched_width = *width;
int matched_height = *height;
int desired_res_area = *width * *height;
for (size_t i = 0; i < arraysize(kWellSupportedResolutions); ++i) {
int area = kWellSupportedResolutions[i]->width *
kWellSupportedResolutions[i]->height;
int diff = std::abs(desired_res_area - area);
if (diff < min_diff) {
min_diff = diff;
matched_width = kWellSupportedResolutions[i]->width;
matched_height = kWellSupportedResolutions[i]->height;
}
}
*width = matched_width;
*height = matched_height;
}
// 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 plug-in, 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 plug-in.
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 "plug-in") 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 int frequency) {
// Create, the control interface for the found plug-in, and release
// the intermediate plug-in.
IOUSBInterfaceInterface** control_interface = NULL;
HRESULT res = (*plugin_interface)->QueryInterface(
plugin_interface,
CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID),
reinterpret_cast<LPVOID*>(&control_interface));
if (!SUCCEEDED(res) || !control_interface ) {
DLOG(ERROR) << "Couldn’t create control interface";
return;
}
base::mac::ScopedIOPluginInterface<IOUSBInterfaceInterface>
control_interface_ref(control_interface);
// 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;
IOUSBInterfaceDescriptor* cs_descriptor = NULL;
IOUSBInterfaceInterface220** interface =
reinterpret_cast<IOUSBInterfaceInterface220**>(control_interface);
while ((descriptor = (*interface)->FindNextAssociatedDescriptor(
interface, descriptor, kUSBAnyDesc))) {
cs_descriptor =
reinterpret_cast<IOUSBInterfaceDescriptor*>(descriptor);
if ((descriptor->bDescriptorType == kVcCsInterface) &&
(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 == 50) ? 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 " << 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 int frequency) {
if (frequency == 0)
return;
DVLOG(1) << "Setting Power Line Frequency to " << frequency << " Hz, device "
<< std::hex << vendor_id << "-" << product_id;
// Compose a search dictionary with vendor and product ID.
CFMutableDictionaryRef query_dictionary =
IOServiceMatching(kIOUSBDeviceClassName);
CFDictionarySetValue(query_dictionary, CFSTR(kUSBVendorName),
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendor_id));
CFDictionarySetValue(query_dictionary, CFSTR(kUSBProductName),
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &product_id));
io_iterator_t usb_iterator;
kern_return_t kr = IOServiceGetMatchingServices(kIOMasterPortDefault,
query_dictionary,
&usb_iterator);
if (kr != kIOReturnSuccess) {
DLOG(ERROR) << "No devices found with specified Vendor and Product ID.";
return;
}
base::mac::ScopedIOObject<io_iterator_t> usb_iterator_ref(usb_iterator);
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);
}
}
const std::string VideoCaptureDevice::Name::GetModel() const {
// Skip the AVFoundation's not USB nor built-in devices.
if (capture_api_type() == AVFOUNDATION && transport_type() != USB_OR_BUILT_IN)
return "";
// Both PID and VID are 4 characters.
if (unique_id_.size() < 2 * kVidPidSize)
return "";
// The last characters of device id is a concatenation of VID and then PID.
const size_t vid_location = unique_id_.size() - 2 * kVidPidSize;
std::string id_vendor = unique_id_.substr(vid_location, kVidPidSize);
const size_t pid_location = unique_id_.size() - kVidPidSize;
std::string id_product = unique_id_.substr(pid_location, kVidPidSize);
return id_vendor + ":" + id_product;
}
VideoCaptureDeviceMac::VideoCaptureDeviceMac(const Name& device_name)
: device_name_(device_name),
tried_to_square_pixels_(false),
task_runner_(base::MessageLoopProxy::current()),
state_(kNotInitialized),
capture_device_(nil),
weak_factory_(this) {
final_resolution_selected_ = AVFoundationGlue::IsAVFoundationSupported();
}
VideoCaptureDeviceMac::~VideoCaptureDeviceMac() {
DCHECK(task_runner_->BelongsToCurrentThread());
[capture_device_ release];
}
void VideoCaptureDeviceMac::AllocateAndStart(
const VideoCaptureParams& params,
scoped_ptr<VideoCaptureDevice::Client> client) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (state_ != kIdle) {
return;
}
int width = params.requested_format.frame_size.width();
int height = params.requested_format.frame_size.height();
int frame_rate = params.requested_format.frame_rate;
// QTKit API can scale captured frame to any size requested, which would lead
// to undesired aspect ratio changes. Try to open the camera with a known
// supported format and let the client crop/pad the captured frames.
if (!AVFoundationGlue::IsAVFoundationSupported())
GetBestMatchSupportedResolution(&width, &height);
client_ = client.Pass();
if (device_name_.capture_api_type() == Name::AVFOUNDATION)
LogMessage("Using AVFoundation for device: " + device_name_.name());
else
LogMessage("Using QTKit for device: " + device_name_.name());
NSString* deviceId =
[NSString stringWithUTF8String:device_name_.id().c_str()];
[capture_device_ setFrameReceiver:this];
if (![capture_device_ setCaptureDevice:deviceId]) {
SetErrorState("Could not open capture device.");
return;
}
if (frame_rate < kMinFrameRate)
frame_rate = kMinFrameRate;
else if (frame_rate > kMaxFrameRate)
frame_rate = kMaxFrameRate;
capture_format_.frame_size.SetSize(width, height);
capture_format_.frame_rate = frame_rate;
capture_format_.pixel_format = PIXEL_FORMAT_UYVY;
// QTKit: Set the capture resolution only if this is VGA or smaller, otherwise
// leave it unconfigured and start capturing: QTKit will produce frames at the
// native resolution, allowing us to identify cameras whose native resolution
// is too low for HD. This additional information comes at a cost in startup
// latency, because the webcam will need to be reopened if its default
// resolution is not HD or VGA.
// AVfoundation is configured for all resolutions.
if (AVFoundationGlue::IsAVFoundationSupported() || width <= kVGA.width ||
height <= kVGA.height) {
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 = device_name_.GetModel();
if (device_model.length() > 2 * kVidPidSize) {
std::string vendor_id = device_model.substr(0, kVidPidSize);
std::string model_id = device_model.substr(kVidPidSize + 1);
int vendor_id_as_int, model_id_as_int;
if (base::HexStringToInt(base::StringPiece(vendor_id), &vendor_id_as_int) &&
base::HexStringToInt(base::StringPiece(model_id), &model_id_as_int)) {
SetAntiFlickerInUsbDevice(vendor_id_as_int, model_id_as_int,
GetPowerLineFrequencyForLocation());
}
}
if (![capture_device_ startCapture]) {
SetErrorState("Could not start capture device.");
return;
}
state_ = kCapturing;
}
void VideoCaptureDeviceMac::StopAndDeAllocate() {
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK(state_ == kCapturing || state_ == kError) << state_;
[capture_device_ stopCapture];
[capture_device_ setCaptureDevice:nil];
[capture_device_ setFrameReceiver:nil];
client_.reset();
state_ = kIdle;
tried_to_square_pixels_ = false;
}
bool VideoCaptureDeviceMac::Init(
VideoCaptureDevice::Name::CaptureApiType capture_api_type) {
DCHECK(task_runner_->BelongsToCurrentThread());
DCHECK_EQ(state_, kNotInitialized);
if (capture_api_type == Name::AVFOUNDATION) {
capture_device_ =
[[VideoCaptureDeviceAVFoundation alloc] initWithFrameReceiver:this];
} else {
capture_device_ =
[[VideoCaptureDeviceQTKit alloc] initWithFrameReceiver:this];
}
if (!capture_device_)
return false;
state_ = kIdle;
return true;
}
void VideoCaptureDeviceMac::ReceiveFrame(
const uint8* video_frame,
int video_frame_length,
const VideoCaptureFormat& frame_format,
int aspect_numerator,
int aspect_denominator) {
// This method is safe to call from a device capture thread, i.e. any thread
// controlled by QTKit/AVFoundation.
if (!final_resolution_selected_) {
DCHECK(!AVFoundationGlue::IsAVFoundationSupported());
if (capture_format_.frame_size.width() > kVGA.width ||
capture_format_.frame_size.height() > kVGA.height) {
// We are requesting HD. Make sure that the picture is good, otherwise
// drop down to VGA.
bool change_to_vga = false;
if (frame_format.frame_size.width() <
capture_format_.frame_size.width() ||
frame_format.frame_size.height() <
capture_format_.frame_size.height()) {
// These are the default capture settings, not yet configured to match
// |capture_format_|.
DCHECK(frame_format.frame_rate == 0);
DVLOG(1) << "Switching to VGA because the default resolution is " <<
frame_format.frame_size.ToString();
change_to_vga = true;
}
if (capture_format_.frame_size == frame_format.frame_size &&
aspect_numerator != aspect_denominator) {
DVLOG(1) << "Switching to VGA because HD has nonsquare pixel " <<
"aspect ratio " << aspect_numerator << ":" << aspect_denominator;
change_to_vga = true;
}
if (change_to_vga)
capture_format_.frame_size.SetSize(kVGA.width, kVGA.height);
}
if (capture_format_.frame_size == frame_format.frame_size &&
!tried_to_square_pixels_ &&
(aspect_numerator > kMaxPixelAspectRatio * aspect_denominator ||
aspect_denominator > kMaxPixelAspectRatio * aspect_numerator)) {
// The requested size results in non-square PAR. Shrink the frame to 1:1
// PAR (assuming QTKit selects the same input mode, which is not
// guaranteed).
int new_width = capture_format_.frame_size.width();
int new_height = capture_format_.frame_size.height();
if (aspect_numerator < aspect_denominator)
new_width = (new_width * aspect_numerator) / aspect_denominator;
else
new_height = (new_height * aspect_denominator) / aspect_numerator;
capture_format_.frame_size.SetSize(new_width, new_height);
tried_to_square_pixels_ = true;
}
if (capture_format_.frame_size == frame_format.frame_size) {
final_resolution_selected_ = true;
} else {
UpdateCaptureResolution();
// Let the resolution update sink through QTKit and wait for next frame.
return;
}
}
// QTKit capture source can change resolution if someone else reconfigures the
// camera, and that is fine: http://crbug.com/353620. In AVFoundation, this
// should not happen, it should resize internally.
if (!AVFoundationGlue::IsAVFoundationSupported()) {
capture_format_.frame_size = frame_format.frame_size;
} else if (capture_format_.frame_size != frame_format.frame_size) {
ReceiveError("Captured resolution " + frame_format.frame_size.ToString() +
", and expected " + capture_format_.frame_size.ToString());
return;
}
client_->OnIncomingCapturedData(video_frame,
video_frame_length,
capture_format_,
0,
base::TimeTicks::Now());
}
void VideoCaptureDeviceMac::ReceiveError(const std::string& reason) {
task_runner_->PostTask(FROM_HERE,
base::Bind(&VideoCaptureDeviceMac::SetErrorState,
weak_factory_.GetWeakPtr(),
reason));
}
void VideoCaptureDeviceMac::SetErrorState(const std::string& reason) {
DCHECK(task_runner_->BelongsToCurrentThread());
DLOG(ERROR) << reason;
state_ = kError;
client_->OnError(reason);
}
void VideoCaptureDeviceMac::LogMessage(const std::string& message) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (client_)
client_->OnLog(message);
}
bool VideoCaptureDeviceMac::UpdateCaptureResolution() {
if (![capture_device_ setCaptureHeight:capture_format_.frame_size.height()
width:capture_format_.frame_size.width()
frameRate:capture_format_.frame_rate]) {
ReceiveError("Could not configure capture device.");
return false;
}
return true;
}
} // namespace media