Skip to content

fix(capture): make Linux preview seek robust at/past the last frame - #551

Open
Beetix wants to merge 4 commits into
getopenscreen:mainfrom
operametrix:fix/linux-decode-eof-drain
Open

fix(capture): make Linux preview seek robust at/past the last frame#551
Beetix wants to merge 4 commits into
getopenscreen:mainfrom
operametrix:fix/linux-decode-eof-drain

Conversation

@Beetix

@Beetix Beetix commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the Linux software decoder's seek path (crates/compositor/src/linux_decode.rs, SwDecoder::decode_at) robust when the preview asks for a frame at or past the last frame of a recording. Previously the preview went blank with decode_at(frame_idx=…) : aucune frame reçue.

The headline fix is the past-end fallback. For a target at/after the last frame, av_seek_frame(BACKWARD) succeeds but lands on a single undecodable packet just before EOF; after avcodec_flush_buffers that reference-less packet yields no frame, so decode_at bailed. The pump loop is now factored into pump_to_target(), and when the seek-based decode returns nothing, decode_at rewinds to 0 and forward-scans — the same linear fallback already used for unindexed WebM — returning the last frame ≤ target instead of erroring.

This surfaces on frame-dropped recordings whose real avg_frame_rate is below the nominal 60 fps (produced by #511): a frame index computed on the 60 fps grid points past the file's true frame count. Measured on a real 56.34 fps / 631-frame capture, decode_at decoded 0..=630 and failed for every index ≥ 631; after this change the full sweep (0..=nb_frames+30) passes.

Two smaller, related seek-path hardening changes ride along (both inert on these captures but correct for other inputs, and empirically verified so — see Testing):

  • EOF drain — drain the decoder with a NULL packet at EOF so a frame held in the reorder buffer is still returned. Needed for streams with B-frames; these app captures are H.264 baseline (has_b_frames=0) so nothing is buffered and it is inert here. Mirrors the fix receive_into already carries.
  • AVERROR_INVALIDDATA constant — the post-seek fragmented-packet guard compared against -0x2A2A2A2A (the tag ****, not an ffmpeg error), so it never matched; corrected to the real AVERROR_INVALIDDATA (-1094995529). Same fix receive_into already received. Not triggered by these indexed MP4s.

Related issue

Fixes #550
Refs #511 (the upstream capture-side frame drops that produce the sub-60fps files which trigger this)

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

N/A — removes an error state in the preview.

Testing

  • cargo test -p openscreen-compositor --lib — 181 pass.
  • Added an env-gated regression test decode_at_never_fails_past_end (same pattern as the OPENSCREEN_GOLDEN_* tests, since no fixture mp4 is committed): asserts decode_at returns a frame for every index in 0..nb_frames+30.
    OPENSCREEN_SWEEP_FILE=/path/to.mp4 cargo test -p openscreen-compositor --lib decode_at_never_fails_past_end -- --ignored
  • Verified on real recordings (56.34, 22.6, 21.4 and 60.0 fps): 0 failures across the full past-end sweep on each; before the past-end fallback, the sub-60fps files failed at every index ≥ their frame count.
  • Isolated each hardening change by toggling it off and re-sweeping: reverting the AVERROR_INVALIDDATA constant and removing the EOF drain both produced byte-identical results on these captures (confirming they are inert here — the captures have no B-frames — while the past-end fallback is the load-bearing fix).
  • Built and manually verified in the packaged Linux AppImage: playing/scrubbing to the last clip of an affected project no longer errors.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved video seeking when indexed seeks do not produce a frame.
    • Ensured delayed frames at the end of a stream are processed correctly.
    • Improved handling of invalid packets during decoding.
    • Corrected selection of the final frame at or before the requested time.
    • Improved behavior when seeking beyond the end of a video.

Beetix and others added 4 commits August 31, 2026 18:42
`decode_at` seeks BACKWARD to the nearest keyframe then decodes forward to
the target. On `av_read_frame` EOF it broke out immediately without sending
a NULL flush packet, so frames still buffered inside the decoder (decode
latency / B-frame reorder) were never received. When the target lands in
the short tail after the last keyframe there are too few packets to beat
that latency, `found` stayed null, and the preview failed with
"decode_at(frame_idx=…) : aucune frame reçue" — intermittently, when
scrubbing to the end.

Drain the decoder at EOF like the sequential `receive_into` path already
does: send a NULL packet, pull the remaining frames into `found`, keeping
the target-pts check so the last frame is still returned when scrubbing
past the end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
decode_at (the seek/scrub path behind Decoder::seek_to) skipped a
misaligned post-seek packet by comparing send_packet's return to
-0x2A2A2A2A — the tag `****`, which is not an ffmpeg error code.
AVERROR_INVALIDDATA is -1094995529, so the guard never matched and a
genuinely fragmented first NAL after a BACKWARD seek fell through to the
fatal bail!, aborting the whole seek. Symptom: the preview failed when
time-travelling/scrubbing (as opposed to reaching the end), even after the
EOF-drain fix.

This is the same defect receive_into was already fixed for on the
sequential pump; apply the identical correction here — compare against the
AVERROR_INVALIDDATA constant, and use the named AVERROR_EAGAIN/AVERROR_EOF
constants in the receive loop instead of bare -11 / -541478725 literals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ropped captures)

