Skip to content

fix(adapters): recreate decode iterator after EAGAIN so stream resumes - #9

Merged
styu12 merged 1 commit into
mainfrom
fix/uvc-recreate-iterator-after-eagain
Apr 14, 2026
Merged

fix(adapters): recreate decode iterator after EAGAIN so stream resumes#9
styu12 merged 1 commit into
mainfrom
fix/uvc-recreate-iterator-after-eagain

Conversation

@styu12

@styu12 styu12 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Symptom

After PR #8 merged, the viewer's camera preview cards populate with the first frame only and then freeze — despite health events showing connected and no error events. The capture thread exits silently after the first EAGAIN retry cycle.

Root cause

PR #8 added EAGAIN handling but called next() on the same decode iterator after catching the exception. PyAV's container.decode() is a Python generator, and generators become permanently exhausted after any exception propagates out of them.

The flow was:

  1. next(frame_iter) → first frame ✓ (camera warmed up)
  2. next(frame_iter)BlockingIOError(35) (between frames) — iterator dies
  3. Handler: time.sleep(0.001); continue
  4. next(frame_iter)StopIteration (iterator is dead)
  5. except StopIteration: break → capture thread exits silently, latest_frame frozen

Fix

Track frame_iter explicitly. Set it to None on every transient-errno retry, and recreate at the top of the next loop iteration:

frame_iter = None
while not self._stop_event.is_set():
    if frame_iter is None:
        frame_iter = iter(self._input.decode(video=0))
    try:
        frame = next(frame_iter)
        ...
    except OSError as exc:
        if exc.errno in _TRANSIENT_ERRNOS or exc.errno is None:
            frame_iter = None    # ← key change
            time.sleep(0.001)
            continue

PyAV's container.decode() returns a fresh generator per call that shares the underlying container's packet queue, so recreation resumes reading without losing session state.

StopIteration is now only hit on true end-of-stream (device disconnect) and exits cleanly.

Test plan

🤖 Generated with Claude Code

The previous fix caught BlockingIOError / OSError(EAGAIN) and retried,
but next() was called on the SAME decode iterator. PyAV's decode() is
a Python generator, and generators become exhausted after any
exception propagates out of them — so every subsequent next() raised
StopIteration. The capture thread then hit 'except StopIteration:
break' and exited silently (no health event), leaving latest_frame
frozen on whatever was set last (usually the very first frame).

This matched the reported symptom: 'health events show connected but
preview is frozen on the first frame.'

Fix: track frame_iter explicitly, set it to None on any transient-
errno path, and recreate via iter(self._input.decode(video=0)) at
the top of the next loop iteration. PyAV's decode() is a fresh
generator per call that shares the underlying container state, so
recreation resumes packet reading without losing the session.

StopIteration now cleanly exits (live cameras only emit it on true
end-of-stream / device disconnect).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@styu12
styu12 merged commit fb4d4be 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