Skip to content

fix(camera): re-assert format + self-heal for long-session capture failures (#48)#57

Merged
ccross2 merged 3 commits into
mainfrom
fix/camera-format-reassert-selfheal
Jul 7, 2026
Merged

fix(camera): re-assert format + self-heal for long-session capture failures (#48)#57
ccross2 merged 3 commits into
mainfrom
fix/camera-format-reassert-selfheal

Conversation

@ccross2

@ccross2 ccross2 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #48 — verification degrading over long sessions on a shared webcam, requiring a visaged restart.

Root cause (corrected from the issue's leak hypothesis)

Not a buffer/fd leak. The v4l crate's MmapStream::drop already issues VIDIOC_REQBUFS(0) + munmap, so the per-capture stream cycle leaks nothing.

The decisive evidence is the error type: the reporter sees NoFaceDetected, which can only arise when capture_frames returned Ok (frames captured, but all-dark / unreadable). Every driver/hardware failure surfaces instead as CameraError::CaptureFailed/DeviceBusy. So the fd, driver and buffers are healthy and still delivering frames — only the frame content is garbage.

Why: visaged negotiates its capture format (S_FMT YUYV 640×360) once, at Camera::open, caches it, and never re-asserts it. On a webcam shared with meeting apps (the reporter's workflow), another process opens the same node, sets its own format, and closes — leaving the device in that format. visaged's next capture then decodes a foreign-format buffer through the stale cache → garbage → NoFaceDetected, until a restart re-runs S_FMT.

Fix

  • camera.rsreassert_format() before each capture. A cheap VIDIOC_G_FMT; S_FMT only fires when the device actually drifted (no-op in the common case). Runs before MmapStream creation, where S_FMT is legal.
  • engine.rs — in-process self-heal. Re-opens the Camera after MAX_CONSECUTIVE_CAPTURE_FAILURES (3) camera-broken results — exactly what a manual restart does — instead of requiring one. Gated by a capture_looks_broken discriminator (NoUsableFrames | Camera(_)) so an absent user, verify timeout, or liveness rejection never triggers a re-open. The two all-dark frames.is_empty() paths now return a new NoUsableFrames variant (a clearer diagnostic than NoFaceDetected).

Why not a persistent stream

A persistent MmapStream (one proposal for #48) holds STREAMON for the daemon's life, claiming exclusive camera ownership — it would block the reporter's meeting apps with EBUSY, regressing the exact shared-webcam workflow this issue is reported under. The per-capture stream is deliberately retained (it releases the camera between verifies).

Tests

  • Added a no-hardware unit test (self_heal_only_arms_on_camera_broken) pinning the discriminator's false-positive property (runs in CI).
  • Hardware-gated soak (~1000 captures) + a foreign-S_FMT reproduction test are noted for a follow-up (they need a real V4L2 camera, so #[ignore]).

Verification

Authored on a host without cargo, so not compiled locally — relying on CI (clippy -D warnings + test). Please review the logic before merge. Diff is +167 / -3 across camera.rs, engine.rs, CHANGELOG.md.

Closes #48.

🤖 Generated with Claude Code

https://claude.ai/code/session_012wU47GVzskAummZqgH6FgF

ccross2 and others added 3 commits July 7, 2026 10:46
…ilures (#48)

visaged negotiated the V4L2 capture format only once, at Camera::open, and
never re-asserted it. On a webcam shared with other applications (e.g. a
video-conferencing app), another process can change the device's format via
VIDIOC_S_FMT and leave it there; visaged then captures wrong-format frames it
decodes as garbage through its stale format cache, surfacing as "no face
detected" until a manual `systemctl restart`.

Root cause is format/state desync, NOT a buffer/fd leak: the `v4l` crate's
MmapStream::drop already issues VIDIOC_REQBUFS(0)+munmap, so per-capture stream
churn leaks nothing. The decisive evidence is the error type — the reporter sees
NoFaceDetected (capture returned Ok), never CaptureFailed — so the fd and buffers
are healthy and delivering frames; only the frame *content* is unusable.

Fix:
- camera: `reassert_format()` before each capture — a cheap VIDIOC_G_FMT; S_FMT
  only fires when the device actually drifted (no-op otherwise).
- engine: in-process self-heal — re-open the Camera after
  MAX_CONSECUTIVE_CAPTURE_FAILURES camera-broken results (what a manual restart
  does), gated by a `capture_looks_broken` discriminator so an absent user,
  verify timeout, or liveness rejection never triggers a re-open. The two
  all-dark `frames.is_empty()` paths now return a clearer `NoUsableFrames`.
- The per-capture stream is retained, so the camera is still released between
  verifies and remains usable by other apps (a persistent stream would hold
  STREAMON and block the reporter's meeting apps — rejected).

Adds a no-hardware unit test for the discriminator.

Closes #48.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wU47GVzskAummZqgH6FgF
@ccross2 ccross2 merged commit ea48298 into main Jul 7, 2026
1 check passed
@ccross2 ccross2 deleted the fix/camera-format-reassert-selfheal branch July 7, 2026 15:09
@ccross2 ccross2 mentioned this pull request Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Verification fails for longer sessions and requires visaged restart

1 participant