How can I locate a UVC camera for PTZ control by AVCaptureDevice.unique_id

I'm writing a program to control a PTZ camera connected via USB.

I can get access to target camera's unique_id, and also other infos provided by AVFoundation. But I don't know how to locate my target USB device to send a UVC ControlRequest.

There's many Cameras with same VendorID and ProductID connected at a time, so I need a more exact way to find out which device is my target.

It looks that the unique_id provided is (locationID<<32|VendorID<<16|ProductID) as hex string, but I'm not sure if I can always assume this behavior won't change.

Is there's a document declares how AVFoundation generate the unique_id for USB camera, so I can assume this convert will always work? Or is there's a way to send a PTZ control request to AVCaptureDevice?

https://stackoverflow.com/questions/40006908/usb-interface-of-an-avcapturedevice

I have seen this similar question. But I'm worrying that Exacting LocationID+VendorID+ProductID from unique_id seems like programming to implementation instead of interface. So, if there's any other better way to control my camera?

here's my example code for getting unique_id:

//
// camera_unique_id_test.mm
//
// 测试代码:使用C++获取当前系统摄像头的AVCaptureDevice unique_id
//
// 编译命令:
// clang++ -framework AVFoundation -framework CoreMedia -framework Foundation
// camera_unique_id_test.mm -o camera_unique_id_test
//

#include <iostream>
#include <string>
#include <vector>

#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>

struct CameraInfo {
  std::string uniqueId;
};

std::vector<CameraInfo> getAllCameraDevices() {
  std::vector<CameraInfo> cameras;

  @autoreleasepool {
    NSArray<AVCaptureDevice*>* devices =
        [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    AVCaptureDevice* defaultDevice =
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // 遍历所有设备
    for (AVCaptureDevice* device in devices) {
      CameraInfo info;

      // 获取unique_id
      info.uniqueId = std::string([device.uniqueID UTF8String]);
      cameras.push_back(info);
    }
  }

  return cameras;
}

int main(int argc, char* argv[]) {
  std::vector<CameraInfo> cameras = getAllCameraDevices();

  for (size_t i = 0; i < cameras.size(); i++) {
    const CameraInfo& camera = cameras[i];
    std::cout << "   设备 " << (i + 1) << ":" << std::endl;
    std::cout << "     unique_id:     " << camera.uniqueId << std::endl;
  }

  return 0;
}

and here's my code for UVC control:


// clang++ -framework Foundation -framework IOKit uvc_test.cpp -o uvc_test

#include <iostream>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOMessage.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USB.h>

CFStringRef CreateCFStringFromIORegistryKey(io_service_t ioService,
                                            const char* key) {
  CFStringRef keyString = CFStringCreateWithCString(kCFAllocatorDefault, key,
                                                    kCFStringEncodingUTF8);
  if (!keyString)
    return nullptr;

  CFStringRef result = static_cast<CFStringRef>(
      IORegistryEntryCreateCFProperty(ioService, keyString, kCFAllocatorDefault,
                                      kIORegistryIterateRecursively));
  CFRelease(keyString);
  return result;
}

std::string GetStringFromIORegistry(io_service_t ioService, const char* key) {
  CFStringRef cfString = CreateCFStringFromIORegistryKey(ioService, key);
  if (!cfString)
    return "";

  char buffer[256];
  Boolean success = CFStringGetCString(cfString, buffer, sizeof(buffer),
                                       kCFStringEncodingUTF8);
  CFRelease(cfString);

  return success ? std::string(buffer) : std::string("");
}

uint32_t GetUInt32FromIORegistry(io_service_t ioService, const char* key) {
  CFStringRef keyString = CFStringCreateWithCString(kCFAllocatorDefault, key,
                                                    kCFStringEncodingUTF8);
  if (!keyString)
    return 0;

  CFNumberRef number = static_cast<CFNumberRef>(
      IORegistryEntryCreateCFProperty(ioService, keyString, kCFAllocatorDefault,
                                      kIORegistryIterateRecursively));
  CFRelease(keyString);

  if (!number)
    return 0;

  uint32_t value = 0;
  CFNumberGetValue(number, kCFNumberSInt32Type, &value);
  CFRelease(number);
  return value;
}

int main() {
  // Get matching dictionary for USB devices
  CFMutableDictionaryRef matchingDict =
      IOServiceMatching(kIOUSBDeviceClassName);

  // Get iterator for matching services
  io_iterator_t serviceIterator;
  IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict,
                               &serviceIterator);

  // Iterate through matching devices
  io_service_t usbService;
  while ((usbService = IOIteratorNext(serviceIterator))) {
    uint32_t locationId = GetUInt32FromIORegistry(usbService, "locationID");
    uint32_t vendorId = GetUInt32FromIORegistry(usbService, "idVendor");
    uint32_t productId = GetUInt32FromIORegistry(usbService, "idProduct");

    IOCFPlugInInterface** plugInInterface = nullptr;
    IOUSBDeviceInterface** deviceInterface = nullptr;
    SInt32 score;

    // Get device plugin interface
    IOCreatePlugInInterfaceForService(usbService, kIOUSBDeviceUserClientTypeID,
                                      kIOCFPlugInInterfaceID, &plugInInterface,
                                      &score);
    // Get device interface

    (*plugInInterface)
        ->QueryInterface(plugInInterface,
                         CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID),
                         (LPVOID*)&deviceInterface);

    (*plugInInterface)->Release(plugInInterface);

    // Try to find UVC control interface using CreateInterfaceIterator
    io_iterator_t interfaceIterator;
    IOUSBFindInterfaceRequest interfaceRequest;
    interfaceRequest.bInterfaceClass = kUSBVideoInterfaceClass;      // 14
    interfaceRequest.bInterfaceSubClass = kUSBVideoControlSubClass;  // 1
    interfaceRequest.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
    interfaceRequest.bAlternateSetting = kIOUSBFindInterfaceDontCare;

    (*deviceInterface)
        ->CreateInterfaceIterator(deviceInterface, &interfaceRequest,
                                  &interfaceIterator);
    (*deviceInterface)->Release(deviceInterface);

    io_service_t usbInterface = IOIteratorNext(interfaceIterator);
    IOObjectRelease(interfaceIterator);

    if (usbInterface) {
      std::cout << "Get UVC device with:" << std::endl;
      std::cout << "locationId: " << std::hex << locationId << std::endl;
      std::cout << "vendorId: " << std::hex << vendorId << std::endl;
      std::cout << "productId: " << std::hex << productId << std::endl
                << std::endl;
      IOObjectRelease(usbInterface);
    }

    IOObjectRelease(usbService);
  }

  IOObjectRelease(serviceIterator);
}

We parse the locationID out of the AVCaptureDevice.uniqueID and then find the IORegistry node with that locationID.

Is there's a document declares how AVFoundation generate the unique_id for USB camera,

no

so I can assume this convert will always work?

you can't

Or is there's a way to send a PTZ control request to AVCaptureDevice?

not that I know of. As far as I know, the only way is what you're doing.

It looks that the unique_id provided is (locationID<<32|VendorID<<16|ProductID) as hex string, but I'm not sure if I can always assume this behavior won't change.

Correct - it has changed in the past, it might change at any time in the future. Not all AVCaptureDevices are UVC, but they all have uniqueIDs.

If you would like an API to clearly identify an AVCapture device in the IORegistry, please file a bug. I already did (in 2019, FB6146541)

How can I locate a UVC camera for PTZ control by AVCaptureDevice.unique_id
 
 
Q