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);  // 平台相关
我的小技巧: 查完capabilities后,记得检查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);
注意: Exynos的MFC支持多平面(multi-planar),所以一定要用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);
}
核心流程总结:
  1. REQBUFS → 申请buffer池
  2. QUERYBUF + mmap → 映射到用户空间
  3. QBUF → 把buffer交给驱动
  4. STREAMON → 启动处理
  5. DQBUF → 取回处理完的buffer
  6. 处理数据后,再QBUF → 循环
  7. 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");
}
避坑指南: 我曾经在Exynos 7420上设置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编码参数、如何控制码率。到时候我会拿一个实际项目中的例子来讲解,保证干货满满。