Skip to content

Commit 62ac036

Browse files
committed
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>
1 parent 87087be commit 62ac036

3 files changed

Lines changed: 306 additions & 7 deletions

File tree

‎package/system/hardware-support/Makefile‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
include $(TOPDIR)/rules.mk
66

77
PKG_NAME:=hardware-support
8-
PKG_RELEASE:=1
8+
PKG_RELEASE:=2
99

1010
PKG_LICENSE:=GPL-2.0-only
1111
PKG_MAINTAINER:=Daniel Golle <daniel@makrotopia.org>
@@ -48,18 +48,29 @@ endef
4848
define Package/video-support
4949
$(call Package/hardware-support/Default)
5050
TITLE:=Video device support
51+
DEPENDS+=+libubox +libblobmsg-json
5152
USERID:=:video=40 :video-capture=36
53+
PKGARCH:=$(ARCH_PACKAGES)
5254
endef
5355

5456
define Package/video-support/description
55-
Feature gate for display and video capture devices. Allocates the video
56-
and video-capture groups and installs the hotplug rules which put the DRM
57-
and V4L2 character devices into them. Kernel modules exposing such devices
58-
and userspace using those groups depend on this package.
57+
Feature gate for display and video devices. Allocates the video and
58+
video-capture groups and installs the hotplug rules which put the DRM
59+
character devices into video.
60+
61+
A uevent carries no device capabilities, so a V4L2 node is created
62+
without a group and handed to v4l-id, which asks the driver and moves
63+
it into video-capture when it can capture video, or into video when it
64+
cannot. A stateless codec is therefore reachable by a media player
65+
holding video, while a camera or an HDMI receiver needs video-capture.
66+
A node whose capabilities could not be established stays owned by root,
67+
so a failure costs a working decoder rather than exposing a camera.
5968
endef
6069

6170
define Package/video-support/install
6271
$(call InstallFragment,$(1),30-video.json)
72+
$(INSTALL_DIR) $(1)/usr/libexec
73+
$(INSTALL_BIN) $(PKG_BUILD_DIR)/v4l-id $(1)/usr/libexec/v4l-id
6374
endef
6475

6576
define Package/accel-support
@@ -178,12 +189,17 @@ define Package/hidraw-support/install
178189
endef
179190

180191
define Build/Prepare
192+
mkdir -p $(PKG_BUILD_DIR)
193+
$(CP) ./src/. $(PKG_BUILD_DIR)/
181194
endef
182195

183196
define Build/Configure
184197
endef
185198

186199
define Build/Compile
200+
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_CPPFLAGS) $(TARGET_LDFLAGS) \
201+
-o $(PKG_BUILD_DIR)/v4l-id $(PKG_BUILD_DIR)/v4l-id.c \
202+
-lubox -lblobmsg_json
187203
endef
188204

189205
$(eval $(call BuildPackage,audio-support))

