Commit Graph
1 Commits
Author SHA1 Message Date
miga-heygenandMiguel Ángel 9460c983e0 fix(engine): recover audio duration by decoding the final frame (#3986)
* fix(engine): recover audio duration from packet timestamps before clamping

An authored `data-duration` on an <audio> element could be silently
shortened when a source file's container-summary duration (mvhd/tkhd/mdhd)
undercounts the real audio length while the packet stream itself is
intact. Add `probeAudioDurationFromPackets()`, which re-derives the true
duration from the last packet's own pts_time + duration_time — figures
ffprobe computes from the stream's own timestamps, not the container's
summary header — and wire it into the compiler's existing clamp check as a
rescue attempt before accepting a shorter value. Works regardless of
codec/profile; verified against real HE-AAC/SBR fixtures and a
binary-patched "lying container" MP4 built for the regression test.

The rescue only fires for candidates the existing clamp predicate already
flagged (no added cost on the common path), runs in parallel across
candidates rather than serially, and requires the recovered duration to
exceed the existing clamp epsilon (absorbing the few milliseconds of
priming-delay drift raw packet timestamps carry relative to an
edit-list-corrected container duration) before overriding the shorter
value.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* fix(engine): move audio duration correction into extractAudioMetadata

The previous approach compared the compiler's own packet-scan probe
against the already-corrected duration returned by extractAudioMetadata,
which meant the correction only applied to one caller and the regression
fixture happened to exercise a pre-existing AAC-LC-specific refinement
instead of the new code path.

Move the fix to the single canonical place duration is derived: replace
the old AAC-LC-only packet-count refinement (a profile allowlist assuming
a fixed 1024-samples-per-frame constant) with a codec-agnostic mechanism
that decodes the final audio frame and reads its own timestamp plus real
sample count. This has no per-codec assumption, so it applies uniformly
regardless of codec or profile, and every caller of extractAudioMetadata
benefits automatically. The compiler's own clamp logic is now unchanged
from its pre-existing form.

Verified against a real, previously-untouched case: a FLAC-in-MP4 file
(a codec the old AAC-LC-only path never corrected) with its mvhd/tkhd/mdhd
duration atoms binary-patched to half their true value while every real
frame stays intact. Confirmed this reproduces on the prior code (by
literally substituting its actual content) and is fixed by the new code.

The decode probe reads CSV output (each line stands alone) rather than
JSON (a single top-level array), and retains only the output tail under a
small cap — a lying container's true duration can be arbitrarily larger
than its declared one, and JSON truncated from the front isn't valid JSON,
so an unbounded scan of a long file silently fell back to the wrong
duration once ffprobe's stdout cap was hit.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* test(producer): update style-7-prod golden for corrected AAC duration probe

The deleted AAC-LC packet-count refinement assumed every demuxed packet
decodes to exactly 1024 samples. For this suite's real audio fixture
(a 48kHz stereo AAC-LC MP4), that assumption overcounts: ffprobe reports
753 demuxed packets, but only 752 of them actually decode into a real
audio frame (verified via raw PCM sample count: 770048 samples / 48000Hz
= 16.0426666...s). The old mechanism's 753 * 1024 / 48000 = 16.064s was
therefore wrong; the new decode-based probe correctly derives ~16.043s
from the real final frame.

Regression-shards caught this as a compilation mismatch on the previous
commit. Verified the corrected golden against the real source: full
compilation, visual (0 failed frames), and audio (correlation 0.966)
checks all pass locally with the actual LFS-hosted golden video, and a
standalone compileForRender() check confirms all other audio-bearing
fixture suites in the repo are unaffected.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* perf(engine): seek the audio decode probe instead of scanning the whole file

probeDecodedAudioDuration ran a full, single-threaded decode of the
entire audio stream on every extractAudioMetadata call (once per audio
element in the HTML compiler, plus audio mixing/padding/encoding
steps). On real 30-minute files this took over a second, and could
approach ffprobe's 30s deadline on multi-hour files while holding a
shared probe slot the whole time.

Add -read_intervals so the probe seeks to ~1 second before the
container's already-known claimed duration and decodes only that
tail. This is safe because the correction only ever raises the
duration: an honest or over-claiming container's seek lands at or past
the real end and finds nothing extra, while an under-claiming
container (the only case this exists to fix) still has real frames
past the seek point. If the tail read finds nothing at all, fall back
to a full unseeked decode before giving up, protecting files whose
seek index is unreliable (an index-less or non-seekably-sourced
container) from silently missing a real correction — mirroring the
existing seek-then-fallback pattern already used by the sibling
final-video-frame probe in this file.

Also replaces the real-file regression test's runtime ffmpeg-spawning
fixture generation with two small, committed binary fixtures (an
honest FLAC-in-MP4 file and a copy with its container duration atoms
halved) plus the ffprobe-static package, so this test's real-file
proof actually runs in CI instead of being silently skipped when no
system ffmpeg is on PATH.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(engine): trim the audio duration probe comments to the invariant

* test(engine): cache tests queue the decode probe and count metadata probes only

* test(engine): probe for ffprobe with a literal argv so the argv contract classifies it

* test(producer): regenerate the speed-ramp golden for the single-encode mix

The audio probe now reads the mixed AAC as exactly 4.000 s instead of the
packet-count 4.032 s (which counted the priming frame), so the final mix
is stream-copied instead of re-encoded once more. Same waveform (sample
correlation 0.9996 vs the old golden), 3.1 dB less true-peak attenuation;
video frames are byte-identical.

* fix(engine): measure decoded audio from the stream start and trust it for raw ADTS

The decoded last-frame end is an absolute timestamp, so subtract the audio stream's start time
(format start as fallback) before comparing it with the start-relative container duration, and
start the tail read at start + duration - 1 s. MPEG-TS audio (1.4 s default start) and offset
MP4s were over-extended by their start time.

Raw ADTS (format aac) has no duration field; ffprobe estimates it from the opening bitrate and
overclaims after a quiet opening. Its decoded timestamps are sample-counted, so for raw ADTS the
decoded end now wins in both directions beyond the margin. Other containers stay upward-only so
an MP4 edit list is never extended.

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-09 21:58:32 +00:00