Repository navigation
feat(producer): a cloud plan can leave its audio to be mixed beside its chunks - #5988
Conversation
Edit accuracy: accurate 271 (base branch 271), smooth 188 of thoseThe gate passes. Quarantined, measured but not gated (0) |
vanceingalls
left a comment
There was a problem hiding this comment.
Reviewed at 4c142905. Required CI is green and there are no other reviews yet.
What holds
- The stitch is sound. The audio track gets a new id,
mvhdnext_track_ID moves past it (TRACK_ID_AToffsets match v0 and v1 boxes), and the audio's samples sit ahead of every chunk's.withIdonly ever writes into the copywithDurationmakes, so the template moov is never mutated. - Nothing a render does today changes. No caller passes
deferAudio.s3Stitch.ts:141still callsbuildStitchedHeader(layouts)with no audio, andcfror a non-mp4 format keeps the plan's own mix. - Byte-identity holds for
<audio>and static audible video. Locally (ffmpeg 6.0), the PR'sbuildPlanAudiotest passes. For a<video src="clip.mp4" data-has-audio="true">composition,buildPlanAudioalso writes the same bytes as the plan'saudio.m4a.
Blocker: buildPlanAudio drops the sound of a video the static parse can't see
The plan's mix includes audio that runExtractVideosStage (plan.ts:1428) appends to composition.audios through appendAutoDetectedVideoAudio (extractVideosStage.ts:544). That append is the only way a <video data-has-audio="true"> gets into the mix when the static parse misses it. The common case is a src assigned from the scene script, a pattern the compiler explicitly supports (core/src/compiler/mediaRenderIds.ts:48). The browser probe then adds the video (probeStage.ts:610), but parseAudioElements never saw a src. buildPlanAudio (plan.ts:1741) runs compile and probe, then mixes at :1756 without that step, so those tracks are missing.
Repro at this head. The composition is <video id="v" data-has-audio="true" data-start="0" data-duration="1"> plus <script>document.getElementById("v").src = "clip.mp4"</script>, where clip.mp4 has an AAC tone:
buildLocalExecutionPlan(…, { chunksCarryAudio: true })writesaudio.m4a.buildPlanAudio(…)returns{ hasAudio: false }and writes nothing.
Once the next PR wires deferAudio, that render ships silent where today's plan has sound. The fix belongs here, because the function's contract is "the mix … that plan would have made". A probe-only pass would do it: ffprobe the audible, non-hidden composition.videos sources and feed appendAutoDetectedVideoAudio, without extracting frames. Alternatively, share the plan's post-extraction audio list. A buildPlanAudio-equals-plan test on a script-assigned src would pin the fix.
Nit: the "carries its own audio" refusal misses chunk 0
sourceTracks (mp4Stitch.ts:359) checks tracks.at(-1), the match for the separate audio's plan slot. When chunk 0 carries audio, its soun joins the plan, and every chunk's audio matches that slot instead, so nothing throws. With a separate file, buildStitchedHeader([chunk0(vide+soun), chunk1(vide+soun)], mix) returns a moov with tracks vide, soun, soun. This is unreachable from the plan, since separateAudio and chunksCarryAudio are exclusive, but the guard and its test only cover chunk ≥1. Checking chunkPlan.some(t => t.handler === "soun") up front closes it.
Verdict: REQUEST CHANGES
Reasoning: buildPlanAudio doesn't make the mix the plan makes for audible videos with a script-assigned src, which would silently drop their sound once it is wired. The rest is sound.
— Via
|
Fixed at 83e86e8, thanks.
|
vanceingalls
left a comment
There was a problem hiding this comment.
Re-reviewed at 73801e70, covering the delta from 4c142905 (plan.ts, mp4Stitch.ts and their tests).
The blocker is closed
buildPlanAudionow runsextractPlanVideosbefore mixing, the same stage the plan runs. SoappendAutoDetectedVideoAudioadds the sound of a<video data-has-audio>whose src a script sets. The plan's own call is now the shared helper, with the same arguments and the samefailureToEnforceandassertVideoExtractionSucceededchecks.deferRangeExtraction: trueis right for an audio-only pass. A deferred long CFR window still carries the probedmetadata(videoFrameExtractor.ts:2259-2305), so itshasAudioreaches the append without extracting frames.- Locally at this head (ffmpeg 6.0), the two
buildPlanAudiotests pass and the script-src one is byte-identical to the plan'saudio.m4a. Removing theextractPlanVideoscall turns the script-src test red.
The nit is closed. The refusal now checks each chunk's own tracks for soun before matching, so chunk 0 is covered too. mp4Stitch.test.ts passes 13/13. Exempting chunk 0 from the check turns the new chunk 0 case red.
Nit (cost, not correctness): the windows extraction won't defer (VFR, HDR transforms, final-frame-only, or a clip of about a second or less) are fully extracted again in buildPlanAudio, only to be discarded with workDir. This doesn't touch the 4K takes this targets (long CFR). A VFR-heavy film would pay the frame extraction twice, once on the plan's critical path and once beside the chunks. A metadata-only pass would avoid that if it ever shows up.
CI: every required check is green at 73801e70.
Verdict: APPROVE
Reasoning: buildPlanAudio now goes through the same extraction the plan does, so a deferred plan's audio matches today's for script-set video sources. The stitch refuses audio-carrying chunks at every index.
— Via
What changes
Nothing a render does today. This adds the producer pieces that let a cloud render mix its audio beside its chunks instead of before them; the Lambda wiring follows in the next PR.
deferAudio): mp4 chunks then render video only.buildPlanAudiomakes that audio track on its own: the same mix and true-peak hold the plan makes today, from the same code, at exactly the video's length.Why
On a long 4K render the plan spends most of its time on audio (about 48 of 57 s on a 20-minute film) while no chunk can start. With audio made beside the chunks, the plan shrinks to the rest and the audio finishes long before the chunks do.
How
buildPlanAudio, so both see the same composition. Video extraction, which adds the sound of audible videos the static parse misses (a src the scene script sets), is one helper too;buildPlanAudioruns it deferred, so it probes without extracting long windows. The mix and the true-peak hold are likewise one helper each.plansSeparateAudiodecides when audio is left out: mp4 withoutcfr, the cases the stitch can join. Any other plan mixes as before.separateAudio, so chunks know to encode video only and the assembler knows to stitch the audio file in.buildStitchedHeader(layouts, audio?)appends the audio file's track to chunk 0's moov under a new track id and indexes its samples first. It refuses any chunk, chunk 0 included, that carries audio of its own when a separate file is given.Tests
plan.test.ts: a plan withdeferAudiohas no audio and recordsseparateAudio;buildPlanAudiowrites a byte-identical file to the audio a carrying plan makes (a hot tone, so the true-peak hold re-encodes too), and the same for a<video data-has-audio>whose src the scene script sets (red before the shared extraction: no audio);cfrstill mixes in the plan.mp4Stitch.test.ts: the separate audio's samples sit before every chunk's, with its edit list, sizes and a new track id; chunk 0 or chunk 1 carrying audio is refused (chunk 0 was red before); the local join writes the same bytes as the in-memory stitch.chunkAudio.test.ts: video-only chunks stitched with the whole plan audio give the same packets in every stream as chunks that carry their slices.cfrdeferring audio, audio payload placed last.buildPlanAudioand today's plan write byte-identical audio (same file, same audio stream md5, 59,376,000 samples = 30,925 frames at 25 fps exactly).Test counts:
plan.test.ts63 pass (the script-src test 3 runs in a row),mp4Stitch.test.ts13,chunkAudio.test.ts9,assemble.test.ts18, each 0 fail; producer typecheck clean.