AVCaptureDevice.uniqueID for UVC devices is unstable - bug or overstated documentation?

The documentation for AVCaptureDevice.uniqueID states the following:

Capture devices have a unique identifier that persists on one system across device connections and disconnections, application restarts, and reboots of the system itself. You can store the value returned by this property to recall or track the status of a specific device in the future.

For UVC capture devices this documentation does not hold. The video uniqueID is a hex string of the form 0x<locationID><vendorID><productID>, and the identifying half is the locationID (bus number plus port path). Which identifies a port, not a device.

I ran a suite of tests with three identical Elgato 4K X capture cards connected to a Mac Studio w/ M3 Ultra running macOS 26.5.2, and reproduced my findings on a MacBook w/ M3 Pro (same macOS version). See the script at the bottom of the post for how uniqueId & USB serial number are being retrieved.

1. The uniqueID follows the port. Swapping two cards between two built-in ports swaps their uniqueIDs:

# Before swap.
4K X   uid=0x2000000fd9009b    serial=A7SNB50424UBQI
4K X   uid=0x12000000fd9009b   serial=A7SNB504219J0R

# After swapping the cards between the same two ports.
4K X   uid=0x2000000fd9009b    serial=A7SNB504219J0R
4K X   uid=0x12000000fd9009b   serial=A7SNB50424UBQI

An app that stored 0x2000000fd9009b to recall a specific capture card now silently opens another.

2. A reboot alone can swap uniqueIDs. External USB controllers (here, PCIe USB cards in two Thunderbolt enclosures) can race for bus numbers at boot, so with every cable left in place, a reboot swapped two of the cards:

# Before reboot.
4K X   uid=0x262000000fd9009b  serial=A7SNB504219J0R
4K X   uid=0x252000000fd9009b  serial=A7SNB50423R73R

# After reboot, no cables touched.
4K X   uid=0x262000000fd9009b  serial=A7SNB50423R73R
4K X   uid=0x252000000fd9009b  serial=A7SNB504219J0R

This behavior is intermittent, a second reboot changed nothing, but a third caused another swap. Cards left alone in built-in ports retain their uniqueIDs across reboots in my testing; the failure requires dynamically enumerated external USB controllers.

3. Even the product ID tail can drift. One unit intermittently enumerates with idProduct 0x009c instead of 0x009b, same port (USB PCIe card in a Thunderbolt enclosure), cables untouched:

# Before reboot.
4K X   uid=0x222000000fd9009b  serial=A7SNB50424UBQI

# After reboot.
4K X   uid=0x222000000fd9009c  serial=A7SNB50424UBQI

IOKit and AVFoundation agree each boot... So the change is upstream of both? I'm uncertain where to place blame for this specific issue (UVC device or macOS).


Audio on the same physical units is unaffected. The audio uniqueID (AppleUSBAudioEngine:...:<serial>:...) embeds the USB serial and stayed stable through every test. So AVCaptureDevice can provide a stable per-device identifier, just not for UVC video devices.

Questions:

  1. Is this a bug, or is the documentation overstating the persistence guarantee for USB video devices?
  2. What is the supported way to identify a specific physical UVC video device across reboots and port changes? The USB serial number is stable and is what I've fallen back on via IOKit, but there is no documented AVFoundation API to retrieve USB serial number from a UVC video AVCaptureDevice.

Related: thread 803759, where the locationID-derived format is described.

Script used for all output above (swift ./list-uvc.swift):

import AVFoundation
import IOKit

func usbSerial(forLocation location: UInt32) -> String? {
    var iterator: io_iterator_t = 0
    guard IOServiceGetMatchingServices(kIOMainPortDefault,
              IOServiceMatching("IOUSBHostDevice"), &iterator) == KERN_SUCCESS else { return nil }
    defer { IOObjectRelease(iterator) }

    var result: String?
    var service = IOIteratorNext(iterator)
    while service != 0 {
        var loc: UInt32 = 0
        if let ref = IORegistryEntryCreateCFProperty(service, "locationID" as CFString, kCFAllocatorDefault, 0)?.takeRetainedValue(),
           let num = ref as? NSNumber {
            loc = num.uint32Value
        }
        if loc == location,
           let ref = IORegistryEntryCreateCFProperty(service, "USB Serial Number" as CFString, kCFAllocatorDefault, 0)?.takeRetainedValue(),
           let serial = ref as? String {
            result = serial
        }
        IOObjectRelease(service)
        if result != nil { break }
        service = IOIteratorNext(iterator)
    }
    return result
}

let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.external],
                                               mediaType: .video,
                                               position: .unspecified)

