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]

为了分配设备缓冲区,应用程序使用所需的缓冲区数量和缓冲区类型(例如 V4L2_BUF_TYPE_VIDEO_CAPTURE)调用 ioctl VIDIOC_REQBUFS ioctl。如果所有缓冲区都未映射,此 ioctl 也可用于更改缓冲区数量或释放已分配的内存。

在应用程序可以访问缓冲区之前,必须使用 mmap() 函数将其映射到它们的地址空间中。可以使用 ioctl VIDIOC_QUERYBUF ioctl 来确定缓冲区在设备内存中的位置。在单平面 API 的情况下,在 v4l2_buffer 结构体中返回的 m.offsetlength 作为第六个和第二个参数传递给 mmap() 函数。当使用多平面 API 时,v4l2_buffer 结构体包含 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] 缓冲区的索引号(v4l2_buffer 结构体的 index)在此处不起作用,它仅标识缓冲区。

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

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

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

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

[捕获示例]