Preparing Media
Transcode video for optimal streaming with the Media Prepare SDK
Introduction
Transcode your video into a streaming-friendly format before uploading to Shelby. The Media Prepare SDK handles adaptive bitrate encoding, segmentation, and manifest generation.
Why Prepare Video?
Raw video files aren't optimized for streaming. Transcoding creates:
- Multiple quality levels - 1080p, 720p, 480p variants for adaptive bitrate
- Segmented files - Small chunks that load progressively
- Manifest files - HLS playlists that index everything for players
Prerequisites
FFmpeg Installation
Install FFmpeg 7.0 or later:
# macOS
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# Windows (via Chocolatey)
choco install ffmpegVerify your installation:
ffmpeg -version
# Should show version 7.0 or higherFFmpeg 7.0+ is required for CMAF output. The SDK will validate your installation automatically.
Transcoding Workflow
Install the SDK
npm install @shelby-protocol/media-prepareCreate a transcoding plan
Use CmafPlanBuilder to configure your transcoding settings:
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();The vodHd_1080p preset creates three quality rungs:
| Rung | Resolution | Bitrate |
|---|---|---|
| 1080p | 1920x1080 | 5 Mbps |
| 720p | 1280x720 | 3 Mbps |
| 480p | 854x480 | 1.2 Mbps |
Execute the plan
import { NodeCmafPlanExecutor } from "@shelby-protocol/media-prepare/node";
const executor = new NodeCmafPlanExecutor();
await executor.execute(plan);
console.log("Transcoding complete!");Check the output
After transcoding completes, you'll have a directory ready for upload:
The master.m3u8 file is the entry point that players use to select the appropriate quality level based on network conditions.
Configuration Options
Video Codecs
Choose the codec that fits your needs:
// H.264 - Maximum compatibility
builder.withVideoCodec({ kind: "x264", preset: "medium", profile: "high" });
// H.265/HEVC - Better compression, slower encoding
builder.withVideoCodec({ kind: "x265", preset: "medium", profile: "main" });
// Copy - No re-encoding (use when source is already optimized)
builder.withVideoCodec({ kind: "copy" });Encoding Presets
Balance speed vs quality with presets (fastest to slowest):
ultrafast → superfast → veryfast → faster → fast → medium → slow → slower → veryslow
Use medium for a good balance. Use slow or slower for final production encodes when time isn't critical.
Custom Bitrate Ladders
Build ladders for specific requirements:
const customLadder = [
{ width: 1920, height: 1080, bitrateBps: 6_000_000, name: "1080p" },
{ width: 1280, height: 720, bitrateBps: 3_000_000, name: "720p" },
{ width: 854, height: 480, bitrateBps: 1_500_000, name: "480p" },
{ width: 640, height: 360, bitrateBps: 800_000, name: "360p" },
];
builder.withVideoLadder(customLadder);Executor Options
Configure the executor for your environment:
const executor = new NodeCmafPlanExecutor({
ffprobe: true, // Auto-detect source properties (default: true)
shaka: true, // Use Shaka Packager for DASH + DRM (default: true)
verbose: true, // Stream FFmpeg logs to stdout
});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");