Node.js
Use wasmpeg server-side in Node — decode, probe, extract audio, and generate thumbnails from files, buffers, or URLs, with the same API as the browser.
The same API runs in Node 18 or newer — no browser, no native addons, no separate build to
install. Node has no WebGPU adapter, so the CPU build is always used and the .wasm is read
from disk by the loader; the bundled binary resolves on its own, so you don’t pass wasmPath.
Setup
The project must be ESM, since wasmpeg ships ESM only. Set
"type": "module"inpackage.json, or give your entry file an.mjsextension. Then install:npm install wasmpegProbe and decode a file. Read it off disk into a
Uint8Arrayand hand that to wasmpeg:import wasmpeg from 'wasmpeg'; import { readFile } from 'node:fs/promises'; await wasmpeg.load(); const bytes = await readFile('clip.mp4'); const info = await wasmpeg.probe(bytes); console.log(info.duration, info.video.width, info.video.height); const dec = await wasmpeg.decode(bytes); let count = 0; for (let frame; (frame = dec.nextFrame()); ) count++; dec.close(); console.log('decoded', count, 'frames');
nextFrame() returns each frame as RGBA8 pixels (Uint8ClampedArray, width × height × 4
bytes), or null at end of stream. probe() gives you format, duration, bitrate, the stream
list, and video/audio details without decoding anything.
Inputs
In Node the inputs are byte buffers or URLs — there’s no File or canvas:
- A
Uint8ArrayorArrayBuffer, typically fromfs.readFile. - An
http(s)URL string, which wasmpeg fetches for you before decoding.
// From a URL
const dec = await wasmpeg.decode('https://example.com/clip.mp4');
Generate a thumbnail to disk
encode reads a source, pulls frames, and writes an encoded container — all in memory. Cap
it at one frame for a thumbnail and write the result with fs:
await wasmpeg.load();
const bytes = await readFile('clip.mp4');
const jpg = await wasmpeg.encode(bytes, {
codec: 'mjpeg',
width: 640,
height: 360,
frames: 1,
});
await writeFile('thumb.jpg', jpg);
encode opens and closes its own decoder and encoder, so there’s nothing to clean up after
it returns. width/height default to the source dimensions; frames caps how many frames
are read (omit it to encode the whole clip).
Extract audio samples
decodeAudio() returns interleaved 32-bit float samples. Pull chunks until it returns
null:
await wasmpeg.load();
const bytes = await readFile('clip.mp4');
const aud = await wasmpeg.decodeAudio(bytes);
console.log(aud.channels, 'channels at', aud.sampleRate, 'Hz');
let total = 0;
for (let chunk; (chunk = aud.nextSamples()); ) total += chunk.length;
aud.close();
console.log('decoded', total, 'samples');
Samples are interleaved by channel (L, R, L, R, … for stereo), so the per-channel count is
total / aud.channels.
Reuse the module across requests
In a server, load once at startup and share the module — don’t call load() per request.
Caching the promise makes the call safe to await from anywhere:
const ready = wasmpeg.load();
http.createServer(async (req, res) => {
await ready;
const chunks = [];
for await (const c of req) chunks.push(c);
const bytes = Buffer.concat(chunks);
const jpg = await wasmpeg.encode(bytes, { codec: 'mjpeg', frames: 1 });
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
res.end(jpg);
}).listen(3000);
encode and probe manage their own resources, so those need no manual cleanup.Notes
- No GPU.
gpu.hasWebGPU()is alwaysfalsein Node; scaling runs throughlibswscaleon the CPU. - CommonJS? The package is ESM-only. From a CJS module, use a dynamic import:
const { default: wasmpeg } = await import('wasmpeg'); - Heavy work blocks the event loop. Each
nextFrame()is synchronous CPU work. For large jobs in a server, run decoding in aworker_threadsworker so request handling stays responsive — the API is the same there.