Mediabunny: FFmpeg's Web-Native Cousin Who Actually Reads the Room
If you have ever tried to convert a video inside a web page, you probably reached for ffmpeg.wasm, watched a multi-megabyte WebAssembly bundle download, and then waited while it churned away on the CPU because the sandbox blocks hardware access. Mediabunny is what happens when someone decides the browser already has everything it needs. It is a pure-TypeScript toolkit for reading, writing, and converting audio and video files — MP4, MOV, WebM, MKV, MP3, WAV, Ogg, FLAC, AAC, MPEG-TS, and even HLS — all running directly in the browser on top of the native WebCodecs API.
Think of it as FFmpeg's web-native cousin: it does the muxing, demuxing, encoding, and decoding you expect, but it ships only the code you actually use and taps into the GPU for the heavy lifting. The package has zero runtime dependencies, weighs as little as ~5 kB gzipped for a single use case, and is built by the same author behind the well-known mp4-muxer and webm-muxer libraries — Mediabunny is the grown-up unification of both, with reading and conversion added on top.
Why It Stands Out
Mediabunny earns its place in your toolbox with a handful of properties that are genuinely hard to find together:
- Hardware-accelerated. Compressed codecs run through the browser's
VideoEncoder/VideoDecoder, so a 1080p H.264 encode can hit roughly 200 fps whereffmpeg.wasmmanages around 25 fps on the same machine. - Tree-shakable. Import only the formats and features you need, and the bundle shrinks accordingly. A WAV-only reader is a few kilobytes; the full feature set is around 70 kB gzipped.
- Bounded memory. Reading and writing happen in lockstep with automatic backpressure, so memory stays effectively O(1) even when you process multi-gigabyte files.
- Broad format support. A long list of containers and codecs, all behind a single consistent
Input/OutputAPI. - HLS built in. VOD and live streams, encrypted segments, and multi-rendition ladders — using the exact same API as every other format.
- Runs beyond the browser. Node.js, Bun, and Deno are supported through the
@mediabunny/serverextension.
The trade-off is honesty about its foundation: compressed codecs depend on WebCodecs (Chrome/Edge 94+, Safari 16.4+), so you should feature-detect and fall back where needed. PCM and WAV always work because they ship with built-in JavaScript encoders.
Getting It Into Your Project
Mediabunny is a single package with no extra setup required.
npm install mediabunny
Or with yarn:
yarn add mediabunny
You will want an ES2021+ environment and, for the best typing experience, TypeScript 5.7 or newer. If you plan to run it server-side, also grab @mediabunny/server for file-path sources and targets.
Peeking Inside a File
The reading API revolves around the Input class. You hand it a set of formats to recognize and a source describing where the bytes live, and it lazily parses only the metadata it needs — no full download required.
import { Input, ALL_FORMATS, BlobSource } from 'mediabunny';
async function inspect(file: File) {
const input = new Input({
formats: ALL_FORMATS,
source: new BlobSource(file),
});
const format = await input.getFormat();
const duration = await input.computeDuration();
const mimeType = await input.getMimeType();
const tags = await input.getMetadataTags();
console.log({ format, duration, mimeType, title: tags.title });
}
The source abstraction is where a lot of the flexibility lives. Use BlobSource for a file picked through an <input type="file">, BufferSource when you already have the whole file in memory, UrlSource to stream over HTTP with range requests and retries, or FilePathSource on the server. Because parsing is lazy, calling computeDuration() on a remote 4 GB movie only fetches the handful of bytes that hold the duration — not the whole file.
You can also drill into individual tracks and even sort or filter them. Suppose you want the highest-resolution video track and only English audio:
import { desc } from 'mediabunny';
const videoTrack = await input.getPrimaryVideoTrack({
sortBy: async (t) => [
desc(await t.getDisplayWidth()),
desc(await t.getBitrate()),
],
});
const englishAudio = await input.getAudioTracks({
filter: async (t) => (await t.getLanguageCode()) === 'eng',
});
Each track exposes rich metadata — getCodec(), getCodecParameterString(), canDecode(), and for video, getDisplayWidth(), getRotation(), getColorSpace(), and hasHighDynamicRange().
Generating Thumbnails on the Fly
Reading raw metadata is useful, but most apps want actual pixels. Mediabunny's sinks turn tracks into something you can render. The CanvasSink is perfect for poster frames and scrubbing strips — give it a list of timestamps and it hands back canvases.
import { Input, ALL_FORMATS, BlobSource, CanvasSink } from 'mediabunny';
async function makeThumbnails(file: File) {
const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(file) });
const videoTrack = await input.getPrimaryVideoTrack();
if (!videoTrack || !(await videoTrack.canDecode())) return;
const sink = new CanvasSink(videoTrack, { width: 320, height: 180 });
const strip: HTMLCanvasElement[] = [];
for await (const result of sink.canvasesAtTimestamps([0, 0.2, 0.4, 0.6])) {
strip.push(result.canvas);
}
return strip;
}
For frame-accurate playback or scroll-synced video, reach for VideoSampleSink, whose decoded samples each have a .draw(ctx, x, y) method you can paint straight onto a <canvas>. There is also AudioBufferSink, which produces Web Audio AudioBuffers ready to feed into an AudioContext for playback or processing. Each sink shares the same lazy, on-demand philosophy, so you only decode the frames you actually ask for.
Converting Without the Headache
The headline feature for most people is conversion, and Mediabunny gives you a high-level Conversion class that figures out the details for you. You pair an Input with a fresh Output — fresh meaning you have not added any tracks to it yet — and let it pick compatible codecs, transmuxing instead of re-encoding wherever it can.
import {
Input,
Output,
WebMOutputFormat,
BufferTarget,
BlobSource,
ALL_FORMATS,
Conversion,
} from 'mediabunny';
async function toWebM(file: File) {
const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(file) });
const output = new Output({
format: new WebMOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input, output });
if (!conversion.isValid) {
console.warn('Some tracks were dropped:', conversion.discardedTracks);
}
conversion.onProgress = (progress) => console.log(`${Math.round(progress * 100)}%`);
await conversion.execute();
return new Blob([output.target.buffer!], { type: 'video/webm' });
}
The onProgress callback reports a value between 0 and 1, which maps neatly onto a progress bar. Conversion options cover the things you usually want: resize with width/height plus a fit mode, rotate, crop, frameRate, codec, bitrate, keyFrameInterval, and a trim: { start, end } for cutting clips. There is even a process(sample) hook for applying custom per-frame effects via a canvas. If you need to bail out, conversion.cancel() aborts cleanly and frees resources.
Building an Adaptive Streaming Ladder in the Browser
This is where Mediabunny goes from convenient to genuinely impressive. HLS support — added in v1.42.0 and described by the author as the biggest addition since the original release — adds only about 30 kB and uses the exact same Input/Output API as everything else. That means you can generate a multi-rendition streaming ladder entirely client-side, with no server transcoding cost at all.
The trick is track fan-out: pass an array of configurations for a single source track and Mediabunny produces one rendition per entry.
import {
Input,
Output,
HlsOutputFormat,
BufferTarget,
BlobSource,
ALL_FORMATS,
Conversion,
} from 'mediabunny';
async function buildLadder(file: File) {
const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(file) });
const output = new Output({
format: new HlsOutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({
input,
output,
video: [
{ height: 1080, bitrate: 'high' },
{ height: 720, bitrate: 'medium' },
{ height: 480, bitrate: 'low' },
],
});
conversion.onProgress = (p) => console.log(`${Math.round(p * 100)}%`);
await conversion.execute();
}
The same HLS engine can do the reverse: stream-download a full playlist over the network and remux or transcode it into a single MP4, fully pipelined with O(1) memory. It handles VOD and live streams, encrypted segments including DRM, byte-range segments, and program-date-time for live broadcasts. A few gaps remain worth knowing — no HLS subtitle support yet, no low-latency HLS, and you cannot write encrypted segments — but for client-side packaging, this is remarkable territory to be standing on.
Driving It From React Without Freezing the UI
Mediabunny is framework-agnostic, so in a React app you typically drive it from event handlers or effects and store results in state. The one rule to live by: encoding and decoding loops are CPU-intensive, so run them in a Web Worker to keep your render thread responsive. Mediabunny is worker-friendly and plays nicely with transferable buffers and streams.
A common pattern wires a file input straight into a conversion with progress bound to component state:
function useConversion() {
const [progress, setProgress] = useState(0);
const [result, setResult] = useState<Blob | null>(null);
const convert = useCallback(async (file: File) => {
const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(file) });
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});
const conversion = await Conversion.init({ input, output });
conversion.onProgress = setProgress;
await conversion.execute();
setResult(new Blob([output.target.buffer!], { type: 'video/mp4' }));
input.dispose();
}, []);
return { progress, result, convert };
}
Notice the input.dispose() call at the end — it cancels any pending reads and frees memory, and you should always pair it with your inputs (the using declaration works too if your toolchain supports it). For very large outputs, swap BufferTarget for a StreamTarget backed by the File System Access API so you never hold the whole file in memory.
Wrapping Up
Mediabunny lands in a sweet spot that has been awkwardly empty for years: a lightweight, modern, TypeScript-first media toolkit that treats the browser as a first-class media platform rather than a place to bolt FFmpeg. By building on WebCodecs it gets hardware acceleration and tiny bundles, and by sharing one coherent Input/Output/Conversion API across every container — including HLS — it keeps the mental model small no matter how ambitious your feature gets.
It is not a universal replacement: legacy browsers and exotic codecs still belong to ffmpeg.wasm, and you should always feature-detect with canEncode/canDecode before committing to a codec. But for in-browser conversion, thumbnailing, recording, frame-accurate playback, and client-side adaptive streaming, Mediabunny is the kind of library that quietly deletes a whole server bill. Give it a file, and watch it work.