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:
ninbura
2026-07-11 00:27:50 -06:00
committed by Romain Beauxis
parent f0fc529cbe
commit c2802e520a
4 changed files with 271 additions and 23 deletions

View File

@@ -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
View File

@@ -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
}

View File

@@ -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

View File

@@ -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 },