for device in session.devices {
    let uid = device.uniqueID
    let location = UInt32(truncatingIfNeeded: strtoull(uid, nil, 16) >> 32)
    let serial = usbSerial(forLocation: location) ?? "N/A"
    print("\(device.localizedName)  uid=\(uid)  serial=\(serial)")
}
Answered by DTS Engineer in 902220022

(As a quick side note, I'd recommend avoiding "Comment" and just replying using a normal post. Among other issues, comments don't notify the same way posts do, which makes it easy for me to miss that you've posted.)

Anyway, getting to specifics:

Per my example, adding PID is potentially problematic. In my testing, it's not reliable, as noted in the original post.

I don't know what's going on with the hardware you're testing with, but this:

  1. Even the product ID tail can drift. One unit intermittently enumerates with idProduct 0x009c instead of 0x009b, same port (USB PCIe card in a Thunderbolt enclosure), cables untouched:

...is pretty weird. That's literally the "Product ID", so having it change directly implies that something is an entirely "different" device.

IOKit and AVFoundation agree each boot... So the change is upstream of both?

It's possible that a driver (KEXT/DEXT) is modifying the PID at load, as this is one of the common modifications codeless KEXTs/DEXTs make to "tune" device behavior. If a codeless DEXT was involved, that also might explain the intermittent behavior— DEXTs can't actually load "at boot", which means you can get different drivers loading if the hardware is attached and boot vs. hot plugged. Note that I don't think our drivers did this, so this would involve a 3rd party driver, even if our driver is what ultimately ended up controlling the device*.

*Codeless KEXTs/DEXTs work by injecting themselves into the earliest part of "probe", modifying the device’s configuration in the I/O registry, then failing probe so that they never actually match against their target.

Is this a bug, or is the documentation overstating the persistence guarantee for USB video devices?

A bit of both? AVCaptureDevice could probably do "more" to try and ensure uniqueness; however, the problem is that this:

What is the supported way to identify a specific physical UVC video device across reboots and port changes?

...isn't ACTUALLY possible. More specifically:

The USB serial number is stable and is what I've fallen back on via IOKit.

That value is stable in the sense that IOKit is returning the data it got from the device (ignoring issues like the edge case above), so it shouldn't really change. However, the problem is that the USB spec doesn't actually require it to be meaningful/unique/useful. The values iManufacturer, iProduct, and iSerialNumber are all string values which the hardware can put anything they want into. Most products do use them "sensibly" (particularly iManufacturer and iProduct), but particularly cheap hardware often uses a fixed iSerialNumber, since it means you don't need to customize the firmware for every device you make.

Though, if VID is unchanging, it wouldn't be a terrible idea to append that to serial.

This VID is "stable", in that it’s specifically assigned and managed by USB-IF, so using someone else’s VID would be "wrong". Most commercial developers buy a VID from the USB-IF, at which point they then decide how they'll assign PIDs to their own products. However, smaller volume products sometimes use the VID of someone in their supply chain (chipset vendor, hardware assembler, etc.) and that vendor also assigns them a PID, so that their product doesn't overlap with someone else.

Shifting to here:

  1. A reboot alone can swap uniqueIDs. External USB controllers (here, PCIe USB cards in two Thunderbolt enclosures) can race for bus numbers at boot, so with every cable left in place, a reboot swapped two of the cards:

Ironically, this is actually a good example of what makes this so messy, as I think the reason bus location was factored into this was actually to make the uniqueID MORE stable when dealing with cheap hardware. AVCaptureDevice was introduced in macOS 10.7, which shipped in July 2011, just a few months after the first Thunderbolt Mac shipped in February of 2011.

Before Thunderbolt, the scenario you’re describing basically can't/doesn't happen. Even if you add multiple PCI USB cards (into PCI slots), the order everything initializes in will be fixed, which ends up stabilizing cheap devices we couldn't uniquely identify. More to the point, that's the same mechanism that stabilizes the ID if/when you plug multiple devices into the Mac’s USB ports.

None of that changes the issue you're dealing with, but hopefully that background context clarifies things.

I've already submitted one (FB23524998), apologies I should have included that in my post.

Thank you for that. I don't know if (or when) anything will change here, but I will discuss the issue with the team and let you know if they have anything else to share.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Please file a bug about this (mine, FB12782458, is two years old).

In the meantime, you can synthesize your own persistent identifier which survives topology changes from vid, pid and serial number.

(As a quick side note, I'd recommend avoiding "Comment" and just replying using a normal post. Among other issues, comments don't notify the same way posts do, which makes it easy for me to miss that you've posted.)

Anyway, getting to specifics:

Per my example, adding PID is potentially problematic. In my testing, it's not reliable, as noted in the original post.

I don't know what's going on with the hardware you're testing with, but this:

  1. Even the product ID tail can drift. One unit intermittently enumerates with idProduct 0x009c instead of 0x009b, same port (USB PCIe card in a Thunderbolt enclosure), cables untouched:

...is pretty weird. That's literally the "Product ID", so having it change directly implies that something is an entirely "different" device.

IOKit and AVFoundation agree each boot... So the change is upstream of both?

It's possible that a driver (KEXT/DEXT) is modifying the PID at load, as this is one of the common modifications codeless KEXTs/DEXTs make to "tune" device behavior. If a codeless DEXT was involved, that also might explain the intermittent behavior— DEXTs can't actually load "at boot", which means you can get different drivers loading if the hardware is attached and boot vs. hot plugged. Note that I don't think our drivers did this, so this would involve a 3rd party driver, even if our driver is what ultimately ended up controlling the device*.

*Codeless KEXTs/DEXTs work by injecting themselves into the earliest part of "probe", modifying the device’s configuration in the I/O registry, then failing probe so that they never actually match against their target.

Is this a bug, or is the documentation overstating the persistence guarantee for USB video devices?

A bit of both? AVCaptureDevice could probably do "more" to try and ensure uniqueness; however, the problem is that this:

What is the supported way to identify a specific physical UVC video device across reboots and port changes?

...isn't ACTUALLY possible. More specifically:

The USB serial number is stable and is what I've fallen back on via IOKit.

That value is stable in the sense that IOKit is returning the data it got from the device (ignoring issues like the edge case above), so it shouldn't really change. However, the problem is that the USB spec doesn't actually require it to be meaningful/unique/useful. The values iManufacturer, iProduct, and iSerialNumber are all string values which the hardware can put anything they want into. Most products do use them "sensibly" (particularly iManufacturer and iProduct), but particularly cheap hardware often uses a fixed iSerialNumber, since it means you don't need to customize the firmware for every device you make.

Though, if VID is unchanging, it wouldn't be a terrible idea to append that to serial.

This VID is "stable", in that it’s specifically assigned and managed by USB-IF, so using someone else’s VID would be "wrong". Most commercial developers buy a VID from the USB-IF, at which point they then decide how they'll assign PIDs to their own products. However, smaller volume products sometimes use the VID of someone in their supply chain (chipset vendor, hardware assembler, etc.) and that vendor also assigns them a PID, so that their product doesn't overlap with someone else.

Shifting to here:

  1. A reboot alone can swap uniqueIDs. External USB controllers (here, PCIe USB cards in two Thunderbolt enclosures) can race for bus numbers at boot, so with every cable left in place, a reboot swapped two of the cards:

Ironically, this is actually a good example of what makes this so messy, as I think the reason bus location was factored into this was actually to make the uniqueID MORE stable when dealing with cheap hardware. AVCaptureDevice was introduced in macOS 10.7, which shipped in July 2011, just a few months after the first Thunderbolt Mac shipped in February of 2011.

Before Thunderbolt, the scenario you’re describing basically can't/doesn't happen. Even if you add multiple PCI USB cards (into PCI slots), the order everything initializes in will be fixed, which ends up stabilizing cheap devices we couldn't uniquely identify. More to the point, that's the same mechanism that stabilizes the ID if/when you plug multiple devices into the Mac’s USB ports.

None of that changes the issue you're dealing with, but hopefully that background context clarifies things.

I've already submitted one (FB23524998), apologies I should have included that in my post.

Thank you for that. I don't know if (or when) anything will change here, but I will discuss the issue with the team and let you know if they have anything else to share.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

That's literally the "Product ID", so having it change directly implies that something is an entirely "different" device.

Yeah, I thought I may just be seeing things that aren't there, or that maybe I somehow modified the character while taking notes. But there more I thought about it the less likely it seemed possible that I could've modified a character in the middle of the terminal output while copying and pasting the text. After enough testing I was able to replicate it multiple times, the PID is definitely changing on some of these capture cards between reboots.


It's possible that a driver (KEXT/DEXT) is modifying the PID at load.

There's no Kernel Extension involved here though, right? I don't even have that capability enabled on the machine in question via recovery mode, unless I'm misunderstanding what you're saying and we're referring to totally different mechanisms. UVC devices are "driverless", right?


Nevertheless, uniqueID does seem like a difficult issue to solve given the context. I'm aware that serial isn't always reliable depending on the situation... but I bet it would work 90%+ of the time (much like UID). I submitted a PR on the FFmpeg repository, which is now merged into Master, that allows users to select video/audio capture devices by UID or serial. Previously you could only select them by name & index which had many issues.

For the grand majority of users UID should be stable. If you're in a situation where USB bus number is changing (physically changing USB ports or Thunderbolt race conditions) serial is likely to be stable. If you're in a situation where you have multiple cheap UVC devices that share a serial number, and bus location is shifting... you'd have to fall back to using UID and verifying it after each boot. I suppose you could automate start up of Thunderbolt hubs/docks with smart plugs to guarantee the startup order, but I digress.

I imagine the user base who falls into the category where both UID & serial is unreliable is likely to be exceedingly small. Would there be any possibility of adding an alternativeID? Or something along those lines.

After enough testing, I was able to replicate it multiple times. The PID is definitely changing on some of these capture cards between reboots.

All I can really say to that is that it’s deeply weird and the sort of thing that the system can't really adapt to. In the more controlled environment of an individual app, you can build around the assumption that the user’s camera configuration is relatively stable, use heuristics to do a "best guess" of which device is which (as the configuration changes), then provide some mechanism to override that behavior if/when the configuration DOES change. That's not something the framework can do.

There's no Kernel Extension involved here though, right?

There's always a kernel extension somewhere... Now, just to clarify, what I was saying there was that this was the only point besides the hardware that COULD change this state, not that it DID change this state.

Moving to specifics...

I don't even have that capability enabled on the machine in question via recovery mode, unless I'm misunderstanding what you're saying and we're referring to totally different mechanisms. UVC devices are "driverless", right?

There's always a driver involved at some level, since that's how the hardware "enters" the system. Practically speaking, "driverless" really means "doesn't require a 3rd party driver", not "doesn't involve a driver at all". I don't think our own drivers would do this, but this system is big and complicated enough that I try not to be too sure of anything.

However, the other issue here is that DEXT (DriverKit Extensions) are built on our KEXT infrastructure and are broadly capable of doing very similar things, including modifying device-level metadata (like the vid/pid) prior to the device becoming visible to the system. I wouldn't normally mention it, except that the way DEXT loading works can mean that you specifically get different configuration/behavior when:

  • The machine is rebooted with a device attached.

VS.

  • The device is hot-plugged while the machine is running.

...because the DEXT loads in the second case but not in the first.

I imagine the user base who falls into the category where both UID & serial is unreliable is likely to be exceedingly small. Would there be any possibility of adding an alternativeID? Or something along those lines.

The suggestion I've made to the team is to provide an accessor for the IOKit object, which would then let you use whatever criteria you wanted to invent. Unfortunately, the real difficulty here is scheduling and prioritization, not necessarily the API work itself.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

The Elgato 4K X enumerates differently depending on the bus speed it is enumerated at. Its PID is different on USB 2, USB 3 Gen 1 (5Gbps) and USB 3 Gen 2 (10Gbps).

One of the confounding variables here is that you're using a PCIe USB card in a Thunderbolt enclosure, and that card seems to be enumerating the 4K X at 5Gbps sometimes, and 10Gbps at other times. The first thing I'd try is a different (better) USB cable.

This won't fix your original problem, but why do you care? You can synthesize your own persistent device identifiers. Undoubtedly, it would be convenient to have a uniqueID which is persistent and stable across topology changes, but you can create your own property and tack it onto your abstraction.

The suggestion I've made to the team is to provide an accessor for the IOKit object, which would then let you use whatever criteria you wanted to invent. Unfortunately, the real difficulty here is scheduling and prioritization, not necessarily the API work itself.

Sounds like a good solution, I hope to see it implemented one day.


The Elgato 4K X enumerates differently depending on the bus speed it is enumerated at. Its PID is different on USB 2, USB 3 Gen 1 (5Gbps) and USB 3 Gen 2 (10Gbps).

I see, that would make perfect sense, I'll try and replicate the issue and check the negotiated speed. I'm using 3 Anker USB 3.2 Gen 2x2 cables, but maybe one of the three is faulty.


This won't fix your original problem, but why do you care? You can synthesize your own persistent device identifiers. Undoubtedly, it would be convenient to have a uniqueID which is persistent and stable across topology changes, but you can create your own property and tack it onto your abstraction.

You're right. However, it's troublesome to convince maintainers of other applications like FFmpeg & OBS to accept a PR/fix. If there was an ID provided by AVCaptureDevice that persisted with reboots it requires little justification.

AVCaptureDevice.uniqueID for UVC devices is unstable - bug or overstated documentation?
 
 
Q