10、USB视频设备开发:UVC协议与Android端实战

大家好,我是老张。今天我们来聊聊USB视频设备开发,也就是UVC协议。说实话,这个主题在Android开发里算是比较冷门的一块,但一旦你搞懂了,外接摄像头、USB相机这些功能就能信手拈来。

我记得第一次接触UVC是在一个工业平板项目上。客户要求外接一个高清摄像头做质检,我心想这不就是插上就能用吗?结果折腾了两周才搞定。嗯,今天就把这些经验分享给你。

10.1 UVC协议概述

UVC,全称USB Video Class,是USB组织定义的一套视频设备标准协议。说白了,就是让摄像头插上电脑或手机就能直接工作,不需要装驱动。

为什么需要UVC?你想想看,如果没有这个标准,每个摄像头厂商都得自己写驱动,那Android手机怎么支持几百种不同的USB摄像头?UVC就是解决了这个兼容性问题。

核心要点:UVC协议定义了视频流传输、设备控制、格式协商等标准接口。只要设备遵循UVC规范,主机端就能用统一的API操作它。

UVC协议主要包含以下几个层面:

  • 视频流接口:负责传输视频帧数据,支持YUV、MJPEG、H.264等格式
  • 控制接口:用于设置亮度、对比度、曝光等参数
  • 格式描述符:告诉主机摄像头支持哪些分辨率和帧率
  • 单元/终端:摄像头内部的功能模块,比如处理单元、扩展单元

我在项目中遇到过一个问题:某款工业摄像头在Windows上工作正常,但插到Android平板上就黑屏。后来发现是它只支持UVC 1.1的某种扩展格式,而Android的UVC驱动默认只支持1.0标准。这种坑,不踩一次真不知道。

10.2 Android端USB摄像头支持

Android从5.0开始就内置了UVC驱动,但真正好用的是从Android 8.0引入的Camera2 API。为什么?因为Camera2 API提供了更精细的摄像头控制能力。

Android系统对USB摄像头的支持分两层:

  1. 内核层:Linux内核的UVC驱动,负责枚举设备、传输数据
  2. 应用层:Camera2 API或第三方库,负责预览、拍照、参数设置

避坑指南:我曾经在Android 6.0的设备上调试UVC摄像头,发现Camera2 API根本用不了。后来查资料才知道,Camera2 API在Android 5.0引入,但直到8.0才完整支持USB摄像头。所以如果你还在用老版本系统,建议用第三方库如UVCCamera。

Camera2 API支持USB摄像头的关键类是CameraManagerCameraDevice。当你插入USB摄像头时,系统会把它当作一个独立的摄像头设备注册到CameraManager中。你可以像操作内置摄像头一样操作它。

10.3 UVC设备的枚举与参数设置

枚举UVC设备,说白了就是找到插在手机上的摄像头。我习惯用两种方式:

方式一:通过USB Host API枚举

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
for (UsbDevice device : deviceList.values()) {
    // 检查设备类是否为视频类(0x0E)
    if (device.getDeviceClass() == UsbConstants.USB_CLASS_VIDEO) {
        // 这就是UVC摄像头
    }
}

方式二:通过CameraManager枚举

CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
String[] cameraIdList = cameraManager.getCameraIdList();
for (String cameraId : cameraIdList) {
    CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
    // 检查是否为外部摄像头
    Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
    if (facing != null && facing == CameraCharacteristics.LENS_FACING_EXTERNAL) {
        // 这就是USB摄像头
    }
}

我个人更推荐第二种方式,因为它直接和Camera2 API对接,省去了很多底层操作。

参数设置这块,UVC摄像头支持哪些参数?我列个表给你看:

参数名称 UVC控制ID 说明
亮度 PU_BRIGHTNESS 0-255,默认128
对比度 PU_CONTRAST 0-255,默认128
饱和度 PU_SATURATION 0-255,默认128
曝光时间 PU_EXPOSURE_TIME 微秒级,自动/手动
白平衡 PU_WHITE_BALANCE 色温值,自动/手动

注意:不是所有UVC摄像头都支持全部参数。有些廉价摄像头只支持亮度和对比度。设置前最好先查询设备支持的控制项,否则会返回错误。

10.4 实战:外接USB摄像头预览与拍照

好了,理论说完了,我们来写点真东西。下面是一个完整的UVC摄像头预览+拍照示例。

首先,在AndroidManifest.xml中声明权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.usb.host" />

然后,打开摄像头并创建预览会话:

// 获取CameraManager
CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
String cameraId = null;

// 找到外部摄像头
for (String id : manager.getCameraIdList()) {
    CameraCharacteristics chars = manager.getCameraCharacteristics(id);
    if (chars.get(CameraCharacteristics.LENS_FACING) == 
        CameraCharacteristics.LENS_FACING_EXTERNAL) {
        cameraId = id;
        break;
    }
}

// 打开摄像头
manager.openCamera(cameraId, new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        // 创建预览请求
        SurfaceTexture texture = textureView.getSurfaceTexture();
        Surface surface = new Surface(texture);
        
        CaptureRequest.Builder builder = camera.createCaptureRequest(
            CameraDevice.TEMPLATE_PREVIEW);
        builder.addTarget(surface);
        
        // 创建会话
        camera.createCaptureSession(Arrays.asList(surface), 
            new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession session) {
                session.setRepeatingRequest(builder.build(), null, null);
            }
        }, null);
    }
}, null);

拍照功能稍微复杂一点。你需要创建一个ImageReader来接收拍照数据:

// 创建ImageReader,分辨率1920x1080,格式JPEG
ImageReader imageReader = ImageReader.newInstance(1920, 1080, 
    ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(reader -> {
    Image image = reader.acquireLatestImage();
    // 保存到文件
    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    saveToFile(bytes);
    image.close();
}, null);

// 创建拍照请求
CaptureRequest.Builder captureBuilder = camera.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(imageReader.getSurface());
session.capture(captureBuilder.build(), null, null);

个人经验:拍照时别忘了处理旋转问题。USB摄像头通常是横向安装的,但手机可能是竖屏。我习惯在拍照前设置JPEG_ORIENTATION参数,或者拍照后通过Exif信息旋转图片。

最后,别忘了在onPause()中释放资源:

@Override
protected void onPause() {
    super.onPause();
    if (cameraDevice != null) {
        cameraDevice.close();
        cameraDevice = null;
    }
}

嗯,到这里,一个完整的UVC摄像头预览和拍照功能就实现了。你可能会问:为什么我插上摄像头没反应?最常见的原因是权限问题——Android 6.0以上需要动态申请USB权限。记得在插入设备时弹出权限请求对话框。

我曾经在一个项目里调试了整整一天,最后发现是USB线的问题。有些廉价USB线只支持充电,不支持数据传输。所以如果你遇到设备枚举不到,先换根线试试。

UVC摄像头开发知识体系 UVC协议 协议标准 视频流接口 · 控制接口 格式描述符 · 单元/终端 Android支持 内核UVC驱动 Camera2 API封装 枚举与参数 USB Host API / CameraManager 实战开发 预览 · 拍照 · 参数设置 UVC协议 → Android支持 → 枚举参数 → 实战开发

好了,这一章的内容就到这里。UVC开发其实不难,关键是理解协议层次和Android的封装方式。下一章我们会深入讲解如何优化UVC摄像头的帧率和画质,以及如何处理多摄像头同时工作的情况。


公众号:蓝海资料掘金营,微信deep3321