From cc1508c75cf13852366517e9c6c132d8d1fc7aea Mon Sep 17 00:00:00 2001 From: Gaggery Tsai Date: Mon, 23 Mar 2026 16:14:41 -0700 Subject: [PATCH] intel-ipu7-camera: use v4l2-relayd for on-demand camera feed Replace camera-feed-watch and camera-session with v4l2-relayd, a single-process daemon that manages the icamerasrc -> v4l2loopback feed. v4l2-relayd uses V4L2_EVENT_PRI_CLIENT_USAGE kernel events to detect when an app starts/stops streaming, providing zero-polling on-demand camera activation with LED control. This is a generic solution that works with all PipeWire apps (Snapshot, Chromium, Firefox, etc.) without per-app wrappers or .desktop patching. The key advantage over the previous camera-feed-watch approach: v4l2-relayd keeps a single persistent v4l2sink writer, switching only the data source (splash vs icamerasrc) via appsrc. This avoids the PipeWire stale mmap buffer issue that caused degraded framerate on subsequent camera sessions when the writer process changed. Changes: - Add v4l2-relayd as package dependency - Add /etc/v4l2-relayd.d/ipu7.conf with icamerasrc pipeline config - Add systemd service override for GST_PLUGIN_PATH and device access - Remove camera-feed-watch, camera-feed-watch.service, camera-session, camera-feed.service, and Snapshot .desktop patching - Restore PipeWireCamera feature flag for Chromium/Brave/Chrome - Enable v4l2-relayd@ipu7.service at install time --- pkgbuilds/edge/intel-ipu7-camera/PKGBUILD | 19 +-- .../edge/intel-ipu7-camera/camera-feed-watch | 144 ------------------ .../camera-feed-watch.service | 12 -- .../intel-ipu7-camera/camera-init.service | 5 +- .../intel-ipu7-camera.install | 48 +++--- .../v4l2-relayd-ipu7-override.conf | 7 + .../intel-ipu7-camera/v4l2-relayd-ipu7.conf | 6 + .../v4l2loopback-modules-load.conf | 3 +- 8 files changed, 58 insertions(+), 186 deletions(-) delete mode 100644 pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch delete mode 100644 pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch.service create mode 100644 pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7-override.conf create mode 100644 pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7.conf diff --git a/pkgbuilds/edge/intel-ipu7-camera/PKGBUILD b/pkgbuilds/edge/intel-ipu7-camera/PKGBUILD index 682f297..c410635 100644 --- a/pkgbuilds/edge/intel-ipu7-camera/PKGBUILD +++ b/pkgbuilds/edge/intel-ipu7-camera/PKGBUILD @@ -2,7 +2,7 @@ pkgname=intel-ipu7-camera pkgver=1.0.1 -pkgrel=3 +pkgrel=4 pkgdesc="Intel IPU7 MIPI camera stack for Hurrican/Performance (OV08X40 + hardware ISP)" arch=('x86_64') url="https://github.com/TsaiGaggery/hurrican_omarchy_enabling" @@ -11,13 +11,13 @@ depends=( 'dkms' 'linux-headers' 'v4l2loopback-dkms' + 'v4l2-relayd' 'gstreamer' 'gst-plugins-base' 'gst-plugins-good' 'gst-plugins-bad' 'jsoncpp' 'libdrm' - 'python' ) makedepends=( 'git' @@ -50,20 +50,19 @@ source=( "v4l2loopback-modprobe.conf" "v4l2loopback-modules-load.conf" "camera-init.service" - "camera-feed-watch" - "camera-feed-watch.service" "hide-ipu7-v4l2.conf" "disable-libcamera.conf" "90-ipu7-psys.rules" "camera-tmpfiles.conf" "ov08x40.yaml" "camera-sleep-hook" + "v4l2-relayd-ipu7.conf" + "v4l2-relayd-ipu7-override.conf" ) sha256sums=( 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' - 'SKIP' 'SKIP' ) prepare() { @@ -200,10 +199,6 @@ package() { # systemd install -Dm644 "${srcdir}/camera-init.service" \ "${pkgdir}/usr/lib/systemd/system/camera-init.service" - install -Dm644 "${srcdir}/camera-feed-watch.service" \ - "${pkgdir}/usr/lib/systemd/user/camera-feed-watch.service" - install -Dm755 "${srcdir}/camera-feed-watch" \ - "${pkgdir}/usr/bin/camera-feed-watch" # WirePlumber install -Dm644 "${srcdir}/hide-ipu7-v4l2.conf" \ @@ -222,4 +217,10 @@ package() { # Sleep hook — staged here, installed to system-sleep by the .install scriptlet install -Dm755 "${srcdir}/camera-sleep-hook" \ "${pkgdir}/usr/share/intel-ipu7-camera/camera-sleep-hook" + + # v4l2-relayd config for IPU7 camera + install -Dm644 "${srcdir}/v4l2-relayd-ipu7.conf" \ + "${pkgdir}/etc/v4l2-relayd.d/ipu7.conf" + install -Dm644 "${srcdir}/v4l2-relayd-ipu7-override.conf" \ + "${pkgdir}/etc/systemd/system/v4l2-relayd@ipu7.service.d/override.conf" } diff --git a/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch b/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch deleted file mode 100644 index 5edfef0..0000000 --- a/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 -"""On-demand camera feed for v4l2loopback. - -Holds /dev/video50 open permanently so v4l2loopback stays in capture mode. -Writes black frames when idle (0% CPU, LED off). Starts the real camera -pipeline when an app opens the device, relaying frames through the same fd. -Stops the camera when all readers close. No fd gap = no stream interruption. -""" - -import fcntl, os, signal, struct, subprocess, sys, threading, time - -DEVICE = "/dev/video50" -W, H = 1920, 1080 -FSIZE = W * H * 3 // 2 -POLL = 2 -STOP_DELAY = 6 - -BLACK = bytes(W * H) + bytes([128] * (W * H // 2)) - -dev = os.open(DEVICE, os.O_WRONLY) -pixfmt = struct.pack("II4sIIII4x188x", W, H, b"NV12", 1, W, FSIZE, 0) -fmt = struct.pack("I4x", 2) + pixfmt -fcntl.ioctl(dev, 0xC0D05605, fmt) -os.write(dev, BLACK) - -# Restart WirePlumber so it sees the capture device -time.sleep(1) -subprocess.run(["systemctl", "--user", "restart", "wireplumber.service"], - capture_output=True, timeout=10) - -cam_proc = None -cam_lock = threading.Lock() -stop_idle = threading.Event() - - -def write_idle(): - while not stop_idle.is_set(): - try: - os.write(dev, BLACK) - except OSError: - pass - stop_idle.wait(2) - - -def relay_camera(): - global cam_proc - env = os.environ.copy() - env["GST_PLUGIN_PATH"] = "/usr/lib/gstreamer-1.0" - cam_proc = subprocess.Popen( - ["gst-launch-1.0", "-q", - "icamerasrc", "device-name=ov08x40-uf", - "sharpness=80", "ev=-1", "saturation=10", - "!", "video/x-raw,format=NV12,width=1920,height=1080", - "!", "videoflip", "method=rotate-180", - "!", "fdsink", "fd=1"], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env, - ) - buf = b"" - while cam_proc.poll() is None: - data = cam_proc.stdout.read(65536) - if not data: - break - buf += data - while len(buf) >= FSIZE: - try: - os.write(dev, buf[:FSIZE]) - except OSError: - pass - buf = buf[FSIZE:] - cam_proc = None - - -def has_readers(): - try: - r = subprocess.run(["fuser", DEVICE], capture_output=True, text=True, timeout=5) - for pid in r.stdout.strip().split(): - pid = pid.strip() - if not pid: - continue - if int(pid) == os.getpid(): - continue - with cam_lock: - if cam_proc and int(pid) == cam_proc.pid: - continue - return True - except Exception: - pass - return False - - -def start_camera(): - stop_idle.set() - t = threading.Thread(target=relay_camera, daemon=True) - t.start() - return t - - -def stop_camera(): - with cam_lock: - if cam_proc: - cam_proc.terminate() - try: - cam_proc.wait(timeout=5) - except subprocess.TimeoutExpired: - cam_proc.kill() - stop_idle.clear() - t = threading.Thread(target=write_idle, daemon=True) - t.start() - return t - - -def shutdown(sig, frame): - stop_idle.set() - with cam_lock: - if cam_proc: - cam_proc.terminate() - os.close(dev) - sys.exit(0) - - -signal.signal(signal.SIGTERM, shutdown) -signal.signal(signal.SIGINT, shutdown) - -# Start idle -idle_thread = threading.Thread(target=write_idle, daemon=True) -idle_thread.start() - -active = False -idle_count = 0 - -while True: - if has_readers(): - idle_count = 0 - if not active: - start_camera() - active = True - else: - if active: - idle_count += 1 - if idle_count >= STOP_DELAY // POLL: - stop_camera() - active = False - idle_count = 0 - time.sleep(POLL) diff --git a/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch.service b/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch.service deleted file mode 100644 index cc0c90b..0000000 --- a/pkgbuilds/edge/intel-ipu7-camera/camera-feed-watch.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Camera feed for v4l2loopback (on-demand, LED off when idle) -After=pipewire.service camera-init.service - -[Service] -Type=simple -ExecStart=/usr/bin/camera-feed-watch -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=default.target diff --git a/pkgbuilds/edge/intel-ipu7-camera/camera-init.service b/pkgbuilds/edge/intel-ipu7-camera/camera-init.service index 2a1085a..18e844a 100644 --- a/pkgbuilds/edge/intel-ipu7-camera/camera-init.service +++ b/pkgbuilds/edge/intel-ipu7-camera/camera-init.service @@ -1,11 +1,12 @@ [Unit] -Description=Initialize camera (CVS ownership then sensor driver) +Description=Initialize camera (CVS ownership, sensor driver, v4l2loopback) After=systemd-modules-load.service +Before=graphical.target [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/bin/bash -c "modprobe intel_cvs && sleep 2 && modprobe ov08x40" +ExecStart=/bin/bash -c "modprobe intel_cvs && sleep 2 && modprobe ov08x40 && modprobe v4l2loopback" [Install] WantedBy=multi-user.target diff --git a/pkgbuilds/edge/intel-ipu7-camera/intel-ipu7-camera.install b/pkgbuilds/edge/intel-ipu7-camera/intel-ipu7-camera.install index 13d881b..797b6d9 100644 --- a/pkgbuilds/edge/intel-ipu7-camera/intel-ipu7-camera.install +++ b/pkgbuilds/edge/intel-ipu7-camera/intel-ipu7-camera.install @@ -1,3 +1,15 @@ +_add_pipewire_camera() { + local conf="$1" + if [ -f "$conf" ] && grep -q 'enable-features=' "$conf" && ! grep -q 'PipeWireCamera' "$conf"; then + sed -i 's/--enable-features=\([^ ]*\)/--enable-features=\1,PipeWireCamera/' "$conf" + fi +} + +_remove_pipewire_camera() { + local conf="$1" + [ -f "$conf" ] && sed -i 's/,PipeWireCamera//g' "$conf" +} + _install_sleep_hook() { cp /usr/share/intel-ipu7-camera/camera-sleep-hook /usr/lib/systemd/system-sleep/camera-sleep-hook chmod 755 /usr/lib/systemd/system-sleep/camera-sleep-hook @@ -7,44 +19,44 @@ post_install() { _install_sleep_hook systemctl daemon-reload systemctl enable camera-init.service - systemctl --global enable camera-feed-watch.service + systemctl enable v4l2-relayd@ipu7.service udevadm control --reload-rules 2>/dev/null || true if [ -n "$SUDO_USER" ]; then usermod -aG video "$SUDO_USER" 2>/dev/null || true - # Start the service in the current user session if one is active - local uid - uid=$(id -u "$SUDO_USER" 2>/dev/null) - if [ -n "$uid" ] && [ -d "/run/user/$uid" ]; then - XDG_RUNTIME_DIR="/run/user/$uid" \ - DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \ - runuser -u "$SUDO_USER" -- \ - systemctl --user start camera-feed-watch.service 2>/dev/null || true - fi + _add_pipewire_camera "/home/$SUDO_USER/.config/chromium-flags.conf" + _add_pipewire_camera "/home/$SUDO_USER/.config/brave-flags.conf" + _add_pipewire_camera "/home/$SUDO_USER/.config/chrome-flags.conf" fi } post_upgrade() { _install_sleep_hook systemctl daemon-reload + systemctl enable v4l2-relayd@ipu7.service + # Disable old camera-feed-watch (replaced by v4l2-relayd) + systemctl --global disable camera-feed-watch.service 2>/dev/null || true udevadm control --reload-rules 2>/dev/null || true + + if [ -n "$SUDO_USER" ]; then + _add_pipewire_camera "/home/$SUDO_USER/.config/chromium-flags.conf" + _add_pipewire_camera "/home/$SUDO_USER/.config/brave-flags.conf" + _add_pipewire_camera "/home/$SUDO_USER/.config/chrome-flags.conf" + fi } pre_remove() { + systemctl disable v4l2-relayd@ipu7.service 2>/dev/null || true + systemctl stop v4l2-relayd@ipu7.service 2>/dev/null || true systemctl --global disable camera-feed-watch.service 2>/dev/null || true systemctl disable camera-init.service 2>/dev/null || true systemctl stop camera-init.service 2>/dev/null || true if [ -n "$SUDO_USER" ]; then - local uid - uid=$(id -u "$SUDO_USER" 2>/dev/null) - if [ -n "$uid" ] && [ -d "/run/user/$uid" ]; then - XDG_RUNTIME_DIR="/run/user/$uid" \ - DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \ - runuser -u "$SUDO_USER" -- \ - systemctl --user stop camera-feed-watch.service 2>/dev/null || true - fi + _remove_pipewire_camera "/home/$SUDO_USER/.config/chromium-flags.conf" + _remove_pipewire_camera "/home/$SUDO_USER/.config/brave-flags.conf" + _remove_pipewire_camera "/home/$SUDO_USER/.config/chrome-flags.conf" fi } diff --git a/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7-override.conf b/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7-override.conf new file mode 100644 index 0000000..850e258 --- /dev/null +++ b/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7-override.conf @@ -0,0 +1,7 @@ +[Service] +Environment=GST_PLUGIN_PATH=/usr/lib/gstreamer-1.0 +# Relax sandboxing for icamerasrc hardware access +PrivateNetwork=no +InaccessibleDirectories= +ReadOnlyDirectories= +DevicePolicy=auto diff --git a/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7.conf b/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7.conf new file mode 100644 index 0000000..6b99357 --- /dev/null +++ b/pkgbuilds/edge/intel-ipu7-camera/v4l2-relayd-ipu7.conf @@ -0,0 +1,6 @@ +VIDEOSRC="icamerasrc device-name=ov08x40-uf sharpness=80 ev=-1 saturation=10 ! videoflip method=rotate-180" +FORMAT=NV12 +WIDTH=3840 +HEIGHT=2160 +FRAMERATE=30/1 +CARD_LABEL="Hardware ISP Camera" diff --git a/pkgbuilds/edge/intel-ipu7-camera/v4l2loopback-modules-load.conf b/pkgbuilds/edge/intel-ipu7-camera/v4l2loopback-modules-load.conf index d394e47..fd1aae3 100644 --- a/pkgbuilds/edge/intel-ipu7-camera/v4l2loopback-modules-load.conf +++ b/pkgbuilds/edge/intel-ipu7-camera/v4l2loopback-modules-load.conf @@ -1 +1,2 @@ -v4l2loopback +# v4l2loopback is loaded by camera-init.service instead of here. +# Loading here causes a race with PipeWire that breaks exclusive_caps.