4. 请求 API

请求 API 的设计目的是允许 V4L2 处理现代设备(无状态编解码器、复杂的摄像头管道等)和 API(Android 编解码器 v2)的要求。其中一个要求是属于同一管道的设备能够在逐帧的基础上重新配置和紧密协作。另一个要求是支持无状态编解码器,这需要将控制应用于特定帧(也称为“逐帧控制”)才能高效使用。

虽然最初的用例是 V4L2,但只要它们使用媒体控制器,它也可以扩展到其他子系统。

在没有请求 API 的情况下支持这些功能并非总是可行,如果可行,效率也会非常低下:用户空间必须刷新媒体管道上的所有活动,为下一帧重新配置它,将要使用该配置处理的缓冲区排队,并等待它们全部可用于出队,然后才能考虑下一帧。这破坏了拥有缓冲区队列的目的,因为实际上一次只会有一个缓冲区排队。

请求 API 允许将管道的特定配置(媒体控制器拓扑结构 + 每个媒体实体的配置)与特定缓冲区相关联。这允许用户空间提前调度多个具有不同配置的任务(“请求”),并且知道配置将在需要时应用以获得预期结果。还可读取请求完成时的配置值。

4.1. 一般用法

请求 API 扩展了媒体控制器 API,并与特定于子系统的 API 合作以支持请求用法。在媒体控制器级别,请求是从支持的媒体控制器设备节点分配的。然后,它们的生命周期通过请求文件描述符以不透明的方式进行管理。存储在请求中的配置数据、缓冲区句柄和处理结果通过扩展为支持请求的子系统特定 API 进行访问,例如采用显式 request_fd 参数的 V4L2 API。

4.2. 请求分配

用户空间使用媒体设备节点的 ioctl MEDIA_IOC_REQUEST_ALLOC 来分配请求。这将返回一个表示请求的文件描述符。通常,会分配多个此类请求。

4.3. 请求准备

然后,标准 V4L2 ioctl 可以接收请求文件描述符,以表明 ioctl 是所述请求的一部分,并且不会立即应用。有关支持此功能的 ioctl 列表,请参阅 ioctl MEDIA_IOC_REQUEST_ALLOC。使用 request_fd 参数设置的配置将存储而不是立即应用,并且排队到请求的缓冲区在请求本身排队之前不会进入常规缓冲区队列。

4.4. 请求提交

一旦指定了请求的配置和缓冲区,就可以通过在请求文件描述符上调用 ioctl MEDIA_REQUEST_IOC_QUEUE 将其排队。请求必须至少包含一个缓冲区,否则将返回 ENOENT。已排队的请求不能再修改。

注意

对于内存到内存设备,你只能将请求用于输出缓冲区,而不能用于捕获缓冲区。尝试将捕获缓冲区添加到请求将导致 EBADR 错误。

如果请求包含多个实体的配置,则各个驱动程序可能会同步,以便在处理缓冲区之前应用请求的管道的拓扑结构。媒体控制器驱动程序会尽最大努力实现,因为由于硬件限制,可能无法实现完美的原子性。

注意

不允许将排队请求与直接排队缓冲区混合使用:无论哪种方法先使用,都会将此锁定到位,直到调用 VIDIOC_STREAMOFF 或设备 关闭为止。如果在较早时通过请求排队了缓冲区,或者反之,则尝试直接排队缓冲区将导致 EBUSY 错误。

即使在使用请求与否的情况下,控制仍然可以在没有请求的情况下设置并立即应用。

注意

通过请求和直接方式设置相同的控制可能会导致未定义的行为!

用户空间可以 poll() 请求文件描述符,以等待请求完成。一旦其所有关联的缓冲区都可用于出队,并且所有关联的控件都已使用完成时的值进行更新,则请求被视为已完成。请注意,用户空间无需等待请求完成即可将其缓冲区出队:可以在请求的中间部分出队的缓冲区可以独立于请求的状态出队。

完成的请求包含请求执行后设备的状态。用户空间可以通过使用请求文件描述符调用 ioctl VIDIOC_G_EXT_CTRLS 来查询该状态。对已排队但尚未完成的请求调用 ioctl VIDIOC_G_EXT_CTRLS 将返回 EBUSY,因为在请求正在进行时,驱动程序随时可能更改控件值。

