v4l2 api基本使用流程 - V4l2学习笔记

v4l2(video for Linux two)是linux为视频设备提供的一套api接口规范,two表示版本为2。利用该接口应用程序可以从摄像头外设读取一系列图片进行处理。本文主要是实现使用该接口获取摄像头图片保存到本地,以此了解v4l2 api的基本使用方法。后续工作如果需要也要深入了解下各种摄像头、图像参数的意义,进阶的话还想学习内核侧v4l2驱动的编写。参考了网络上的一些博客123等。

使用v4l2从摄像头读取图片的基本流程为:

  • 打开设备
  • 检查和设置设备属性
  • 设置图片帧格式
  • 设置input/output方式
  • 循环获取数据
  • 关闭设备

v4l2相关的数据结构、枚举值等都定义在<linux/videodev2.h>中。有如下几个需要提前知道的地方。

  • ioctl()函数用于操作设备,声明如下。

    c

    #include <sys/ioctl.h>
    int ioctl(int fd, unsigned long op, ...);  /* glibc, BSD */

    其中fd为打开设备的fd。参数op为操作类型,不同类型的设备可能不同,例如v4l2的api使用VIDIOC_*的宏来表示这些操作数。其后的参数是不定长的,根据不同的参数op应该接着传入不同的参数,用于设置或获取信息。

  • 程序中定义了如下的几个宏,包括LOG_INFO和LOG_ERROR打印日志和errExit用于判断函数是否调用成功,如果失败就打印原因后退出。

    c

    #define LOG(level) std::cout << std::endl << #level << " " << __FILE__ << ":" << __LINE__ << " "
    #define LOG_INFO LOG(INFO)
    #define LOG_ERROR LOG(ERROR)
    
    #define errExit(stmt)                                                                      \
        {                                                                                      \
            if ((stmt) < 0) {                                                                  \
                LOG_ERROR << #stmt << ", errno: " << errno << ", errstr: " << strerror(errno); \
                exit(1);                                                                       \
            }                                                                                  \
        }

一般情况下设备地址就是/dev/video0。对于同一个摄像头物理设备可能对应多个video设备地址,一般第一个可以用来采图,其余有其它作用,具体还未研究。像我的笔记本就有/dev/video0~/dev/video3四个设备,但只有打开第一个能出jpg格式的图片。可以使用如下命令对比这些设备的信息diff:

shell

vimdiff <(v4l2-ctl -d /dev/video0 --all) <(v4l2-ctl -d /dev/video1 --all)

在代码实现中,可以简单的以O_RDWR可读写的方式打开摄像头设备的地址。

c

int vfd = -1;
errExit(vfd = open("/dev/video0", O_RDWR));

设备属性通过结构体v4l2_capability表示,使用op为VIDIOC_QUERYCAP的ioctrl可以获取设备属性信息。并且检查设备是否支持视频捕获功能。

c++

errExit(ioctl(vfd, VIDIOC_QUERYCAP, &cap));
LOG_INFO << "driver: " << cap.driver << " , card: " << cap.card << " , bus: " << cap.bus_info << " , version: " << cap.version
          << " , cap: " << cap.capabilities;
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
    LOG_ERROR << "not capture device";
    return -1;
}

其中结构体v4l2_capability的声明如下。

c

/**
  * struct v4l2_capability - Describes V4L2 device caps returned by VIDIOC_QUERYCAP
  *
  * @driver:    name of the driver module (e.g. "bttv")
  * @card:    name of the card (e.g. "Hauppauge WinTV")
  * @bus_info:    name of the bus (e.g. "PCI:" + pci_name(pci_dev) )
  * @version:    KERNEL_VERSION
  * @capabilities: capabilities of the physical device as a whole
  * @device_caps:  capabilities accessed via this particular device (node)
  * @reserved:    reserved fields for future extensions
  */
struct v4l2_capability {
 __u8 driver[16];
 __u8 card[32];
 __u8 bus_info[32];
 __u32   version;
 __u32 capabilities;
 __u32 device_caps;
 __u32 reserved[3];
};

