mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2026-08-09 01:21:06 +00:00
avdevice/avfoundation: add device selection by USB serial and unique ID
A capture device could previously be selected only by index or by name. Both are unreliable when multiple audio/video devices share a name or across reboots. AVFoundation reorders the device indices, and the USB video uniqueID embeds the macOS locationID, which can be reassigned on reboot or replug and can then resolve to a different physical device. Add -video_device_id and -audio_device_id, which take a prefixed identifier: uid:<unique ID> or serial:<USB serial number>. The USB serial number is the only identifier that stays pegged to a given physical unit. For video it is resolved to the device's current locationID via IOKit. For audio it is matched against the uniqueID, which already embeds it. The unique ID covers devices that have no serial, such as virtual camera/audio devices. -list_devices additionally prints each device's uniqueID and USB serial so the values can be discovered. IOKit is detected in configure and used only when available. Signed-off-by: Gabriel Balaich <ffmpeg@ninbura.com>
This commit is contained in:
@@ -9,6 +9,7 @@ version <next>:
|
||||
- MVR demuxer
|
||||
- latticepal filter
|
||||
- DVD-Audio LPCM decoder and demuxing support
|
||||
- AVFoundation input device selection by unique ID and USB serial number
|
||||
|
||||
|
||||
version 9.0:
|
||||
|
||||
4
configure
vendored
4
configure
vendored
@@ -2688,6 +2688,7 @@ HAVE_LIST="
|
||||
ffnvcodec_cuarray
|
||||
gzip
|
||||
ioctl_posix
|
||||
iokit
|
||||
libdrm_getfb2
|
||||
makeinfo_html
|
||||
opencl_d3d11
|
||||
@@ -4014,7 +4015,7 @@ android_camera_indev_deps="android camera2ndk mediandk pthreads"
|
||||
alsa_indev_deps="alsa"
|
||||
alsa_outdev_deps="alsa"
|
||||
avfoundation_indev_deps="avfoundation corevideo coremedia pthreads AVCaptureSession"
|
||||
avfoundation_indev_suggest="coregraphics applicationservices"
|
||||
avfoundation_indev_suggest="coregraphics applicationservices iokit"
|
||||
avfoundation_indev_extralibs="-framework Foundation"
|
||||
audiotoolbox_outdev_deps="audiotoolbox pthreads AudioObjectPropertyAddress"
|
||||
audiotoolbox_outdev_extralibs="-framework AudioToolbox -framework CoreAudio"
|
||||
@@ -7108,6 +7109,7 @@ enabled avfoundation && {
|
||||
disable coregraphics applicationservices
|
||||
check_lib coregraphics CoreGraphics/CoreGraphics.h CGGetActiveDisplayList "-framework CoreGraphics" ||
|
||||
check_lib applicationservices ApplicationServices/ApplicationServices.h CGGetActiveDisplayList "-framework ApplicationServices"
|
||||
check_lib iokit IOKit/IOKitLib.h IOServiceGetMatchingServices "-framework IOKit"
|
||||
check_objc_class AVFoundation/AVFoundation.h AVCaptureSession
|
||||
}
|
||||
|
||||
|
||||
@@ -150,8 +150,8 @@ AVFoundation supports the following options:
|
||||
@table @option
|
||||
|
||||
@item -list_devices <TRUE|FALSE>
|
||||
If set to true, a list of all available input devices is given showing all
|
||||
device names and indices.
|
||||
If set to true, a list of all available input devices is given showing their
|
||||
index, name, unique ID, and USB serial number (when available).
|
||||
|
||||
@item -video_device_index <INDEX>
|
||||
Specify the video device by its index. Overrides anything given in the input filename.
|
||||
@@ -159,6 +159,18 @@ Specify the video device by its index. Overrides anything given in the input fil
|
||||
@item -audio_device_index <INDEX>
|
||||
Specify the audio device by its index. Overrides anything given in the input filename.
|
||||
|
||||
@item -video_device_id <ID>
|
||||
Specify the video device by a prefixed identifier, as shown by
|
||||
@option{-list_devices}: @code{uid:<UID>} selects by AVFoundation unique ID,
|
||||
@code{serial:<SERIAL>} by USB serial number. The unique ID of a UVC device
|
||||
may not be stable across reboots. Only UVC devices expose a serial number.
|
||||
Overrides anything given in the input filename.
|
||||
|
||||
@item -audio_device_id <ID>
|
||||
Specify the audio device by a prefixed identifier. See
|
||||
@option{-video_device_id}; for audio, the serial is only available for
|
||||
class-compliant USB devices.
|
||||
|
||||
@item -pixel_format <FORMAT>
|
||||
Request the video device to use a specific pixel format.
|
||||
If the specified format is not supported, a list of available formats is given
|
||||
|
||||
@@ -25,7 +25,19 @@
|
||||
* @author Thilo Borgmann <thilo.borgmann@mail.de>
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#if HAVE_IOKIT
|
||||
# import <IOKit/IOKitLib.h>
|
||||
/* kIOMainPortDefault replaced kIOMasterPortDefault in the macOS 12 SDK. */
|
||||
# if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 120000
|
||||
# define AVF_IO_MAIN_PORT_DEFAULT kIOMainPortDefault
|
||||
# else
|
||||
# define AVF_IO_MAIN_PORT_DEFAULT kIOMasterPortDefault
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "libavutil/channel_layout.h"
|
||||
@@ -113,6 +125,8 @@ typedef struct
|
||||
char *url;
|
||||
char *video_filename;
|
||||
char *audio_filename;
|
||||
char *video_device_id;
|
||||
char *audio_device_id;
|
||||
|
||||
int num_video_devices;
|
||||
|
||||
@@ -479,7 +493,7 @@ static int add_video_device(AVFormatContext *s, AVCaptureDevice *video_device)
|
||||
NSDictionary *capture_dict;
|
||||
dispatch_queue_t queue;
|
||||
|
||||
if (ctx->video_device_index < ctx->num_video_devices) {
|
||||
if (!ctx->video_is_screen) {
|
||||
capture_input = (AVCaptureInput*) [[[AVCaptureDeviceInput alloc] initWithDevice:video_device error:&error] autorelease];
|
||||
} else {
|
||||
capture_input = (AVCaptureInput*) video_device;
|
||||
@@ -852,6 +866,221 @@ static NSArray* getDevicesWithMediaType(AVMediaType mediaType) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if HAVE_IOKIT
|
||||
static int avf_io_get_string(io_service_t service, CFStringRef key, char *buf, size_t size)
|
||||
{
|
||||
CFTypeRef ref = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0);
|
||||
int ok = ref && CFGetTypeID(ref) == CFStringGetTypeID() && CFStringGetCString(ref, buf, size, kCFStringEncodingUTF8);
|
||||
if (ref)
|
||||
CFRelease(ref);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int avf_io_get_uint32(io_service_t service, CFStringRef key, uint32_t *out)
|
||||
{
|
||||
CFTypeRef ref = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0);
|
||||
int ok = ref && CFGetTypeID(ref) == CFNumberGetTypeID() && CFNumberGetValue(ref, kCFNumberSInt32Type, out);
|
||||
if (ref)
|
||||
CFRelease(ref);
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_IOKIT
|
||||
static int64_t avf_usb_location_for_serial(const char *serial)
|
||||
{
|
||||
int64_t location = -1;
|
||||
io_iterator_t iterator = 0;
|
||||
io_service_t service;
|
||||
|
||||
if (IOServiceGetMatchingServices(AVF_IO_MAIN_PORT_DEFAULT,
|
||||
IOServiceMatching("IOUSBHostDevice"), &iterator) != KERN_SUCCESS)
|
||||
return -1;
|
||||
|
||||
while (location < 0 && (service = IOIteratorNext(iterator))) {
|
||||
char found[512];
|
||||
uint32_t loc;
|
||||
if (avf_io_get_string(service, CFSTR("USB Serial Number"), found, sizeof(found)) &&
|
||||
!strcmp(found, serial) &&
|
||||
avf_io_get_uint32(service, CFSTR("locationID"), &loc))
|
||||
location = loc;
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iterator);
|
||||
|
||||
return location;
|
||||
}
|
||||
#else
|
||||
static int64_t avf_usb_location_for_serial(const char *serial)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if HAVE_IOKIT
|
||||
static NSString *avf_usb_serial_for_location(uint32_t location)
|
||||
{
|
||||
NSString *serial = nil;
|
||||
io_iterator_t iterator = 0;
|
||||
io_service_t service;
|
||||
|
||||
if (IOServiceGetMatchingServices(AVF_IO_MAIN_PORT_DEFAULT,
|
||||
IOServiceMatching("IOUSBHostDevice"), &iterator) != KERN_SUCCESS)
|
||||
return nil;
|
||||
|
||||
while (!serial && (service = IOIteratorNext(iterator))) {
|
||||
char found[512];
|
||||
uint32_t loc;
|
||||
if (avf_io_get_uint32(service, CFSTR("locationID"), &loc) && loc == location &&
|
||||
avf_io_get_string(service, CFSTR("USB Serial Number"), found, sizeof(found)))
|
||||
serial = [NSString stringWithUTF8String:found];
|
||||
IOObjectRelease(service);
|
||||
}
|
||||
IOObjectRelease(iterator);
|
||||
|
||||
return serial;
|
||||
}
|
||||
#else
|
||||
static NSString *avf_usb_serial_for_location(uint32_t location)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
// USB video uniqueID = locationID<<32 | VID<<16 | PID; match on the locationID.
|
||||
static AVCaptureDevice *avf_video_device_with_serial(const char *serial,
|
||||
NSArray *devices, NSArray *devices_muxed, int *is_muxed)
|
||||
{
|
||||
int64_t location = avf_usb_location_for_serial(serial);
|
||||
NSArray *lists[2] = { devices, devices_muxed };
|
||||
|
||||
if (location < 0)
|
||||
return nil;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (AVCaptureDevice *device in lists[i]) {
|
||||
NSString *uid = [device uniqueID];
|
||||
if ([uid hasPrefix:@"0x"] &&
|
||||
(uint32_t)(strtoull([uid UTF8String], NULL, 16) >> 32) == (uint32_t)location) {
|
||||
*is_muxed = (i == 1);
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// CoreAudio USB-audio UID: AppleUSBAudioEngine:manufacturer:device:serial:interfaces
|
||||
#if HAVE_IOKIT
|
||||
static NSString *avf_audio_serial_for_uid(NSString *uid)
|
||||
{
|
||||
if (![uid hasPrefix:@"AppleUSBAudioEngine:"])
|
||||
return nil;
|
||||
NSArray<NSString *> *fields = [uid componentsSeparatedByString:@":"];
|
||||
if (fields.count < 5)
|
||||
return nil;
|
||||
NSString *serial = fields[fields.count - 2];
|
||||
if (serial.length && avf_usb_location_for_serial([serial UTF8String]) >= 0)
|
||||
return serial;
|
||||
return nil;
|
||||
}
|
||||
#else
|
||||
static NSString *avf_audio_serial_for_uid(NSString *uid)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
static AVCaptureDevice *avf_audio_device_with_serial(const char *serial, NSArray *devices)
|
||||
{
|
||||
NSString *want = [NSString stringWithUTF8String:serial];
|
||||
|
||||
for (AVCaptureDevice *device in devices)
|
||||
if ([avf_audio_serial_for_uid([device uniqueID]) isEqualToString:want])
|
||||
return device;
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
static AVCaptureDevice *avf_device_with_uid(const char *uid,
|
||||
NSArray *devices, NSArray *devices_muxed, int *is_muxed)
|
||||
{
|
||||
NSString *want = [NSString stringWithUTF8String:uid];
|
||||
NSArray *lists[2] = { devices, devices_muxed };
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (AVCaptureDevice *device in lists[i]) {
|
||||
if ([[device uniqueID] isEqualToString:want]) {
|
||||
if (is_muxed)
|
||||
*is_muxed = (i == 1);
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Best-effort serial string for -list_devices, or nil.
|
||||
static NSString *avf_device_listing_serial(AVCaptureDevice *device)
|
||||
{
|
||||
NSString *uid = [device uniqueID];
|
||||
|
||||
if ([uid hasPrefix:@"0x"]) {
|
||||
unsigned long long value = strtoull([uid UTF8String], NULL, 16);
|
||||
NSString *serial = avf_usb_serial_for_location((uint32_t)(value >> 32));
|
||||
return serial.length ? serial : nil;
|
||||
}
|
||||
return avf_audio_serial_for_uid(uid);
|
||||
}
|
||||
|
||||
static void avf_log_device_entry(AVFContext *ctx, int index, AVCaptureDevice *device)
|
||||
{
|
||||
NSString *serial = avf_device_listing_serial(device);
|
||||
|
||||
if (serial)
|
||||
av_log(ctx, AV_LOG_INFO, "[%d] %s [uid:%s] [serial:%s]\n", index,
|
||||
[[device localizedName] UTF8String], [[device uniqueID] UTF8String],
|
||||
[serial UTF8String]);
|
||||
else
|
||||
av_log(ctx, AV_LOG_INFO, "[%d] %s [uid:%s]\n", index,
|
||||
[[device localizedName] UTF8String], [[device uniqueID] UTF8String]);
|
||||
}
|
||||
|
||||
// Returns 1 if a device id was set (*device = match, or nil after logging on miss), else 0.
|
||||
static int avf_device_from_id(AVFContext *ctx, AVMediaType media_type,
|
||||
NSArray *devices, NSArray *devices_muxed,
|
||||
const char *id, AVCaptureDevice **device, int *is_muxed)
|
||||
{
|
||||
BOOL is_audio = [media_type isEqualToString:AVMediaTypeAudio];
|
||||
const char *kind = is_audio ? "Audio" : "Video";
|
||||
const char *value = NULL;
|
||||
|
||||
if (!id)
|
||||
return 0;
|
||||
|
||||
if (av_strstart(id, "serial:", &value)) {
|
||||
if (is_audio)
|
||||
*device = avf_audio_device_with_serial(value, devices);
|
||||
else
|
||||
*device = avf_video_device_with_serial(value, devices, devices_muxed, is_muxed);
|
||||
if (!*device)
|
||||
av_log(ctx, AV_LOG_ERROR,
|
||||
"%s capture device with serial number '%s' not found\n", kind, value);
|
||||
} else if (av_strstart(id, "uid:", &value)) {
|
||||
*device = avf_device_with_uid(value, devices, devices_muxed, is_muxed);
|
||||
if (!*device)
|
||||
av_log(ctx, AV_LOG_ERROR,
|
||||
"%s capture device with unique ID '%s' not found\n", kind, value);
|
||||
} else {
|
||||
av_log(ctx, AV_LOG_ERROR,
|
||||
"Invalid %s device id '%s': expected 'uid:<unique ID>' or 'serial:<serial number>'\n",
|
||||
is_audio ? "audio" : "video", id);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int avf_read_header(AVFormatContext *s)
|
||||
{
|
||||
int ret = 0;
|
||||
@@ -863,6 +1092,7 @@ static int avf_read_header(AVFormatContext *s)
|
||||
// Find capture device
|
||||
NSArray *devices = getDevicesWithMediaType(AVMediaTypeVideo);
|
||||
NSArray *devices_muxed = getDevicesWithMediaType(AVMediaTypeMuxed);
|
||||
NSArray *audio_devices = getDevicesWithMediaType(AVMediaTypeAudio);
|
||||
|
||||
ctx->num_video_devices = [devices count] + [devices_muxed count];
|
||||
|
||||
@@ -879,14 +1109,12 @@ static int avf_read_header(AVFormatContext *s)
|
||||
int index = 0;
|
||||
av_log(ctx, AV_LOG_INFO, "AVFoundation video devices:\n");
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
const char *name = [[device localizedName] UTF8String];
|
||||
index = [devices indexOfObject:device];
|
||||
av_log(ctx, AV_LOG_INFO, "[%d] %s\n", index, name);
|
||||
index = [devices indexOfObject:device];
|
||||
avf_log_device_entry(ctx, index, device);
|
||||
}
|
||||
for (AVCaptureDevice *device in devices_muxed) {
|
||||
const char *name = [[device localizedName] UTF8String];
|
||||
index = [devices count] + [devices_muxed indexOfObject:device];
|
||||
av_log(ctx, AV_LOG_INFO, "[%d] %s\n", index, name);
|
||||
index = [devices count] + [devices_muxed indexOfObject:device];
|
||||
avf_log_device_entry(ctx, index, device);
|
||||
}
|
||||
#if !TARGET_OS_IPHONE && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070
|
||||
if (num_screens > 0) {
|
||||
@@ -901,9 +1129,8 @@ static int avf_read_header(AVFormatContext *s)
|
||||
av_log(ctx, AV_LOG_INFO, "AVFoundation audio devices:\n");
|
||||
devices = getDevicesWithMediaType(AVMediaTypeAudio);
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
const char *name = [[device localizedName] UTF8String];
|
||||
int index = [devices indexOfObject:device];
|
||||
av_log(ctx, AV_LOG_INFO, "[%d] %s\n", index, name);
|
||||
int index = [devices indexOfObject:device];
|
||||
avf_log_device_entry(ctx, index, device);
|
||||
}
|
||||
goto fail;
|
||||
}
|
||||
@@ -921,7 +1148,11 @@ static int avf_read_header(AVFormatContext *s)
|
||||
sscanf(ctx->audio_filename, "%d", &ctx->audio_device_index);
|
||||
}
|
||||
|
||||
if (ctx->video_device_index >= 0) {
|
||||
if (avf_device_from_id(ctx, AVMediaTypeVideo, devices, devices_muxed,
|
||||
ctx->video_device_id, &video_device, &ctx->video_is_muxed)) {
|
||||
if (!video_device)
|
||||
goto fail;
|
||||
} else if (ctx->video_device_index >= 0) {
|
||||
if (ctx->video_device_index < ctx->num_video_devices) {
|
||||
if (ctx->video_device_index < [devices count]) {
|
||||
video_device = [devices objectAtIndex:ctx->video_device_index];
|
||||
@@ -1022,23 +1253,23 @@ static int avf_read_header(AVFormatContext *s)
|
||||
}
|
||||
|
||||
// get audio device
|
||||
if (ctx->audio_device_index >= 0) {
|
||||
NSArray *devices = getDevicesWithMediaType(AVMediaTypeAudio);
|
||||
|
||||
if (ctx->audio_device_index >= [devices count]) {
|
||||
if (avf_device_from_id(ctx, AVMediaTypeAudio, audio_devices, nil,
|
||||
ctx->audio_device_id, &audio_device, NULL)) {
|
||||
if (!audio_device)
|
||||
goto fail;
|
||||
} else if (ctx->audio_device_index >= 0) {
|
||||
if (ctx->audio_device_index >= [audio_devices count]) {
|
||||
av_log(ctx, AV_LOG_ERROR, "Invalid audio device index\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
audio_device = [devices objectAtIndex:ctx->audio_device_index];
|
||||
audio_device = [audio_devices objectAtIndex:ctx->audio_device_index];
|
||||
} else if (ctx->audio_filename &&
|
||||
strncmp(ctx->audio_filename, "none", 4)) {
|
||||
if (!strncmp(ctx->audio_filename, "default", 7)) {
|
||||
audio_device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
||||
} else {
|
||||
NSArray *devices = getDevicesWithMediaType(AVMediaTypeAudio);
|
||||
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
for (AVCaptureDevice *device in audio_devices) {
|
||||
if (!strncmp(ctx->audio_filename, [[device localizedName] UTF8String], strlen(ctx->audio_filename))) {
|
||||
audio_device = device;
|
||||
break;
|
||||
@@ -1059,7 +1290,7 @@ static int avf_read_header(AVFormatContext *s)
|
||||
}
|
||||
|
||||
if (video_device) {
|
||||
if (ctx->video_device_index < ctx->num_video_devices) {
|
||||
if (!ctx->video_is_screen) {
|
||||
av_log(s, AV_LOG_DEBUG, "'%s' opened\n", [[video_device localizedName] UTF8String]);
|
||||
} else {
|
||||
av_log(s, AV_LOG_DEBUG, "'%s' opened\n", [[video_device description] UTF8String]);
|
||||
@@ -1316,6 +1547,8 @@ static const AVOption options[] = {
|
||||
{ "list_devices", "list available devices", offsetof(AVFContext, list_devices), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "video_device_index", "select video device by index for devices with same name (starts at 0)", offsetof(AVFContext, video_device_index), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "audio_device_index", "select audio device by index for devices with same name (starts at 0)", offsetof(AVFContext, audio_device_index), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "video_device_id", "select video device by prefixed id (uid:<unique ID> or serial:<USB serial number>)", offsetof(AVFContext, video_device_id), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "audio_device_id", "select audio device by prefixed id (uid:<unique ID> or serial:<USB serial number>)", offsetof(AVFContext, audio_device_id), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "pixel_format", "set pixel format", offsetof(AVFContext, pixel_format), AV_OPT_TYPE_PIXEL_FMT, {.i64 = AV_PIX_FMT_YUV420P}, 0, INT_MAX, AV_OPT_FLAG_DECODING_PARAM},
|
||||
{ "framerate", "set frame rate", offsetof(AVFContext, framerate), AV_OPT_TYPE_VIDEO_RATE, {.str = "ntsc"}, 0, INT_MAX, AV_OPT_FLAG_DECODING_PARAM },
|
||||
{ "video_size", "set video size", offsetof(AVFContext, width), AV_OPT_TYPE_IMAGE_SIZE, {.str = NULL}, 0, 0, AV_OPT_FLAG_DECODING_PARAM },
|
||||
|
||||
Reference in New Issue
Block a user