ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android UVCCamera示例解析:从USB权限到H.264硬编码

Android UVCCamera示例解析:从USB权限到H.264硬编码 简介面向Android开发者的USB摄像头集成示例工程演示如何通过UVCCamera库在Android设备上实现外接摄像头视频预览。工程适配Android USB主机模式与Android Studio 4.x环境内容覆盖USB设备连接与断开事件监听、UVCCamera对象初始化、预览Surface绑定、分辨率与帧率等相机参数调整以及实时视频流显示等关键环节。包体共3846个文件约57.5MB以Java/C源码、class字节码、so动态库和jar包为主同时包含gradle构建脚本、XML资源、AndroidManifest权限声明、aidl接口定义及大量编译中间产物便于从源码到运行产物完整对照。目前已有460人学习下载适合需要为应用接入USB摄像头能力的Android中高级开发者可从中获取可运行Demo与集成思路处理不同硬件兼容和性能优化问题。1. Android UVCCamera 示例到底在解决什么问题把一个 UVC 协议的 USB 摄像头插到 Android 手机上系统相机里是看不到画面的。Android framework 的 CameraManager 只枚举主板传感器外接摄像头不会出现在 getCameraIdList() 里Camera2 API 也无从谈起。要让外接 UVC 设备出画面只能绕开 framework用 USB Host 权限走 libusb/libuvc 原生通道逐帧读取等时端点里的视频流转交 GPU 显示。「Android UVCCamera示例.rar」就是这条技术路线的工程打包一份带示例 App 的 UVC 库工程能申请 USB 权限、切换预览尺寸、硬编 H.264、拍 JPEG。这篇按解压、导入、跑通、改参数的顺序覆盖 Android Studio 导入、USB 权限回调、分辨率 16 对齐闪退以及预览、编码、拍照三条链路和验证命令适合做工业平板、USB 采集盒、显微镜相机、直播外接摄像头的 Android 工程师。2. 解压 Android UVCCamera 示例压缩包后先理清目录再谈导入2.1 示例压缩包的典型目录结构与 jniLibs 归属这类 .rar 在 Windows 下解压时先注意三件事路径必须全英文且不含空格目录层数不要超过三级解压后留意杀毒软件是否把.so文件隔离了。前两点影响的是如果之后要重编 JNIndk-build 对长路径和中文路径非常敏感第三点更隐蔽——.so被隔离后工程一切正常装到真机上运行才报UnsatisfiedLinkError排查方向就偏了。解压完成后先对照一下目录结构常见的打包方式如下表压缩包内路径内容导入后角色UVCCamera/Android Library 模块Java 层 APIUSBMonitor、UVCCamera被示例 App 依赖UVCCamera/src/main/jni/libusb、libuvc 与 JNI 层源码只有重编 so 时才需要 NDKUVCCameraDemo/app/src/main/jniLibs/预编译 libUVCCamera.soarmeabi-v7a、arm64-v8a真机运行的关键UVCCameraDemo/示例 App 工程直接导入的入口多数流传的压缩包追根溯源来自 GitHub 上一个流传很广的 UVCCamera 开源库里面同时带着源码和编译产物所以出现「既有 jni 源码又有预编译 so」的双份结构。导入之前先确认jniLibs目录如果只有armeabi-v7a而没有arm64-v8a而你的测试机是 64 位 CPUApp 默认按 64 位进程启动加载 so 时直接崩。临时解法是在build.gradle里加abiFilters armeabi-v7a强制 32 位进程先跑通再说。2.2 导入前的 SDK、NDK 与 Gradle 对齐老工程的通病是 compileSdk 太低、Gradle 版本旧、还没有 AndroidX。用 Android Studio 打开前先把build.gradle里这几项改齐android { compileSdk 34 defaultConfig { minSdk 18 targetSdk 30 ndk { abiFilters armeabi-v7a, arm64-v8a } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }minSdk 18是示例工程自带的下限低于这个值的设备没有稳定的 USB Host 权限流程不需要为了兼容再降低targetSdk保持在 30 附近是有意为之——老示例把 JPEG 直接写公共存储目录targetSdk 升到 31 以上会被分区存储拦截未适配 MediaStore 前别往上提。abiFilters限定两个 arm ABI避免装到 x86 模拟器上运行到一半才报库找不到。compileSdk 34 对这套老代码没有影响问题集中在 JNI不在 Java 编译目标。如果后续打算自己改 JNI 重编 so用 NDK r21 及以下的版本成功率最高r23 之后对 Android.mk 里废弃标志位的检查更严格。只跑示例 App 的话jni 目录完全不用碰直接用包里预编译好的 so 即可。2.3 用 Android Studio 导入示例工程并连真机的最小步骤导入过程按下面顺序操作能避开绝大多数 sync 报错解压到纯英文路径目录层级不超过三级。Windows 下路径总长超过 240 个字符ndk-build 和 CMake 都可能直接失败。Android Studio 里 File → Open选择包含settings.gradle的根目录不是选 UVCCameraDemo 那一层。老工程的 Gradle wrapper 版本过低时AS 会提示升级把gradle-wrapper.properties里的 distributionUrl 改成 8.x 即可。Gradle sync 的报错集中在两类compileSdk 不存在、android.support 包已废弃。前者按本机已装 SDK 版本修改后者把android.support.*依赖逐个替换成androidx.*对应包。手机开启开发者选项并授权 USB 调试。插摄像头时用带独立供电的 OTG 线或 HUB工业平板上尤其重要。供电不足的表现是设备反复枚举日志里 USB disconnect 循环出现。点 Run。App 启动后插上摄像头系统弹出 USB 权限对话框允许后预览画面出现。跑起来之前先确认设备是否被系统识别adb shell dumpsys usb | grep -B2 -A10 -i uvcdumpsys usb能看到已连接 USB 设备的 vendorId、productId以及内核是否把它识别为 UVC 设备。如果这里有信息但 App 不弹权限框问题多半在 Manifest 里 device_filter 的 VID/PID 过滤如果弹了框但打开失败是接口被占用的老问题下一章展开。3. UVCCamera 示例的 USB 权限回调与 16 对齐分辨率列表3.1 USB Host 模式下的设备枚举与权限回调闭环示例工程的 USB 管理走 USBMonitor 这个封装类核心是 OnDeviceConnectListener 的四个回调。权限语义是这样闭环的设备插入触发onAttach里面只做一件事——申请权限用户点允许之后系统才回调onConnect此时拿到的UsbControlBlock才能传给 UVCCamera 去 open拔出时触发onDisconnect这里必须释放摄像头否则下一次插入会拿到已经 close 的坏句柄。private final USBMonitor.OnDeviceConnectListener listener new USBMonitor.OnDeviceConnectListener() { Override public void onAttach(UsbDevice device) { // 设备插入先要权限别在这里 open mUSBMonitor.requestPermission(device); } Override public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) { if (mUVCCamera null) { mUVCCamera new UVCCamera(); mUVCCamera.open(ctrlBlock); } } Override public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) { releaseCamera(); // 释放顺序stopPreview - release - close } };回调的顺序不能乱在onAttach里直接 open 是新手最常见的错误这时代理权限还没拿到open 必然返回打开失败。onConnect里的createNew参数在设备重复连接时可能出现 false老代码里一般不去判断它但你可以在releaseCamera()之后顺手检查一次。权限授权一次后系统会记住「应用 设备」这个组合下次插入直接走onConnect不再弹框要重新弹出授权框在系统设置里清掉该 App 的存储数据或者执行adb shell pm clear package。3.2 USB_DEVICE_ATTACHED 广播与应用自启动如果 App 进程被杀掉权限框是无法主动弹出来的只能靠系统广播把 Activity 拉起来。示例工程在 Manifest 里给 MainActivity 挂上了设备插拔的意图过滤器activity android:name.MainActivity android:launchModesingleTask intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activitydevice_filter.xml里示例工程用的是全匹配resources !-- 空 usb-device 元素表示匹配所有 USB 设备 -- usb-device / /resources空usb-device /意味着任何 USB 设备插入都会唤起 MainActivityU 盘、键鼠、读卡器插上去都会触发。工业场景一般改成白名单在device_filter.xml里写死自家摄像头的 vendorId 和 productIdusb-device vendor-id1133 product-id2094 /但 UVC 设备 VID/PID 太杂白名单维护成本高。常见做法是保留全匹配在onAttach里用device.getVendorId()做软过滤不匹配就提示「非支持的摄像头」并 return既不影响权限流程也不至于被任意 USB 设备反复唤醒。3.3 getSupportedSize 列表为什么切换分辨率会闪退设备连上后UVCCamera.getSupportedSize()能拿到固件里 VIDEO_STREAMING 描述符声明的所有帧尺寸ListSize sizes mUVCCamera.getSupportedSize(); for (Size size : sizes) { Log.v(TAG, UVC size: size.getWidth() x size.getHeight()); }这里返回的 Size 在较新版本里是android.util.Size老版本可能是库内自定义类打印时注意别直接转字符串。列表通常按固件顺序排列第一项不一定是推荐项得结合带宽判断。社区里关于「uvccamera 切换分辨率导致闪退 因为没有对齐16」的反馈非常多原因集中在 JNI 侧libuvc 与解码代码按 16 对齐申请帧缓冲宽高不是 16 的整数倍时最后一行数据跨出缓冲区边界表现为切换分辨率的瞬间 SIGSEGV 或花屏。UVC 固件里常见的 1920×1080高度 1080 除以 16 是 67.52592×1944 同理直接设置就踩雷。常见尺寸的对齐情况如下常见 UVC 尺寸宽 ÷ 16高 ÷ 16可直接使用640 × 4804030是1280 × 7208045是1920 × 108012067.5否需对齐2592 × 1944162121.5否需对齐800 × 6005037.5否需对齐对齐函数和切换流程这样写private Size align16(Size size) { int w size.getWidth() ~15; // 向下取整到 16 的倍数 int h size.getHeight() ~15; return new Size(w, h); } private void switchResolution(Size target) { Size aligned align16(target); mUVCCamera.stopPreview(); // 先停流释放旧缓冲 mUVCCamera.setPreviewSize(aligned.getWidth(), aligned.getHeight()); mUVCCamera.startPreview(); // 用新尺寸重启等时传输 } ~15是位运算形式的向下对齐效果等同(w / 16) * 16。切换顺序有讲究必须先stopPreview()再setPreviewSize()直接改尺寸不停流native 侧缓冲队列还停在旧尺寸上崩溃稳定复现。另外对齐后的尺寸如果不在getSupportedSize()列表里setPreviewSize不报错但画面会花最稳的写法是先从列表里过滤一遍能用的尺寸再对齐。提示对齐会改变预览比例TextureView 的onSurfaceTextureSizeChanged里要做等比例缩放居中否则画面上下出黑边或被拉伸变形。4. UVCCamera 示例的预览、H.264 硬编码与拍照三条链路4.1 从 USB 等时端点到 SurfaceTexture 的预览通路UVC 的视频流走等时isochronous端点libusb 轮询读取 payloadlibuvc 按传输头里header的帧标记把 payload 重组为一帧。如果设备输出的是 MJPEGnative 层还要先软解出 YUV 才能上屏。之后画面的出口有两个一个是SurfaceTexture走 GPU 渲染另一个是setFrameCallback把裸数据交到 Java 侧。示例主界面走 SurfaceTexture 这条通路核心代码很短SurfaceTexture st mTextureView.getSurfaceTexture(); st.setDefaultBufferSize(mPreviewWidth, mPreviewHeight); mUVCCamera.setPreviewTexture(st); mUVCCamera.setPreviewSize(mPreviewWidth, mPreviewHeight); mUVCCamera.startPreview();setDefaultBufferSize决定 SurfaceTexture 内部缓冲区的像素宽高建议与预览尺寸保持一致不一致时 GPU 侧按纹理坐标裁剪白白浪费带宽。setPreviewTexture把 UVC 输出绑定到这个 SurfaceTexture相当于指定数据出口setPreviewSize才是和设备协商流的尺寸设备不支持时 libuvc 会退回固件默认值表现是画面比例变了startPreview开启等时传输循环之后setOnFrameAvailableListener会被高频触发。连等时传输之前先算一笔带宽账。USB 2.0 高速模式理论 480 Mbps实际可用大约 320 到 400 MbpsYUYV 是每像素 2 字节的裸流码率上升非常快尺寸 30fpsYUYV 码率MJPEG 码率典型USB 2.0 可行性640 × 480147 Mbps5 – 15 Mbps都可行1280 × 720442 Mbps10 – 30 MbpsYUYV 接近极限MJPEG 可行1920 × 1080995 Mbps20 – 60 MbpsYUYV 不可行MJPEG 可行所以看到 1080p 的 UVC 摄像头先确认它的出厂输出格式。示例工程里一般默认优先选 MJPEG 流YUYV 做降级备选没有明显画质诉求的场景保持 MJPEG 是最稳的。帧率上不去时先降一档分辨率不要先怀疑业务代码。4.2 用 MediaCodec Surface 输入做 H.264 硬编码示例工程里还有一个编码 Activity核心思路是让 UVC 预览面直接接到编码器的输入 Surface中间不经过 Java 层拷贝这是 Android 上从 UVC 到 H.264 延迟最低的做法MediaFormat format MediaFormat.createVideoFormat(video/avc, width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mMediaCodec MediaCodec.createEncoderByType(video/avc); mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface encoderSurface mMediaCodec.createInputSurface(); mMediaCodec.start(); // 把 UVC 预览直接输出到编码器输入面绕开 TextureView mUVCCamera.setPreviewTexture(encoderSurface); mUVCCamera.startPreview();这里KEY_COLOR_FORMAT必须设成COLOR_FormatSurface配置成 YUV 裸格式反而无法用 Surface 输入。KEY_BIT_RATE按场景调720p 用 4 到 8 Mbps 画质都能接受码率给太高对 UVC 的等时传输反而是压力。KEY_I_FRAME_INTERVAL 1表示每 1 秒一个关键帧直播切流起播快如果只是本地录像放宽到 2 到 5 秒可以省码率。编码器输出侧是标准的 drain 循环逐帧取已编码数据交给 MediaMuxerMediaCodec.BufferInfo info new MediaCodec.BufferInfo(); int outIndex; while ((outIndex mMediaCodec.dequeueOutputBuffer(info, 10_000)) 0) { ByteBuffer buffer mMediaCodec.getOutputBuffer(outIndex); if ((info.flags MediaCodec.BUFFER_FLAG_CODEC_CONFIG) ! 0) { // SPS/PPS 参数集MediaMuxer 添加轨道时会消费 } else { mMuxer.writeSampleData(mVideoTrackIndex, buffer, info); } mMediaCodec.releaseOutputBuffer(outIndex, false); }BufferInfo里offset和size指明有效数据的区间presentationTimeUs必须单调递增否则部分播放器直接拒绝播放flags里的BUFFER_FLAG_CODEC_CONFIG表示这一包是 SPS/PPS要在第一次writeSampleData之前让 MediaMuxer 消费掉也就是addTrack之后、正式写帧之前处理好这一个分支。4.3 帧回调与拍照YUYV、MJPEG、NV21 的数据格式不依赖编码器、想在 Java 层拿原始帧时用setFrameCallback。示例里典型的回调写法mUVCCamera.setFrameCallback(new UVCCamera.IFrameCallback() { Override public void onFrame(ByteBuffer frame) { frame.clear(); int length frame.remaining(); byte[] buf new byte[length]; frame.get(buf); // 第二参数传 YUYV 时length width * height * 2 } }, UVCCamera.FRAME_FORMAT_YUYV);帧格式与用途对照如下常量帧数据布局每帧大小直接用途FRAME_FORMAT_YUYVYUYV 4:2:2 交错排列width × height × 2软渲染、转 NV21 后编码FRAME_FORMAT_MJPEG每帧一包独立 JPEG 字节流不固定直接存图、BitmapFactory 解图FRAME_FORMAT_NV21NV21 半平面前 w×h 是 Y后 w×h/2 是 VUwidth × height × 3/2转 JPEG或喂给 MediaCodec 裸流输入拍照落地要分设备MJPEG 设备最简单直接在回调里BitmapFactory.decodeByteArray存盘一帧一个 JPEGYUYV 设备得先转 NV21再用YuvImage压缩成 JPEGbyte[] nv21 yuyvToNv21(yuyvBytes, w, h); // 注意 UV 顺序 YuvImage image new YuvImage(nv21, ImageFormat.NV21, w, h, null); FileOutputStream fos new FileOutputStream(saveFile); image.compressToJpeg(new Rect(0, 0, w, h), 90, fos); fos.close();YuvImage只认 NV21/NV12YUYV 不能直接传转换时最常见的错误是红蓝互换多半是 V/U 分量顺序写反。另外onFrame跑在 native 回调线程里面做任何耗时操作都会造成丢帧和 USB 缓冲溢出正确做法是把byte[]丢给独立线程池处理。提示帧回调的 ByteBuffer 是 native 侧直接映射的frame.clear()之后立即拷贝不要在回调里持有这个 ByteBuffer 到别的线程使用前一个缓冲还没释放时下一帧写入会覆盖数据。5. 稳定化改造切换分辨率不闪退再用 dumpsys 验证带宽5.1 切换分辨率不闪退的完整操作顺序把第三章的对齐逻辑固化成工程里的统一入口替换示例里散落的setPreviewSize调用private void switchResolution(Size raw) { int w raw.getWidth() ~15; // 对齐 16 int h raw.getHeight() ~15; mUVCCamera.stopPreview(); // 先停流 mUVCCamera.setPreviewSize(w, h); SurfaceTexture st mTextureView.getSurfaceTexture(); if (st ! null) st.setDefaultBufferSize(w, h); mUVCCamera.startPreview(); // 再重启 }这里面三个要点缺一不可切换前必须停流尺寸对齐后要确认它在getSupportedSize()列表里setDefaultBufferSize必须和预览尺寸一致。对齐把 1920×1080 变成 1920×1072画面会有极轻微的上下裁切这是用稳定性换比例视觉上几乎看不出来。如果接受不了裁切更省事的路线是直接把 1280×720 作为最高档位宽高天然对齐绕开 1080p。5.2 用 dumpsys 与帧时间戳定位「没画面」和「帧率低」「插上没画面」按下面的顺序定位一条命令对照一层adb shell dumpsys usb adb logcat -s UVCCamera USBMonitor先看dumpsys usb输出里有没有目标摄像头的 manufacturer、product 信息没有说明 OTG 供电或接触问题换带供电的 HUB 复测有设备信息但 App 不弹权限框查device_filter.xml的 VID/PID 是否匹配弹了框但 open 报errno-1是接口被占用常见原因是内核 uvcvideo 驱动或另一个应用抢先 claimInterface拔插一次或换一台不带 uvcvideo 驱动的设备复测。帧率达不达标在帧回调里给相邻两帧打时间戳算最直接long lastTs 0; Override public void onFrame(ByteBuffer frame) { long now System.nanoTime(); if (lastTs 0) { float fps 1_000_000_000f / (now - lastTs); Log.v(TAG, fps fps); } lastTs now; }算出来的 fps 持续低于设置值 15% 以上而且 CPU 占用不高优先怀疑 USB 带宽而不是代码逻辑把分辨率降一档或者把 MJPEG 的帧尺寸调小再测。这套检查顺序固定下来之后新机型开箱五分钟就能区分供电、权限、接口占用和带宽四类问题不用再对着 Logcat 猜。本文还有配套的精品资源点击获取
返回列表