Merge pull request #60 from TsaiGaggery/fix-camera-session

intel-ipu7-camera: use v4l2-relayd for on-demand camera feed
This commit is contained in:
David Heinemeier Hansson
2026-03-25 13:06:52 +01:00
committed by GitHub
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.