Universal compression and archiving for browsers and Node.js.
One API. Three engines. Isomorphic ZIP & Media processing.
omni-compress is a high-performance, isomorphic compression library. It automatically routes media compression (images/audio/video) to the fastest available engine at runtime — native Web APIs, FFmpeg WebAssembly, or OS-level binaries — and provides built-in ZIP archiving for any file type.
| Problem | How omni-compress solves it |
|---|---|
| Browser and Node need different code paths | Single Isomorphic API — environment detection is automatic |
| Archiving or batching needs separate libs | Built-in ZIP archive() and archiveStream() for any file type |
| FFmpeg Wasm is heavy (~30 MB) and slow to load | Uses native OffscreenCanvas/WebCodecs for standard formats (0 KB Wasm) |
| Media processing freezes the UI | ALL browser work runs in Web Workers with zero-copy Transferable transfers |
| FFmpeg Wasm is too slow | Multi-threading support via @ffmpeg/core-mt (requires COOP/COEP) |
| Wasm memory leaks crash browser tabs | FFmpeg singleton with idle-timeout auto-termination; VFS cleanup per-operation |
| Large files crash the browser silently | FileTooLargeError thrown before loading files > 250 MB into Wasm |
| Fast Path fails on unsupported browsers | Automatic fallback from Fast Path to Heavy Path on any runtime error |
| No way to cancel a running compression | Full AbortSignal support — terminates Wasm/child process on abort |
| Silent failures when file extension ≠ content | detectFormat() reads magic bytes to identify the real format |
npm install omni-compress
# bun
bun add omni-compress
# pnpm
pnpm add omni-compress
# yarn
yarn add omni-compress
Previously published as
@dharanish/omni-compress(deprecated — please migrate toomni-compress).
Node.js users: For the Node adapter to work, install
ffmpeg-static(bundled as an optional dependency) or ensureffmpegis available on your systemPATH.
AVIF in the browser: AVIF encoding uses
@jsquash/avif(standalone libaom-av1 Wasm from Google's Squoosh, 1.1 MB gzipped). It is bundled automatically -- no extra install or configuration needed. No SharedArrayBuffer or special headers required.
import { compressImage, compressAudio, compressVideo } from 'omni-compress';
// Image → WebP
const { blob, ratio } = await compressImage(imageFile, {
format: 'webp',
quality: 0.8,
});
console.log(`Saved ${Math.round((1 - ratio) * 100)}%`);
// Audio → Opus (with cancellation)
const controller = new AbortController();
const { blob: audio } = await compressAudio(audioFile, {
format: 'opus',
bitrate: '96k',
onProgress: (p) => console.log(`${p}%`),
signal: controller.signal,
});
// Video → MP4
const { blob: video } = await compressVideo(videoFile, {
format: 'mp4',
bitrate: '1M',
});
// Archive multiple files into a ZIP
import { archive } from 'omni-compress';
const { blob: zip } = await archive([
{ name: 'photo.webp', data: blob },
{ name: 'audio.opus', data: audio },
{ name: 'video.mp4', data: video },
]);
compressImage(input, options): Promise<CompressResult>Compresses an image using the fastest available engine (OffscreenCanvas fast path, FFmpeg Wasm heavy path, or native ffmpeg on Node).
import { compressImage } from 'omni-compress';
const result = await compressImage(file, {
format: 'webp',
quality: 0.8,
maxWidth: 1920,
signal: controller.signal,
});
// result.blob → the compressed Blob
// result.ratio → e.g. 0.62 (38% smaller)
// result.originalSize / result.compressedSize → bytes
ImageOptions
| Property | Type | Default | Description |
|---|---|---|---|
format |
'webp' | 'avif' | 'jpeg' | 'png' |
— | Target output format |
quality |
number |
0.8 |
Lossy quality 0.0 – 1.0 |
maxWidth |
number |
— | Max output width in px (aspect ratio preserved) |
maxHeight |
number |
— | Max output height in px (aspect ratio preserved) |
preserveMetadata |
boolean |
false |
Keep EXIF data in the output |
useWorker |
boolean |
Auto | Force Web Worker (true) or Main Thread (false) |
onProgress |
(percent: number) => void |
— | Progress callback 0 – 100 |
signal |
AbortSignal |
— | Cancel the operation — throws AbortError when signalled |
compressAudio(input, options): Promise<CompressResult>Compresses an audio file via WebCodecs (fast path) or FFmpeg Wasm (heavy path).
AudioOptions
| Property | Type | Default | Description |
|---|---|---|---|
format |
'opus' | 'mp3' | 'flac' | 'wav' | 'aac' |
— | Target output format |
bitrate |
string |
'128k' |
Target bitrate, e.g. '96k', '192k' |
channels |
1 | 2 |
Auto | Output channel count (1 = mono, 2 = stereo) |
sampleRate |
number |
Auto | Output sample rate in Hz, e.g. 48000 |
preserveMetadata |
boolean |
false |
Keep audio tags in the output |
useWorker |
boolean |
Auto | Force Web Worker (true) or Main Thread (false) |
onProgress |
(percent: number) => void |
— | Progress callback 0 – 100 |
signal |
AbortSignal |
— | Cancel the operation — throws AbortError when signalled |
compressVideo(input, options): Promise<CompressResult>Compresses a video file via WebCodecs (fast path foundation) or FFmpeg Wasm (heavy path).
VideoOptions
| Property | Type | Default | Description |
|---|---|---|---|
format |
'mp4' | 'webm' |
— | Target output format |
bitrate |
string |
'1M' |
Target video bitrate, e.g. '500k', '2M' |
fps |
number |
Auto | Output frame rate |
maxWidth |
number |
— | Max output width in px |
maxHeight |
number |
— | Max output height in px |
preserveMetadata |
boolean |
false |
Keep metadata in the output |
useWorker |
boolean |
Auto | Force Web Worker (true) or Main Thread (false) |
onProgress |
(percent: number) => void |
— | Progress callback 0 – 100 |
signal |
AbortSignal |
— | Cancel the operation — throws AbortError when signalled |
CompressResultcompressImage, compressAudio, and compressVideo return a CompressResult:
interface CompressResult {
blob: Blob; // The compressed output
originalSize: number; // Input size in bytes
compressedSize: number; // Output size in bytes
ratio: number; // compressedSize / originalSize (< 1.0 = smaller)
format: string; // Target format used (e.g. 'webp')
}
archive(entries, options?): Promise<ArchiveResult>Compresses an array of files into a ZIP archive. Works identically in browser and Node.js.
import { archive } from 'omni-compress';
const result = await archive(
[
{ name: 'images/photo.webp', data: imageBlob },
{ name: 'audio/track.opus', data: audioBlob },
],
{ level: 6, signal: controller.signal },
);
// result.blob → the ZIP Blob (application/zip)
// result.ratio → compression ratio
archiveStream(entries, options?): ReadableStream<Uint8Array>Streaming ZIP output — prefer this for large archives where you want to start sending bytes before all entries are compressed.
import { archiveStream } from 'omni-compress';
const stream = archiveStream(entries, { level: 6 });
const response = new Response(stream, {
headers: { 'Content-Type': 'application/zip' },
});
ArchiveOptions
| Property | Type | Default | Description |
|---|---|---|---|
format |
'zip' |
'zip' |
Archive format (only ZIP supported currently) |
level |
0 – 9 |
6 |
fflate deflate level (0 = store, 9 = max compression) |
onProgress |
(percent: number) => void |
— | Progress callback 0 – 100 |
signal |
AbortSignal |
— | Cancel — throws AbortError |
detectFormat(buffer): string | nullReads the first 16 bytes of a buffer and returns the file's actual format from its magic bytes — not its extension.
import { detectFormat } from 'omni-compress';
const buffer = await file.arrayBuffer();
const format = detectFormat(buffer);
// e.g. 'webp', 'jpeg', 'flac', 'ogg', null (unknown)
Supported signatures: jpeg, png, gif, webp, wav, avif, flac, ogg, mp3, aac.
OmniCompressor.process()is deprecated as of v2.0. It will continue to work until v3.0 but returns a rawBlobinstead of the richerCompressResult. Migrate tocompressImage()orcompressAudio().
import { OmniCompressor } from 'omni-compress';
/** @deprecated Use compressImage() or compressAudio() instead */
const blob = await OmniCompressor.process(file, {
type: 'image',
format: 'webp',
quality: 0.8,
});
Pass an AbortSignal to any compression or archive call to cancel it mid-flight:
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const result = await compressImage(file, {
format: 'avif',
signal: controller.signal,
});
} catch (err) {
if (err instanceof AbortError) {
console.log('Compression was cancelled');
}
}
When a browser compression is cancelled, the underlying Web Worker is terminated (killing FFmpeg Wasm mid-run) and a fresh worker is created for the next call. On Node.js, the ffmpeg child process receives SIGTERM.
All library errors extend OmniCompressError and carry a machine-readable code field:
import {
OmniCompressError,
FileTooLargeError,
FormatNotSupportedError,
InvalidOptionsError,
AbortError,
EncoderError,
} from 'omni-compress';
try {
await compressImage(file, { format: 'webp' });
} catch (err) {
if (err instanceof FileTooLargeError) {
console.log(err.fileSize, err.maxSize); // bytes
} else if (err instanceof AbortError) {
console.log('Cancelled'); // err.code === 'ABORTED'
} else if (err instanceof FormatNotSupportedError) {
console.log(err.format); // e.g. 'hevc'
}
}
| Error Class | Code | When Thrown |
|---|---|---|
OmniCompressError |
— | Base class for all library errors |
FileTooLargeError |
FILE_TOO_LARGE |
Input exceeds 250 MB (browser) — prevents Wasm OOM |
FormatNotSupportedError |
FORMAT_NOT_SUPPORTED |
Requested format is not valid for the given media type |
InvalidOptionsError |
INVALID_OPTIONS |
Options object is missing required fields or contains invalid values |
AbortError |
ABORTED |
AbortSignal fired before or during processing |
EncoderError |
ENCODER_FAILED |
FFmpeg or fflate encoder threw — wraps the underlying cause |
| Format | Fast Path (OffscreenCanvas) | Heavy Path (FFmpeg Wasm) | Node (OS binary) |
|---|---|---|---|
| WebP | ✅ | ✅ libwebp | ✅ ffmpeg |
| AVIF | ❌ (not supported by OffscreenCanvas) | ✅ @jsquash/avif (libaom-av1) | ✅ ffmpeg |
| JPEG | ✅ | ✅ | ✅ ffmpeg |
| PNG | ✅ | ✅ | ✅ ffmpeg |
| HEIC | — | ✅ | ✅ ffmpeg |
| TIFF | — | ✅ | ✅ ffmpeg |
| Format | Fast Path (WebCodecs) | Heavy Path (FFmpeg Wasm) | Node (OS binary) |
|---|---|---|---|
| MP3 | — | ✅ libmp3lame | ✅ ffmpeg |
| Opus/OGG | ✅ (Opus) | ✅ libopus | ✅ ffmpeg |
| FLAC | — | ✅ flac | ✅ ffmpeg |
| WAV | — | ✅ | ✅ ffmpeg |
| AAC | ✅ | ✅ | ✅ ffmpeg |
| Format | Heavy Path (FFmpeg Wasm) | Node (OS binary) |
|---|---|---|
| MP4 | ✅ libx264 | ✅ ffmpeg |
| WebM | ✅ libvpx-vp9 | ✅ ffmpeg |
compressImage() / compressAudio() / compressVideo()
│
▼
┌─────────┐
│ Router │ ← Evaluates runtime + format + size
└────┬────┘
│
┌────┴────────────────────────────┐
│ │ │
▼ ▼ ▼
Fast Path Heavy Path Node Adapter
(Native) (FFmpeg Wasm) (child_process)
│ │ │
OffscreenCanvas @ffmpeg/ffmpeg OS ffmpeg binary
WebCodecs (A/V) Multi-threaded Via ffmpeg-static
│ │ │
└────────────────┴────────────────┘
│
┌───────────┴───────────┐
│ │
Main Thread Path Web Worker Path
(High Speed) (Isolation)
Files < 4MB Files > 4MB
Zero-latency Non-blocking
omni-compress includes a smart switching engine that dynamically chooses between the Main Thread and Web Workers:
postMessage communication latency (~50-150ms), matching the performance of legacy main-thread-only libraries like compressorjs.
API Docs — full TypeDoc-generated reference for all functions, options, types, and error classes. Auto-regenerated on every release.
Why omni-compress — feature matrix vs 7 competitors, quality benchmarks, architecture overview.
| From | Guide |
|---|---|
browser-image-compression |
Migrate from browser-image-compression |
compressorjs |
Migrate from compressorjs |
jimp |
Migrate from jimp |
lamejs |
Migrate from lamejs |
heic2any |
Migrate from heic2any |
@ffmpeg/ffmpeg |
Migrate from @ffmpeg/ffmpeg |
The Live Demo features a Neo-Brutalist "Laboratory" UI with 25 distinct persona-based themes (Shakespeare, Picasso, Aryabhata, etc.) supporting multiple languages with culturally relevant quotes and accurate technical terminology.
We welcome contributions! Please see the Contributing Guide for setup instructions and guidelines.
MIT © Dharanish V