4.5. 回收和销毁

最后,可以丢弃或重用已完成的请求。在请求文件描述符上调用 close() 将使该文件描述符无法使用,并且一旦内核不再使用该请求,该请求将被释放。也就是说,如果请求已排队,然后关闭文件描述符,则在驱动程序完成请求之前不会释放该请求。

ioctl MEDIA_REQUEST_IOC_REINIT 将清除请求的状态并使其再次可用。此操作不会保留任何状态:请求就像刚分配一样。

4.6. 编解码器设备的示例

对于 编解码器等用例,请求 API 可用于将特定控件与驱动程序要应用的 OUTPUT 缓冲区相关联,从而允许用户空间提前排队许多此类缓冲区。它还可以利用请求在请求完成时捕获控件状态的能力,以读取可能发生更改的信息。

放入代码中,在获取请求后,用户空间可以为其分配控件和一个 OUTPUT 缓冲区

struct v4l2_buffer buf;
struct v4l2_ext_controls ctrls;
int req_fd;
...
if (ioctl(media_fd, MEDIA_IOC_REQUEST_ALLOC, &req_fd))
        return errno;
...
ctrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
ctrls.request_fd = req_fd;
if (ioctl(codec_fd, VIDIOC_S_EXT_CTRLS, &ctrls))
        return errno;
...
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.flags |= V4L2_BUF_FLAG_REQUEST_FD;
buf.request_fd = req_fd;
if (ioctl(codec_fd, VIDIOC_QBUF, &buf))
        return errno;

请注意,由于此处没有要报告的逐帧设置,因此不允许将请求 API 用于 CAPTURE 缓冲区。

一旦请求完全准备好,就可以将其排队到驱动程序

if (ioctl(req_fd, MEDIA_REQUEST_IOC_QUEUE))
        return errno;

然后,用户空间可以通过在其文件描述符上调用 poll() 来等待请求完成,或者开始将 CAPTURE 缓冲区出队。最有可能的是,它希望尽快获取 CAPTURE 缓冲区,这可以使用常规的 VIDIOC_DQBUF 完成

struct v4l2_buffer buf;

memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(codec_fd, VIDIOC_DQBUF, &buf))
        return errno;

请注意,此示例为了简单起见,假设每个 OUTPUT 缓冲区将有一个 CAPTURE 缓冲区,但这并非必须如此。

然后,在通过轮询请求文件描述符确保请求已完成后,我们可以通过调用 VIDIOC_G_EXT_CTRLS 来查询其完成时的控件值。这对于我们希望在生成捕获缓冲区后立即查询值的易失性控件特别有用。

struct pollfd pfd = { .events = POLLPRI, .fd = req_fd };
poll(&pfd, 1, -1);
...
ctrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
ctrls.request_fd = req_fd;
if (ioctl(codec_fd, VIDIOC_G_EXT_CTRLS, &ctrls))
        return errno;

一旦我们不再需要该请求,我们可以使用 ioctl MEDIA_REQUEST_IOC_REINIT 重复使用该请求...

if (ioctl(req_fd, MEDIA_REQUEST_IOC_REINIT))
        return errno;

...或关闭其文件描述符以完全处置它。

close(req_fd);

4.7. 简单捕获设备的示例

对于简单的捕获设备,可以使用请求来指定要为给定 CAPTURE 缓冲区应用的控件。

struct v4l2_buffer buf;
struct v4l2_ext_controls ctrls;
int req_fd;
...
if (ioctl(media_fd, MEDIA_IOC_REQUEST_ALLOC, &req_fd))
        return errno;
...
ctrls.which = V4L2_CTRL_WHICH_REQUEST_VAL;
ctrls.request_fd = req_fd;
if (ioctl(camera_fd, VIDIOC_S_EXT_CTRLS, &ctrls))
        return errno;
...
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.flags |= V4L2_BUF_FLAG_REQUEST_FD;
buf.request_fd = req_fd;
if (ioctl(camera_fd, VIDIOC_QBUF, &buf))
        return errno;

一旦请求完全准备好,就可以将其排队到驱动程序

if (ioctl(req_fd, MEDIA_REQUEST_IOC_QUEUE))
        return errno;

然后,用户空间可以像上面的 M2M 示例中一样,将缓冲区出队、等待请求完成、查询控件并回收请求。