diff --git a/package/system/hardware-support/Makefile b/package/system/hardware-support/Makefile index 44ff9a7bcd0..efaff763a44 100644 --- a/package/system/hardware-support/Makefile +++ b/package/system/hardware-support/Makefile @@ -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 @@ -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)) diff --git a/package/system/hardware-support/files/30-video.json b/package/system/hardware-support/files/30-video.json index 18cce422019..43be8a903bc 100644 --- a/package/system/hardware-support/files/30-video.json +++ b/package/system/hardware-support/files/30-video.json @@ -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" ] + ] ] ] diff --git a/package/system/hardware-support/src/v4l-id.c b/package/system/hardware-support/src/v4l-id.c new file mode 100644 index 00000000000..b0ad91c17fc --- /dev/null +++ b/package/system/hardware-support/src/v4l-id.c @@ -0,0 +1,272 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (C) 2026 OpenWrt.org + * + * Video4Linux device identification. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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); +}