diff --git a/apps/media-worker/src/producer/assemble-builder.test.ts b/apps/media-worker/src/producer/assemble-builder.test.ts index 353c86f..d1d81b4 100644 --- a/apps/media-worker/src/producer/assemble-builder.test.ts +++ b/apps/media-worker/src/producer/assemble-builder.test.ts @@ -135,7 +135,7 @@ test("per-item chains feed one concat v=1:a=1 in timeline order", () => { assert.match(chains[0]!, /^\[0:v\]scale=1280:720[^;]*\[v0\]$/); assert.match(chains[1]!, /^\[0:a\]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo\[a0\]$/); assert.match(chains[2]!, /^\[1:v\]scale=1280:720[^;]*\[v1\]$/); - assert.equal(chains[3]!, `anullsrc=r=${ASSEMBLE_PRESET.sampleRate}:cl=stereo,aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo[a1]`); + assert.equal(chains[3]!, `anullsrc=r=${ASSEMBLE_PRESET.sampleRate}:cl=stereo,atrim=0:2.000,asetpts=PTS-STARTPTS,aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo[a1]`); // The rotated clip transposes before its fit chain and applies its volume. assert.ok(chains[4]!.startsWith("[2:v]transpose=1,scale=")); assert.ok(chains[5]!.includes("volume=-6dB")); diff --git a/apps/media-worker/src/producer/assemble-builder.ts b/apps/media-worker/src/producer/assemble-builder.ts index 4465dd3..8fb990c 100644 --- a/apps/media-worker/src/producer/assemble-builder.ts +++ b/apps/media-worker/src/producer/assemble-builder.ts @@ -141,7 +141,7 @@ export function clipVideoChain(rotate: EdlRotate, fit: "cover" | "contain", aspe return `${rotated}scale=${W}:${H}:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,fps=${ASSEMBLE_PRESET.fps},format=yuv420p`; } -function clipAudioChain(volumeDb: number, hasAudio: boolean, inputIndex: number): string { +function clipAudioChain(volumeDb: number, hasAudio: boolean, inputIndex: number, trimSpanSec: string): string { const aformat = `aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo`; if (!hasAudio) return `anullsrc=r=${ASSEMBLE_PRESET.sampleRate}:cl=stereo,aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo`; return `[${inputIndex}:a]${aformat}${volumeDb === 0 ? "" : `,volume=${volumeDb}dB`}`; @@ -201,7 +201,7 @@ export function buildAssembleFilter(plan: AssemblePlan, index: AssembleInputInde const at = index.timeline[i]; if (at === undefined) fail(`clip ${i} has no input index`); chains.push(`[${at}:v]${clipVideoChain(entry.rotate, project.fit, project.aspect)}[${v}]`); - chains.push(`${clipAudioChain(entry.volumeDb, source.hasAudio, at)}[${a}]`); + chains.push(`${clipAudioChain(entry.volumeDb, source.hasAudio, at, ((entry.trimEndMs - entry.trimStartMs) / 1000).toFixed(3))}[${a}]`); clipCursor += 1; } else { const source = plan.cards[cardCursor]; @@ -210,7 +210,7 @@ export function buildAssembleFilter(plan: AssemblePlan, index: AssembleInputInde if (at === undefined) fail(`card ${i} has no input index`); // -loop 1 -t {dur} supplies the frames; the chain only normalizes to the canvas. chains.push(`[${at}:v]scale=${EDL_ASPECTS[project.aspect].w}:${EDL_ASPECTS[project.aspect].h},setsar=1,fps=${ASSEMBLE_PRESET.fps},format=yuv420p[${v}]`); - chains.push(`anullsrc=r=${ASSEMBLE_PRESET.sampleRate}:cl=stereo,aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo[${a}]`); + chains.push(`anullsrc=r=${ASSEMBLE_PRESET.sampleRate}:cl=stereo,atrim=0:${(source.durationMs / 1000).toFixed(3)},asetpts=PTS-STARTPTS,aformat=sample_fmts=fltp:sample_rates=${ASSEMBLE_PRESET.sampleRate}:channel_layouts=stereo[${a}]`); cardCursor += 1; } });