Build guides

Building a video compressor that fits an exact size limit

Fitting a video under a size limit is the same problem as fitting an image under one, except that a single attempt can take a minute. The search strategy that works for images is unusable here, and the engine is large enough that shipping it at all turned into its own problem.

What it is built with

  • ffmpeg.wasm (single-threaded core), so no cross-origin isolation is required
  • The shared target-size engine, in its analytic-then-verify mode
  • `DecompressionStream` for expanding the gzipped core at runtime

Step 2: choose the slower core on purpose

ffmpeg.wasm has a multithreaded build, and it is meaningfully faster. It also needs SharedArrayBuffer, which needs cross-origin isolation, which blocks advertising scripts on the page that carries it.

The first version of this tool deliberately took the single-threaded core: slower, but it runs anywhere, on any page, with no header requirements at all. Speed was the thing worth trading, because a tool that works everywhere and takes longer is better than a faster one that silently fails to start on a page configured slightly differently.

The isolated route exists now, and the trade is documented in the headers file rather than in somebody’s memory.

Step 3: ship a 30 MB engine past a 25 MB limit

The ffmpeg core is 30.7 MiB. The host caps a single file at 25 MiB. This does not fail at build time; the site builds perfectly and the upload is rejected afterwards, which is the worst place for a limit to bite because nothing local catches it.

The fix is a build step that gzips oversized wasm to 9.8 MiB and a loader that expands it with `DecompressionStream` before handing it to ffmpeg. The build step also fails loudly if anything still exceeds the limit, which moves the failure from the deploy back to the build where it belongs.

The compressed file is named `.wasmz` rather than `.wasm.gz`, and that is deliberate. A server that recognises the `.gz` suffix may serve it with `Content-Encoding: gzip`, `fetch` would then decode it transparently, and the loader would try to decompress it a second time. An extension that nothing special-cases makes that entire class of bug unreachable.

What went wrong

The most useful part of any build write-up, and the part usually left out.

  • A limit enforced by your host at upload time is invisible to your build. Assert it in the build, or you will find out from a failed deploy.
  • Naming a gzipped file `.gz` invites the server to decode it for you, which breaks a loader that expects to decode it itself.
  • A first run downloads the engine, which is several megabytes even compressed. The interface has to say that is what is happening, or a long first wait looks like a hang.
  • Duration has to come from the decoded video rather than from the container header, which is sometimes absent or wrong, and a wrong duration makes the analytic bitrate wrong by the same factor.

Use the tool: Compress a video to fit a limit · How YappyKit works