@bsky.app/video-compressor
v0.2.0
Published
Hardware-accelerated video compression for React Native and Expo. iOS + Android use VideoToolbox / MediaCodec; web uses WebCodecs via mediabunny.
Readme
@bsky.app/video-compressor
Video compression for React Native and Expo across iOS, Android, and web.
iOS uses AVAssetReader/AVAssetWriter + VideoToolbox. Android uses
MediaCodec + MediaMuxer. Web uses WebCodecs through
mediabunny.
Installation
npm install @bsky.app/video-compressorUsage
import {compress, probe} from '@bsky.app/video-compressor'
const meta = await probe(uri)
const controller = new AbortController()
const result = await compress(
uri,
{
targetBitrate: 3_000_000, // 0 = let the encoder pick
maxSize: 1920, // long-edge cap in px
codec: 'auto', // 'auto' | 'h264' | 'hevc'
frameRateCap: 30,
},
{
onProgress: p => console.log(p), // 0..1
signal: controller.signal,
},
)probe(uri) returns a VideoMetadata object — width, height, duration,
bitrate, codec, frame rate, rotation, etc. compress returns the output
uri, file size, mimeType, dimensions, duration, and the codec actually
used.
Cancellation is wired through AbortSignal. Calling abort() mid-run cancels
the underlying native session and rejects the promise with an AbortError.
Platform behavior
| Platform | Backend | | -------- | -------------------------------------------------------------------------------------------------------------------------- | | iOS | AVAssetReader/Writer + VideoToolbox | | Android | MediaCodec + MediaMuxer (GL color-space convert) | | Web | WebCodecs via mediabunny. Falls back avc → hevc → vp9 → vp8 by browser support; container switches MP4 ↔ WebM accordingly. |
compress() can also return the input unchanged. Set passthroughGif: false
to force-encode GIFs (default skips them), or set
passthroughBelowBytes: 25_000_000 to skip files under a size threshold. On
web, missing WebCodecs is also a passthrough. The result's passthroughReason
tells you which path it took.
Input format support
Covered by the fixture corpus in example/assets/fixtures (see
example/scripts/generate-fixtures.sh). Both example apps have a "Run all
fixtures" button that runs the full corpus through compress and shows
✅/❌ per fixture, so support claims can be re-verified after any change.
| Input | iOS | Android | Web | | ------------------------------ | ------------------ | ------------------ | -------------------- | | H.264 MP4 (incl. 4K, portrait) | ✅ | ✅ | ✅ | | HEVC MOV | ✅ | ✅ | ✅ ¹ | | HEVC HDR (HLG) MOV | ✅ tone-mapped SDR | ✅ tone-mapped SDR | ✅ ¹ tone-mapped SDR | | VP9 WebM | ❌ | ✅ | ✅ | | H.264 AVI | ❌ | ❌ | ❌ |
¹ Needs browser HEVC decode support (Safari; Chrome with hardware decode).
Example app
A small Expo app demonstrating the module — and benchmarking it against
react-native-compressor
— lives in example/. See example/README.md for run instructions and the
optional bench/compare.sh ffprobe-based comparison script. A Vite app
exercising the web backend lives in web-example/.
