video-support: put the V4L2 nodes in the video group

The rule placed every video4linux and media node in video-capture, but
most are not capture devices: on an rk3588 board five of the six V4L2
nodes are the RGA blitter and the Hantro and rkvdec stateless codecs,
and only the HDMI receiver captures anything. A media player then had to
join video-capture to reach a decoder, which also granted it every
camera on the box.

Put the DRM and media nodes in video, and add v4l-id, which runs
VIDIOC_QUERYCAP and prefers device_caps over capabilities so that what
it describes is the node. Given a device alone it prints one boolean per
V4L2_CAP_ flag as JSON. Given a rule string as well, a list of
CAP[,CAP...]:GROUP separated by semicolons in which * matches any node,
it puts the node into the group of the first rule which matches it.

The rule string is parsed in full before the device is opened, so a
capability name the kernel does not define is reported whichever rule it
appears in, and no node is touched. A V4L2 node is created with no group
and v4l-id assigns one, since exec is queued by procd and may not run at
all; holding the node at root until the driver answers keeps a camera
out of video.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle committed 2026-10-05 20:09:55 +01:00
1 parent 87087bec5c
commit 62ac036be1
3 files changed
+306 -7

No files matched your search

+21 -5
View File
@@ -5,7 +5,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=hardware-support
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_LICENSE:=GPL-2.0-only
PKG_MAINTAINER:=Daniel Golle <daniel@makrotopia.org>
@@ -48,18 +48,29 @@ endef
define Package/video-support
$(call Package/hardware-support/Default)
TITLE:=Video device support
DEPENDS+=+libubox +libblobmsg-json
USERID:=:video=40 :video-capture=36
PKGARCH:=$(ARCH_PACKAGES)
endef
define Package/video-support/description
Feature gate for display and video capture devices. Allocates the video
and video-capture groups and installs the hotplug rules which put the DRM
and V4L2 character devices into them. Kernel modules exposing such devices
and userspace using those groups depend on this package.
Feature gate for display and video devices. Allocates the video and
video-capture groups and installs the hotplug rules which put the DRM
character devices into video.
A uevent carries no device capabilities, so a V4L2 node is created
without a group and handed to v4l-id, which asks the driver and moves
it into video-capture when it can capture video, or into video when it
cannot. A stateless codec is therefore reachable by a media player
holding video, while a camera or an HDMI receiver needs video-capture.
A node whose capabilities could not be established stays owned by root,
so a failure costs a working decoder rather than exposing a camera.
endef
define Package/video-support/install
$(call InstallFragment,$(1),30-video.json)
$(INSTALL_DIR) $(1)/usr/libexec
$(INSTALL_BIN) $(PKG_BUILD_DIR)/v4l-id $(1)/usr/libexec/v4l-id
endef
define Package/accel-support
@@ -178,12 +189,17 @@ define Package/hidraw-support/install
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/. $(PKG_BUILD_DIR)/
endef
define Build/Configure
endef
define Build/Compile
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_CPPFLAGS) $(TARGET_LDFLAGS) \
-o $(PKG_BUILD_DIR)/v4l-id $(PKG_BUILD_DIR)/v4l-id.c \
-lubox -lblobmsg_json
endef
$(eval $(call BuildPackage,audio-support))
@@ -11,7 +11,18 @@
]
],
[ "if",
[ "eq", "SUBSYSTEM", [ "video4linux", "media" ] ],
[ "makedev", "/dev/%DEVNAME%", "0660", "video-capture" ]
[ "eq", "SUBSYSTEM", "media" ],
[
[ "makedev", "/dev/%DEVNAME%", "0660", "video" ],
[ "return" ]
]
],
[ "if",
[ "eq", "SUBSYSTEM", "video4linux" ],
[
[ "makedev", "/dev/%DEVNAME%", "0660" ],
[ "exec", "/usr/libexec/v4l-id", "/dev/%DEVNAME%",
"video_capture,video_capture_mplane:video-capture;*:video" ]
]
]
]
@@ -0,0 +1,272 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (C) 2026 OpenWrt.org
*
* Video4Linux device identification.
*/
#include <fcntl.h>
#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <linux/videodev2.h>
#include <libubox/blobmsg_json.h>
static const struct {
const char *name;
unsigned int mask;
} capabilities[] = {
{ "video_capture", V4L2_CAP_VIDEO_CAPTURE },
{ "video_output", V4L2_CAP_VIDEO_OUTPUT },
{ "video_overlay", V4L2_CAP_VIDEO_OVERLAY },
{ "vbi_capture", V4L2_CAP_VBI_CAPTURE },
{ "vbi_output", V4L2_CAP_VBI_OUTPUT },
{ "sliced_vbi_capture", V4L2_CAP_SLICED_VBI_CAPTURE },
{ "sliced_vbi_output", V4L2_CAP_SLICED_VBI_OUTPUT },
{ "rds_capture", V4L2_CAP_RDS_CAPTURE },
{ "video_output_overlay", V4L2_CAP_VIDEO_OUTPUT_OVERLAY },
{ "hw_freq_seek", V4L2_CAP_HW_FREQ_SEEK },
{ "rds_output", V4L2_CAP_RDS_OUTPUT },
{ "video_capture_mplane", V4L2_CAP_VIDEO_CAPTURE_MPLANE },
{ "video_output_mplane", V4L2_CAP_VIDEO_OUTPUT_MPLANE },
{ "video_m2m_mplane", V4L2_CAP_VIDEO_M2M_MPLANE },
{ "video_m2m", V4L2_CAP_VIDEO_M2M },
{ "tuner", V4L2_CAP_TUNER },
{ "audio", V4L2_CAP_AUDIO },
{ "radio", V4L2_CAP_RADIO },
{ "modulator", V4L2_CAP_MODULATOR },
{ "sdr_capture", V4L2_CAP_SDR_CAPTURE },
{ "ext_pix_format", V4L2_CAP_EXT_PIX_FORMAT },
{ "sdr_output", V4L2_CAP_SDR_OUTPUT },
{ "meta_capture", V4L2_CAP_META_CAPTURE },
{ "readwrite", V4L2_CAP_READWRITE },
{ "edid", V4L2_CAP_EDID },
{ "streaming", V4L2_CAP_STREAMING },
{ "meta_output", V4L2_CAP_META_OUTPUT },
{ "touch", V4L2_CAP_TOUCH },
{ "io_mc", V4L2_CAP_IO_MC },
};
#define CAPABILITY_COUNT (sizeof(capabilities) / sizeof(*capabilities))
/* a rule names at least one distinct capability, or is the catch-all */
#define RULE_COUNT (CAPABILITY_COUNT + 1)
struct rule {
const char *group;
unsigned int mask; /* zero matches any node */
};
static int usage(const char *argv0)
{
fprintf(stderr,
"Usage: %s DEVICE [RULES]\n\n"
"Describe a V4L2 device as a JSON object on stdout, with one\n"
"boolean per capability.\n\n"
"With RULES, print nothing and put the device node into the\n"
"group of the first rule which matches it. RULES is a list of\n"
"CAP[,CAP...]:GROUP separated by semicolons, in which a\n"
"capability list of * matches any node.\n",
argv0);
return 2;
}
/* device_caps describes the node, capabilities the whole device */
static unsigned int device_caps(const struct v4l2_capability *cap)
{
if (cap->capabilities & V4L2_CAP_DEVICE_CAPS)
return cap->device_caps;
return cap->capabilities;
}
static int report(const struct v4l2_capability *cap)
{
static struct blob_buf b;
unsigned int caps;
char *json;
size_t i;
caps = device_caps(cap);
blob_buf_init(&b, 0);
blobmsg_add_string(&b, "driver", (const char *)cap->driver);
blobmsg_add_string(&b, "card", (const char *)cap->card);
blobmsg_add_string(&b, "bus_info", (const char *)cap->bus_info);
blobmsg_add_u64(&b, "version", cap->version);
blobmsg_add_u64(&b, "capabilities", cap->capabilities);
blobmsg_add_u64(&b, "device_caps", caps);
for (i = 0; i < CAPABILITY_COUNT; i++)
blobmsg_add_u8(&b, capabilities[i].name,
!!(caps & capabilities[i].mask));
json = blobmsg_format_json(b.head, true);
if (!json) {
blob_buf_free(&b);
return 1;
}
printf("%s\n", json);
free(json);
blob_buf_free(&b);
return 0;
}
static int capability_mask(char *names, unsigned int *mask)
{
char *name, *state;
bool known;
size_t i;
*mask = 0;
for (name = strtok_r(names, ",", &state); name;
name = strtok_r(NULL, ",", &state)) {
known = false;
for (i = 0; i < CAPABILITY_COUNT; i++) {
if (strcmp(capabilities[i].name, name))
continue;
*mask |= capabilities[i].mask;
known = true;
break;
}
if (!known) {
fprintf(stderr, "v4l-id: unknown capability: %s\n",
name);
return 2;
}
}
return 0;
}
static int node_chgrp(const char *device, const char *group)
{
struct group *grp;
grp = getgrnam(group);
if (!grp) {
fprintf(stderr, "v4l-id: no such group: %s\n", group);
return 5;
}
if (chown(device, (uid_t)-1, grp->gr_gid) < 0) {
fprintf(stderr, "v4l-id: cannot set group on %s: %m\n", device);
return 6;
}
return 0;
}
static int rules_parse(char *rules, struct rule *parsed, size_t *count)
{
char *rule, *state, *group;
size_t n;
int err;
n = 0;
for (rule = strtok_r(rules, ";", &state); rule;
rule = strtok_r(NULL, ";", &state)) {
if (n == RULE_COUNT) {
fprintf(stderr, "v4l-id: too many rules\n");
return 2;
}
group = strchr(rule, ':');
if (!group || !group[1]) {
fprintf(stderr, "v4l-id: rule has no group: %s\n",
rule);
return 2;
}
*group++ = '\0';
if (!*rule) {
fprintf(stderr, "v4l-id: rule has no capability\n");
return 2;
}
parsed[n].group = group;
parsed[n].mask = 0;
if (strcmp(rule, "*")) {
err = capability_mask(rule, &parsed[n].mask);
if (err)
return err;
}
n++;
}
*count = n;
return 0;
}
static int classify(const struct v4l2_capability *cap, const char *device,
const struct rule *parsed, size_t count)
{
unsigned int caps;
size_t i;
caps = device_caps(cap);
for (i = 0; i < count; i++)
if (!parsed[i].mask || (caps & parsed[i].mask))
return node_chgrp(device, parsed[i].group);
return 0;
}
int main(int argc, char *argv[])
{
struct rule parsed[RULE_COUNT];
struct v4l2_capability cap;
const char *device;
size_t count;
char *rules;
int fd, err;
if (argc < 2 || argc > 3)
return usage(argv[0]);
device = argv[1];
rules = (argc == 3) ? argv[2] : NULL;
count = 0;
if (rules) {
err = rules_parse(rules, parsed, &count);
if (err)
return err;
}
fd = open(device, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return 3;
memset(&cap, 0, sizeof(cap));
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
close(fd);
return 4;
}
close(fd);
if (rules)
return classify(&cap, device, parsed, count);
return report(&cap);
}