decode_at bailed with "aucune frame reçue" whenever the requested index
was at or past the file's last frame. On a frame-dropped capture whose real
avg_frame_rate (e.g. 56.34) is below the nominal 60, a frame index computed
on the 60fps grid lands past the true frame count, so the preview failed
the moment it switched to such a clip.

Mechanism: for a target at/after the last frame, av_seek_frame BACKWARD
succeeds but lands on a lone undecodable packet just before EOF; after
avcodec_flush_buffers that reference-less frame yields nothing even when
drained, so found stayed null. Confirmed by sweeping decode_at over a real
56.34fps/631-frame recording: indices 0..=630 decoded, 631+ failed.

Fix: extract the packet pump into pump_to_target(); when the seek-based
pump returns no frame, rewind to 0 and forward-scan (the same linear scan
already used as the unindexed-WebM fallback). The forward pump keeps the
last frame <= target, so a past-end index yields the final frame instead of
erroring. Adds an env-gated regression test (OPENSCREEN_SWEEP_FILE) that
asserts decode_at never fails for 0..nb_frames+30.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Empirically (decode_at index sweep on a real 56.34fps recording) the EOF
drain returns zero frames on the app's own captures: they are H.264
baseline with has_b_frames=0, so the sequential receive loop already has
every frame and there is nothing buffered to drain. The drain stays
necessary for any stream WITH B-frames (imported video, other codecs),
where the last frame is held in the reorder buffer. Document that so the
drain is not mistaken for dead code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Beetix
Beetix requested a review from EtienneLescot as a code owner August 31, 2026 18:30
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 97ab091e-ab5e-4bb2-9a2f-a7f638761b32

📥 Commits

Reviewing files that changed from the base of the PR and between dcb1864 and 4fdb37c.

📒 Files selected for processing (1)
  • crates/compositor/src/linux_decode.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The Linux decoder now retries failed indexed seeks with sequential pumping, drains delayed frames at EOF, uses shared FFmpeg error constants, centralizes final frame validation, and adds an ignored regression test for indices at and beyond the stream’s final frame.

Changes

Linux decoder handling

Layer / File(s) Summary
Seek fallback and frame state
crates/compositor/src/linux_decode.rs
decode_at passes a seconds-based target to pump_to_target, retries from the stream start when an indexed seek returns no frame, and performs final frame validation and timestamp updates.
EOF draining and decoder errors
crates/compositor/src/linux_decode.rs
pump_to_target flushes delayed decoder frames at demuxer EOF, retains the latest frame at or before the target, skips invalid packets, and uses shared AVERROR_* constants.
Beyond-duration regression coverage
crates/compositor/src/linux_decode.rs
An ignored, environment-gated test requests indices through and beyond the estimated stream duration and requires a frame for each request.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to 4fdb3

The Linux preview now recovers frames at or beyond the recording end, including draining delayed decoder frames. The change is mergeable with owner awareness because malformed or interrupted media could still be mistaken for end-of-file, and repeated decoder failures may increase memory use through unreleased temporary frames.

Sequence Diagram(s)

sequenceDiagram
  participant SwDecoder
  participant demuxer
  participant FFmpegDecoder
  SwDecoder->>demuxer: Seek to target timestamp
  demuxer-->>SwDecoder: Seek result
  SwDecoder->>FFmpegDecoder: Decode packets
  FFmpegDecoder-->>SwDecoder: Frame or no frame
  SwDecoder->>demuxer: Rewind and scan sequentially when needed
  demuxer-->>SwDecoder: Packets through target
  SwDecoder->>FFmpegDecoder: Flush delayed frames at EOF
  FFmpegDecoder-->>SwDecoder: Final decoded frames
Loading

Suggested reviewers: etiennelescot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: robust Linux preview seeking at or beyond the last frame.
Description check ✅ Passed The description follows the repository template and provides the summary, issue links, change type, release impact, platform impact, and detailed testing information.
Linked Issues check ✅ Passed The changes satisfy issue #550 by falling back to forward scanning and returning the last available frame for targets at or beyond the recording end. The EOF drain and AVERROR_INVALIDDATA fixes also m…
Out of Scope Changes check ✅ Passed The changes remain within scope. The decoder hardening, regression test, and related FFmpeg error handling directly support the linked issue and stated PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 1 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #550 by falling back to forward scanning and returning the last available frame for targets at or beyond the recording end. The EOF drain and AVERROR_INVALIDDATA fixes also match the linked issue requirements.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.97.1)

Clippy execution failed


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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]: Linux preview 'aucune frame reçue' when seeking at/past the last frame of a recording

1 participant