avutil/hwcontext_v4l2request: Probe for a capable media and video device

Probe all media devices and its linked video devices to locate a video
device that support stateless decoding of the specific codec using the
V4L2 Request API.

A video device is deemed capable when all tests pass, e.g. kernel
drivers support the coded pixel format and the frame size is supported.

Basic flow for initialization follows the kernel Memory-to-memory
Stateless Video Decoder Interface > Initialization [1].

[1] https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/dev-stateless-decoder.html#initialization

Co-developed-by: Jernej Skrabec <jernej.skrabec@gmail.com>
Signed-off-by: Jernej Skrabec <jernej.skrabec@gmail.com>
Co-developed-by: Alex Bee <knaerzche@gmail.com>
Signed-off-by: Alex Bee <knaerzche@gmail.com>
Signed-off-by: Jonas Karlman <jonas@kwiboo.se>
This commit is contained in:
Jonas Karlman 2024-08-06 09:06:02 +00:00
parent bb10d3d5a1
commit 3f72885fb2
2 changed files with 510 additions and 5 deletions

9
configure vendored
View file

@ -2023,6 +2023,7 @@ EXTERNAL_LIBRARY_LIST="
libtorch
libtwolame
libuavs3d
libudev
libv4l2
libvmaf
libvorbis
@ -3248,8 +3249,8 @@ dxva2_deps="dxva2api_h DXVA2_ConfigPictureDecode ole32 user32"
ffnvcodec_deps_any="libdl LoadLibrary"
mediacodec_deps="android mediandk pthreads"
nvdec_deps="ffnvcodec"
v4l2_request_deps="linux_media_h v4l2_timeval_to_ns libdrm"
v4l2_request_suggest="libdrm"
v4l2_request_deps="linux_media_h v4l2_timeval_to_ns libdrm libudev"
v4l2_request_suggest="libdrm libudev"
vaapi_x11_deps="xlib_x11"
videotoolbox_hwaccel_deps="videotoolbox pthreads"
videotoolbox_hwaccel_extralibs="-framework QuartzCore"
@ -4210,7 +4211,7 @@ swscale_suggest="libm stdatomic"
avcodec_extralibs="pthreads_extralibs iconv_extralibs dxva2_extralibs liblcevc_dec_extralibs lcms2_extralibs"
avfilter_extralibs="pthreads_extralibs"
avutil_extralibs="d3d11va_extralibs d3d12va_extralibs mediacodec_extralibs nanosleep_extralibs pthreads_extralibs vaapi_drm_extralibs vaapi_x11_extralibs vaapi_win32_extralibs vdpau_x11_extralibs"
avutil_extralibs="d3d11va_extralibs d3d12va_extralibs mediacodec_extralibs nanosleep_extralibs pthreads_extralibs v4l2_request_extralibs vaapi_drm_extralibs vaapi_x11_extralibs vaapi_win32_extralibs vdpau_x11_extralibs"
# programs
ffmpeg_deps="avcodec avfilter avformat threads"
@ -7478,6 +7479,8 @@ fi
if enabled v4l2_request; then
check_func_headers "linux/media.h linux/videodev2.h" v4l2_timeval_to_ns
check_pkg_config libudev libudev libudev.h udev_new
v4l2_request_extralibs="$libudev_extralibs"
fi
check_headers sys/videoio.h

View file

