1. Browser Recording Is Convenient, but Output Mixing Is Limited
Canvas capture can create a stream from visuals, but it does not solve independent audio placement and mixing.
Code:
const canvas = document.querySelector('#scene');
const stream = canvas.captureStream(30);
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm'
});
recorder.start();
// This captures the canvas frames only.
// Separate audio clips still need another pipeline.
Result:
Waiting. The animation can be captured as frames, but the separate audio timeline is empty.
2. Load ffmpeg.wasm from Local Core Files
The article recommends hosting the FFmpeg runtime files locally because the core file is large and slow to fetch from a remote CDN.
Code:
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
corePath: './ffmpeg-core.js',
log: true
});
await ffmpeg.load();
Result:
This is a local simulation of the loading step. The important idea is that the browser must fetch and initialize a large WASM runtime before any command can run.
3. Composite One Silent Video with One Audio File
The first useful FFmpeg step is mapping the original video stream and the new audio stream into a single MP4 output.
Code:
await ffmpeg.run(
'-i', 'bj.mp4',
'-i', 'record.mp3',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v:0',
'-map', '1:a:0',
'output.mp4'
);
Result:
4. Understand adelay and amix in filter_complex
The key leap is delaying individual audio streams first, then mixing those delayed streams into one output label.
Code:
const filter =
'[1]adelay=2000|2000[aout1];' +
'[2]adelay=10000|10000[aout2];' +
'[aout1][aout2]amix=2[aout]';
await ffmpeg.run(
'-i', 'video.mp4',
'-i', 'voice-1.mp3',
'-i', 'voice-2.mp3',
'-filter_complex', filter,
'-map', '0:v:0',
'-map', '[aout]',
'output.mp4'
);
Result:
2000ms
10000ms
[aout]
5. Composite Four Delayed Audio Clips into One Video
This mirrors the article’s final working pattern: delay each input, mix all delayed labels, and map the mixed audio into the output.
Code:
await ffmpeg.run(
'-i', 'zhangxinxu.mp4',
'-i', '1.mp3',
'-i', '2.mp3',
'-i', '3.mp3',
'-i', '4.mp3',
'-filter_complex',
'[1]adelay=2000|2000[aout1];[2]adelay=10000|10000[aout2];[3]adelay=15000|15000[aout3];[4]adelay=20000|20000[aout4];[aout1][aout2][aout3][aout4]amix=4[aout]',
'-c:v', 'copy',
'-c:a', 'aac',
'-map', '0:v:0',
'-map', '[aout]',
'output.mp4'
);
Result:
6. Practical Limits: Size, Startup Time, and Memory
The article’s conclusion is pragmatic: ffmpeg.wasm is powerful, but large downloads and memory pressure make it better for internal tools or selected users.
Code:
if (!window.WebAssembly) {
throw new Error('WebAssembly is not available');
}
try {
new WebAssembly.Memory({ initial: 256 });
console.log('Enough memory for this demo');
} catch (error) {
console.error('WebAssembly.Memory(): could not allocate memory');
}
Result:
Large WASM payloads block the feature until the runtime is downloaded and initialized.
Even on a local network, loading can feel slow compared with ordinary page scripts.
Some devices cannot allocate enough WebAssembly memory for FFmpeg workloads.
Checked in this browser
Attempts a small browser-safe allocation
Best for controlled users, VIP features, or internal production tools