RK3576 基于 V4L2 的 UVC 摄像头自动获取 H.264 / MJPEG / YUYV 格式技术详解
深度结合《MEDAI-V1 产品使用说明书》与《MEDAI V1 技术参数》· 面向 CSDN 发布的实战型技术文档
适用平台:RK3576 / MEDAI-V1 智能音视频处理设备

0. 写在前面
UVC(USB Video Class)是 MEDAI-V1 五大输入源之一,也是绿幕抠图(4.4.3 节主源固定 UVC) 与 手势识别(6.2 节实时识别) 的唯一指定源。但同样是"插上一个 USB 摄像头",不同摄像头吐出的数据天差地别:有的给原始 YUYV、有的给逐帧 MJPEG、高端云台(Obsbot、大疆 Pocket3 等)甚至直接给摄像头内编码的 H.264。
摄像头到底吐哪种格式、能不能自动识别并选最优——这正是 V4L2(Video for Linux 2)框架 要解决的问题。本篇先讲清 V4L2 框架与 UVC 子系统原理,再给出一份自动枚举并择优 H.264/MJPEG/YUYV 三种格式 的完整代码,最后深度结合 MEDAI-V1 说明书落到真实配置。
1. V4L2 框架技术详解
1.1 V4L2 是什么
V4L2 是 Linux 内核统一的视频设备驱动框架。无论是 USB 摄像头、HDMI 采集卡、MIPI CSI 摄像头,只要挂上 V4L2 驱动,用户空间就能用同一套 ioctl 打开 /dev/videoX 收帧。它把"硬件差异"屏蔽掉,让上层(我们的推流/AI 程序)只面对标准接口。
在 RK3576 上,V4L2 的应用链路是:
[ UVC 摄像头 ] –USB3.0–> [ uvcvideo 驱动(V4L2) ] –/dev/videoX–> [ 用户态采集程序 ]
│
(H264) 直送封装/预览
(MJPEG) → VPU 硬解(MJPEG) → NV12
(YUYV) → RGA/MPP 处理 → NV12
▼
[ 统一 NV12 → 编码 → 推流 ]
1.2 V4L2 核心对象
| video_device | 暴露给用户态的 /dev/videoX 节点 | UVC 摄像头插上即生成一个 |
| v4l2_subdev | 子设备(sensor/ISP),处理具体像素流水线 | 摄像头内部 ISP |
| vb2(videobuf2) | 缓冲队列框架,管理 DMA 缓冲 | REQBUFS/QBUF/DQBUF 的底层 |
| media controller | 把多个 subdev 连成拓扑图 | 复杂管线用,UVC 一般单节点 |
对 UVC 来说,最关键的是 video_device + vb2:摄像头驱动已经把 USB 包拼成帧并塞进 vb2 缓冲,我们要做的只是"取帧"。
1.3 关键 ioctl 流程
一次标准采集的生命周期:
open("/dev/videoX")
└─ VIDIOC_QUERYCAP 查询能力(是否支持视频捕获/流控)
└─ VIDIOC_ENUM_FMT 枚举摄像头支持的像素格式(★本篇重点)
└─ VIDIOC_ENUM_FRAMESIZES 枚举某格式下的分辨率/帧率
└─ VIDIOC_S_FMT 设定格式 + 分辨率 + 帧率(Probe/Commit 协商)
└─ VIDIOC_REQBUFS 申请内核缓冲(MMAP / USERPTR / DMABUF)
└─ VIDIOC_QUERYBUF + mmap 把缓冲映射到用户态
└─ VIDIOC_QBUF 把空缓冲入队
└─ VIDIOC_STREAMON 开流
└─ [循环] VIDIOC_DQBUF 取已满缓冲(拿到一帧)→ 处理 → QBUF 还回去
└─ VIDIOC_STREAMOFF / close 关流
其中 VIDIOC_ENUM_FMT 就是"自动识别摄像头格式"的入口——它把摄像头固件里声明的所有格式逐个吐出来,我们据此判断它支持 H.264 / MJPEG / YUYV 中的哪些。
1.4 UVC 子系统:为什么插上就有画面
UVC 是 USB-IF 制定的免驱视频类规范。符合 UVC 的摄像头(即说明书兼容列表里那些)插入 Linux 后,内核 uvcvideo 驱动自动加载,按 UVC 描述符里的 VideoControl/VideoStreaming 接口枚举出 video_device,无需厂商驱动。
MEDAI-V1 技术参数明确列了兼容设备:Obsbot 寻影系列、Insta360(360Insta)、大疆系列、壹秘、Pocket3、绿联、甜甜圈、Obsbot Tiny2/Meet2 等。这些全部走标准 UVC,插上即被 V4L2 识别。
1.5 三种格式详解与带宽含义
| YUYV | 'YUYV' | 未压缩原始 YUV 4:2:2 | ~148 MB/s(极高) | RGA 转 NV12 + MPP 编码,最吃总线 |
| MJPEG | 'MJPG' | 每帧独立 JPEG 压缩 | 1540 MB/s | VPU 的 MJPEG 硬解(解码参数含 MJPEG)→ NV12 |
| H.264 | 'H264' | 摄像头内置 H.264 编码(UVC 1.5) | 515 MB/s | 直接解封装,可免 VPU 解码送封装/预览 |
关键洞察:为什么 4K 摄像头几乎都只报 MJPEG/H.264?因为 4K@30 的 YUYV 高达 ~597 MB/s,USB3.0 实测有效带宽也仅 ~400 MB/s,原始格式根本传不过来。说明书 4.3.1 节 UVC 给的 2160P/1080P 选项,底层正是靠 MJPEG 或 H.264 压缩才能跑通——这也是"切换分辨率需重启"的硬件原因之一(Probe/Commit 重新协商 + 解码链路重建)。
1.6 格式自动探测(与第九篇呼应)
第九篇讲推流,第四篇讲 UVC+MPP 解码时提到过:UVC 的 Probe/Commit 机制要求"设置格式后必须回读确认"。V4L2 侧对应两个动作:
- VIDIOC_ENUM_FMT / VIDIOC_ENUM_FRAMESIZES:只读枚举,看清摄像头家底;
- VIDIOC_TRY_FMT:试探某格式+分辨率是否被接受(不真正改状态);
- VIDIOC_S_FMT:真正提交(= Commit),之后回读 v4l2_format 确认实际生效值(可能被摄像头降级)。
自动获取三种格式,就是"先枚举、再按策略 TRY/S_FMT 择优"的过程。
2. 自动获取三种格式的工程实现
2.1 择优策略(结合 RK3576 算力)
MEDAI-V1 的目标是"把画面送进统一 NV12 画布再编码",所以选格式的原则是 尽量让摄像头替我们干重活、少占 VPU 总线:
优先级(同为 4K 时尤其明显):
H.264 > MJPEG > YUYV
① ② ③
免解码 VPU硬解 RGA转码+MPP编码
- 选 H.264:摄像头已编码,设备几乎零处理即可预览/推流(最省);
- 选 MJPEG:VPU 有 MJPEG 硬解能力(技术参数解码参数含 MJPEG),一帧一包免拆包,性价比高;
- 兜底 YUYV:只有老旧/低配摄像头才给,必须 RGA 转 NV12 再 MPP 编码,带宽压力大,仅 1080P 及以下实用。
2.2 决策还受"分辨率"约束
若用户选了 2160P(4K)但摄像头只支持 YUYV 4K(极少),应强制降级到 MJPEG/H.264 的 4K 而非用 YUYV——否则 USB 总线撑不住。代码里用 VIDIOC_TRY_FMT 对每个候选格式 + 目标分辨率试探,取第一个通过的。
3. 完整代码:v4l2_uvc_capture.c
下面给出枚举 + 自动择优 + mmap 采集 的完整示例(约 290 行),可直接在 RK3576(或任意 Linux)编译运行。
/******************************************************************************
* v4l2_uvc_capture.c — MEDAI-V1 / RK3576 UVC 自动获取 H264/MJPEG/YUYV 示例
* 编译:gcc v4l2_uvc_capture.c -o v4l2_uvc_capture
* 运行:./v4l2_uvc_capture /dev/video0 2160 (分辨率 2160=4K / 1080 / 720)
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#ifndef v4l2_fourcc
#define v4l2_fourcc(a,b,c,d) ((__u32)(a)|((__u32)(b)<<8)|((__u32)(c)<<16)|((__u32)(d)<<24))
#endif
#define FMT_YUYV v4l2_fourcc('Y','U','Y','V')
#define FMT_MJPG v4l2_fourcc('M','J','P','G')
#define FMT_H264 v4l2_fourcc('H','2','6','4')
#define FMT_NAME(f) (char[5]){ (f)&0xff,(f)>>8&0xff,(f)>>16&0xff,(f)>>24&0xff,0 }
/* ———- 1. 枚举摄像头支持的所有像素格式 ———- */
static void enum_formats(int fd){
struct v4l2_fmtdesc fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("== 摄像头支持的像素格式 ==\\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0){
printf(" [%s] %s\\n", FMT_NAME(fmt.pixelformat), fmt.description);
/* 顺便枚举该格式下的分辨率 */
struct v4l2_frmsizeenum fs;
memset(&fs, 0, sizeof(fs));
fs.pixelformat = fmt.pixelformat;
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fs) == 0){
if (fs.type == V4L2_FRMSIZE_TYPE_DISCRETE)
printf(" %ux%u\\n", fs.discrete.width, fs.discrete.height);
else break;
fs.index++;
}
fmt.index++;
}
}
/* ———- 2. 试探某格式+分辨率是否被支持(TRY_FMT, 不改状态) ———- */
static int try_format(int fd, __u32 pixfmt, int width, int height){
struct v4l2_format f;
memset(&f, 0, sizeof(f));
f.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
f.fmt.pix.pixelformat = pixfmt;
f.fmt.pix.width = width;
f.fmt.pix.height = height;
if (ioctl(fd, VIDIOC_TRY_FMT, &f) < 0) return 0;
/* 回读:若摄像头实际给的不是请求的格式/分辨率,说明不支持 */
if (f.fmt.pix.pixelformat != pixfmt) return 0;
return 1;
}
/* ———- 3. 自动择优:H264 > MJPEG > YUYV(受分辨率约束) ———- */
static __u32 pick_best_format(int fd, int width, int height){
if (try_format(fd, FMT_H264, width, height)) return FMT_H264;
if (try_format(fd, FMT_MJPG, width, height)) return FMT_MJPG;
if (try_format(fd, FMT_YUYV, width, height)) return FMT_YUYV;
return 0; /* 都不支持 */
}
/* ———- 4. 提交格式(S_FMT = Probe/Commit) ———- */
static __u32 commit_format(int fd, __u32 pixfmt, int *w, int *h){
struct v4l2_format f;
memset(&f, 0, sizeof(f));
f.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
f.fmt.pix.pixelformat = pixfmt;
f.fmt.pix.width = *w;
f.fmt.pix.height = *h;
f.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(fd, VIDIOC_S_FMT, &f) < 0){
perror("S_FMT"); return 0;
}
/* 回读实际生效值:摄像头可能降级分辨率/帧率 */
*w = f.fmt.pix.width;
*h = f.fmt.pix.height;
return f.fmt.pix.pixelformat;
}
/* ———- 5. mmap 采集循环 ———- */
struct buf { void *start; size_t len; };
static int start_capture(int fd, int want_w, int want_h, __u32 *out_fmt){
/* 自动择优并提交 */
__u32 best = pick_best_format(fd, want_w, want_h);
if (!best){ fprintf(stderr, "无可用格式(目标 %dx%d)\\n", want_w, want_h); return –1; }
int w = want_w, h = want_h;
__u32 real = commit_format(fd, best, &w, &h);
printf("自动选定格式: %s 实际分辨率: %dx%d\\n", FMT_NAME(real), w, h);
*out_fmt = real;
/* 申请 4 个 MMAP 缓冲 */
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0){ perror("REQBUFS"); return –1; }
struct buf *bufs = calloc(req.count, sizeof(struct buf));
for (unsigned i = 0; i < req.count; i++){
struct v4l2_buffer b;
memset(&b, 0, sizeof(b));
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
b.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &b);
bufs[i].len = b.length;
bufs[i].start = mmap(NULL, b.length, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, b.m.offset);
/* 入队空缓冲 */
ioctl(fd, VIDIOC_QBUF, &b);
}
/* 开流 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
/* 采集 30 帧演示 */
for (int n = 0; n < 30; n++){
struct v4l2_buffer b;
memset(&b, 0, sizeof(b));
b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
b.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &b) < 0){ perror("DQBUF"); break; }
/* 此处一帧到手,按格式分流处理:
H264 -> 解封装直接送封装/预览(最省)
MJPG -> 送 VPU MJPEG 硬解 -> NV12
YUYV -> RGA 转 NV12 -> MPP 编码
演示仅打印大小 */
printf(" 帧#%d 格式=%s 字节=%u\\n", n, FMT_NAME(real), b.bytesused);
ioctl(fd, VIDIOC_QBUF, &b); /* 还回去 */
}
ioctl(fd, VIDIOC_STREAMOFF, &type);
for (unsigned i = 0; i < req.count; i++) munmap(bufs[i].start, bufs[i].len);
free(bufs);
return 0;
}
int main(int argc, char **argv){
const char *dev = (argc > 1) ? argv[1] : "/dev/video0";
int target = (argc > 2) ? atoi(argv[2]) : 1080; /* 2160/1080/720 */
int w = (target >= 2160) ? 3840 : (target >= 1080 ? 1920 : 1280);
int h = (target >= 2160) ? 2160 : (target >= 1080 ? 1080 : 720);
int fd = open(dev, O_RDWR);
if (fd < 0){ perror("open"); return –1; }
enum_formats(fd); /* 先看摄像头家底 */
__u32 fmt;
start_capture(fd, w, h, &fmt); /* 自动择优 + 采集 */
close(fd);
return 0;
}
代码要点解读(与设备实现对应):
4. 深度结合《MEDAI-V1 使用说明书》
4.1 UVC 配置(说明书 4.3.1)
UVC 支持 2160P(4K) 和 1080P 两种分辨率;切换分辨率后需重启设备方可生效。
代码里 target=2160 即对应"2160P 选项"。之所以"切分辨率要重启",正是因为换到 4K 通常要把格式从 YUYV 切到 MJPEG/H.264(1.5 节带宽账),这涉及 UVC Probe/Commit 重新协商 + 下游 VPU/RGA 解码链路重建——热切换风险高,设备选择"重启原子重建"最稳(呼应第七篇"UVC 切换分辨率属编码契约本身")。
4.2 兼容摄像头列表(技术参数)
USB 信号输入 UVC 支持:Obsbot 寻影系列 / Insta360(360Insta)/ 大疆系列 / 壹秘 / Pocket3 / Obsbot Tiny2 / Meet2 / 绿联 / 甜甜圈 等。
这些均符合标准 UVC(1.1 节),插上即被 uvcvideo 驱动识别为 /dev/videoX,本篇代码 enum_formats 能直接列出它们的能力。注意:大疆 Pocket3、Obsbot 等高端云台常默认输出 H.264,自动择优会优先选它(最省 VPU)。
4.3 UVC 是五输入源之一(说明书 3.2.2)
视频输出(HDMI 显示与编码源)可选五源:HDMI / UVC / NDI / NET1 / NET2。
UVC 就是其中之一。无论它内部是 H.264/MJPEG/YUYV,经过本篇"自动获取+统一 NV12"后,与其它四源在画布上地位平等(见 RGA 适配篇)。
4.4 UVC = 绿幕主源(说明书 4.4.3)
绿幕配置:背景上传本地背景图;输入源:主源固定为 UVC。
绿幕抠图对实时性要求高,必须吃"最干净、最易处理的画面"。自动择优后若为 H.264/MJPEG,先硬解成 NV12 再进 OpenGL 绿幕管线(绿幕篇);若为 YUYV 则先 RGA 转 NV12。固定 UVC 也避免了"切源导致绿幕断流"。
4.5 UVC = 手势识别源(说明书 6.2)
通过 UVC 摄像头识别手势并切换信号源;识别后需等待 5 秒进入下次识别。
手势识别需要逐帧喂给 NPU,所以 UVC 帧必须及时、低延迟到达。自动选 H.264/MJPEG 能让摄像头侧预处理,总线占用低,给 NPU 留足余量。
4.6 音频跟随(说明书 3.2.1)
音频输入可选:跟随视频 / MIC / WLM。选"跟随视频"时声音跟随视频源。
UVC 摄像头若带麦克风(多数云台有),V4L2 会额外暴露一个 /dev/audio 或复合设备,音频随视频流进设备做 AAC 编码(技术参数声音编码 AAC 48K 双通道),对应"跟随视频"。
4.7 接口与供电(技术参数)
USB 接口:USB3.0 × 2;UVC 走其中一个 USB3.0;设备 2 路 USB3.0 分别为 UVC 摄像头与无线 MIC/U 盘。
USB3.0 的带宽(~400 MB/s 有效)是 MJPEG/H.264 4K 能跑通的硬件前提(1.5 节)。两个 USB3.0 分工明确:一个给 UVC、一个给无线 MIC 或录制 U 盘(4.3.2),互不抢带宽。
5. 自动格式决策实战步骤
6. 优化清单与常见问题
6.1 提升采集稳定性的 5 条
6.2 FAQ
Q1:插上摄像头 ls /dev/video* 没有新节点?
A:多为非标准 UVC(需厂商驱动)或 USB 供电不足;确认是说明书兼容列表内机型,换 USB3.0 口,或外接供电。
Q2:枚举只看到 YUYV,没有 H.264/MJPEG?
A:低配摄像头只吐原始格式。4K 下会严重占 USB 带宽,建议降到 1080P 或换支持 MJPEG/H.264 的云台(如 Obsbot/Pocket3)。
Q3:选了 2160P 但画面卡顿/掉帧?
A:YUYV 4K 超 USB3 带宽;优质摄像头在 4K 默认走 MJPEG/H.264。重启设备让格式重新协商(7.2 第 3 条),并在预监确认实际分辨率。
Q4:切换分辨率后没变化?
A:UVC 切换分辨率必须重启设备生效(4.3.1 / 7.2 第 3 条),不是 Web 点一下即时生效。
Q5:绿幕/手势不工作?
A:二者固定吃 UVC 主源(4.4.3 / 6.2),确认 UVC 已正常连且预监有画面;手势识别后需等 5 秒再识别(6.2 / 7.2 第 8 条)。
Q6:MJPEG 帧怎么用?
A:送 RK3576 VPU 的 MJPEG 硬解(解码参数含 MJPEG)→ NV12,一包一帧免拆包(第四篇详解)。
Q7:H.264 摄像头数据能直接推流吗?
A:可以。H.264 直送解封装/封装,几乎不占 VPU,最适合多路分发;但预览如需缩放/叠加仍需先解成 NV12(走 RGA)。
Q8:双 USB3.0 怎么分工?
A:一个接 UVC 摄像头,一个接无线 MIC 或录制 U 盘(技术参数),分开走带宽互不干扰。
7. 小结
V4L2 是 Linux 视频设备的统一入口,uvcvideo 驱动让所有标准 UVC 摄像头插上即用。MEDAI-V1 通过 枚举(ENUM_FMT)→ 试探(TRY_FMT)→ 择优提交(S_FMT) 的自动流程,在 H.264 / MJPEG / YUYV 三种格式中按"尽量让摄像头预处理、少占 VPU 总线"的原则选最优,并把任意格式统一收敛到 NV12 画布——这既是绿幕、手势识别能稳定工作的数据基础,也是 4K UVC 能跑通的关键(靠 MJPEG/H.264 压缩压住 USB3 带宽)。
系列预告:本篇是 MEDAI-V1 / RK3576 技术专栏第 10 篇。前序《拉流 / 绿幕 / MPP 编解码 / UVC+MPP 解码 / 综合性能 / RGA 适配 / MPP×RGA 统一编码 / FFmpeg 拉流 / librtmp 推流》可与本篇组合,构成完整"输入采集 → 处理 → 编码 → 分发"技术体系。
本文档基于《MEDAI-V1 产品使用说明书 V1.0》与《MEDAI V1 技术参数》撰写,代码示例为说明性参考实现,实际部署请以设备固件与官方 SDK 为准。
网硕互联帮助中心





评论前必须登录!
注册