也可以通过结构体v4l2_fmtdesc和op为VIDIOC_ENUM_FMT循环获取支持的格式列表。

c++

v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (ioctl(vfd, VIDIOC_ENUM_FMT, &fmtdesc) != -1) {
    LOG_INFO << "index: " << fmtdesc.index << ", desc: " << fmtdesc.description;
    ++fmtdesc.index;
}

通过结构体v4l2_format和op为VIDIOC_S_FMT设置帧格式。

c++

struct v4l2_format fmt {};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
errExit(ioctl(vfd, VIDIOC_S_FMT, &fmt));

一般使用mmap传输方式映射内核空间的图片到用户空间,相较于直接read()减少内核空间到用户空间的一次数据拷贝。

首先通过VIDIOC_REQBUFS向v4l2申请内核空间缓存的数量。

c++

struct v4l2_requestbuffers req;
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
errExit(ioctl(vfd, VIDIOC_REQBUFS, &req));

然后通过VIDIOC_QUERYBUF获取循环每个缓存在内核buffer中的偏移量,从而使用mmap映射到用户内存空间,并且将用户空间的起始地址和长度保存起来,供后边从中拿取图片。

c++

struct v4l2_buffer buf {};
for (int n_buffer = 0; n_buffer < req.count; ++n_buffer) {
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    buf.index = n_buffer;
    errExit(ioctl(vfd, VIDIOC_QUERYBUF, &buf));
    LOG_INFO << "length: " << buf.length << ", offset: " << buf.m.offset << ", fd: " << buf.m.fd << ", index: " << buf.index;
    buffers[n_buffer].length = buf.length;
    buffers[n_buffer].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vfd, buf.m.offset);
    if (MAP_FAILED == buffers[n_buffer].start) {
        perror("mmap");
        continue;
    }
    errExit(ioctl(vfd, VIDIOC_QBUF, &buf)); // 将buf放入驱动队列
}

特别注意要使用VIDIOC_QBUF将buf放入驱动的队列中,否则队列大小为空直接取数据是会被永远阻塞

首先使用VIDIOC_STREAMON开启数据流。之后通过VIDIOC_DQBUF从驱动队列中消费图片,找到buf缓冲index对应的mmap映射用户空间的地址来获取图片。最后调用VIDIOC_QBUF将消费后的图片放入队列中。通过这个循环可以源源不断的取图片形成视频。我这里仅仅保存了8张图片用于验证。

c

struct v4l2_buffer capture_buf {};
capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
capture_buf.memory = V4L2_MEMORY_MMAP;
int img_cnt = 0;
for (int i = 0; img_cnt < BUFFER_COUNT * 2; ++i) {
    errExit(ioctl(vfd, VIDIOC_DQBUF, &capture_buf));
    LOG_INFO << "idx: " << i << ", bytesused: " << capture_buf.bytesused;
    if (0 != capture_buf.bytesused && 0 == i % 10) {
        ++img_cnt;
        // 保存图片到本地
        int imgfd = open((std::string("capture_") + std::to_string(i) + ".jpg").c_str(), O_WRONLY | O_CREAT, 0666);
        errExit(imgfd);
        errExit(write(imgfd, buffers[capture_buf.index].start, capture_buf.bytesused));
        errExit(close(imgfd));
    }

    if (0 != ioctl(vfd, VIDIOC_QBUF, &capture_buf)) {
        perror("VIDIOC_QBUF");
        return 1;
    }
}

最后通过VIDIOC_STREAMOFF关闭数据流,munmap取消之前的mmap映射的地址,close()关闭设备fd。

c++

errExit(ioctl(vfd, VIDIOC_STREAMOFF, &buf.type));
for (int i = 0; i < BUFFER_COUNT; ++i) {
    munmap(buffers[i].start, buffers[i].length);
}
close(vfd);

c++

#include <fcntl.h>
#include <linux/videodev2.h>
#include <memory.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <unistd.h>

#include <iostream>
#include <string>

