fix(metadata): best-effort nb_frames cascade + count_frames utility#5
Conversation
Container headers can lie about the frame count: some MOV/MP4 files declare more frames than actually decode (e.g. 41 declared, 33 real). nb_frames now cross-checks the cheap candidate (header, else packet count) against duration x fps; on agreement it is trusted at no extra cost, on disagreement the stream is decoded for the ground truth. Non-seekable streams keep the header/estimate behavior. Adds a public count_frames() for callers that need a definitive count regardless of what the header claims, and a trimmed real-world fixture whose header declares 11 frames while only 3 decode. Closes #4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR refactors frame counting in ChangesFrame Counting Strategy Overhaul
Sequence DiagramsequenceDiagram
participant video_metadata_from_container
participant _best_effort_nb_frames
participant _count_video_packets
participant _count_decoded_frames
video_metadata_from_container->>_best_effort_nb_frames: (stream, duration, fps, seekable)
_best_effort_nb_frames->>_best_effort_nb_frames: candidate_header = stream.frames
_best_effort_nb_frames->>_count_video_packets: get packet count
_best_effort_nb_frames->>_best_effort_nb_frames: estimate = round(duration * fps)
alt Not seekable
_best_effort_nb_frames->>_best_effort_nb_frames: return best cheap signal
else Seekable and signals disagree
_best_effort_nb_frames->>_count_decoded_frames: decode full stream
_count_decoded_frames->>_best_effort_nb_frames: return decoded count
end
_best_effort_nb_frames->>video_metadata_from_container: nb_frames
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly Related Issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #4.
Problem
video_metadata(path).nb_framestrusted the container header, but headers can lie: some MOV/MP4 files declare more frames than actually decode (the fixture in #4 declares 41, only 33 decode — 24% off). This caused false-positive frame-count-mismatch reports in sign/data's pose validation.Approach
Per discussion, no
nb_frames_certainflag —nb_framesjust does its best, escalating only when needed:stream.frames), else packet count (one demux pass, no decoding) when the header is absent (Matroska/WebM)round(duration × fps)— agreement within 1 frame → trust it (zero extra cost; healthy files take the same path as before)read_frames_from_streamon a pipe) can't rewind, so they keep header-else-estimateOn the #4 fixture: header=41, derived=31 → disagreement → decode → 33 ✓
Also adds the public
count_frames(source)from #4 — explicit full-decode ground truth for callers that need certainty regardless of the header.Known limitation
The
cH27PiYX6gIclass from #4 (header and duration×fps agree at 4,340; truth 4,089) is not caught: when both cheap signals agree we trust them. Catching it would require demuxing every file even when signals agree — a full I/O pass on all healthy files. Can be revisited as an opt-in if that class proves common.Tests
tests/assets/buggy_mov_header.mov(1.7 MB, trimmed from the 5.5 MB original with-c copy, bug preserved: header declares 11 frames, only 3 decode)test_nb_frames_matches_decoded_framesparametrized over the missing-header mkv/webm and the lying-header mov, assertingnb_frames == count_frames(path)— no hardcoded counts🤖 Generated with Claude Code
Note
Medium Risk
Frame-count logic now may full-decode some seekable files when metadata disagrees, changing
nb_framesfor buggy headers; behavior is intentional but affects downstream validation that relied on header counts.Overview
video_metadata().nb_framesno longer trusts the container header alone. Seekable inputs use_best_effort_nb_frames: header or packet demux as a cheap candidate, cross-check againstround(duration × fps)(within 1 frame → keep candidate), and full decode only when those signals disagree. Non-seekable sources still get header or duration×fps only.Adds public
count_frames(source)for an explicit full-decode ground truth when callers need certainty.Tests drop
read_frames_exactin favor ofcount_frames, addbuggy_mov_header.mov, and parametrizetest_nb_frames_matches_decoded_framesover missing-header Matroska/WebM and lying-header MOV.Reviewed by Cursor Bugbot for commit 844a5bd. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Improvements