This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

The following commit(s) were added to refs/heads/master by this push:
     new c2802e520a avdevice/avfoundation: add device selection by USB serial 
and unique ID
c2802e520a is described below

commit c2802e520a08dbf0c939ec8b5653b29dfd886427
Author:     ninbura <[email protected]>
AuthorDate: Sat Jul 11 00:27:50 2026 -0600
Commit:     Romain Beauxis <[email protected]>
CommitDate: Sun Aug 2 12:10:11 2026 -0500

    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 <[email protected]>
---
 Changelog                  |   1 +
 configure                  |   4 +-
 doc/indevs.texi            |  16 ++-
 libavdevice/avfoundation.m | 273 +++++++++++++++++++++++++++++++++++++++++----
 4 files changed, 271 insertions(+), 23 deletions(-)

diff --git a/Changelog b/Changelog
index ce57599aa2..38f1e10263 100644
--- a/Changelog
+++ b/Changelog
@@ -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:
diff --git a/configure b/configure
index 61a948d14d..8ac272a155 100755
--- a/configure
+++ b/configure
@@ -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
 }
 
diff --git a/doc/indevs.texi b/doc/indevs.texi
index 7c3c7114a4..31dfc098bb 100644
--- a/doc/indevs.texi
+++ b/doc/indevs.texi
@@ -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
diff --git a/libavdevice/avfoundation.m b/libavdevice/avfoundation.m
index 6f52989fc9..88b814ecf1 100644
--- a/libavdevice/avfoundation.m
+++ b/libavdevice/avfoundation.m
@@ -25,7 +25,19 @@
  * @author Thilo Borgmann <[email protected]>
  */
 
+#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 },

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to