feat(presence-detect): back off while another app holds the camera

Before each tick, check whether another process already has the real camera
open (fuser, which sees same-user holders without root — every camera app runs
as us). If so, skip the read entirely for that tick: attempting would be
pointless (we'd get EBUSY) and a simultaneous open could make the other app
fail. A camera in use is proof the user is present, so mark presence and wait
it out, touching the camera zero times for the whole duration another app holds
it — instead of poking it every tick and racing the app for access.

V4L2 has no way to be notified that another app wants the device, so this
can't preempt an in-progress read; it just guarantees we never contend while
another app is already streaming. Falls through to a normal read (and the
Python helper's exit-3 busy path) if fuser isn't installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-07-11 04:11:16 +02:00
parent 83687764d8
commit c2fa8d3e75
1 changed files with 23 additions and 1 deletions

View File

@ -63,6 +63,17 @@ _camera_id() {
fi fi
} }
# True if any process OTHER than this daemon currently holds the real camera
# open — a video call, howdy, etc. `fuser` lists same-user holders without root
# (every camera app runs as us), and we only ever call this when we aren't
# reading the camera ourselves, so any holder is another app. If fuser isn't
# installed we return false: the tick then just tries the camera and the Python
# helper's busy/in-use path (exit 3) still covers an already-held device.
_camera_in_use() {
command -v fuser >/dev/null 2>&1 || return 1
fuser -s "/dev/video${1}" 2>/dev/null
}
# Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the # Prints current CPU-or-RAM usage as a fraction (0..1): the higher of the
# core-count-normalized 1-minute load average and the used-RAM fraction. Load # core-count-normalized 1-minute load average and the used-RAM fraction. Load
# average is a cheap, sampling-free proxy for "CPU busy" — no extra measurement # average is a cheap, sampling-free proxy for "CPU busy" — no extra measurement
@ -147,10 +158,21 @@ while true; do
fi fi
DEVICE="$(_camera_id)" DEVICE="$(_camera_id)"
now="$(date +%s)"
# Back-off: if another app already holds the camera (a video call, howdy,
# ...), don't even try to read it — attempting would be pointless (we'd just
# get EBUSY) and a simultaneous open could make the app itself fail. A camera
# in use is proof you're present, so mark presence and wait it out instead.
if _camera_in_use "$DEVICE"; then
LAST_MOTION="$now"; touch "$PRESENCE_FLAG"; _start_inhibit
sleep "$(_next_interval)"
continue
fi
# Run the OpenCV motion detector; stderr suppressed to keep the journal clean. # Run the OpenCV motion detector; stderr suppressed to keep the journal clean.
python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null python3 "$PYTHON_DETECT" "$DEVICE" 2>/dev/null
rc=$? rc=$?
now="$(date +%s)"
case $rc in case $rc in
# Motion (0) or camera busy/in use (3): user is present. rc=3 means # Motion (0) or camera busy/in use (3): user is present. rc=3 means
# another app (a video call, howdy) is holding the camera, which is # another app (a video call, howdy) is holding the camera, which is