3.2. 流式 I/O (内存映射)

v4l2_capability 结构的 capabilities 字段中的 V4L2_CAP_STREAMING 标志被设置时,输入和输出设备支持此 I/O 方法。该结构由 ioctl VIDIOC_QUERYCAP ioctl 返回。有两种流式传输方法,要确定是否支持内存映射风格,应用程序必须调用 ioctl VIDIOC_REQBUFS ioctl,并将内存类型设置为 V4L2_MEMORY_MMAP

流式传输是一种 I/O 方法,其中应用程序和驱动程序之间仅交换指向缓冲区的指针,而不复制数据本身。内存映射主要用于将设备内存中的缓冲区映射到应用程序的地址空间中。设备内存可以是例如图形卡上的视频内存,带有视频捕获附加组件。然而,作为长期以来最有效的 I/O 方法,许多其他驱动程序也支持流式传输,在 DMA 可访问的主内存中分配缓冲区。

一个驱动程序可以支持许多缓冲区集。每个集合都由唯一的缓冲区类型值标识。这些集合是独立的,每个集合可以保存不同类型的数据。要同时访问不同的集合,必须使用不同的文件描述符。[1]

要分配设备缓冲区,应用程序使用所需的缓冲区数量和缓冲区类型调用 ioctl VIDIOC_REQBUFS ioctl,例如 V4L2_BUF_TYPE_VIDEO_CAPTURE。此 ioctl 也可用于更改缓冲区数量或释放已分配的内存,前提是没有缓冲区仍在映射。

在应用程序可以访问缓冲区之前,它们必须使用 mmap() 函数将其映射到其地址空间中。可以使用 ioctl VIDIOC_QUERYBUF ioctl 确定缓冲区在设备内存中的位置。在单平面 API 情况下,struct v4l2_buffer 中返回的 m.offsetlength 作为第六个和第二个参数传递给 mmap() 函数。使用多平面 API 时,struct v4l2_buffer 包含 struct v4l2_plane 结构的数组,每个结构都包含自己的 m.offsetlength。使用多平面 API 时,每个缓冲区的每个平面都必须单独映射,因此对 mmap() 的调用次数应等于缓冲区数量乘以每个缓冲区中的平面数量。不得修改偏移量和长度值。请记住,缓冲区是在物理内存中分配的,而不是虚拟内存,虚拟内存可以交换到磁盘。应用程序应尽快使用 munmap() 函数释放缓冲区。

3.2.1. 示例:在单平面 API 中映射缓冲区

struct v4l2_requestbuffers reqbuf;
struct {
    void *start;
    size_t length;
} *buffers;
unsigned int i;

memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = 20;

if (-1 == ioctl (fd, VIDIOC_REQBUFS, &reqbuf)) {
    if (errno == EINVAL)
        printf("Video capturing or mmap-streaming is not supported\\n");
    else
        perror("VIDIOC_REQBUFS");

    exit(EXIT_FAILURE);
}

/* We want at least five buffers. */

if (reqbuf.count < 5) {
    /* You may need to free the buffers here. */
    printf("Not enough buffer memory\\n");
    exit(EXIT_FAILURE);
}

buffers = calloc(reqbuf.count, sizeof(*buffers));
assert(buffers != NULL);

for (i = 0; i < reqbuf.count; i++) {
    struct v4l2_buffer buffer;

    memset(&buffer, 0, sizeof(buffer));
    buffer.type = reqbuf.type;
    buffer.memory = V4L2_MEMORY_MMAP;
    buffer.index = i;

    if (-1 == ioctl (fd, VIDIOC_QUERYBUF, &buffer)) {
        perror("VIDIOC_QUERYBUF");
        exit(EXIT_FAILURE);
    }

    buffers[i].length = buffer.length; /* remember for munmap() */

    buffers[i].start = mmap(NULL, buffer.length,
                PROT_READ | PROT_WRITE, /* recommended */
                MAP_SHARED,             /* recommended */
                fd, buffer.m.offset);

    if (MAP_FAILED == buffers[i].start) {
        /* If you do not exit here you should unmap() and free()
           the buffers mapped so far. */
        perror("mmap");
        exit(EXIT_FAILURE);
    }
}

/* Cleanup. */

for (i = 0; i < reqbuf.count; i++)
    munmap(buffers[i].start, buffers[i].length);

3.2.2. 示例:在多平面 API 中映射缓冲区

struct v4l2_requestbuffers reqbuf;
/* Our current format uses 3 planes per buffer */
#define FMT_NUM_PLANES = 3

struct {
    void *start[FMT_NUM_PLANES];
    size_t length[FMT_NUM_PLANES];
} *buffers;
unsigned int i, j;

memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
reqbuf.memory = V4L2_MEMORY_MMAP;
reqbuf.count = 20;

