npm package: @shelby-protocol/media-prepare
Description: 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
Latest version: 0.0.1

# @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

```bash
# 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:browser
```

### Testing

- `pnpm test` — Fast unit tests (core logic, HLS+CMAF builder), CI‑safe
- `pnpm test:browser` — Browser tests using Playwright; runs FFmpeg.wasm in real browser
- `pnpm 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)

```ts
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)

```ts
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/core@0.12.9/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:
```bash
brew install ffmpeg
```

For 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

```javascript
// ❌ 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 `SharedArrayBuffer` and `MessagePort`
- Intentionally blocks Node.js execution with runtime checks

#### @ffmpeg/core Limitations in Node.js

```javascript
// ❌ This also fails in Node.js
import createFFmpegCore from '@ffmpeg/core';
const Module = await createFFmpegCore({}); // ReferenceError: self is not defined
```

**Why 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

```ts
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

```ts
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:

```bash
# Browser WebAssembly FFmpeg
npm install @ffmpeg/ffmpeg @ffmpeg/util @ffmpeg/core
```

Note: These are optional peer dependencies since Node.js applications use native FFmpeg binaries instead.