最近的項目里面需要對UVC攝像頭進行操控,簡單的了解了下相關的知識。
首先UVC全稱為USB video(device) class,是微軟與另外幾家設備廠商聯(lián)合推出的為USB視頻捕獲設備定義的協(xié)議標準,目前已成為USB org標準之一。在USB中文網(wǎng)上其實有比較詳細的描述,但是新手直接上來就看這個協(xié)議其實是比較懵逼的筹淫。所以可以參考UVCCamera這個安卓項目的源碼去輔助理解。
USB描述符
當我們連接到一個UVC設備之后其實第一步應該先獲取它的描述符來看看它具體支持哪些操作。UVCCamera里是使用libusb獲取到USB的設備描述符之后在uvc_scan_control里面解析的庸推。實際上我們也可以利用安卓應用層的UsbDeviceConnection.getRawDescriptors接口獲取到USB的描述符然后再java層解析:
val manager = context.getSystemService(AppCompatActivity.USB_SERVICE) as UsbManager
val deviceIterator: Iterator<UsbDevice> = manager.deviceList.values.iterator()
while (deviceIterator.hasNext()) {
val device = deviceIterator.next()
if (device.vendorId == targetVid && device.productId == targetPid) {
val connect = manager.openDevice(device)
val desc = connect.rawDescriptors
// 解析usb描述符
connect.close()
return
}
}
這里拿到的是一個byte數(shù)組,我們先要理解什么是usb描述符才能去解析它。usb描述符其實就是描述usb的屬性和用途的,四種主要描述符的邏輯結構大概如下:
- 設備描述符: 每一個USB設備只有一個設備描述符浇冰,主要向主機說明設備類型贬媒、端點0最大包長、設備版本肘习、配置數(shù)量等等
- 配置描述符: 每一個USB設備至少有一個或者多個配置描述符,但是主機同一時間只能選擇某一種配置,標準配置描述符主要向主機描述當前配置下的設備屬性若专、所需電流豁翎、支持的接口數(shù)、配置描述符集合長度等等投蝉。
- 接口描述符 : 每一個USB配置下至少有一個或者多個接口描述符养葵,接口描述符主要說明設備類型、此接口下使用的端點數(shù)(不包括0號端點)墓拜,一個接口就是實現(xiàn)一種功能港柜,實現(xiàn)這種功能可能需要端點0就夠了,可能還需要其它的端點配合咳榜。
- 端點描述符: 每一個USB接口下可以有多個端點描述符夏醉,端點描述符用來描述符端點的各種屬性。
端點是實現(xiàn)USB設備功能的物理緩沖區(qū)實體涌韩,USB主機和設備是通過端點進行數(shù)據(jù)交互的
描述符解析
所有類型的描述符的前兩個字節(jié)定義都是一樣的,第一個字節(jié)指定描述符的長度畔柔,而第二個字節(jié)表示描述符類型。所以我們拿到rawDescriptors之后可以用下面的代碼去遍歷解析:
var index = 0
val descriptorTypes = mapOf(
0x01.toByte() to "DEVICE",
0x02.toByte() to "CONFIG",
0x04.toByte() to "INTERFACE",
0x05.toByte() to "ENDPOINT",
)
while (index < desc.size) {
descriptorTypes[desc[index + 1]]?.let {
val indent = " ".repeat(desc[index + 1].toInt())
Log.d(TAG, "${indent}$it")
}
index += desc[index]
}
我這個調試設備的描述符解析如下:
DEVICE
CONFIG
INTERFACE
ENDPOINT
INTERFACE
ENDPOINT
INTERFACE
ENDPOINT
ENDPOINT
可以看到它有一個設備描述符,這個設備描述符下有個一個配置描述符,這個配置描述符下有三個接口描述符,每個接口描述符下又有一到兩個端點描述符臣樱。
知道了描述符的類型就能在USB標準里找到它具體的數(shù)據(jù)結構去解析,例如設備描述符的定義如下:
struct libusb_device_descriptor {
/** Size of this descriptor (in bytes) */
uint8_t bLength;
/** Descriptor type. Will have value
* \ref libusb_descriptor_type::LIBUSB_DT_DEVICE LIBUSB_DT_DEVICE in this
* context. */
uint8_t bDescriptorType;
/** USB specification release number in binary-coded decimal. A value of
* 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, etc. */
uint16_t bcdUSB;
/** USB-IF class code for the device. See \ref libusb_class_code. */
uint8_t bDeviceClass;
/** USB-IF subclass code for the device, qualified by the bDeviceClass
* value */
uint8_t bDeviceSubClass;
/** USB-IF protocol code for the device, qualified by the bDeviceClass and
* bDeviceSubClass values */
uint8_t bDeviceProtocol;
/** Maximum packet size for endpoint 0 */
uint8_t bMaxPacketSize0;
/** USB-IF vendor ID */
uint16_t idVendor;
/** USB-IF product ID */
uint16_t idProduct;
/** Device release number in binary-coded decimal */
uint16_t bcdDevice;
/** Index of string descriptor describing manufacturer */
uint8_t iManufacturer;
/** Index of string descriptor describing product */
uint8_t iProduct;
/** Index of string descriptor containing device serial number */
uint8_t iSerialNumber;
/** Number of possible configurations */
uint8_t bNumConfigurations;
};
描述符類型定義的id可以參考lsusb的libusb_descriptor_type枚舉:
enum libusb_descriptor_type {
/** Device descriptor. See libusb_device_descriptor. */
LIBUSB_DT_DEVICE = 0x01,
/** Configuration descriptor. See libusb_config_descriptor. */
LIBUSB_DT_CONFIG = 0x02,
/** String descriptor */
LIBUSB_DT_STRING = 0x03,
/** Interface descriptor. See libusb_interface_descriptor. */
LIBUSB_DT_INTERFACE = 0x04,
/** Endpoint descriptor. See libusb_endpoint_descriptor. */
LIBUSB_DT_ENDPOINT = 0x05,
/** Interface Association Descriptor.
* See libusb_interface_association_descriptor */
LIBUSB_DT_INTERFACE_ASSOCIATION = 0x0b,
/** BOS descriptor */
LIBUSB_DT_BOS = 0x0f,
/** Device Capability descriptor */
LIBUSB_DT_DEVICE_CAPABILITY = 0x10,
/** HID descriptor */
LIBUSB_DT_HID = 0x21,
/** HID report descriptor */
LIBUSB_DT_REPORT = 0x22,
/** Physical descriptor */
LIBUSB_DT_PHYSICAL = 0x23,
/** Hub descriptor */
LIBUSB_DT_HUB = 0x29,
/** SuperSpeed Hub descriptor */
LIBUSB_DT_SUPERSPEED_HUB = 0x2a,
/** SuperSpeed Endpoint Companion descriptor */
LIBUSB_DT_SS_ENDPOINT_COMPANION = 0x30
};
可以看到描述符的類型其實不止上面四種,還有很多其他的類型靶擦。例如我就能從UVC 相機終端描述符里面的bmControls字段解析出相機具體支持的操作:
mControls:使用位圖來表示支持的視頻流。
- D0:掃描模式 //掃描模式(逐行掃描或隔行掃描)
- D1:自動曝光模式
- D2:自動曝光優(yōu)先級
- D3:曝光時間(絕對值)
- D4:曝光時間(相對)
- D5:焦點(絕對)
- D6:焦點(相對)
- ...
在libusb里面這個UVC相機終端描述符會作為接口描述符的拓展信息保存:
static int parse_interface(libusb_context *ctx,
struct libusb_interface *usb_interface, const uint8_t *buffer, int size)
{
...
begin = buffer;
/* Skip over any interface, class or vendor descriptors */
while (size >= DESC_HEADER_LENGTH) {
...
/* If we find another "proper" descriptor then we're done */
if (header->bDescriptorType == LIBUSB_DT_INTERFACE ||
header->bDescriptorType == LIBUSB_DT_ENDPOINT ||
header->bDescriptorType == LIBUSB_DT_CONFIG ||
header->bDescriptorType == LIBUSB_DT_DEVICE)
break;
buffer += header->bLength;
parsed += header->bLength;
size -= header->bLength;
}
/* Copy any unknown descriptors into a storage area for */
/* drivers to later parse */
ptrdiff_t len = buffer - begin;
if (len > 0) {
void *extra = malloc((size_t)len);
...
memcpy(extra, begin, (size_t)len);
ifp->extra = extra;
ifp->extra_length = (int)len;
}
...
}
所以在uvc_scan_control里面就從接口描述符的extra信息里面去解析UVC的相關描述符:
uvc_error_t uvc_scan_control(uvc_device_t *dev, uvc_device_info_t *info) {
...
for (interface_idx = 0; interface_idx < info->config->bNumInterfaces; ++interface_idx) {
if_desc = &info->config->interface[interface_idx].altsetting[0];
MARK("interface_idx=%d:bInterfaceClass=%02x,bInterfaceSubClass=%02x", interface_idx, if_desc->bInterfaceClass, if_desc->bInterfaceSubClass);
// select first found Video control
if (if_desc->bInterfaceClass == LIBUSB_CLASS_VIDEO/*14*/ && if_desc->bInterfaceSubClass == 1) // Video, Control
break;
...
}
...
buffer = if_desc->extra;
buffer_left = if_desc->extra_length;
while (buffer_left >= 3) { // parseX needs to see buf[0,2] = length,type
block_size = buffer[0];
parse_ret = uvc_parse_vc(dev, info, buffer, block_size);
if (parse_ret != UVC_SUCCESS) {
ret = parse_ret;
break;
}
buffer_left -= block_size;
buffer += block_size;
}
...
}
uvc_error_t uvc_parse_vc(uvc_device_t *dev, uvc_device_info_t *info,
const unsigned char *block, size_t block_size) {
int descriptor_subtype;
uvc_error_t ret = UVC_SUCCESS;
UVC_ENTER();
if (block[1] != LIBUSB_DT_CS_INTERFACE/*36*/) { // not a CS_INTERFACE descriptor??
UVC_EXIT(UVC_SUCCESS);
return UVC_SUCCESS; // UVC_ERROR_INVALID_DEVICE;
}
descriptor_subtype = block[2];
switch (descriptor_subtype) {
case UVC_VC_HEADER:
ret = uvc_parse_vc_header(dev, info, block, block_size);
break;
case UVC_VC_INPUT_TERMINAL:
ret = uvc_parse_vc_input_terminal(dev, info, block, block_size);
break;
case UVC_VC_OUTPUT_TERMINAL:
break;
case UVC_VC_SELECTOR_UNIT:
break;
case UVC_VC_PROCESSING_UNIT:
ret = uvc_parse_vc_processing_unit(dev, info, block, block_size);
break;
case UVC_VC_EXTENSION_UNIT:
ret = uvc_parse_vc_extension_unit(dev, info, block, block_size);
break;
default:
LOGW("UVC_ERROR_INVALID_DEVICE:descriptor_subtype=0x%02x", descriptor_subtype);
ret = UVC_ERROR_INVALID_DEVICE;
}
UVC_EXIT(ret);
return ret;
}
只要找到bInterfaceClass等于14,bInterfaceSubClass等于1的視頻控制接口,然后在它的拓展信息里面找到UVC_VC_INPUT_TERMINAL(0x02)
類型的描述符就是我們需要的UVC 相機終端描述符
USB通訊
前面有說到端點是實現(xiàn)USB設備功能的物理緩沖區(qū)實體雇毫,USB主機和設備是通過端點進行數(shù)據(jù)交互的
,之前做HID設備通訊的時候流程是找到bInterfaceClass
為UsbConstants.USB_CLASS_HID(0x03)
類型的接口,在它下面找到輸入端點去寫入請求,然后找到輸出端點去讀取設備響應玄捕。
但是UVC的攝像頭控制并不是用視頻控制接口去讀寫,而是直接使用USB設備不屬于任何接口的0號端口去進行通訊。
例如uvc_get_pantilt_abs里面在傳輸數(shù)據(jù)的時候就沒有指定端口號:
uvc_error_t uvc_get_pantilt_abs(uvc_device_handle_t *devh, int32_t *pan, int32_t *tilt,
enum uvc_req_code req_code) {
uint8_t data[8];
uvc_error_t ret;
ret = libusb_control_transfer(devh->usb_devh, REQ_TYPE_GET, req_code,
UVC_CT_PANTILT_ABSOLUTE_CONTROL << 8,
devh->info->ctrl_if.input_term_descs->request,
data, sizeof(data), CTRL_TIMEOUT_MILLIS);
if (LIKELY(ret == sizeof(data))) {
*pan = DW_TO_INT(data);
*tilt = DW_TO_INT(data + 4);
return UVC_SUCCESS;
} else {
return ret;
}
}
因為在libusb_control_transfer里面調用libusb_fill_control_transfer去填充信息的時候就會把端口指定為0號端口
int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle,
uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
unsigned char *data, uint16_t wLength, unsigned int timeout)
{
...
libusb_fill_control_transfer(transfer, dev_handle, buffer,
sync_transfer_cb, &completed, timeout); // 填充transfer信息
transfer->flags = LIBUSB_TRANSFER_FREE_BUFFER;
r = libusb_submit_transfer(transfer); // 發(fā)送請求
if (UNLIKELY(r < 0)) {
libusb_free_transfer(transfer);
return r;
}
sync_transfer_wait_for_completion(transfer); // 等待回復
...
}
static inline void libusb_fill_control_transfer(
struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data,
unsigned int timeout)
{
struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *) buffer;
transfer->dev_handle = dev_handle;
transfer->endpoint = 0; // 指定0號端口
...
}
涉及到使用USB進行通訊的4種方式:
- 控制傳輸 - 設備接入主機時棚放,需要通過控制傳輸去獲取USB設備的描述符以及對設備進行識別枚粘,在設備的枚舉過程中都是使用控制傳輸進行數(shù)據(jù)交換。
- 同步傳輸 - 也叫等時傳輸,用于要求數(shù)據(jù)連續(xù)飘蚯、實時且數(shù)據(jù)量大的場合馍迄,其對傳輸延時十分敏感福也,類似用于USB攝像設備,USB語音設備等等攀圈。
- 中斷傳輸 - 用于數(shù)據(jù)量小的數(shù)據(jù)不連續(xù)的但實時性高的場合的一種傳輸方式暴凑,主要應用于人機交互設備(HID)中的USB鼠標和USB鍵盤等。
- 批量傳輸 - 用于數(shù)據(jù)量大但對時間要求又不高的場合的一種傳輸方式赘来,類似用于USB打印機和USB掃描儀等等现喳。
控制傳輸
控制傳輸是usb設備一定會支持的傳輸方式,因為描述符就是通過這種方式獲取的.
在安卓應用層我們可以通過調用UsbDeviceConnection.controlTransfer來實現(xiàn),參考UVCCamera里面uvc_get_pantilt_abs里面獲取PanTilt值的c代碼,在java層可以用下面代碼獲取
private const val CONTROL_REQ_TYPE_GET = 0xa1
private const val UVC_GET_CUR = 0x81
val connection = usbManager.openDevice(device)
// 先claim bInterfaceClass為CC_VIDEO(0x0E) bInterfaceSubClass為SC_VIDEOCONTROL(0x01)的攝像頭控制接口
val vcInterface = UsbUtils.getInterface(device, UsbConstants.USB_CLASS_VIDEO, USB_SUBCLASS_VIDEO_CONTROL)
connection.claimInterface(vcInterface, true)
// 然后發(fā)送控制指令獲取PanTilt絕對值
val buff = ByteArray(8)
val index = getPanTiltControlIndex(connection)
val value = CT_PANTILT_ABSOLUTE_CONTROL.shl(8)
connection.controlTransfer(CONTROL_REQ_TYPE_GET, UVC_GET_CUR, value, index, buff, buff.size, 100)
// buff前四個byte組合起來是pan值
// buff后四個byte組合起來是tilt值
val pan = bytes[0].toUByte().toInt().shl(0) or
bytes[1].toUByte().toInt().shl(8) or
bytes[2].toUByte().toInt().shl(16) or
bytes[3].toUByte().toInt().shl(24)
val tilt = bytes[4].toUByte().toInt().shl(0) or
bytes[5].toUByte().toInt().shl(8) or
bytes[6].toUByte().toInt().shl(16) or
bytes[7].toUByte().toInt().shl(24)
connection.releaseInterface(usbInterface)
connection.close()
這里解釋下上面的值如何來的,首先看GET_CUR的文檔:
requestType | request | value | index | buffer | length |
---|---|---|---|---|---|
10100001(接口或實體) — — — — — 10100010(端點) |
GET_CUR GET_MIN GET_MAX GET_RES GET_LEN GET_INFO GET_DEF |
UVC中大多數(shù)情況下取值都為控制選擇器CS(高字節(jié)),低字節(jié)為零撕捍。當實體ID取不同值時則該字段取值也會有所不同 | 實體ID(高字節(jié))拿穴、接口(低字節(jié)) — — — — — 端點(低字節(jié)) |
用來接收數(shù)據(jù)或者發(fā)送數(shù)據(jù)的buffer | buffer的大小 |
value
例如我們現(xiàn)在要獲取PanTilt的絕對值,那么在value字段部分文檔里面可以看到當Entity ID值為Camera Terminal時:
ControlSelector | Value |
---|---|
... | ... |
CT_PANTILT_ABSOLUTE_CONTROL | 0x0D |
... | ... |
又因為value的值為控制選擇器CS(高字節(jié)),低字節(jié)為零忧风。
所以value的值應該是0x0D << 8
index
而Entity ID值為Camera Terminal
指的是終端描述符的bTerminalID, 由于它屬于控制接口描述符的extra信息,所以還需要指的該接口的bInterfaceNunber:
val USB_DESC_TYPE_INTERFACE_LEN = 9.toByte()
val USB_DESC_TYPE_INTERFACE = 0x04.toByte()
val USB_DESC_TYPE_CS_INTERFACE = 0x24.toByte()
val USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL = 0x02.toByte()
val USB_SUBCLASS_VIDEO_CONTROL = 0x01
private fun getPanTiltControlIndex(connection: UsbDeviceConnection): Int {
val desc = connection.rawDescriptors ?: return -1
var index = 0
var isInVideoControlInterface = false
var interfaceNumber = 0
while (index < desc.size) {
if (desc[index] == USB_DESC_TYPE_INTERFACE_LEN
&& desc[index + 1] == USB_DESC_TYPE_INTERFACE
&& desc[index + 5] == UsbConstants.USB_CLASS_VIDEO.toByte()
&& desc[index + 6] == USB_SUBCLASS_VIDEO_CONTROL.toByte()
) {
// 找到bInterfaceClass為CC_VIDEO(0x0E) bInterfaceSubClass為SC_VIDEOCONTROL(0x01)的攝像頭控制接口的interfaceNumber
isInVideoControlInterface = true
interfaceNumber = desc[index + 2].toInt()
} else if (isInVideoControlInterface) {
if (desc[index + 1] != USB_DESC_TYPE_CS_INTERFACE) {
return -1
}
if (desc[index + 2] == USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL) {
// 在攝像頭控制接口下找到bDescriptorType為CS_INTERFACE(0x24) bDescriptorSubtype為VC_INPUT_TERMINAL(0x02)的攝像頭終端描述符
// 獲取它的bTerminalID用來和前面獲取到的攝像頭控制接口的interfaceNumber拼接成index
return desc[index + 3].toInt().shl(8).or(interfaceNumber)
}
}
index += desc[index]
}
return -1
}
request
我們要獲取的是當前值所以request是GET_CUR(0x81),其他值的定如下:
名稱 | 值 | 說明 |
---|---|---|
RC_UNDEFINED | 0x00 | 未定義 |
SET_CUR | 0x01 | 設置屬性 |
GET_CUR | 0x81 | 獲取當前屬性 |
GET_MIN | 0x82 | 獲取最小設置屬性 |
GET_MAX | 0x83 | 獲取最大設置屬性 |
GET_RES | 0x84 | 獲取分辨率屬性 |
GET_LEN | 0x85 | 獲取數(shù)據(jù)長度屬性 |
GET_INF | 0x86 | 獲取設備支持的特定類請求屬性 |
GET_DEF | 0x87 | 獲取默認屬性 |
requestType
最后再來看requestType,由于index需要選擇的是實體ID(高字節(jié))默色、接口(低字節(jié))
所以requestType應該是10100001(接口或實體)
。它的值這么奇怪是因為requestType的每個bit都是有意義的:
由于命令接受者為接口,所以我們在發(fā)送控制指令前還是需要找到這個接口用claimInterface去鎖定它狮腿。
使用uvc控制指令的坑
似乎是因為使用安卓的Camera2等接口去讀取攝像頭畫面的時候會使用到這個控制接口,所以如果在預覽的時候去claimInterface鎖定它就會造成畫面卡死腿宰。
看起來似乎需要完全使用uvc自己從視頻流接口讀取畫面,而不能一半用uvc去控制攝像頭另一半用安卓原生api去獲取預覽畫面≡迪幔或者用取巧的方法在發(fā)送控制指令的時候先停止預覽,發(fā)送完再開始吃度。
其他三種傳輸
其他三種傳輸都是需要找到對應的端點才能進行通訊的,所以需要先獲取到端點信息.用UsbInterface.getEndpoint去遍歷接口下的端點,然后判斷端點的類型和讀寫方向:
for (i in 0 until usbInterface.endpointCount) {
val usbEndpoint = usbInterface.getEndpoint(i)
when (usbEndpoint.type) {
UsbConstants.USB_ENDPOINT_XFER_BULK -> {
// 批量傳輸
if (usbEndpoint.direction == UsbConstants.USB_DIR_OUT) {
// 可寫入端點
} else if (usbEndpoint.direction == UsbConstants.USB_DIR_IN) {
// 可讀取端點
}
}
UsbConstants.USB_ENDPOINT_XFER_ISOC -> {
// 中斷傳輸
}
UsbConstants.USB_ENDPOINT_XFER_INT -> {
// 同步傳輸
}
}
}
他們最終都是通過UsbDeviceConnection.bulkTransfer去調用的,例如可以先寫入請求在讀取響應:
val requestBuffer = ByteArray(256)
// 將數(shù)據(jù)保存到requestBuffer
// 然后往寫入端點寫入請求數(shù)據(jù)
connection.bulkTransfer(outPoint, sendBuff, sendBuff.size, timeout)
// 從讀取端點讀取響應
val responseBuffer = ByteArray(256)
connection.bulkTransfer(inPoint, responseBuffer, responseBuffer.size, timeout)