@shelby-protocol/media-prepare
v0.0.1
Published
FFmpeg presets and a declarative builder for CMAF + HLS: multi‑rung ladders, multi‑audio, trickplay, safe segment sizing, and clean `var_stream_map`. The library produces FFmpeg argument arrays only — no manual folder or manifest creation — and works in b
Readme
@shelby-protocol/media-prepare
FFmpeg presets and a declarative builder for CMAF + HLS: multi‑rung ladders, multi‑audio, trickplay, safe segment sizing, and clean var_stream_map. The library produces FFmpeg argument arrays only — no manual folder or manifest creation — and works in both Node (native FFmpeg) and the browser (FFmpeg.wasm).
We use CMAF + HLS for broad device/DRM support: HLS provides the playlists and CMAF provides fragmented MP4 media segments (.m4s).
Usage
Subpath exports
@shelby-protocol/media-prepare/core— platform-agnostic utilities and Zod schemas@shelby-protocol/media-prepare/node— Node helpers (ffmpeg executor, system checks, media probe)@shelby-protocol/media-prepare/browser— Browser helpers (FFmpeg.wasm executor, probe)
Key points
- Declarative: Build a typed plan, then render FFmpeg args.
- Zod validated: Inputs and the IR are runtime-validated (with helpful messages) and fully typed via
z.infer. - No file IO helpers: You execute FFmpeg with the args; it writes playlists and segments.
- Ergonomic builder:
withLadder,withAudio,withTrickplay,withSegments.
Build & Scripts
This package uses tsup for builds and Biome for lint/format.
Development
# Install dependencies
pnpm install
# Build the package
pnpm run build
# Run linting
pnpm run lint
# Auto-fix linting issues
pnpm run fmt
# Run tests (core + browser shim)
pnpm test
# Run local system tests (requires FFmpeg installed)
pnpm test:local
# Run browser tests with Playwright
pnpm test:browserTesting
pnpm test— Fast unit tests (core logic, HLS+CMAF builder), CI‑safepnpm test:browser— Browser tests using Playwright; runs FFmpeg.wasm in real browserpnpm test:local— System integration tests that execute your local FFmpeg (requires FFmpeg installed)
Local tests expect FFmpeg v7+ with encoders like libx264/libx265/libaom; they're excluded from CI.
The browser tests validate that the generated args are compatible with FFmpeg.wasm and can produce actual HLS+CMAF output.
Quick Start
Node (native FFmpeg)
import { planHlsCmaf, x264, aac, presets } from "@shelby-protocol/media-prepare/core/hls-cmaf";
import { execFfmpeg } from "@shelby-protocol/media-prepare/node";
import * as fs from "node:fs/promises";
const plan = planHlsCmaf()
.input("input.mp4")
.outputDir("out")
.withLadder(presets.vodHd_1080p)
.withVideoEncoder(x264())
.withAudio(aac(), { language: "eng", bitrateBps: 128_000, default: true })
.withSegments({ mode: "auto", maxBlobBytes: 10 * 1024 * 1024, safety: 0.9, minSeconds: 1, align: 1 })
.hlsCmaf()
.render.ffmpegArgs();
// Execute ffmpeg
await fs.rm(plan.outputDir, { recursive: true, force: true });
await execFfmpeg(plan.args, { precreate: plan.variantNames.map(name => `${plan.outputDir}/${name}`) });Browser (FFmpeg.wasm)
import { planHlsCmaf } from "@shelby-protocol/media-prepare/core/hls-cmaf";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { toBlobURL, fetchFile } from "@ffmpeg/util";
const plan = planHlsCmaf()
.input("input.mp4")
.outputDir("/out")
.withLadder([{ width: 854, height: 480, bitrateBps: 1_000_000, name: "480p" }])
.withVideoEncoder({ kind: "copy" }) // copy‑mode recommended in wasm
.withSegments({ mode: "fixed", segmentSeconds: 4 })
.hlsCmaf()
.render.ffmpegArgs();
const ffmpeg = new FFmpeg();
const baseURL = "https://unpkg.com/@ffmpeg/[email protected]/dist/esm";
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
});
await ffmpeg.writeFile("input.mp4", await fetchFile(file));
// Create output directories
for (const variant of plan.variantNames) {
await ffmpeg.createDir(`/out/${variant}`);
}
await ffmpeg.exec(plan.args);System Requirements
For Node.js usage, ensure you have:
- FFmpeg >= 7.0 with required codecs (libx264, libx265, libvpx, libaom, libfdk_aac)
- FFprobe (usually bundled with FFmpeg)
- Shaka Packager (optional)
Install on macOS:
brew install ffmpegFor Shaka Packager, download from https://github.com/shaka-project/shaka-packager. Get it from the Releases page https://github.com/shaka-project/shaka-packager/releases
The SystemChecker utility will validate your system setup and provide installation guidance for your platform.
WebAssembly vs Native FFmpeg: Understanding the Differences
Why FFmpeg.wasm Packages Don't Work in Node.js
Common Misconception: "WebAssembly runs everywhere, so @ffmpeg/ffmpeg should work in Node.js"
Reality: While WebAssembly itself can run in Node.js, the @ffmpeg/ffmpeg and @ffmpeg/core packages are specifically designed for browser environments and will fail in Node.js.
@ffmpeg/ffmpeg Limitations in Node.js
// ❌ This fails in Node.js
import { FFmpeg } from '@ffmpeg/ffmpeg';
const ffmpeg = new FFmpeg();
await ffmpeg.load(); // Error: "does not support nodejs"Why it fails:
- Built around Web Workers for non-blocking browser processing
- Uses browser-specific APIs like
SharedArrayBufferandMessagePort - Intentionally blocks Node.js execution with runtime checks
@ffmpeg/core Limitations in Node.js
// ❌ This also fails in Node.js
import createFFmpegCore from '@ffmpeg/core';
const Module = await createFFmpegCore({}); // ReferenceError: self is not definedWhy it fails:
- References browser globals like
self,window,document - Expects browser's fetch API and URL handling
- Designed for browser's WebAssembly loading mechanisms
Recommended Approaches by Environment
| Environment | Recommended Solution | Performance | Use Case | |-------------|---------------------|-------------|-----------| | Node.js Server | Native FFmpeg (args + spawn) | ⚡ Excellent | Server-side processing | | Browser Client | @ffmpeg/ffmpeg (WebAssembly) | ✅ Good | Client-side processing | | Edge/Serverless | Native FFmpeg binary | ⚡ Excellent | Serverless functions |
Node.js: Native FFmpeg
import { planHlsCmaf, x264, aac } from "@shelby-protocol/media-prepare/core/hls-cmaf";
import { execFfmpeg } from "@shelby-protocol/media-prepare/node";
import * as fs from "node:fs/promises";
const plan = planHlsCmaf()
.input("/path/to/video.mp4")
.outputDir("/path/to/output")
.withLadder([
{ width: 1920, height: 1080, bitrateBps: 5_000_000, name: "1080p" },
{ width: 1280, height: 720, bitrateBps: 3_000_000, name: "720p" },
])
.withVideoEncoder(x264())
.withAudio(aac(), { language: "eng", bitrateBps: 128_000, default: true })
.withSegments({ mode: "auto", maxBlobBytes: 10 * 1024 * 1024, safety: 0.9, minSeconds: 1, align: 1 })
.hlsCmaf()
.render.ffmpegArgs();
await fs.rm(plan.outputDir, { recursive: true, force: true });
await execFfmpeg(plan.args, { precreate: plan.variantNames.map(name => `${plan.outputDir}/${name}`) });Advantages:
- ⚡ Performance: Native binaries are 3-10x faster than WebAssembly
- 🛠️ Full Feature Set: Access to all FFmpeg codecs and filters
- 💾 Memory Efficiency: Better memory management for large files
- 🔧 System Integration: Hardware acceleration support
Browser: WebAssembly FFmpeg
import { planHlsCmaf } from "@shelby-protocol/media-prepare/core/hls-cmaf";
import { FFmpeg } from "@ffmpeg/ffmpeg"; // peer dependency
import { fetchFile } from "@ffmpeg/util";
const plan = planHlsCmaf()
.input("input.mp4")
.outputDir("/out")
.withLadder([{ width: 854, height: 480, bitrateBps: 1_000_000, name: "480p" }])
.withVideoEncoder({ kind: "copy" }) // copy mode recommended in wasm
.withSegments({ mode: "fixed", segmentSeconds: 4 })
.hlsCmaf()
.render.ffmpegArgs();
const ffmpeg = new FFmpeg();
await ffmpeg.load();
await ffmpeg.writeFile("input.mp4", await fetchFile(file));
await ffmpeg.exec(plan.args);Advantages:
- 🌐 Client-Side: No server upload required
- 🔒 Privacy: Files never leave user's device
- 📱 Universal: Works in any modern browser
- ⚡ Scalable: Offloads processing from servers
Limitations:
- 🐌 Performance: Slower than native FFmpeg
- 💾 Memory: Limited by browser WebAssembly constraints
- 🎛️ Codecs: Subset of full FFmpeg codec support
- 📁 File Size: Best for smaller files (<100MB)
Peer Dependencies
For browser WebAssembly integration, install the optional peer dependencies:
# Browser WebAssembly FFmpeg
npm install @ffmpeg/ffmpeg @ffmpeg/util @ffmpeg/coreNote: These are optional peer dependencies since Node.js applications use native FFmpeg binaries instead.
