Drop in a video or audio file, choose what you want out, and Mediabunny rewraps or transcodes it right here in your browser. Then read the source below — every file is open, and nothing is ever uploaded.
Drop or pick several files to batch-convert or join them. URLs stream via Mediabunny's UrlSource — must be a direct file link on a CORS-enabled host (not a YouTube/streaming page).
Convert a file above and these fill with real numbers from your own device — processing time, throughput, and how many times faster than real-time it ran. No synthetic benchmark.
| Mediabunny | ffmpeg.wasm | Server transcode | MediaRecorder | |
|---|---|---|---|---|
| Where it runs | Your browser · WebCodecs | Your browser · wasm | Your server | Your browser |
| Download to start | ~156 KB gz ¹ | ~25–31 MB wasm ² | — | 0 · built-in |
| Speed | HW-accelerated; instant remux | Software codecs, slower | Fast, but + up/download | Real-time only (1×) |
| Uploads your file? | No | No | Yes | No |
| COOP/COEP headers | Not needed | Needed for threads ² | — | Not needed |
| Cost | Free | Free | $ server time | Free |
| Works offline | Yes | Yes | No | Yes |
| Format control | Full API | Full · CLI string | Full | Minimal |
¹ Full library, gzipped, before tree-shaking — measured from the installed package; a convert-only import is smaller. ² ffmpeg.wasm core size & cross-origin-isolation requirement per its project docs. Remaining cells are qualitative.
When the source codec already fits the target container, Mediabunny copies the packets straight through — instant and lossless, no re-encode. Otherwise it transcodes with WebCodecs.
Everything runs on-device — turn Wi-Fi off and it still converts. Nothing leaves the tab.
1import {2 Input, Output, Conversion, ALL_FORMATS,3 BlobSource, BufferTarget,4 Mp4OutputFormat, QUALITY_HIGH,5} from 'mediabunny';6import { videoProcess } from './transforms';78// One conversion with the full option set — all in the browser.9export async function convert(file, opts = {}) {10 const input = new Input({ source: new BlobSource(file), formats: ALL_FORMATS });11 const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget() });1213 const conversion = await Conversion.init({14 input, output,15 trim: opts.trim, // { start, end } in seconds16 video: {17 codec: 'avc',18 bitrate: opts.bitrate ?? QUALITY_HIGH, // a number (target size) or QUALITY_*19 width: opts.width, height: opts.height, fit: opts.fit,20 rotate: opts.rotate,21 crop: opts.crop, // { left, top, width, height }22 frameRate: opts.frameRate,23 alpha: opts.keepAlpha ? 'keep' : 'discard',24 process: videoProcess(opts), // filters / watermark / speed25 },26 audio: opts.mute27 ? { discard: true }28 : { codec: 'aac', bitrate: opts.bitrate ?? QUALITY_HIGH,29 numberOfChannels: opts.channels, sampleRate: opts.sampleRate },30 tags: opts.stripMetadata ? {} : undefined, // {} strips, undefined copies31 });3233 if (!conversion.isValid) {34 throw new Error(conversion.discardedTracks.map((t) => t.reason).join(', '));35 }36 conversion.onProgress = opts.onProgress; // 0 → 137 await conversion.execute();3839 // The finished file — it never left the device.40 return new Blob([output.target.buffer], { type: 'video/mp4' });41}