ffmpegwasm

Activity

Stars and mentions · Last 30 days, Oct 6 – Nov 4

GitHub stars81
Mentions1
02468Oct 6Oct 20Nov 4
UTC days · repositories ffmpegwasm owns1 repository

Top repositories

ffmpegwasm’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside ffmpegwasm’s account where ffmpegwasm opened or reviewed pull requests in the past 365 days.

Who noticed ffmpegwasm

Everyone who starred or forked ffmpegwasm’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

1post
Nov 50 posts
  1. Trimming video in the browser with FFmpeg.wasm: 6 things I had to figure out In my previous post I wrote about converting images to JPG with Canvas. This time it's video: a trim tool and a video to MP3 converter for CompressPhoto. Both run FFmpeg compiled to WebAssembly, right in the browser tab. The file never goes to a server. Getting a demo working with ffmpeg.wasm takes ten minutes. Making it usable for real people took longer. Here's what I ran into. 1. Use the single-threaded core ffmpeg.wasm has a multi-threaded core, and it's faster. But it needs SharedArrayBuffer. That means serving the page with two headers: Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp Those headers isolate the page. On my site they would break Google sign-in, which uses a popup, and third-party scripts like analytics. So I use the single-threaded @ffmpeg/core. It's slower, but it works on any page without special headers. 2. Host the 32 MB core yourself, and show real progress The WASM file is about 32 MB. I don't load it from a CDN. A build script copies it from node_modules into public/ffmpeg/<version>/. Because the version is in the path, the file can be cached forever. 32 MB is a long wait on a phone, so users need a progress bar. Here's the catch: the server sends the file compressed. Content-Length is the compressed size, so loaded / total is wrong. I hard-code the uncompressed size and stream the download myself: const CORE_WASM_BYTES = 32_232_419; // size of ffmpeg-core.wasm 0.12.10, uncompressed async function fetchWasm(onProgress) { const res = await fetch(`/ffmpeg/0.12.10/ffmpeg-core.wasm`); const reader = res.body.getReader(); const chunks = []; let loaded = 0; for (;;) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; onProgress(Math.min(0.99, loaded / CORE_WASM_BYTES)); } return URL.createObjectURL(new Blob(chunks, { type: "application/wasm" })); } await ffmpeg.load({ coreURL, wasmURL: await fetchWasm(setProgress) }); A test checks that the core version in the code matches package.json. So an upgrade fails the build until I update the path and this size together. 3. Don't copy the video into memory: mount it Most examples do this: await ffmpeg.writeFile("input.mp4", await fetchFile(file)); That copies the whole video into WebAssembly memory. A 700 MB phone video plus the output can crash the tab. ffmpeg.wasm can mount the File with WORKERFS instead. FFmpeg then reads it in chunks, straight from disk: // WORKERFS exposes the file under its own name, so give it a safe one. // new File([file], ...) doesn't copy the data: it's the same Blob. const input = new File([file], "source.mp4", { type: file.type }); await ffmpeg.createDir("/input").catch(() => {}); await ffmpeg.mount("WORKERFS", { files: [input] }, "/input"); await ffmpeg.exec(["-i", "/input/source.mp4", /* ... */ "output.mp4"]); const data = await ffmpeg.readFile("output.mp4"); await ffmpeg.unmount("/input"); Only the result lives in memory now. That's why the trim tool accepts files up to 1 GB, far more than I could allow for server-side tools. The safe file name matters too. Real file names contain spaces, quotes and emoji. You don't want them anywhere near FFmpeg arguments. 4. Offer two trim modes: fast and precise The fast mode copies the streams without re-encoding: ["-ss", start, "-i", input, "-t", duration, "-map", "0:v:0?", "-map", "0:a:0?", "-c", "copy", "-avoid_negative_ts", "make_zero", "-movflags", "+faststart", "output.mp4"] It finishes in seconds even on a phone. The catch: with stream copy, the cut can only happen on a keyframe. The clip may start a little earlier than the user asked. The precise mode re-encodes to H.264 and AAC: ["-ss", start, "-i", input, "-t", duration, "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "160k", "-movflags", "+faststart", "output.mp4"] Two details are worth noting. H.264 with yuv420p needs even width and height, so the scale filter rounds them down. Some phone videos have odd dimensions, and without the filter the encode just fails. Also, in fast mode I keep the original container. Stream copy keeps the original codecs, and not every container accepts every codec. For example, AMR audio from an old 3GP file won't fit into MP4. The ? in -map 0:a:0? makes the audio track optional. Without it, a silent screen recording fails with "matches no streams". 5. Progress, cancel and errors all come from the log exec() doesn't report progress, but FFmpeg's log does. I read Duration: once and then every time= line: ffmpeg.on("log", ({ message }) => { const t = /time=\s*(\d+):(\d{2}):(\d{2}(?:\.\d+)?)/.exec(message); if (t && total) onProgress((+t[1] * 3600 + +t[2] * 60 + +t[3]) / total); }); For files the browser can't preview, like MKV or some HEVC videos, I get the duration by running ffmpeg -i input with no output. FFmpeg prints the stream info and exits with an error. That's expected, and the log is all I need. Cancelling is blunt. exec() can't stop halfway, so on abort I terminate the worker. The next task creates a new one. The core is already in the HTTP cache, so restarting is quick. Errors are classified from the log too. "moov atom not found" means a broken or incomplete file. "Cannot enlarge memory" means the tab ran out of memory. Each case gets its own message instead of "something went wrong". 6. Run one job at a time The single-threaded FFmpeg instance can't run two commands at once. If a user starts a second file, it waits in a simple promise chain: let chain = Promise.resolve(); function run(job) { const task = chain.then(job); chain = task.catch(() => {}); // the next job waits even if this one failed return task; } Was it worth it? For trimming and extracting audio, yes. The fast trim is instant, there's no server bill, and the privacy story is simple: the file stays on your device. For heavy re-encoding it's a different story. Single-threaded WASM is several times slower than native FFmpeg, and on a phone a long precise trim takes a while. That's why the precise mode is opt-in. You can try the result here: Trim video online and Video to MP3. Have you shipped ffmpeg.wasm in production? I'd like to know how you handle memory on phones.

    1 comment

Achievements

Awards earned by the repositories ffmpegwasm owns. Select an award to embed its badge.

Gold
11st place finishes
Silver
02nd place finishes
Bronze
13rd place finishes
Awarded repositories
1Owned by ffmpegwasm now

Repositories

Ordered by gold, then silver, then bronze.

  1. ffmpeg.wasmBest: 1st by stars, 607 stars101

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. ffmpeg.wasm
  2. ffmpeg.wasm

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.