Node.js Guide
Transcoding with native FFmpeg in Node.js
This guide covers using @shelby-protocol/media-prepare with native FFmpeg in Node.js.
Prerequisites
FFmpeg Installation
Install FFmpeg 7.0 or later with required codecs:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# Windows (via Chocolatey)
choco install ffmpegVerify Installation
ffmpeg -version
# Should show version 7.0 or higher
ffmpeg -encoders | grep libx264
# Should show libx264 encoderThe library requires FFmpeg 7.0+ for CMAF/HLS output. Use
SystemChecker.validateSystemOrThrow() to verify your installation meets requirements.
Basic Usage
Install the package
npm install @shelby-protocol/media-prepareCreate a transcoding plan
import {
CmafPlanBuilder,
videoLadderPresets,
} from "@shelby-protocol/media-prepare/core";
const plan = new CmafPlanBuilder()
.withInput("input.mp4")
.withOutputDir("output")
.withVideoLadder(videoLadderPresets.vodHd_1080p)
.withVideoCodec({ kind: "x264", preset: "medium" })
.addAudioTrack({
language: "eng",
bitrateBps: 128_000,
default: true,
})
.withSegmentDuration(4)
.withHlsOutput()
.build();Execute the plan
import { NodeCmafPlanExecutor } from "@shelby-protocol/media-prepare/node";
const executor = new NodeCmafPlanExecutor();
await executor.execute(plan);
console.log("Transcoding complete!");Executor Options
The NodeCmafPlanExecutor accepts configuration options:
const executor = new NodeCmafPlanExecutor({
shaka: true, // Use Shaka packager (HLS + DASH + Widevine)
ffprobe: true, // Auto-detect source properties
verbose: false,
});| Option | Default | Description |
|---|---|---|
shaka | true | Use Shaka Packager for CMAF packaging (enables DASH + Widevine). Set to false to fall back to FFmpeg packaging (HLS-only, no DRM). |
ffprobe | true | Run FFprobe to infer frame rate and stream metadata automatically. Disable if FFprobe is unavailable. |
verbose | false | Stream FFmpeg/Shaka logs to stdout for easier debugging. |
FFprobe Integration
When ffprobe is enabled, the executor automatically detects:
- Frame rate (for accurate segment calculation)
- Duration
- Codec information
- Stream metadata
const executor = new NodeCmafPlanExecutor({ ffprobe: true });
// Frame rate hint is automatically populated from source
await executor.execute(plan);ffprobe is enabled by default. Disable it only if FFprobe is not installed or you want to provide withFrameRateHint() manually.
Shaka Packager
For DRM support (Widevine), keep the Shaka packager enabled (the default):
import {
CmafPlanBuilder,
testWidevineEncryptionOptions,
} from "@shelby-protocol/media-prepare/core";
import { NodeCmafPlanExecutor } from "@shelby-protocol/media-prepare/node";
const executor = new NodeCmafPlanExecutor({ shaka: true });
const plan = new CmafPlanBuilder()
// ... other config
.withWidevineEncryption(testWidevineEncryptionOptions)
.build();
await executor.execute(plan);Set shaka: false if Shaka Packager is not installed or if you explicitly want to use the FFmpeg packager (HLS-only, no DRM/DASH output).
Shaka Packager must be installed separately. See the Shaka Packager documentation for installation instructions.
System Checker
Verify your system meets requirements before transcoding:
import { SystemChecker } from "@shelby-protocol/media-prepare/node";
const requirements = await SystemChecker.checkRequirements();
console.log(`FFmpeg version: ${requirements.ffmpegVersion ?? "unknown"}`);
console.log(`Has libx264: ${requirements.codecs.libx264}`);
console.log(`Has libx265: ${requirements.codecs.libx265}`);
if (!requirements.ffmpegVersionValid) {
console.error("FFmpeg 7.0+ is required for CMAF output.");
}
// Throw helpful errors when requirements aren't met
await SystemChecker.validateSystemOrThrow();Monitoring Progress
The Node executor does not currently emit structured progress events. Enable the verbose
option to stream FFmpeg/Shaka logs directly to stdout:
const executor = new NodeCmafPlanExecutor({ verbose: true });
await executor.execute(plan);If you need granular progress information, parse the FFmpeg logs in your own wrapper process.
Error Handling
Handle common errors:
import { NodeCmafPlanExecutor } from "@shelby-protocol/media-prepare/node";
const executor = new NodeCmafPlanExecutor();
try {
await executor.execute(plan);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ENOENT") {
console.error("FFmpeg (or FFprobe/Shaka) is not installed or not in PATH");
return;
}
console.error("Transcoding failed:", err.message);
throw err;
}Complete Example
import * as fs from "node:fs/promises";
import {
CmafPlanBuilder,
videoLadderPresets,
} from "@shelby-protocol/media-prepare/core";
import { NodeCmafPlanExecutor } from "@shelby-protocol/media-prepare/node";
async function transcodeVideo(inputPath: string, outputDir: string) {
// Clean output directory
await fs.rm(outputDir, { recursive: true, force: true });
// Build plan
const plan = new CmafPlanBuilder()
.withInput(inputPath)
.withOutputDir(outputDir)
.withVideoLadder(videoLadderPresets.vodHd_1080p)
.withVideoCodec({ kind: "x264", preset: "medium", profile: "high" })
.addAudioTrack({
language: "eng",
bitrateBps: 128_000,
default: true,
})
.withSegmentDuration(4)
.withHlsOutput()
.force()
.build();
// Execute
const executor = new NodeCmafPlanExecutor({
ffprobe: true,
verbose: true,
});
console.log("Starting transcoding...");
await executor.execute(plan);
console.log("Transcoding complete!");
// List output files
const files = await fs.readdir(outputDir, { recursive: true });
console.log("Output files:", files);
}
transcodeVideo("input.mp4", "output");