V4L2(Video For Linux Two) 是Linux内核提供给应用程序访问音、视频驱动的统一接口。这里描述的是如何从遵循V4L2规范的Camera设备读取Video帧。
1. 打开设备
int fd = open (“/dev/video0”, O_RDWR | O_NONBLOCK, 0);
2. 查询设备的Capability
查询设备的capability,可以从capability判断设备的类型、特性等。这一步不是必需的,但如果程序需要支持多种型号的设备,capability就很有用了。
struct v4l2_capability cap;
int ret = ioctl (fd, VIDIOC_QUERYCAP, &cap);
一般的设备支持read()和write(),读写需要复制数据。对于Camera设备,应用程序和驱动只交换缓存指针,不拷贝数据。这种I/O方法叫做“流”。
如果capability包含V4L2_CAP_STREAMING标志,则支持“流”。
3. 设置视频源
如果一个video设备有多个视频源,则可能需要使用VIDIOC_S_INPUT,在视频源间切换。
struct v4l2_input inp;
inp.index = 0;
ret = ioctl (fd, VIDIOC_S_INPUT, &inp);
4. 设置Capture参数
Capture参数包括模式、帧率等。
struct v4l2_streamparm params;
memset (¶ms, 0, sizeof(params));
params.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
params.parm.capture.capturemode = V4L2_MODE_HIGHQUALITY;
params.parm.capture.timeperframe.numerator = 1;
params.parm.capture.timeperframe.denominator = 25;
ret = ioctl (fd, VIDIOC_S_PARM, ¶ms);
5. 设置视频参数
视频参数包括缓存类型、视频大小、场类型、帧格式等。
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width;
fmt.fmt.pix.height = height;
fmt.fmt.pix_mp.field = V4L2_FIELD_NONE;
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV21;
ret = ioctl (fd, VIDIOC_S_FMT, &fmt);
设备可能不支持指定的视频参数,可以用VIDIOC_G_FMT检查设置是否成功。
struct v4l2_format fmt;
ret = ioctl (fd, VIDIOC_G_FMT, &fmt);
6. 分配缓存
Camera设备有三个队列。缓存刚分配时放在EMPTY队列,等待设备填充时放在IN队列,设备填充好时放在OUT队列。
下面的代码中,使用V4L2_MEMORY_MMAP要求设备分配6个缓存。这时缓存放在EMPTY队列。
struct v4l2_requestbuffers rb;
memset (&rb, 0, sizeof(rb));
rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
rb.memory = V4L2_MEMORY_MMAP;
rb.count = 6;
ret = ioctl (fd, VIDIOC_REQBUFS, &rb);
V4L2_MEMORY_MMAP之外还可能有其他选择,如V4L2_MEMORY_DMABUF。这时缓存不是设备分配,而是应用程序传进去。它可能来自其他Video设备,或任何能分配DMABUF的设备。
7. 得到所有缓存,放入设备的 EMPTY 队列
下面的代码中,使用索引值依次得到6个缓存。对每个缓存,使用VIDIOC_QUERYBUF从EMPTY 队列取出,再使用VIDIOC_QBUF放到 IN 队列。如果应用层代码需要处理帧,则可以使用mmap()将它映射到虚拟地址空间。
void* mmap[6];
int mlen;
struct v4l2_buffer buf;
for (int i = 0; i < 6; i++)
{
memset (&buf, 0, sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
// get it from EMPTY queue.
ret = ioctl (fd, VIDIOC_QUERYBUF, &buf);
// map it to file descriptor
mmap[i] = mmap (0, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
mlen = buf.length;
// put it into IN queue.
ret = ioctl (fd, VIDIOC_QBUF, &buf);
}
如果设备支持,也可以使用VIDIOC_EXPBUF导出buffer,得到dmafd。
- 其他模块,如OpenGL,可以使用这个dmafd显示图像。
- 也可以使用send_fd()将dmafd传给其他进程处理。这样做的好处是避免大块数据的复制。
struct v4l2_exportbuffer expbuf;
memset (&expbuf, 0, sizeof(expbuf));
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
expbuf.index = i;
ret = ioctl (fd, VIDIOC_EXPBUF, &expbuf);
int dmfd = expbuf.fd;
8. 开始Capture
使用VIDIOC_STREAMON要求设备开始工作。
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl (fd, VIDIOC_STREAMON, &type);
9. 读帧
下面的代码监听设备,如果有数据,则使用VIDIOC_DQBUF读取帧,然后传给其他线程做进一步处理。
fd_set fds;
FD_ZERO (&fds);
FD_SET (fd, &fds);
ret = select (fd + 1, &fds, NULL, NULL, NULL);
if (ret > 0)
{
struct v4l2_buffer buf;
memset (&buf, 0, sizeof(v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ret = ioctl (fd, VIDIOC_DQBUF, buf);
// hand it over for next step
...
}
10. 归还缓存
其他线程处理帧后,使用VIDIOC_QBUF将缓存归还给设备。前面已经做过这件事了。
struct v4l2_buffer buf;
memset (&buf, 0, sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ret = ioctl (fd, VIDIOC_QBUF, &buf);
11. 缓存 单平面 vs 多平面
这个例子中演示的设备是单平面模式 V4L2_BUF_TYPE_VIDEO_CAPTURE,就是一个v4l2_buffer缓存中只有一个平面。
另外一种设备是多平面模式V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,就是一个v4l2_buffer缓存中有一个或多个平面。
参考资料
Linux之V4L2基础编程
https://www.cnblogs.com/emouse/archive/2013/03/04/2943243.html
ioctl VIDIOC_EXPBUF
https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/vidioc-expbuf.html#vidioc-expbuf
Streaming I/O (DMA buffer importing)
https://blog.csdn.net/jk198310/article/details/78365224
V4L2文档翻译:输入和输出
https://blog.csdn.net/airk000/article/details/25033269