‎package/system/hardware-support/files/30-video.json‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,18 @@
1111
]
1212
],
1313
[ "if",
14-
[ "eq", "SUBSYSTEM", [ "video4linux", "media" ] ],
15-
[ "makedev", "/dev/%DEVNAME%", "0660", "video-capture" ]
14+
[ "eq", "SUBSYSTEM", "media" ],
15+
[
16+
[ "makedev", "/dev/%DEVNAME%", "0660", "video" ],
17+
[ "return" ]
18+
]
19+
],
20+
[ "if",
21+
[ "eq", "SUBSYSTEM", "video4linux" ],
22+
[
23+
[ "makedev", "/dev/%DEVNAME%", "0660" ],
24+
[ "exec", "/usr/libexec/v4l-id", "/dev/%DEVNAME%",
25+
"video_capture,video_capture_mplane:video-capture;*:video" ]
26+
]
1627
]
1728
]
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/* SPDX-License-Identifier: GPL-2.0-only */
2+
/*
3+
* Copyright (C) 2026 OpenWrt.org
4+
*
5+
* Video4Linux device identification.
6+
*/
7+
8+
#include <fcntl.h>
9+
#include <grp.h>
10+
#include <stdio.h>
11+
#include <stdlib.h>
12+
#include <string.h>
13+
#include <unistd.h>
14+
#include <sys/ioctl.h>
15+
#include <sys/stat.h>
16+
#include <linux/videodev2.h>
17+
18+
#include <libubox/blobmsg_json.h>
19+
20+
static const struct {
21+
const char *name;
22+
unsigned int mask;
23+
} capabilities[] = {
24+
{ "video_capture", V4L2_CAP_VIDEO_CAPTURE },
25+
{ "video_output", V4L2_CAP_VIDEO_OUTPUT },
26+
{ "video_overlay", V4L2_CAP_VIDEO_OVERLAY },
27+
{ "vbi_capture", V4L2_CAP_VBI_CAPTURE },
28+
{ "vbi_output", V4L2_CAP_VBI_OUTPUT },
29+
{ "sliced_vbi_capture", V4L2_CAP_SLICED_VBI_CAPTURE },
30+
{ "sliced_vbi_output", V4L2_CAP_SLICED_VBI_OUTPUT },
31+
{ "rds_capture", V4L2_CAP_RDS_CAPTURE },
32+
{ "video_output_overlay", V4L2_CAP_VIDEO_OUTPUT_OVERLAY },
33+
{ "hw_freq_seek", V4L2_CAP_HW_FREQ_SEEK },
34+
{ "rds_output", V4L2_CAP_RDS_OUTPUT },
35+
{ "video_capture_mplane", V4L2_CAP_VIDEO_CAPTURE_MPLANE },
36+
{ "video_output_mplane", V4L2_CAP_VIDEO_OUTPUT_MPLANE },
37+
{ "video_m2m_mplane", V4L2_CAP_VIDEO_M2M_MPLANE },
38+
{ "video_m2m", V4L2_CAP_VIDEO_M2M },
39+
{ "tuner", V4L2_CAP_TUNER },
40+
{ "audio", V4L2_CAP_AUDIO },
41+
{ "radio", V4L2_CAP_RADIO },
42+
{ "modulator", V4L2_CAP_MODULATOR },
43+
{ "sdr_capture", V4L2_CAP_SDR_CAPTURE },
44+
{ "ext_pix_format", V4L2_CAP_EXT_PIX_FORMAT },
45+
{ "sdr_output", V4L2_CAP_SDR_OUTPUT },
46+
{ "meta_capture", V4L2_CAP_META_CAPTURE },
47+
{ "readwrite", V4L2_CAP_READWRITE },
48+
{ "edid", V4L2_CAP_EDID },
49+
{ "streaming", V4L2_CAP_STREAMING },
50+
{ "meta_output", V4L2_CAP_META_OUTPUT },
51+
{ "touch", V4L2_CAP_TOUCH },
52+
{ "io_mc", V4L2_CAP_IO_MC },
53+
};
54+
55+
#define CAPABILITY_COUNT (sizeof(capabilities) / sizeof(*capabilities))
56+
57+
/* a rule names at least one distinct capability, or is the catch-all */
58+
#define RULE_COUNT (CAPABILITY_COUNT + 1)
59+
60+
struct rule {
61+
const char *group;
62+
unsigned int mask; /* zero matches any node */
63+
};
64+
65+
static int usage(const char *argv0)
66+
{
67+
fprintf(stderr,
68+
"Usage: %s DEVICE [RULES]\n\n"
69+
"Describe a V4L2 device as a JSON object on stdout, with one\n"
70+
"boolean per capability.\n\n"
71+
"With RULES, print nothing and put the device node into the\n"
72+
"group of the first rule which matches it. RULES is a list of\n"
73+
"CAP[,CAP...]:GROUP separated by semicolons, in which a\n"
74+
"capability list of * matches any node.\n",
75+
argv0);
76+
77+
return 2;
78+
}
79+
80+
/* device_caps describes the node, capabilities the whole device */
81+
static unsigned int device_caps(const struct v4l2_capability *cap)
82+
{
83+
if (cap->capabilities & V4L2_CAP_DEVICE_CAPS)
84+
return cap->device_caps;
85+
86+
return cap->capabilities;
87+
}
88+
89+
static int report(const struct v4l2_capability *cap)
90+
{
91+
static struct blob_buf b;
92+
unsigned int caps;
93+
char *json;
94+
size_t i;
95+
96+
caps = device_caps(cap);
97+
98+
blob_buf_init(&b, 0);
99+
blobmsg_add_string(&b, "driver", (const char *)cap->driver);
100+
blobmsg_add_string(&b, "card", (const char *)cap->card);
101+
blobmsg_add_string(&b, "bus_info", (const char *)cap->bus_info);
102+
blobmsg_add_u64(&b, "version", cap->version);
103+
blobmsg_add_u64(&b, "capabilities", cap->capabilities);
104+
blobmsg_add_u64(&b, "device_caps", caps);
105+
106+
for (i = 0; i < CAPABILITY_COUNT; i++)
107+
blobmsg_add_u8(&b, capabilities[i].name,
108+
!!(caps & capabilities[i].mask));
109+
110+
json = blobmsg_format_json(b.head, true);
111+
if (!json) {
112+
blob_buf_free(&b);
113+
return 1;
114+
}
115+
116+
printf("%s\n", json);
117+
free(json);
118+
blob_buf_free(&b);
119+
120+
return 0;
121+
}
122+
123+
static int capability_mask(char *names, unsigned int *mask)
124+
{
125+
char *name, *state;
126+
bool known;
127+
size_t i;
128+
129+
*mask = 0;
130+
131+
for (name = strtok_r(names, ",", &state); name;
132+
name = strtok_r(NULL, ",", &state)) {
133+
known = false;
134+
135+
for (i = 0; i < CAPABILITY_COUNT; i++) {
136+
if (strcmp(capabilities[i].name, name))
137+
continue;
138+
139+
*mask |= capabilities[i].mask;
140+
known = true;
141+
break;
142+
}
143+
144+
if (!known) {
145+
fprintf(stderr, "v4l-id: unknown capability: %s\n",
146+
name);
147+
return 2;
148+
}
149+
}
150+
151+
return 0;
152+
}
153+
154+
static int node_chgrp(const char *device, const char *group)
155+
{
156+
struct group *grp;
157+
158+
grp = getgrnam(group);
159+
if (!grp) {
160+
fprintf(stderr, "v4l-id: no such group: %s\n", group);
161+
return 5;
162+
}
163+
164+
if (chown(device, (uid_t)-1, grp->gr_gid) < 0) {
165+
fprintf(stderr, "v4l-id: cannot set group on %s: %m\n", device);
166+
return 6;
167+
}
168+
169+
return 0;
170+
}
171+
172+
static int rules_parse(char *rules, struct rule *parsed, size_t *count)
173+
{
174+
char *rule, *state, *group;
175+
size_t n;
176+
int err;
177+
178+
n = 0;
179+
180+
for (rule = strtok_r(rules, ";", &state); rule;
181+
rule = strtok_r(NULL, ";", &state)) {
182+
if (n == RULE_COUNT) {
183+
fprintf(stderr, "v4l-id: too many rules\n");
184+
return 2;
185+
}
186+
187+
group = strchr(rule, ':');
188+
if (!group || !group[1]) {
189+
fprintf(stderr, "v4l-id: rule has no group: %s\n",
190+
rule);
191+
return 2;
192+
}
193+
194+
*group++ = '\0';
195+
196+
if (!*rule) {
197+
fprintf(stderr, "v4l-id: rule has no capability\n");
198+
return 2;
199+
}
200+
201+
parsed[n].group = group;
202+
parsed[n].mask = 0;
203+
204+
if (strcmp(rule, "*")) {
205+
err = capability_mask(rule, &parsed[n].mask);
206+
if (err)
207+
return err;
208+
}
209+
210+
n++;
211+
}
212+
213+
*count = n;
214+
215+
return 0;
216+
}
217+
218+
static int classify(const struct v4l2_capability *cap, const char *device,
219+
const struct rule *parsed, size_t count)
220+
{
221+
unsigned int caps;
222+
size_t i;
223+
224+
caps = device_caps(cap);
225+
226+
for (i = 0; i < count; i++)
227+
if (!parsed[i].mask || (caps & parsed[i].mask))
228+
return node_chgrp(device, parsed[i].group);
229+
230+
return 0;
231+
}
232+
233+
int main(int argc, char *argv[])
234+
{
235+
struct rule parsed[RULE_COUNT];
236+
struct v4l2_capability cap;
237+
const char *device;
238+
size_t count;
239+
char *rules;
240+
int fd, err;
241+
242+
if (argc < 2 || argc > 3)
243+
return usage(argv[0]);
244+
245+
device = argv[1];
246+
rules = (argc == 3) ? argv[2] : NULL;
247+
count = 0;
248+
249+
if (rules) {
250+
err = rules_parse(rules, parsed, &count);
251+
if (err)
252+
return err;
253+
}
254+
255+
fd = open(device, O_RDONLY | O_CLOEXEC);
256+
if (fd < 0)
257+
return 3;
258+
259+
memset(&cap, 0, sizeof(cap));
260+
261+
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
262+
close(fd);
263+
return 4;
264+
}
265+
266+
close(fd);
267+
268+
if (rules)
269+
return classify(&cap, device, parsed, count);
270+
271+
return report(&cap);
272+
}

0 commit comments

Comments
 (0)