Media Prepare

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 ffmpeg

Verify Installation

ffmpeg -version
# Should show version 7.0 or higher

ffmpeg -encoders | grep libx264
# Should show libx264 encoder

The 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-prepare

Create a transcoding plan

transcode.ts
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

transcode.ts
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,
});
OptionDefaultDescription
shakatrueUse Shaka Packager for CMAF packaging (enables DASH + Widevine). Set to false to fall back to FFmpeg packaging (HLS-only, no DRM).
ffprobetrueRun FFprobe to infer frame rate and stream metadata automatically. Disable if FFprobe is unavailable.
verbosefalseStream 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

transcode.ts
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");