EncodingCandidate(640, 360, 450),
EncodingCandidate(640, 360, 800),
EncodingCandidate(1280, 720, 1600),
EncodingCandidate(1280, 720, 2800),
EncodingCandidate(1920, 1080, 3800),
EncodingCandidate(1920, 1080, 5200),
]
### 3. Transcoding Execution
Production probe passes must handle failures gracefully and avoid blocking the main thread. We use `subprocess` with explicit stream management and concurrent execution.
```python
import subprocess
import logging
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
logger = logging.getLogger(__name__)
def execute_transcode_pass(source: Path, candidate: EncodingCandidate, output_dir: Path) -> Path:
destination = output_dir / f"{candidate.identifier}.mp4"
if destination.exists():
return destination
cmd = [
"ffmpeg", "-y", "-i", str(source),
"-vf", f"scale={candidate.resolution_w}:{candidate.resolution_h}",
"-c:v", "libx264",
"-preset", "medium",
"-b:v", f"{candidate.target_bitrate_kbps}k",
"-maxrate", f"{candidate.target_bitrate_kbps}k",
"-bufsize", f"{candidate.target_bitrate_kbps * 2}k",
"-c:a", "aac", "-b:a", "96k",
"-movflags", "+faststart",
str(destination)
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
logger.info(f"Transcoded {candidate.identifier}")
except subprocess.CalledProcessError as exc:
logger.error(f"Transcode failed for {candidate.identifier}: {exc}")
raise
return destination
4. Perceptual Scoring
VMAF requires both the encoded stream and the reference source to share identical dimensions before comparison. We scale both to 1080p, extract the JSON log, and parse the pooled mean score.
import json
def measure_perceptual_quality(reference: Path, encoded: Path) -> float:
log_file = encoded.with_suffix(".vmaf.json")
filter_chain = (
f"[0:v]scale=1920:1080:flags=bicubic[main];"
f"[1:v]scale=1920:1080:flags=bicubic[ref];"
f"[main][ref]libvmaf=log_path={log_file}:log_fmt=json"
)
cmd = [
"ffmpeg", "-i", str(encoded), "-i", str(reference),
"-lavfi", filter_chain,
"-f", "null", "-"
]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with open(log_file, "r") as fh:
metrics = json.load(fh)
return float(metrics["pooled_metrics"]["vmaf"]["mean"])
5. Ladder Optimization
The convex hull algorithm identifies the Pareto frontier in the bitrate-VMAF space. Points that require higher bitrate for equal or lower quality are discarded. We enforce a minimum quality floor to prevent shipping unusable rungs.
MINIMUM_VMAF_THRESHOLD = 70.0
def extract_pareto_front(scored_points: list[tuple[int, float]]) -> list[tuple[int, float]]:
sorted_by_bitrate = sorted(scored_points, key=lambda x: x[0])
optimal_rungs = []
for bitrate, vmaf in sorted_by_bitrate:
if vmaf < MINIMUM_VMAF_THRESHOLD:
continue
while optimal_rungs and optimal_rungs[-1][1] <= vmaf:
optimal_rungs.pop()
optimal_rungs.append((bitrate, vmaf))
return optimal_rungs
6. Orchestration & Manifest Generation
The main controller ties the pipeline together, manages concurrency, and outputs a packaging-ready JSON structure.
import click
import json
from rich.console import Console
console = Console()
@click.command()
@click.argument("source_file", type=click.Path(exists=True, path_type=Path))
def orchestrate_ladder_generation(source_file: Path):
work_dir = Path("renditions") / source_file.stem
work_dir.mkdir(parents=True, exist_ok=True)
scored_results = []
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {
executor.submit(execute_transcode_pass, source_file, candidate, work_dir): candidate
for candidate in PROBE_MATRIX
}
for future in as_completed(futures):
candidate = futures[future]
encoded_path = future.result()
vmaf_index = measure_perceptual_quality(source_file, encoded_path)
scored_results.append((candidate, vmaf_index))
console.print(f"[cyan]{candidate.identifier}[/cyan] β VMAF {vmaf_index:.1f}")
bitrate_vmaf_pairs = [(c.target_bitrate_kbps, v) for c, v in scored_results]
hull = extract_pareto_front(bitrate_vmaf_pairs)
final_ladder = []
for bitrate, vmaf in hull:
matched = next(c for c in PROBE_MATRIX if c.target_bitrate_kbps == bitrate)
final_ladder.append({
"width": matched.resolution_w,
"height": matched.resolution_h,
"bitrate_kbps": matched.target_bitrate_kbps,
"vmaf_score": round(vmaf, 1)
})
manifest_path = source_file.with_suffix(".ladder.json")
manifest_path.write_text(json.dumps({"source": str(source_file), "ladder": final_ladder}, indent=2))
console.print(f"\n[green]Manifest written to {manifest_path}[/green]")
for rung in final_ladder:
console.print(f" {rung['height']}p @ {rung['bitrate_kbps']}k β VMAF {rung['vmaf_score']}")
if __name__ == "__main__":
orchestrate_ladder_generation()
Architecture Rationale
- Probe-First Strategy: Encoding a small grid before committing to full production transcodes prevents wasted CDN spend on inefficient rungs.
- Parallel Execution: FFmpeg 7.0's internal parallelism handles single-pass efficiency, but
ThreadPoolExecutor distributes independent probe candidates across CPU cores, cutting wall-clock time by ~60%.
- VMAF Normalization: Scaling both reference and encoded streams to 1080p eliminates resolution bias. VMAF's neural network expects consistent spatial dimensions for accurate feature extraction.
- Pareto Optimization: The convex hull removes dominated points. If 3500k delivers VMAF 95.1 and 5200k delivers 95.3, the 1700k delta is mathematically unjustified for most delivery scenarios.
Pitfall Guide
1. Ignoring Content Complexity Variance
Explanation: VMAF scores are relative to the source material. A talking-head video may peak at VMAF 92 with 3000k, while a sports clip requires 5500k to reach the same score. Cross-title comparisons are misleading.
Fix: Always evaluate ladders per-asset. Use catalog segmentation (low/medium/high motion) to set different quality floors and probe densities.
2. Skipping Resolution Sanity Checks
Explanation: The convex hull may return multiple rungs at the same resolution (e.g., 720p @ 1500k and 720p @ 2800k). Packaging systems often reject duplicate resolutions or cause client ABR confusion.
Fix: Post-process the hull to enforce one rung per resolution tier, or explicitly allow multi-bitrate resolutions only if your packager supports it.
3. Misconfiguring VMAF Reference Scaling
Explanation: Feeding a 360p encoded file and a 1080p source directly into libvmaf without scaling produces artificially low scores due to spatial mismatch.
Fix: Always apply identical scaling filters to both inputs before the libvmaf filter graph. Bicubic or lanczos interpolation is recommended for consistency.
4. Over-Probing Without Concurrency Controls
Explanation: Expanding the probe grid to 20+ points without parallelization turns the optimization step into a pipeline bottleneck.
Fix: Limit concurrent workers to available CPU cores minus two. Use FFmpeg's -threads flag to cap per-process usage, preventing system thrashing.
5. Neglecting Audio Bitrate in Total Egress
Explanation: Focusing exclusively on video bitrate ignores audio's contribution to CDN costs. A 96k AAC track adds ~0.5% overhead, but multi-channel or lossless audio can skew savings calculations.
Fix: Include audio bitrate in your total egress model. Consider normalizing audio to 128k AAC or Opus across all rungs to simplify bandwidth accounting.
6. Treating VMAF as Absolute Quality
Explanation: VMAF correlates with human opinion but is not a universal quality standard. Different content types, display devices, and viewing distances shift the perceptual threshold.
Fix: Use VMAF as a relative optimization metric, not an absolute guarantee. Validate final ladders with subjective testing on target devices before full rollout.
7. Hardcoding Convex Hull Without Quality Floors
Explanation: The mathematical hull may select a 360p @ 400k rung with VMAF 68. While technically optimal for bitrate, it delivers poor user experience on modern displays.
Fix: Enforce a minimum VMAF threshold (typically 70-75) and a maximum rung count (4-6). This prevents mathematically efficient but practically unusable ladders.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Low-motion catalog (interviews, e-learning) | Per-title with 8-point probe, VMAF floor 75 | High bitrate savings with minimal quality variance | -35% to -45% egress |
| High-motion catalog (sports, action) | Per-title with 12-point probe, VMAF floor 80 | Preserves quality where static ladders fail | -5% to -15% egress |
| Mixed long-tail library | Automated per-title with dynamic rung limits | Adapts to content diversity without manual tuning | -20% to -30% egress |
| Small volume (<10k hrs/mo) | Static ladder + managed encoding API | Probe compute cost outweighs CDN savings | Baseline + API fees |
| Live streaming | Online per-title with sliding window analysis | Requires real-time complexity estimation | +10% compute, -20% peak bitrate |
Configuration Template
# config.py
import os
from pathlib import Path
# FFmpeg & VMAF Settings
FFMPEG_BINARY = os.getenv("FFMPEG_PATH", "ffmpeg")
VMAF_MODEL_PATH = os.getenv("VMAF_MODEL", "vmaf_v0.6.1.json")
VMAF_SCALE_RESOLUTION = (1920, 1080)
VMAF_LOG_FORMAT = "json"
# Probe Configuration
MAX_CONCURRENT_PROBES = min(os.cpu_count() or 4, 8)
MINIMUM_VMAF_THRESHOLD = 72.0
MAX_LADDER_RUNGS = 5
RESOLUTION_TIER_LIMIT = 1 # Max rungs per resolution
# Output Settings
RENDITIONS_DIR = Path("renditions")
MANIFEST_SUFFIX = ".ladder.json"
AUDIO_CODEC = "aac"
AUDIO_BITRATE = "128k"
VIDEO_PRESET = "medium"
MOVFLAGS = "+faststart"
Quick Start Guide
- Install Dependencies: Ensure FFmpeg 7.0+ is compiled with
--enable-libvmaf. Install Python 3.11+ and run pip install click ffmpeg-python rich.
- Define Your Probe Matrix: Edit
PROBE_MATRIX in the orchestrator script to match your target resolutions and CDN tier limits. Start with 6 points.
- Execute the Probe: Run
python3 ladder.py /path/to/source.mp4. The script will transcode candidates, score them, and output a .ladder.json manifest.
- Integrate with Packaging: Feed the generated JSON into your HLS/DASH packager. Replace static ladder configurations with the per-title manifest for automated ABR playlist generation.
- Validate & Iterate: Check VMAF scores against your quality floor. If savings are insufficient, expand the probe grid to 12-16 points and adjust the minimum VMAF threshold based on subjective testing.