Vite
Use wasmpeg in a Vite project (including React, Vue, or Svelte on Vite) — install, serve the WASM, decode in a component, and move work into a worker.
Vite is the smoothest setup. It serves the package’s co-located .wasm during dev and
emits it on build with no extra config, because the loader resolves the binary relative to
its own module URL and Vite understands that pattern. Everything here applies to any Vite
app — vanilla, React, Vue, or Svelte.
Install
npm install wasmpeg
wasmpeg is ESM-only, which is exactly what Vite expects, so there’s nothing to configure in
vite.config.js for the common case.
Load once, then decode
You only need to call wasmpeg.load() a single time per page. It’s idempotent — calling it
again returns immediately — but loading and compiling the WASM still costs something, so do
it once and share the loaded module.
In the browser, decode() accepts a File or Blob straight from an <input type="file">
or a drag-and-drop event — no need to read it into an ArrayBuffer yourself. Each call to
nextFrame() returns the next frame as RGBA8 pixels (Uint8ClampedArray, width × height × 4
bytes), or null at the end of the stream.
close() the decoder when you’re done with it. Each open decoder holds a session and
native buffers; leaking them will eventually exhaust the available slots. Close it in your
cleanup function, or in a finally block for one-shot work.Probe before you decode
If you only need dimensions, duration, or stream info — say, to decide whether a file is even
a video before spending time decoding — use probe():
await wasmpeg.load();
const info = await wasmpeg.probe(file);
console.log(info.format, info.duration, info.video.width, info.video.height);
console.log('streams:', info.streams.map((s) => s.type).join(', '));
If the WASM 404s
The default loader fetches the binary from a URL relative to the package. If your build
output doesn’t co-locate it — or you’ve hit a 404 in the network tab for cpu.wasm — copy
the binaries into public/ and point the loader at the .js:
cp node_modules/wasmpeg/dist/cpu.* public/wasmpeg/
await wasmpeg.load({ wasmPath: '/wasmpeg/cpu.js' }); // cpu.wasm must sit beside it
public/ is served at the site root by Vite, so this works in dev and in the production
build without bundler involvement. Keep both cpu.js and cpu.wasm together: the .js
resolves its .wasm from the same directory.
Run decoding in a Web Worker
Decoding is CPU-bound and synchronous inside each nextFrame() call, so a long clip will
stutter the main thread. The whole API works unchanged inside a Web Worker — Vite has
first-class worker support, so you can import one with the ?worker suffix.
// decode.worker.js
let ready;
self.onmessage = async (e) => {
ready ??= wasmpeg.load();
await ready;
const dec = await wasmpeg.decode(e.data.file);
try {
for (let frame; (frame = dec.nextFrame()); ) {
// Transfer the buffer so it isn't copied across the boundary.
self.postMessage(
{ width: dec.width, height: dec.height, frame },
[frame.buffer],
);
}
} finally {
dec.close();
self.postMessage({ done: true });
}
};
// main.js
const worker = new DecodeWorker();
worker.postMessage({ file });
worker.onmessage = (e) => {
if (e.data.done) return;
const { width, height, frame } = e.data;
ctx.putImageData(new ImageData(frame, width, height), 0, 0);
};
Transferring frame.buffer (rather than letting it be structured-cloned) hands the pixel
memory to the main thread with no copy. Don’t read frame in the worker after transferring
it — the buffer is detached on this side once it’s posted.
nextFrame() from requestAnimationFrame so you decode
roughly one frame per repaint, rather than running a tight loop that races ahead of what the
screen can show.