Skip to content

fix(adapters): tolerate EAGAIN/EINTR in UVC capture loop - #8

Merged
styu12 merged 1 commit into
mainfrom
fix/uvc-capture-loop-eagain
Apr 14, 2026
Merged

fix(adapters): tolerate EAGAIN/EINTR in UVC capture loop#8
styu12 merged 1 commit into
mainfrom
fix/uvc-capture-loop-eagain

Conversation

@styu12

@styu12 styu12 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the capture loop ended: BlockingIOError(35, 'Resource temporarily unavailable', '0') health event that kills the UVC capture thread immediately after connect() on macOS, leaving the viewer's stream card frozen on a single frame.

Root cause

AVFoundation (macOS) and V4L2 (Linux) raise BlockingIOError / OSError(EAGAIN) from the decode iterator during camera warmup and between frames. This is the demuxer saying "no frame ready yet — try again," NOT a fatal error. The previous for frame in self._input.decode(video=0): structure caught it under a broad except Exception and terminated the thread.

Fix

Drive the decode iterator manually via next() so EAGAIN can be caught and retried per-call:

errno meaning action
4 (EINTR) interrupted syscall 1 ms sleep, retry
11 (Linux EAGAIN) no frame ready 1 ms sleep, retry
35 (macOS EAGAIN) no frame ready 1 ms sleep, retry
None PyAV omits errno on 'not ready' 1 ms sleep, retry
other (EIO, ENODEV, ...) genuinely fatal health event + exit

Test plan

  • 11/11 UVC tests pass including two new tests:
    • test_blocking_io_error_does_not_kill_loop — inject EAGAIN 3x, verify real frames arrive and no health event emitted
    • test_fatal_os_error_still_ends_loop — inject ENODEV, verify fatal path still works
  • Manual smoke test with examples/iphone_mac_webcam/record.py — streams should render live in the viewer, no BlockingIOError health events

🤖 Generated with Claude Code

AVFoundation (macOS) and V4L2 (Linux) raise BlockingIOError or
OSError(EAGAIN) from the decode iterator during camera warmup and
occasionally between frames. The previous for-in-decode loop treated
every exception as fatal, killing the capture thread and surfacing as
'capture loop ended: BlockingIOError(35, ...)' in the viewer health
panel within a second of connect() — despite the camera being fine.

Drive the iterator manually via next() so EAGAIN can be caught per
call. Catch all OSError variants and dispatch on errno:

- 4 (EINTR), 11 (Linux EAGAIN), 35 (macOS EAGAIN), None: sleep 1ms
  and retry. These are 'not ready yet' signals, never fatal.
- Other errnos (EIO, ENODEV, etc.): fall through to the fatal
  health-event path.

Non-OSError exceptions keep the original fatal behavior.

Adds two tests:
- test_blocking_io_error_does_not_kill_loop — inject EAGAIN 3x, then
  real frames; verify frames arrive and no health event emitted.
- test_fatal_os_error_still_ends_loop — inject ENODEV; verify the
  loop exits and emits a health event.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@styu12
styu12 merged commit 553ae0f into main Apr 14, 2026
0 of 4 checks passed
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.

1 participant