3、V4L2框架基础:理解Video for Linux 2的核心概念
好,咱们进入第三章。这一章我打算聊聊V4L2,也就是Video for Linux 2。说实话,我刚接触Exynos平台做视频开发那会儿,最头疼的就是这个V4L2。它像个黑盒子,文档又少,全靠自己摸索。但搞明白了之后你会发现,它其实就是一套很优雅的接口规范。
V4L2是什么?说白了,它就是Linux内核给视频设备(摄像头、编解码器、显示控制器)定义的一套统一接口。你想想看,没有它的话,每个硬件厂商都得自己搞一套驱动API,那上层应用就乱套了。Exynos的MFC(Multi-Format Codec)硬件加速器,就是通过V4L2暴露给用户空间的。
3.1 V4L2的核心对象:设备节点与能力查询
在Exynos平台上,视频编解码设备通常对应/dev/videoX节点。比如我调试过的Exynos 5422,MFC编码器是/dev/video10,解码器是/dev/video11。当然,不同芯片型号可能不一样,你得查datasheet。
拿到设备节点后,第一件事就是查询能力。用VIDIOC_QUERYCAP这个ioctl。我个人习惯先查这个,确认驱动是不是我想要的。
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <fcntl.h>
int fd = open("/dev/video10", O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("QUERYCAP failed");
close(fd);
return -1;
}
printf("Driver: %s\n", cap.driver); // 比如 "s5p-mfc"
printf("Card: %s\n", cap.card); // 比如 "s5p-mfc-dec"
printf("Bus info: %s\n", cap.bus_info); // 平台相关
cap.capabilities字段是否包含V4L2_CAP_VIDEO_M2M。Exynos的MFC是Memory-to-Memory设备,不是摄像头那种capture设备。这个搞错了,后面流程全乱套。
3.2 Buffer管理:从申请到入队
V4L2的buffer管理,是我觉得最核心也最容易踩坑的地方。它分三个步骤:申请、映射、入队。
第一步:申请Buffer
用VIDIOC_REQBUFS告诉驱动你要多少个buffer。我一般申请4个,够用又不浪费。
struct v4l2_requestbuffers reqbufs;
memset(&reqbufs, 0, sizeof(reqbufs));
reqbufs.count = 4; // 申请4个buffer
reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; // 编码输入用OUTPUT
reqbufs.memory = V4L2_MEMORY_MMAP; // 用mmap方式
if (ioctl(fd, VIDIOC_REQBUFS, &reqbufs) < 0) {
perror("REQBUFS failed");
}
这里有个坑——count字段。你申请4个,驱动可能只给你3个。所以reqbufs.count返回后要重新读一下。我在Exynos 5433上就遇到过,申请4个只给了2个,当时排查了半天。
第二步:查询并映射Buffer
申请完后,用VIDIOC_QUERYBUF拿到每个buffer的物理信息,然后mmap到用户空间。
struct v4l2_buffer buf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0; // 查询第0个buffer
buf.m.planes = planes;
buf.length = VIDEO_MAX_PLANES;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("QUERYBUF failed");
}
// 映射到用户空间
void *addr = mmap(NULL, planes[0].length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, planes[0].m.mem_offset);
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE而不是V4L2_BUF_TYPE_VIDEO_OUTPUT。单平面接口在MFC上会直接返回错误。我曾经因为这个浪费了一整天。
第三步:Buffer入队
映射完成后,把buffer交给驱动。用VIDIOC_QBUF。
struct v4l2_buffer buf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
buf.m.planes = planes;
buf.length = 1; // 这里注意,planes数组大小
planes[0].bytesused = input_data_size; // 告诉驱动数据长度
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("QBUF failed");
}
3.3 队列操作:Stream on/off与Dequeue
Buffer入队后,还不能开始干活。你得先调用VIDIOC_STREAMON。这个ioctl告诉驱动:我要开始处理数据了。
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("STREAMON failed");
}
嗯,这里要注意:编码器和解码器各有两个队列——OUTPUT队列(你喂数据给驱动)和CAPTURE队列(驱动吐数据给你)。两个队列都要分别调用STREAMON。
处理完数据后,用VIDIOC_DQBUF把buffer取回来。这个调用可能会阻塞,直到驱动处理完。
struct v4l2_buffer buf;
struct v4l2_plane planes[VIDEO_MAX_PLANES];
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
buf.memory = V4L2_MEMORY_MMAP;
buf.m.planes = planes;
buf.length = VIDEO_MAX_PLANES;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("DQBUF failed");
} else {
printf("Got buffer %d, size %d\n",
buf.index, planes[0].bytesused);
}
- REQBUFS → 申请buffer池
- QUERYBUF + mmap → 映射到用户空间
- QBUF → 把buffer交给驱动
- STREAMON → 启动处理
- DQBUF → 取回处理完的buffer
- 处理数据后,再QBUF → 循环
- STREAMOFF → 停止
3.4 ioctl调用:控制命令的瑞士军刀
V4L2里几乎所有操作都是通过ioctl完成的。除了上面说的那些,还有几个常用的:
| ioctl命令 | 用途 | 我在Exynos上的经验 |
|---|---|---|
VIDIOC_S_FMT |
设置格式(分辨率、像素格式等) | 编码前必须设,分辨率不对直接挂 |
VIDIOC_G_FMT |
获取当前格式 | 调试时经常用,确认驱动认没认 |
VIDIOC_S_PARAM |
设置编码参数(码率、GOP等) | Exynos的码率控制靠这个调 |
VIDIOC_ENUM_FMT |
枚举支持的格式 | 移植时必查,不同Exynos支持格式不同 |
VIDIOC_SUBSCRIBE_EVENT |
订阅事件(比如解码完成) | 高级用法,异步处理时很爽 |
举个例子,设置编码格式:
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
fmt.fmt.pix_mp.width = 1920;
fmt.fmt.pix_mp.height = 1080;
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12M; // Exynos MFC常用
fmt.fmt.pix_mp.num_planes = 2; // NV12是两平面
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("S_FMT failed");
}
V4L2_PIX_FMT_NV12(单平面),结果编码出来的视频全是花的。后来才发现MFC只支持V4L2_PIX_FMT_NV12M(多平面)。所以一定要先ENUM_FMT查一下驱动支持什么。
3.5 小结:V4L2在Exynos上的实战要点
好了,这一章的内容差不多就这些。总结几个我反复踩过的坑:
- 多平面是必须的:Exynos MFC只支持multi-planar接口,别用单平面
- 两个队列要分开管理:OUTPUT和CAPTURE各有各的buffer池,别混了
- STREAMON顺序有讲究:一般先开OUTPUT再开CAPTURE,反过来可能死锁
- DQBUF可能返回EAGAIN:如果用非阻塞模式,记得处理这个错误码
下一章我们会深入Exynos MFC的具体配置,包括如何设置H.264编码参数、如何控制码率。到时候我会拿一个实际项目中的例子来讲解,保证干货满满。