diff --git a/Changelog b/Changelog index ce57599aa2..38f1e10263 100644 --- a/Changelog +++ b/Changelog @@ -9,6 +9,7 @@ version : - 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 -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 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 Specify the audio device by its index. Overrides anything given in the input filename. +@item -video_device_id +Specify the video device by a prefixed identifier, as shown by +@option{-list_devices}: @code{uid:} selects by AVFoundation unique ID, +@code{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 +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 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 */ +#include "config.h" + #import +#if HAVE_IOKIT +# import + /* 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 #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 *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:' or 'serial:'\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: or serial:)", 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: or serial:)", 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 },