if (ioctl(fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
    if (errno == EINVAL)
        printf("Video capturing or mmap-streaming is not supported\\n");
    else
        perror("VIDIOC_REQBUFS");

    exit(EXIT_FAILURE);
}

/* We want at least five buffers. */

if (reqbuf.count < 5) {
    /* You may need to free the buffers here. */
    printf("Not enough buffer memory\\n");
    exit(EXIT_FAILURE);
}

buffers = calloc(reqbuf.count, sizeof(*buffers));
assert(buffers != NULL);

for (i = 0; i < reqbuf.count; i++) {
    struct v4l2_buffer buffer;
    struct v4l2_plane planes[FMT_NUM_PLANES];

    memset(&buffer, 0, sizeof(buffer));
    buffer.type = reqbuf.type;
    buffer.memory = V4L2_MEMORY_MMAP;
    buffer.index = i;
    /* length in struct v4l2_buffer in multi-planar API stores the size
     * of planes array. */
    buffer.length = FMT_NUM_PLANES;
    buffer.m.planes = planes;

    if (ioctl(fd, VIDIOC_QUERYBUF, &buffer) < 0) {
        perror("VIDIOC_QUERYBUF");
        exit(EXIT_FAILURE);
    }

    /* Every plane has to be mapped separately */
    for (j = 0; j < FMT_NUM_PLANES; j++) {
        buffers[i].length[j] = buffer.m.planes[j].length; /* remember for munmap() */

        buffers[i].start[j] = mmap(NULL, buffer.m.planes[j].length,
                 PROT_READ | PROT_WRITE, /* recommended */
                 MAP_SHARED,             /* recommended */
                 fd, buffer.m.planes[j].m.mem_offset);

        if (MAP_FAILED == buffers[i].start[j]) {
            /* If you do not exit here you should unmap() and free()
               the buffers and planes mapped so far. */
            perror("mmap");
            exit(EXIT_FAILURE);
        }
    }
}

/* Cleanup. */

for (i = 0; i < reqbuf.count; i++)
    for (j = 0; j < FMT_NUM_PLANES; j++)
        munmap(buffers[i].start[j], buffers[i].length[j]);

从概念上讲,流式驱动程序维护两个缓冲区队列,一个传入队列和一个传出队列。它们将锁定到视频时钟的同步捕获或输出操作与受随机磁盘或网络延迟以及其他进程抢占的应用程序分开,从而降低了数据丢失的可能性。这些队列组织为 FIFO,缓冲区将按照在传入 FIFO 中排队的顺序输出,并按照从传出 FIFO 中出队的顺序捕获。

驱动程序可能需要始终保持最少数量的缓冲区入队才能正常工作,除此之外,应用程序可以预先入队的缓冲区数量或出队和处理的缓冲区数量没有限制。它们也可以以与缓冲区出队不同的顺序入队,并且驱动程序可以以任何顺序填充入队的缓冲区。[2] 缓冲区索引号 (struct v4l2_buffer index) 在此处不起作用,它仅标识缓冲区。

最初,所有映射的缓冲区都处于出队状态,驱动程序无法访问。对于捕获应用程序,通常首先将所有映射的缓冲区入队,然后开始捕获并进入读取循环。在这里,应用程序等待直到可以出队一个已填充的缓冲区,并在不再需要数据时将缓冲区重新入队。输出应用程序填充缓冲区并将其入队,当堆叠了足够的缓冲区时,使用 VIDIOC_STREAMON 启动输出。在写入循环中,当应用程序用完空闲缓冲区时,它必须等待直到可以出队并重新使用一个空缓冲区。

要对缓冲区进行入队和出队,应用程序使用 VIDIOC_QBUFVIDIOC_DQBUF ioctl。可以使用 ioctl VIDIOC_QUERYBUF ioctl 随时确定缓冲区的映射、入队、已满或为空的状态。有两种方法可以暂停应用程序的执行,直到可以出队一个或多个缓冲区。默认情况下,当传出队列中没有缓冲区时,VIDIOC_DQBUF 会阻塞。当 O_NONBLOCK 标志被赋予 open() 函数时,当没有可用的缓冲区时,VIDIOC_DQBUF 会立即返回一个 EAGAIN 错误代码。select()poll() 函数始终可用。

要启动和停止捕获或输出,应用程序调用 VIDIOC_STREAMONVIDIOC_STREAMOFF ioctl。

实现内存映射 I/O 的驱动程序必须支持 VIDIOC_REQBUFSVIDIOC_QUERYBUFVIDIOC_QBUFVIDIOC_DQBUFVIDIOC_STREAMONVIDIOC_STREAMOFF ioctl,mmap()munmap()select()poll() 函数。[3]

[捕获示例]