#define BUFFER_COUNT 4
#define WIDTH 640
#define HEIGHT 480
#define LOG(level) std::cout << std::endl << #level << " " << __FILE__ << ":" << __LINE__ << " "
#define LOG_INFO LOG(INFO)
#define LOG_ERROR LOG(ERROR)

#define errExit(stmt)                                                                      \
    {                                                                                      \
        if ((stmt) < 0) {                                                                  \
            LOG_ERROR << #stmt << ", errno: " << errno << ", errstr: " << strerror(errno); \
            exit(1);                                                                       \
        }                                                                                  \
    }

struct buffer {
    void *start;
    uint length;
} buffers[BUFFER_COUNT];

int main() {
    int vfd = -1;
    errExit(vfd = open("/dev/video0", O_RDWR));
    v4l2_capability cap{0};
    errExit(ioctl(vfd, VIDIOC_QUERYCAP, &cap));
    LOG_INFO << "driver: " << cap.driver << " , card: " << cap.card << " , bus: " << cap.bus_info << " , version: " << cap.version
             << " , cap: " << cap.capabilities;
    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        LOG_ERROR << "not capture device";
        return -1;
    }

    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (ioctl(vfd, VIDIOC_ENUM_FMT, &fmtdesc) != -1) {
        LOG_INFO << "index: " << fmtdesc.index << ", desc: " << fmtdesc.description;
        ++fmtdesc.index;
    }

    struct v4l2_format fmt {};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = WIDTH;
    fmt.fmt.pix.height = HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG;
    fmt.fmt.pix.field = V4L2_FIELD_NONE;
    errExit(ioctl(vfd, VIDIOC_S_FMT, &fmt));

    struct v4l2_requestbuffers req;
    req.count = BUFFER_COUNT;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    errExit(ioctl(vfd, VIDIOC_REQBUFS, &req));

    struct v4l2_buffer buf {};
    for (int n_buffer = 0; n_buffer < req.count; ++n_buffer) {
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = n_buffer;
        errExit(ioctl(vfd, VIDIOC_QUERYBUF, &buf));
        LOG_INFO << "length: " << buf.length << ", offset: " << buf.m.offset << ", fd: " << buf.m.fd << ", index: " << buf.index;
        buffers[n_buffer].length = buf.length;
        buffers[n_buffer].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vfd, buf.m.offset);
        if (MAP_FAILED == buffers[n_buffer].start) {
            perror("mmap");
            continue;
        }
        errExit(ioctl(vfd, VIDIOC_QBUF, &buf));
    }

    errExit(ioctl(vfd, VIDIOC_STREAMON, &buf.type));

    // 等待fd可读。可以省略
    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(vfd, &fds);
    struct timeval tv {};
    tv.tv_sec = 2;
    errExit(select(vfd + 1, &fds, NULL, NULL, &tv));

    struct v4l2_buffer capture_buf {};
    capture_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    capture_buf.memory = V4L2_MEMORY_MMAP;
    int img_cnt = 0;
    for (int i = 0; img_cnt < BUFFER_COUNT * 2; ++i) {
        errExit(ioctl(vfd, VIDIOC_DQBUF, &capture_buf));
        LOG_INFO << "idx: " << i << ", bytesused: " << capture_buf.bytesused;
        if (0 != capture_buf.bytesused && 0 == i % 10) {
            ++img_cnt;
            int imgfd = open((std::string("capture_") + std::to_string(i) + ".jpg").c_str(), O_WRONLY | O_CREAT, 0666);
            errExit(imgfd);
            errExit(write(imgfd, buffers[capture_buf.index].start, capture_buf.bytesused));
            errExit(close(imgfd));
        }

        if (0 != ioctl(vfd, VIDIOC_QBUF, &capture_buf)) {
            perror("VIDIOC_QBUF");
            return 1;
        }
    }
    errExit(ioctl(vfd, VIDIOC_STREAMOFF, &buf.type));
    for (int i = 0; i < BUFFER_COUNT; ++i) {
        munmap(buffers[i].start, buffers[i].length);
    }
    close(vfd);
}