fix(video): repair partial H.264 MP4 composition offsets - #513
Conversation
… still delivers (#513) The merged policy is exact: measured against a healthy twin whose later ctts offsets were zeroed in place, 1800 of 1800 packet times and every DTS come back identical. Two things around it were not. The hold grew with the sequence. Packets were published only at the next IDR, so a coded video sequence was held whole, across ALL streams, and the budget behind it (1024 interleaved packets) was reached by an ordinary GOP: measured at 30 fps with one stereo AAC track, 390 pictures survived and 420 did not, because 420 video packets carry about 656 audio packets with them. The advertised ceiling of 512 pictures was therefore never the one that governed. And reaching it threw. After the first sequence had confirmed, a refusal ended the source read instead of the repair, which on a scene-cut fixture (a 30-picture zero-offset sequence, then 420) killed the session before it drew anything: `state=error("Source read failed (code -1)")` with `hasFirstFrameReadyForDisplay =false`, where main plays the same file through with the judder the repair exists to remove. Both follow from holding a plan instead of reading a slot. A picture's slot is a packet away: rank r needs slot r, so a picture coded ahead of its own slot waits exactly the mini-GOP the reorder created and nothing waits for a sequence to end. `SlotLadder` records the decode slots as they arrive and hands each picture the one its rank owns; the FIFO drains from the front, so the container's own order is what leaves the demuxer. Measured deepest wait: 6 packets, on the same 420-picture sequence, against 154 for a 60-picture one before. There is no sequence length this cannot repair now, so the ceiling that governed is gone rather than raised. Every refusal hands the held packets back exactly as they arrived and lets the rest of that sequence stream through; the next IDR is a fresh candidate. A repair that cannot say where a picture belongs still has to deliver it. The regression fixture his suite could not reach is in the controls script now: a scene-cut GOP structure, because a uniform one-second GOP cannot cross a ceiling and therefore cannot show one gone. His numeric ladder, including the 25 to 29.97 transition, stays the unit fixture and now also pins the wait depth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tQ8MerNbZQdWBpDStitoD
…mini-GOP, and a refusal still delivers (#513)
The merge left its conflict markers in the workflow file, which GitHub rejects before any job starts. Keeps the software packet-cache step and the partial composition-offset step side by side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tQ8MerNbZQdWBpDStitoD
|
Merged, with the delivery around the policy rebuilt on top in The policy is exact. Measured against a healthy twin, the same file with the later Two things around it did not hold up, and both trace back to holding a plan rather than reading a slot. The hold grew with the sequence. And reaching it threw. The loopback segment producer reads ahead, so it met the throw before the player had drawn a frame. Same file on What changed. A picture's slot is a packet away, not a plan away: rank r needs slot r, so a picture coded ahead of its own slot waits exactly the mini-GOP the reorder created. Measured on both twins, Every refusal now hands the held packets back exactly as they arrived and lets the rest of that sequence stream through; the next IDR is a fresh candidate. A repair that cannot say where a picture belongs still has to deliver it, because the judder is a far smaller failure than a session that stops. Two notes on the harness, since both are why this was not visible from it:
The numeric ladder, including the 25 to 29.97 transition, is unchanged as the unit fixture and now also pins the wait depth. Full suite green on both runners, and the runtime controls pass on all three fixtures in CI. Not released yet. A retest on the original source once it is would be welcome, and one thing worth knowing from it: whether any sequence in that file is long enough that the old ceiling would have been reached. |
|
Released in 6.74.0 ( |
Summary
Fix persistent judder when an H.264 MP4 has valid composition offsets at its head but zero offsets in later reordered IDR sequences. The existing healthy-head verdict disables repair for the entire session, so a seek into the malformed region escapes detection.
This is an independent follow-up to #409, not a dependency on #510, #511 or #512. It changes no host UI or decoder routing and leaves the existing whole-file missing-ctts policy intact.
Evidence
The reported source is progressive SDR H.264 High level 4.0, 1920x1080 in MP4, nominal 30000/1001, time base 1/90000, with AAC-LC 48 kHz. Direct sample-table inspection found valid offsets for the first 3272 video samples, then 279588 zero offsets. At about 700 seconds all 240 inspected packet PTS equalled DTS while parsed POC reversed 68 times; a 120-frame decode had 34 original frame-PTS regressions. Native telemetry still reported about 30 FPS, sufficient buffer and no aggregate drops/stalls.
The first faulty GOP changes from 3600-tick to 3003/3004-tick intervals inside the sequence, so a short constant-cadence guess does not safely repair the continuous boundary.
What changed
Test plan
bash Scripts/test-h264-partial-composition-controls.sh: passed on this branch, including healthy→broken→healthy, EOF, abandoned pending/ready queues, malformed post-confirmation timestamps and all-stream bounds.swift testand other platform builds were not run locally; the existing CI jobs plus the new generated regression remain the upstream gate. Generated fixtures prove the timestamp regression, not the original movie's visual judder.See
docs/partial-composition-regression.mdfor reproduction, measurements and limitations. This is a narrow compatibility policy, not a general VFR timestamp reconstructor.Checklist
CHANGELOG.mdupdated