mirror of
https://github.com/openwrt/openwrt.git
synced 2026-10-13 19:48:16 +04:00
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:
1 parent
87087bec5c
commit
62ac036be1
3 files changed
+306
-7
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user