From d2f61fdcf3cd949ee7a93c728e09d5ffbaf00e7e Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 3 Feb 2026 23:50:33 +1300 Subject: [PATCH] fix(quick-003): trim audio to match stitched video duration After all video frames are processed (phase 1), the final video duration is known from highestWrittenTimestamp. Audio is now trimmed to that exact duration before being encoded into the MP4 (phase 2). Prevents audio tracks from extending beyond the video. Co-Authored-By: Claude Opus 4.5 --- src/hooks/useStitchVideos.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/hooks/useStitchVideos.ts b/src/hooks/useStitchVideos.ts index f95efc91..fe405ff0 100644 --- a/src/hooks/useStitchVideos.ts +++ b/src/hooks/useStitchVideos.ts @@ -154,6 +154,32 @@ export async function checkEncoderSupport(): Promise { } } +/** + * Trim an AudioBuffer to a target duration in seconds. + * If the buffer is shorter than the target, returns it unchanged. + */ +function trimAudioBuffer(buffer: AudioBuffer, targetDuration: number): AudioBuffer { + const targetSamples = Math.floor(targetDuration * buffer.sampleRate); + + if (targetSamples >= buffer.length) { + return buffer; + } + + const trimmed = new AudioBuffer({ + length: Math.max(1, targetSamples), + numberOfChannels: buffer.numberOfChannels, + sampleRate: buffer.sampleRate, + }); + + for (let ch = 0; ch < buffer.numberOfChannels; ch++) { + trimmed.getChannelData(ch).set( + buffer.getChannelData(ch).subarray(0, targetSamples) + ); + } + + return trimmed; +} + /** * Standalone async function to stitch multiple video blobs together sequentially */ @@ -384,10 +410,13 @@ export async function stitchVideosAsync( } } - // Encode audio after all video frames have been queued so container metadata stays accurate + // Phase 2: Apply audio, trimmed to match the stitched video duration if (audioSource && pendingAudioBuffer) { updateProgress('processing', 'Encoding audio track...', 92); - await audioSource.add(pendingAudioBuffer); + + const videoDuration = highestWrittenTimestamp + frameInterval; + const trimmedBuffer = trimAudioBuffer(pendingAudioBuffer, videoDuration); + await audioSource.add(trimmedBuffer); await audioSource.close(); }