4. 请求 API

请求 API 旨在允许 V4L2 处理现代设备(无状态编解码器、复杂摄像头管道等)和 API(Android Codec 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 MEDIA_IOC_REQUEST_ALLOC 以获取支持此功能的 ioctl 列表。使用 request_fd 参数设置的配置会被存储而不是立即应用,并且排队到请求的缓冲区在请求本身排队之前不会进入常规缓冲区队列。

4.4. 请求提交

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

注意

对于 memory-to-memory devices,您只能将请求用于输出缓冲区,而不能用于捕获缓冲区。尝试将捕获缓冲区添加到请求将导致 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. 编解码器设备的示例

对于诸如 codecs 之类的用例,请求 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;

请注意,不允许将 Request 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 示例中一样,将缓冲区出队,等待请求完成,查询控件并回收请求。