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]

[捕獲示例]