@ -26,6 +26,7 @@
#include <unistd.h>
#include <drm_fourcc.h>
#include <libudev.h>
#include "avassert.h"
#include "hwcontext_drm.h"
@ -33,7 +34,16 @@
#include "hwcontext_v4l2request_internal.h"
#include "mem.h"
typedef struct V4L2RequestVideoDecoder {
dev_t media_dev;
dev_t video_dev;
uint32_t *pixelformats;
int nb_pixelformats;
} V4L2RequestVideoDecoder;
typedef struct V4L2RequestDeviceContext {
V4L2RequestVideoDecoder *decoders;
int nb_decoders;
} V4L2RequestDeviceContext;
typedef struct V4L2RequestFramesContext {
@ -119,25 +129,517 @@ static int v4l2request_set_drm_descriptor(AVDRMFrameDescriptor *desc,
static void v4l2request_device_uninit(AVHWDeviceContext *hwdev)
{
V4L2RequestDeviceContext *hwctx = hwdev->hwctx;
av_freep(&hwctx->decoders);
hwctx->nb_decoders = 0;
}
static int v4l2request_device_create(AVHWDeviceContext *hwdev, const char *device,
AVDictionary *opts, int flags)
{
V4L2RequestDeviceContext *hwctx = hwdev->hwctx;
hwctx->decoders = NULL;
hwctx->nb_decoders = 0;
// TODO: enumerate V4L2 Request API capable video decoders
// and fill hwctx->decoders and hwctx->nb_decoders,
// limit to decoders for the media 'device' when specified
return 0;
}
static int v4l2request_set_format(AVHWFramesContext *hwfc,
enum v4l2_buf_type type,
uint32_t pixelformat,
uint32_t buffersize)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct v4l2_format format = {
.type = type,
};
if (V4L2_TYPE_IS_MULTIPLANAR(type)) {
format.fmt.pix_mp.width = hwfc->width;
format.fmt.pix_mp.height = hwfc->height;
format.fmt.pix_mp.pixelformat = pixelformat;
format.fmt.pix_mp.plane_fmt[0].sizeimage = buffersize;
format.fmt.pix_mp.num_planes = 1;
} else {
format.fmt.pix.width = hwfc->width;
format.fmt.pix.height = hwfc->height;
format.fmt.pix.pixelformat = pixelformat;
format.fmt.pix.sizeimage = buffersize;
}
if (ioctl(fctxi->video_fd, VIDIOC_S_FMT, &format) < 0)
return AVERROR(errno);
return 0;
}
static int v4l2request_select_capture_format(AVHWFramesContext *hwfc)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
enum v4l2_buf_type type = fctxi->capture.format.type;
struct v4l2_format format = {
.type = type,
};
struct v4l2_fmtdesc fmtdesc = {
.index = 0,
.type = type,
};
uint32_t pixelformat;
// Get the driver preferred (or default) format
if (ioctl(fctxi->video_fd, VIDIOC_G_FMT, &format) < 0)
return AVERROR(errno);
pixelformat = V4L2_TYPE_IS_MULTIPLANAR(type) ?
format.fmt.pix_mp.pixelformat :
format.fmt.pix.pixelformat;
// Use the driver preferred format when it is supported
for (int i = 0; i < FF_ARRAY_ELEMS(v4l2request_capture_pixelformats); i++) {
if (pixelformat == v4l2request_capture_pixelformats[i].pixelformat &&
(fctx->bit_depth == v4l2request_capture_pixelformats[i].bit_depth ||
!fctx->bit_depth))
return v4l2request_set_format(hwfc, type, pixelformat, 0);
}
// Otherwise, use first format that is supported
while (ioctl(fctxi->video_fd, VIDIOC_ENUM_FMT, &fmtdesc) >= 0) {
for (int i = 0; i < FF_ARRAY_ELEMS(v4l2request_capture_pixelformats); i++) {
if (fmtdesc.pixelformat == v4l2request_capture_pixelformats[i].pixelformat &&
(fctx->bit_depth == v4l2request_capture_pixelformats[i].bit_depth ||
!fctx->bit_depth))
return v4l2request_set_format(hwfc, type, fmtdesc.pixelformat, 0);
}
fmtdesc.index++;
}
return AVERROR(errno);
}
static int v4l2request_try_framesize(AVHWFramesContext *hwfc,
uint32_t pixelformat)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct v4l2_frmsizeenum frmsize = {
.index = 0,
.pixel_format = pixelformat,
};
// Enumerate and check if frame size is supported
while (ioctl(fctxi->video_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) >= 0) {
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE &&
hwfc->width == frmsize.discrete.width &&
hwfc->height == frmsize.discrete.height) {
return 0;
} else if ((frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE ||
frmsize.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) &&
hwfc->width >= frmsize.stepwise.min_width &&
hwfc->height >= frmsize.stepwise.min_height &&
hwfc->width <= frmsize.stepwise.max_width &&
hwfc->height <= frmsize.stepwise.max_height) {
return 0;
}
frmsize.index++;
}
return AVERROR(errno);
}
static int v4l2request_try_format(AVHWFramesContext *hwfc,
enum v4l2_buf_type type,
uint32_t pixelformat)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct v4l2_fmtdesc fmtdesc = {
.index = 0,
.type = type,
};
// Enumerate and check if format is supported
while (ioctl(fctxi->video_fd, VIDIOC_ENUM_FMT, &fmtdesc) >= 0) {
if (fmtdesc.pixelformat == pixelformat)
return 0;
fmtdesc.index++;
}
return AVERROR(errno);
}
static int v4l2request_set_controls(AVHWFramesContext *hwfc,
struct v4l2_ext_control *control, int count)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct v4l2_ext_controls controls = {
.controls = control,
.count = count,
};
if (!control || !count)
return 0;
if (ioctl(fctxi->video_fd, VIDIOC_S_EXT_CTRLS, &controls) < 0)
return AVERROR(errno);
return 0;
}
static int v4l2request_probe_video_device(AVHWFramesContext *hwfc,
const char *path,
uint32_t pixelformat,
uint32_t buffersize)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct v4l2_capability capability;
struct v4l2_create_buffers buffers;
unsigned int capabilities;
int ret;
/*
* Open video device in non-blocking mode to support decoding using
* multiple queued requests, required for e.g. multi stage decoding.
*/
fctxi->video_fd = open(path, O_RDWR | O_NONBLOCK);
if (fctxi->video_fd < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to open video device %s: %s (%d)\n",
path, strerror(errno), errno);
return ret;
}
// Query capabilities of the video device
if (ioctl(fctxi->video_fd, VIDIOC_QUERYCAP, &capability) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to query capabilities of %s: %s (%d)\n",
path, strerror(errno), errno);
goto fail;
}
// Use device capabilities of the opened device when supported
capabilities = (capability.capabilities & V4L2_CAP_DEVICE_CAPS) ?
capability.device_caps : capability.capabilities;
// Ensure streaming is supported on the video device
if ((capabilities & V4L2_CAP_STREAMING) != V4L2_CAP_STREAMING) {
ret = AVERROR(EINVAL);
av_log(hwfc, AV_LOG_VERBOSE, "Device %s is missing streaming capability\n", path);
goto fail;
}
// Ensure multi- or single-planar API can be used
if ((capabilities & V4L2_CAP_VIDEO_M2M_MPLANE) == V4L2_CAP_VIDEO_M2M_MPLANE) {
fctxi->output.format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
fctxi->capture.format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
} else if ((capabilities & V4L2_CAP_VIDEO_M2M) == V4L2_CAP_VIDEO_M2M) {
fctxi->output.format.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
fctxi->capture.format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
} else {
ret = AVERROR(EINVAL);
av_log(hwfc, AV_LOG_VERBOSE, "Device %s is missing mem2mem capability\n", path);
goto fail;
}
// Query OUTPUT buffer capabilities
buffers = (struct v4l2_create_buffers) {
.count = 0,
.memory = V4L2_MEMORY_MMAP,
.format.type = fctxi->output.format.type,
};
if (ioctl(fctxi->video_fd, VIDIOC_CREATE_BUFS, &buffers) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR,
"Failed to query OUTPUT buffer capabilities of %s: %s (%d)\n",
path, strerror(errno), errno);
goto fail;
}
fctxi->output.capabilities = buffers.capabilities;
// Ensure requests can be used
if ((buffers.capabilities & V4L2_BUF_CAP_SUPPORTS_REQUESTS) !=
V4L2_BUF_CAP_SUPPORTS_REQUESTS) {
ret = AVERROR(EINVAL);
av_log(hwfc, AV_LOG_VERBOSE, "Device %s is missing support for requests\n", path);
goto fail;
}
// Ensure the codec pixelformat can be used
ret = v4l2request_try_format(hwfc, fctxi->output.format.type, pixelformat);
if (ret < 0) {
av_log(hwfc, AV_LOG_VERBOSE, "Device %s is missing support for pixelformat %s\n",
path, av_fourcc2str(pixelformat));
goto fail;
}
// Ensure frame size is supported, when driver support ENUM_FRAMESIZES
ret = v4l2request_try_framesize(hwfc, pixelformat);
if (ret < 0 && ret != AVERROR(ENOTTY)) {
av_log(hwfc, AV_LOG_VERBOSE,
"Device %s is missing support for frame size %dx%d of pixelformat %s\n",
path, hwfc->width, hwfc->height, av_fourcc2str(pixelformat));
goto fail;
}
// Set the codec pixelformat and OUTPUT buffersize to be used
ret = v4l2request_set_format(hwfc, fctxi->output.format.type, pixelformat, buffersize);
if (ret < 0) {
av_log(hwfc, AV_LOG_ERROR,
"Failed to set OUTPUT pixelformat %s of %s: %s (%d)\n",
av_fourcc2str(pixelformat), path, strerror(errno), errno);
goto fail;
}
// Get format details for OUTPUT buffers
if (ioctl(fctxi->video_fd, VIDIOC_G_FMT, &fctxi->output.format) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to get OUTPUT format: %s (%d)\n",
strerror(errno), errno);
goto fail;
}
/*
* Set any codec specific controls that can help assist the driver
* make a decision on what CAPTURE buffer format can be used.
*/
ret = v4l2request_set_controls(hwfc, fctx->init_controls, fctx->nb_init_controls);
if (ret < 0) {
av_log(hwfc, AV_LOG_VERBOSE,
"Failed to set %d control(s): %s (%d)\n",
fctx->nb_init_controls, strerror(errno), errno);
goto fail;
}
// Select a supported CAPTURE buffer format
ret = v4l2request_select_capture_format(hwfc);
if (ret < 0) {
av_log(hwfc, AV_LOG_VERBOSE,
"Failed to select a CAPTURE format %s of %s: %s (%d)\n",
av_fourcc2str(pixelformat), path, strerror(errno), errno);
goto fail;
}
// Query CAPTURE buffer capabilities
buffers = (struct v4l2_create_buffers) {
.count = 0,
.memory = V4L2_MEMORY_MMAP,
.format.type = fctxi->capture.format.type,
};
if (ioctl(fctxi->video_fd, VIDIOC_CREATE_BUFS, &buffers) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR,
"Failed to query CAPTURE buffer capabilities of %s: %s (%d)\n",
path, strerror(errno), errno);
goto fail;
}
fctxi->capture.capabilities = buffers.capabilities;
// Get format details for CAPTURE buffers
if (ioctl(fctxi->video_fd, VIDIOC_G_FMT, &fctxi->capture.format) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to get CAPTURE format: %s (%d)\n",
strerror(errno), errno);
goto fail;
}
// All tests passed, video device should be capable
return 0;
fail:
if (fctxi->video_fd >= 0) {
close(fctxi->video_fd);
fctxi->video_fd = -1;
}
return ret;
}
static int v4l2request_probe_video_devices(AVHWFramesContext *hwfc,
struct udev *udev,
uint32_t pixelformat,
uint32_t buffersize)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
struct media_device_info device_info;
struct media_v2_topology topology = {0};
struct media_v2_interface *interfaces;
struct udev_device *device;
const char *path;
dev_t devnum;
int ret;
if (ioctl(fctxi->media_fd, MEDIA_IOC_DEVICE_INFO, &device_info) < 0)
return AVERROR(errno);
if (ioctl(fctxi->media_fd, MEDIA_IOC_G_TOPOLOGY, &topology) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to get media topology: %s (%d)\n",
strerror(errno), errno);
return ret;
}
if (!topology.num_interfaces)
return AVERROR(ENOENT);
interfaces = av_calloc(topology.num_interfaces, sizeof(struct media_v2_interface));
if (!interfaces)
return AVERROR(ENOMEM);
topology.ptr_interfaces = (__u64)(uintptr_t)interfaces;
if (ioctl(fctxi->media_fd, MEDIA_IOC_G_TOPOLOGY, &topology) < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to get media topology: %s (%d)\n",
strerror(errno), errno);
goto fail;
}
ret = AVERROR(ENOENT);
for (int i = 0; i < topology.num_interfaces; i++) {
if (interfaces[i].intf_type != MEDIA_INTF_T_V4L_VIDEO)
continue;
devnum = makedev(interfaces[i].devnode.major, interfaces[i].devnode.minor);
device = udev_device_new_from_devnum(udev, 'c', devnum);
if (!device)
continue;
path = udev_device_get_devnode(device);
if (path)
ret = v4l2request_probe_video_device(hwfc, path, pixelformat, buffersize);
udev_device_unref(device);
// Stop when we have found a capable video device
if (!ret) {
av_log(hwfc, AV_LOG_INFO,
"Using V4L2 media driver %s (%u.%u.%u) for %s\n",
device_info.driver,
device_info.driver_version >> 16,
(device_info.driver_version >> 8) & 0xff,
device_info.driver_version & 0xff,
av_fourcc2str(pixelformat));
break;
}
}
fail:
av_free(interfaces);
return ret;
}
static int v4l2request_probe_media_device(AVHWFramesContext *hwfc,
struct udev_device *device,
uint32_t pixelformat,
uint32_t buffersize)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
AVV4L2RequestFramesContextInternal *fctxi = fctx->internal;
const char *path;
int ret;
path = udev_device_get_devnode(device);
if (!path)
return AVERROR(ENODEV);
// Open enumerated media device
fctxi->media_fd = open(path, O_RDWR);
if (fctxi->media_fd < 0) {
ret = AVERROR(errno);
av_log(hwfc, AV_LOG_ERROR, "Failed to open media device %s: %s (%d)\n",
path, strerror(errno), errno);
return ret;
}
// Probe video devices of current media device
ret = v4l2request_probe_video_devices(hwfc, udev_device_get_udev(device),
pixelformat, buffersize);
// Cleanup when no capable video device was found
if (ret < 0) {
close(fctxi->media_fd);
fctxi->media_fd = -1;
}
return ret;
}
static int v4l2request_probe_media_devices(AVHWFramesContext *hwfc,
struct udev *udev,
uint32_t pixelformat,
uint32_t buffersize)
{
struct udev_enumerate *enumerate;
struct udev_list_entry *devices;
struct udev_list_entry *entry;
struct udev_device *device;
int ret;
enumerate = udev_enumerate_new(udev);
if (!enumerate)
return AVERROR(ENOMEM);
udev_enumerate_add_match_subsystem(enumerate, "media");
udev_enumerate_scan_devices(enumerate);
devices = udev_enumerate_get_list_entry(enumerate);
ret = AVERROR(ENOENT);
udev_list_entry_foreach(entry, devices) {
const char *path = udev_list_entry_get_name(entry);
if (!path)
continue;
device = udev_device_new_from_syspath(udev, path);
if (!device)
continue;
// Probe media device for a capable video device
ret = v4l2request_probe_media_device(hwfc, device, pixelformat, buffersize);
udev_device_unref(device);
// Stop when we have found a capable media and video device
if (!ret)
break;
}
udev_enumerate_unref(enumerate);
return ret;
}
static int v4l2request_open_decoder(AVHWFramesContext *hwfc)
{
AVV4L2RequestFramesContext *fctx = hwfc->hwctx;
uint32_t buffersize;
struct udev *udev;
int ret;
// Ensure codec pixelformat is set
if (!fctx->pixelformat)
return AVERROR(EINVAL);
// TODO: locate a decoder supporting the requested pixelformat
// FIXME: locate a decoder using hwdevice context decoders
return AVERROR(ENOSYS);
udev = udev_new();
if (!udev)
return AVERROR(ENOMEM);
buffersize = FFMAX(hwfc->width * hwfc->height * 3 / 2, 256 * 1024);
// Probe all media devices (auto-detection)
ret = v4l2request_probe_media_devices(hwfc, udev, fctx->pixelformat, buffersize);
udev_unref(udev);
return ret;
}
static AVBufferRef *v4l2request_v4l2_buffer_alloc(AVHWFramesContext *hwfc,