简介:UVC4UnityAndroid是一套面向Unity引擎开发者的安卓平台USB摄像头集成方案,基于USB视频类标准,解决Unity中无法直接使用USB摄像头的痛点,适用于远程监控、工业检测、AR交互等场景。项目覆盖摄像头权限申请、设备枚举、视频流获取与纹理渲染等关键环节,开发者需掌握Unity、C#及安卓原生开发工具包基础。压缩包共285个文件,约59.83MB,包含安卓原生库、C#脚本、场景与预设、材质、着色器、动态库插件及完整工程设置,另有文本说明文档辅助理解,可直接导入运行,也可作为二次开发基础。目前已有835人学习下载。通过该资源可掌握基于Java本地接口调用安卓摄像头接口的完整思路,获得可用的USB视频类插件与示例场景,节省自行编译调试时间,并理解图像处理、权限配置与性能优化的关键细节,适合想快速集成USB摄像头的Unity开发者。 最近在折腾一个项目:想在 Android 平板或者手机上,通过 OTG 接一台工业 UVC 摄像头,把画面实时显示到 Unity 里。原本以为就是申请个 USB 权限的小活,结果一上手才发现,Android 的 Camera2 API 根本不认外接 UVC 摄像头,USB 设备可以枚举出来,但你就是拿不到预览数据。折腾了几天,我把整套链路走通,也给这层封装起了个名字:UVC4UnityAndroid。这篇文章从原理讲到接入,再到我踩过的坑,适合要在 Unity 里做外接 USB 相机、USB 内窥镜、或者搞工业视觉验证的开发者参考。
1. 为什么 Android 对 USB 摄像头“视而不见”
先说结论:Android 不是不支持 UVC 摄像头,而是它的上层 Camera 体系不把它当作摄像头。这个认知差异,决定了后面所有实现路径。
1.1 UVC 的定位和 Android USB Host 的关系
UVC 是 USB Video Class 的缩写,简单说就是 USB 标准化组织为视频设备定义的一套通用协议。只要摄像头遵循 UVC 规范,插到 Windows、Linux 和 macOS 上,系统自带的驱动就能识别,不需要额外安装厂商驱动。Android 从 3.1 开始支持 USB Host,底层内核同样带了 uvcvideo 驱动,所以在内核层面,外接摄像头其实是“可见”的。
但问题出在上层。普通 Android App 默认接触不到/dev/video*这一层设备节点,SDK 也没有提供直接读取这些节点的 API。系统暴露给应用的是 UsbManager,它能让你拿到 UsbDevice 的引用,却不会帮你把视频流解出来。也就是说,Android 确实“知道”这个 USB 摄像头插上来了,但应用层必须自己走 USB 协议去和设备通信,数据才能流出来。
1.2 问题根源:Camera2 API 和 UVC 设备不走同一条通道
现代 Android 相机接口 Camera2 的 cameraId 列表,来源于相机 HAL 层,也就是手机主板上物理连接的摄像头,包括后置、前置、深度摄像头等。外接 USB 摄像头属于 USB device 子系统,不在 HAL camera 设备的管辖范围内。所以你在 Unity 里用 WebCamTexture,拿到的设备列表永远只有机内摄像头,不管外面插了多少个 UVC 设备。
这套逻辑在很多项目里都会被误解。我见过有人调试老半天,以为接上 USB 摄像头之后 WebCamTexture.devices 里会多一个条目,结果当然没有。USB 摄像头进入 Android 视角的方式,是作为 UsbDevice,而不是 CameraDevice。这直接决定了后续整套实现的走向:你必须自己处理 UVC 的控制面和数据面,而不是等系统帮你分割好。
2. UVC4UnityAndroid 的数据通路:从 USB 像素流到 Unity 纹理
既然 Camera2 走不通,那就直接去 USB 层抓数据。这里需要想清楚整条链路的每一环,不然做到一半很容易卡住。
2.1 技术选型:为什么是 libuvc + JNI,而不是调用 Camera2
在 Android 的 Native 世界里,比较成熟的底层库有 V4L2 和 libuvc 两条路线。V4L2 需要访问/dev/video*,普通 App 拿不到这个设备节点,除非你 root 了手机。而 libuvc 基于 libusb,可以通过 UsbDeviceConnection 拿到的 native 文件描述符直接与设备交互,不需要 root。这个关键差异,让 libuvc 成了更现实的选择。
| 方案 | 需要 root | SDK 可集成性 | 成熟度 |
|---|---|---|---|
| V4L2 | 是 | 低,需要设备节点权限 | 依赖具体内核 |
| libuvc + libusb | 否 | 高,可通过 JNI 直接对接 | 跨平台,多项目验证 |
这里多说一句,libuvc 并不是 Android SDK 的原生部分,但它对 UVC 协议的支持很全面,能处理设备枚举、控制属性、数据流开启、帧回调等事情。我们在 UVC4UnityAndroid 里就是把它编译成 native so,再通过 JNI 暴露给 Unity 的 C# 层。
2.2 一条完整的像素链路
最终跑通的数据通路大概是这样的:
UsbDevice(Java 层获得)→ UsbDeviceConnection.setInterface → getFileDescriptor() → 通过 JNI 传给 native libusb → libuvc_init / uvc_open → 查询设备支持的分辨率和像素格式 → uvc_start_streaming → 回调线程收到视频帧 → YUYV/MJPEG 转 RGBA → 通过 JNI 把像素字节传给 Unity → Texture2D 显示。
实际调用的关键点,是 UsbDeviceConnection.getFileDescriptor() 这个 native 文件描述符。libusb 有一个 Android 专用补丁,支持从已有的 native fd 构建上下文,大致流程是先用libusb_set_option()关闭自动设备发现,再用libusb_wrap_sys_device()把文件描述符包装成 libusb 设备。只有这样,才能用非 root 权限操作 USB 设备,这也是整套方案能落地的前提。
2.3 为什么视频流必须走原生层,不能纯 Java 搞定
在 Java/Kotlin 层能不能直接读 UVC?理论上可以,你也可以在 Java 层做 YUV 到 RGBA 的转换,但实际做出来会有几个问题:一是字节数组拷贝频繁,性能很难上去;二是 MJPEG 解码在 Java 层非常吃 CPU;三是 UVC 的控制属性,比如曝光、对焦、白平衡,如果自己用 Java 解析控制请求,代码量和调试成本都很大。
UVC4UnityAndroid 把原生层作为核心,Java 层只负责 USB 权限和生命周期管理,Unity 侧只负责纹理更新和业务逻辑。这样分工以后,每一层都干自己最擅长的事,后面做优化也好定位瓶颈。最忌讳的是一股脑把像素处理塞到 Unity C# 里,那样帧率很难看,GC 也会把人逼疯。
3. 接入 UVC4UnityAndroid 的逐步实现
下面是我整理出来的最小接入流程。这套步骤只要求你能编译 Unity 工程,并且对 Android Studio 有一点基础就够用。
3.1 环境准备:Unity 工程和 Android 插件的边界
我用的环境是 Unity 2021 LTS 和 Android Studio Hedgehog(2023.1.1),Android SDK 建议 targetSdkVersion 31 以上。UVC4UnityAndroid 最终会交给 Unity 一个 aar 文件和一个 C# 封装脚本,native so 文件放在 aar 的 jniLibs 目录下。这里建议 as 架构只放 arm64-v8a,如果你要兼容很老的一批 32 位机器,再额外放 armeabi-v7a。
工程层面,尽量让 native 代码和 Unity 工程解耦。我习惯的做法是:先在 Android Studio 里把 aar 编译好,再把它放进 Unity 的 Plugins/Android 目录。这样调试插件时可以快速迭代,不会因为 Unity 工程重打一次包而浪费时间。
3.2 清单文件与 USB 权限申请细节
AndroidManifest 里需要声明 USB Host 支持。完整声明如下:
<uses-feature android:name="android.hardware.usb.host" android:required="false" /> <uses-permission android:name="android.permission.USB_PERMISSION" />注意android:required="false",因为 UVC 摄像头不是所有设备都必备的功能,这样普通手机安装应用也不会被过滤掉。真正请求权限时,要通过 UsbManager 的 requestPermission 方法动态申请:
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager val deviceList = usbManager.deviceList val device = deviceList.values.firstOrNull() if (device != null) { usbManager.requestPermission(device, pendingIntent) }权限弹窗出来之后,用户点击允许,系统会把授权结果通过广播发回来。这个动态授权流程不能省,尤其是 Android 11 以后的版本,对 USB 权限的处理更严格。
3.3 最简 Demo:用 C# 脚本显示摄像头画面
Unity 侧的核心脚本非常短。假设插件导出了一个 UvcBridge 类:
var bridge = new UvcBridge(); bridge.Initialize(); bridge.StartStream(640, 480, OnFrame); void OnFrame(Color32[] pixels) { texture.SetPixels32(pixels); texture.Apply(); }这段代码能跑通,但性能很一般,因为每帧都创建了 Color32[],还要做一次像素级拷贝。真正生产环境里我会改成复用一个 NativeArray 或者直接拿 native 纹理指针更新,但作为最小 Demo,验证链路稳定已经足够了。第一次跑通的时候,能看到画面上出现摄像头画面,基本就说明整套通路没问题。
3.4 热插拔事件的 Unity 侧回调
USB 摄像头和手机上的摄像头不一样,用户可以随时拔掉,所以热插拔事件必须处理。Android 端通过监听 ACTION_USB_DEVICE_ATTACHED 和 ACTION_USB_DEVICE_DETACHED 广播来感知设备变化,再通过 UnitySendMessage 把事件回调到 Unity 场景里的某个 GameObject。
C# 侧可以这样包装:
void OnEnable() { UvcDeviceManager.OnAttach += StartCamera; UvcDeviceManager.OnDetach += StopCamera; } void OnDisable() { UvcDeviceManager.OnAttach -= StartCamera; UvcDeviceManager.OnDetach -= StopCamera; }这里有个容易被忽略的点:如果插入摄像头前应用已经启动,系统可能不会重新发 attach 广播,而你也不能等用户拔了再插才初始化。所以我一般会在 OnEnable 先主动扫一遍现有设备,确保应用启动时已经有摄像头也能正常开启。
4. 这两周踩过的典型坑与排查思路
这一部分全是实际操作中遇到的问题。每个坑我都按照“现象 → 怀疑方向 → 验证 → 解决”的顺序记录,方便你复现的时候直接定位。
4.1 设备列表为空:供电和 USB 枚举问题
现象是摄像头插上以后没有任何反应,getDeviceList()直接返回空,logcat 里也看不到 usb 相关日志。一开始我以为是代码问题,后来换了根 OTG 线就好了,原因是普通 OTG 线给 USB 摄像头的供电不稳定。
排查思路:先打开 Android 的开发者选项里的 USB 调试,插上摄像头,看系统日志里有没有usb 1-1: new high-speed USB device之类的枚举成功记录。如果没有,九成是硬件问题,优先换线、换 OTG 转接头,或者使用带供电的 USB HUB。工业级 UVC 摄像头功耗往往比较高,在平板上用原装 OTG 口带不动的情况很常见。
实操建议:准备一个带外部供电的 USB HUB,能把一半的诡异现象直接排除掉。这个坑我在不同项目里遇到不止一次,最后都是供电问题。
4.2 权限弹窗不出或一闪而过
现象是调用 requestPermission 后弹窗不出现,或者刚出现就消失,然后相机一直无法打开。检查后发现是因为 Unity 的 Activity 可能还没处于 RESUMED 状态,而 Android 的 USB 权限申请必须由前台 Activity 发起。
解决方法是在 Unity 侧先拿到当前 Activity,再确保 onResume 之后发起权限请求。如果通过 androidJavaObject 直接调用 requestPermission,时机不对就只能等很久或者干脆不弹。
另外,部分国产 ROM 在 targetSdkVersion 31 以后对 USB 权限弹窗有额外过滤。如果确认代码流程没问题,但弹窗还是异常,可以在 Manifest 里临时声明 QUERY_ALL_PACKAGES 试试,定位问题后尽量用更精准的<queries>替代,避免上架合规问题。
4.3 画面黑屏:YUYV/MJPEG 格式与解码路径
权限都通了,设备也打开了,但画面上什么都没有。这种情况下 80% 是视频格式没有正确匹配。UVC 摄像头最常见的输出格式是 YUYV 和 MJPEG,libuvc 开流之前,需要先调用uvc_get_stream_ctrl_format_size去匹配摄像头实际支持的分辨率和格式组合。
有些摄像头宣称支持 1080p,但 1080p 下面只支持 MJPEG,不支持 YUYV。如果 libuvc 按 YUYV 去请求 1080p,设备会拒绝,导致回调一直没有帧。我的调试策略是:先用 640x480 和默认格式把完整链路跑通,再逐步提高分辨率。看到黑屏时,先别去怀疑纹理问题,先确认uvc_start_streaming的回调函数是否真的被调用了。只要回调有数据,后面显示只是时间问题。
4.4 画面方向不对与 Native Crash
画面方向问题是 UVC 摄像头的物理安装方向导致的。外接摄像头不像手机内置摄像头有传感器方向信息,Android 系统根本不知道该把画布转多少度。所以画面上出现上下颠倒,或者左右镜像,都是正常的。解决办法很简单:在 Unity 的 shader 里旋转 UV,或者给 RawImage 适当设置 rotation 和 scale,不需要改 Native 代码。
Native Crash 的问题则多半出在 so 架构或依赖缺失上。常见错误是 aar 里只有 libuvc 的 so,却漏了 libusb 的 so,导致加载时直接UnsatisfiedLinkError,或者运行到一半崩溃。还有一种情况是同时包含多个架构,但 libuvc 和 libusb 不是同一套 ABI,混编也会崩。建议编译的时候严格统一 ABI,宁可只保留 arm64-v8a,也不要混着发。
5. 性能调优与生产环境落地
Demo 跑通只是第一步。真正要做产品,还要面对帧率、延迟、内存、发热这些问题。
5.1 帧率瓶颈:解码和内存拷贝
在 UVC 摄像头方案里,帧率瓶颈通常在三处:MJPEG 解码、像素格式转换、JNI 数据拷贝。如果摄像头支持 YUYV 无压缩格式,优先选 YUYV,因为 UVC 的 MJPEG 解码在 CPU 上很昂贵。我曾经用一款 720p 摄像头测试,MJPEG 模式在平板上只能跑 18 帧左右,切到 YUYV 后能上 30 帧。
但 YUYV 的缺点是带宽占用高,对 USB 总线压力大。实际项目里,我会根据摄像头能力和目标设备做几组组合测试,记录下来“分辨率 + 格式 + 实际帧率”的矩阵,再决定默认参数。
5.2 使用纹理复用和 Buffer Pool 减少 GC
C# 侧如果每帧都创建新的 Texture2D 或 Color32[],GC 压力会非常大,卡顿很快就会显现。正确做法是提前分配好一块像素缓冲区,整个生命周期里不断复用;纹理也只用同一个 Texture2D 对象,每帧调用 SetPixels32 / SetPixelData / LoadRawTextureData 更新内容。
Native 侧的 ByteBuffer 也要复用。不要在 JNI 回调里每帧 new byte[],否则 native 堆的内存分配和释放会拖慢帧率。更好的方案是在初始化时根据分辨率算出 buffer 大小,一次性分配,之后所有帧都往同一个 buffer 里写,通过帧序号或时间戳来区分新旧数据。
5.3 真实设备压测与 simpleperf 定位
调优不能靠感觉,最好用工具。Android 自带的 simpleperf 可以抓取 CPU 热点,用它分析 native 层的函数调用,能很快看出瓶颈是在解码还是在拷贝。我的步骤是:跑一个持续 120 秒的采集,同时观察 Unity Profiler 里的帧率曲线,最后把两边的数据对照起来看。
如果发现解码占比很高,可以考虑降低分辨率或换格式;如果发现 JNI 拷贝占比高,就把更多处理挪到 native 侧,只把最终的 RGBA 数据交给 Unity。压测时也要关注设备温度,长时间输出高分辨率视频,部分平板会触发降频,这时候帧率断崖式下跌,单纯看代码是看不出原因的。
5.4 继续往这个框架上加东西
UVC 协议不只是出视频流,它还有一个控制接口,能调整曝光、对焦、白平衡、增益等参数。UVC4UnityAndroid 里我把控制属性做成了透传接口,后续如果你要接带云台的 UVC 相机,或者做自动对焦逻辑,都能复用同一套底层链路。也可以继续扩展成双摄像头输入,或者直接把 AI 识别逻辑接到 native 层,减少一次像素回传。
最后再分享一个实际操作中的体会。我最早图省事,在 Java 层把 MJPEG 解码成 Bitmap,再转成 Texture,结果帧率惨不忍睹,还伴随着明显发热。后来把解码彻底移到 native,配合 buffer 复用和单纹理更新,数据通路才算稳定下来。如果你也在做类似的事,我的建议是先用 640x480 跑通全链路,再慢慢提高分辨率;给摄像头单独供电,能帮你排除掉一半的诡异现象。剩下的就是慢慢打磨目标设备上的参数组合,急不来。
本文还有配套的精品资源,点击获取