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
This commit is contained in:
Gaggery Tsai
2026-03-24 22:14:08 -07:00
parent 5024f3d41c
commit cc1508c75c
8 changed files with 58 additions and 186 deletions
+10 -9
View File
@@ -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"
}
@@ -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)
@@ -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
@@ -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
@@ -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
}
@@ -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
@@ -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"
@@ -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.