If you’ve ever tried to compress a video online, you’ve probably hit at least one of these:
- You don’t want to upload private footage to a random server
- Uploading a 500MB file on hotel Wi‑Fi is a non-starter
- “Free” tools add watermarks or hide the download behind paywalls
So I built a local-first video compressor: it runs entirely in the browser using FFmpeg.wasm. Your file never leaves your device.
Try it: https://compressvideo.net/
Source (demo repo): https://github.com/xzmhxdxh/compressvideo-wasm
What “100% local” really means
The UX is still “online” (a web page), but the video bytes stay local:
- You pick a video file
- The browser loads the FFmpeg WebAssembly core
- FFmpeg runs in your tab and writes the output into its in-memory filesystem
- You download the compressed MP4 from a Blob URL
No upload step, no server storage, no post-processing watermarking.
The minimal demo (Vite + React)
I kept the repo intentionally small: one page, one component, and a safe default preset.
- Video codec: H.264 (MP4 output)
- Mode: CRF-based compression (default CRF 28)
- Resolution cap: 1080p (scale=-2:1080)
- Audio: AAC 128k
If you want the full product UI (modes, advanced settings, i18n, etc.), use the hosted site. This repo focuses on the core engine loop.
Loading FFmpeg.wasm in a way that actually works
One common gotcha: you usually need to load the core from a URL, and many setups require converting it into a Blob URL first.
In this demo, I load the core directly from the official @ffmpeg/core package on unpkg:
TypeScript
const CORE_BASE = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd'
const coreURL = await toBlobURL(`${CORE_BASE}/ffmpeg-core.js`, 'text/javascript')
const wasmURL = await toBlobURL(`${CORE_BASE}/ffmpeg-core.wasm`, 'application/wasm')
await ffmpeg.load({ coreURL, wasmURL })
The compression loop
At a high level, local compression is just four steps:
Write the input file into FFmpeg FS
Run FFmpeg
Read the output file from FFmpeg FS
Create a downloadable Blob URL
TypeScript
await ffmpeg.writeFile(inputName, await fetchFile(file))
await ffmpeg.exec([
'-i', inputName,
'-vcodec', 'libx264',
'-preset', 'fast',
'-crf', '28',
'-vf', 'scale=-2:1080',
'-c:a', 'aac',
'-b:a', '128k',
'output.mp4',
])
const data = await ffmpeg.readFile('output.mp4')
const bytes = data as Uint8Array
const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
const url = URL.createObjectURL(new Blob([buf], { type: 'video/mp4' }))
Progress UI (and why it can lie)
FFmpeg.wasm provides an encoding progress callback:
TypeScript
ffmpeg.on('progress', ({ progress }) => {
setRunProgress(Math.round((progress ?? 0) * 100))
})
It’s useful, but you should treat it as “best effort”:
Progress may jump
Some steps (loading core, writing FS, reading FS) aren’t part of encoding progress
In the demo I show two independent progress bars: “Engine loading” and “Encoding”.
The real-world limits (what breaks)
1) Browser memory limits
Local compression means your browser must hold the input file (and often the output) in memory. Very large files can fail even on desktop browsers, and mobile browsers are much tighter.
2) iOS is the hardest environment
Safari on iPhone/iPad tends to kill heavy WebAssembly workloads earlier. If you need to reliably process large videos on iOS, a cloud fallback is often necessary.
3) CPU usage and battery
FFmpeg encoding is CPU-heavy. This is expected. For long videos, cloud processing can be significantly faster and will save battery.
When to choose local vs cloud
- Choose local if privacy matters, or your file is moderate in size, or uploading is slow.
- Choose cloud if you need speed, you’re on mobile, or the file is huge. My default is “local-first”, with a cloud fallback only when it’s clearly needed.
Try it + source
- Live product: https://compressvideo.net/
- Source code (demo repo): https://github.com/xzmhxdxh/compressvideo-wasm
If you want, tell me your target use case (email attachments, social media uploads, archiving, etc.) and I’ll share an FFmpeg argument preset that matches it.
Top comments (0)