Deep dive
How it works
Parts
- Analyzer — probes the source (size, fps, HDR, audio, shot boundaries).
- Probe encoder — encodes a grid of (height, bitrate) points.
- Quality scorer — VMAF or PSNR per probe against the scaled source.
- Ladder selector — hull plus spacing rules (code below).
- Encoder workers — chunked parallel encode with closed GOPs.
- Packager — fMP4 segments and HLS/DASH manifests.
- Thumbnailer — poster, sprite sheet, image variants.
Operations
- Decode and analyze; normalize odd input (interlaced, variable frame rate, rotation).
- Probe a grid, for example 5 heights x 3-4 bitrates, fast presets; measure kbps and score each.
- Compute the upper convex hull of (kbps, quality) over all probes. A rendition is only worth shipping if it is on it.
- Pick rungs along the hull: minimum quality step (a few VMAF points, tuned by viewing tests), minimum bitrate ratio between neighbours (a design choice; 1.5x here), a cap at the quality where more bits are invisible, and a maximum rung count.
- Final encode of each rung with identical forced keyframes (below) and a
maxratecap so declaredBANDWIDTHis honest. - Package: 2-6 s segments, an init segment per rendition, a manifest with
BANDWIDTH,RESOLUTION,CODECS. - Audio: one or two renditions (for example AAC 64 and 128 kbit/s); cheap, not on the hull.
- Images: a poster frame, a scrub sprite sheet, resized stills in WebP or AVIF.
source --> analyze --> probe grid (h x kbps) --> score --> hull --> rungs
|
+--------------- chunks x rungs on N workers <--------------+
v
fMP4 segments (IDR-aligned) --> HLS/DASH manifest --> object store --> CDN
poster / sprite / image variants -----------------------^
Fixed vs per-title vs per-shot. A fixed ladder (for example Apple's example table: 416x234 at 145 kbit/s up to 1080p at 7.8 Mbit/s H.264) is a safe default and needs no probes. Per-title (Netflix, 2015) runs the hull per title, so an animated title can reach 1080p at a bitrate where a sports match is still at 540p. Per-shot (Netflix Dynamic Optimizer, 2018) builds a hull per shot and picks, for each target, the per-shot points of equal slope (a Lagrangian trade-off), so bits go where quality per bit is highest; shots are stitched at keyframes. Typical use: fixed ladder first, per-title for popular items.
Keyframes and segments. Every segment begins with an IDR from a closed GOP, at identical timestamps in all rungs, so the player can switch at any segment. Apple's spec: IDRs every 2 s, segments nominally 6 s, each starting with an IDR. Disable per-rung scene-cut keyframes (or force the same cuts everywhere): they move boundaries.
Examples across fields. Social video: cheap ladder at upload, upgrade after views accumulate. VOD: per-title or per-shot, several codecs. E-learning: slides and talking heads reach good quality at very low rates, so ladders are short. Video calls: sender-side layers (for example 180p, 360p, 720p) are a ladder without probes or segments. Surveillance and telemedicine archives: a low-rate proxy for browsing, the original kept untouched. Game-stream recordings: 60 fps, high motion, bitrate-hungry rungs.
Code
Rust, std only. The table is synthetic, shaped like real curves (not measured on any title). It computes the hull, picks rungs, and asserts their properties.
/// One probe encode: (height, average kbps, VMAF).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pt { pub height: u32, pub kbps: f64, pub vmaf: f64 }
fn cross(o: &Pt, a: &Pt, b: &Pt) -> f64 {
(a.kbps - o.kbps) * (b.vmaf - o.vmaf) - (a.vmaf - o.vmaf) * (b.kbps - o.kbps)
}
/// Upper convex hull of rate-quality: best quality per rate, whichever resolution gets it.
pub fn upper_hull(pts: &[Pt]) -> Vec<Pt> {
let mut p = pts.to_vec();
p.sort_by(|a, b| a.kbps.total_cmp(&b.kbps).then(b.vmaf.total_cmp(&a.vmaf)));
// 1. Pareto front: drop points that cost more and look no better.
let mut front: Vec<Pt> = Vec::new();
for q in p {
if front.last().map_or(true, |l| q.vmaf > l.vmaf && q.kbps > l.kbps) { front.push(q); }
}
// 2. Monotone chain: pop while the last turn is not clockwise.
let mut h: Vec<Pt> = Vec::new();
for q in front {
while h.len() >= 2 && cross(&h[h.len() - 2], &h[h.len() - 1], &q) >= 0.0 { h.pop(); }
h.push(q);
}
h
}
/// Greedy rungs on the hull: >= `min_ratio` x bitrate and >= `min_dq` VMAF over the
/// previous rung; stop at `q_cap`; thin to `max_rungs`.
pub fn pick(hull: &[Pt], min_ratio: f64, min_dq: f64, q_cap: f64, max_rungs: usize) -> Vec<Pt> {
let mut r = vec![hull[0]];
for p in &hull[1..] {
let l = r.last().unwrap();
if p.kbps >= l.kbps * min_ratio && p.vmaf >= l.vmaf + min_dq { r.push(*p); }
if p.vmaf >= q_cap { break; }
}
while r.len() > max_rungs { // drop the rung with the smallest quality gap to its neighbours
let i = (1..r.len() - 1).min_by(|&a, &b|
(r[a + 1].vmaf - r[a - 1].vmaf).total_cmp(&(r[b + 1].vmaf - r[b - 1].vmaf))).unwrap();
r.remove(i);
}
r
}
fn main() {
// SYNTHETIC numbers: diminishing returns, higher resolutions win only at higher rates.
let t: [(u32, f64, f64); 18] = [
(270, 150., 50.), (270, 300., 63.), (270, 600., 68.),
(360, 300., 60.), (360, 600., 73.), (360, 1000., 77.),
(540, 600., 68.), (540, 1000., 80.), (540, 1600., 85.),
(720, 1200., 78.), (720, 2000., 87.), (720, 3000., 90.),
(1080, 2500., 84.), (1080, 4000., 92.), (1080, 6000., 95.), (1080, 8000., 96.5),
(720, 1500., 83.), (540, 800., 75.),
];
let pts: Vec<Pt> = t.iter().map(|&(height, kbps, vmaf)| Pt { height, kbps, vmaf }).collect();
let hull = upper_hull(&pts);
let rungs = pick(&hull, 1.5, 3.0, 95.0, 6);
// convex, and nothing beats the hull
let slope = |a: &Pt, b: &Pt| (b.vmaf - a.vmaf) / (b.kbps - a.kbps);
assert!(hull.windows(3).all(|w| slope(&w[0], &w[1]) > slope(&w[1], &w[2])));
assert!(hull.iter().all(|h| !pts.iter().any(|p| p.kbps <= h.kbps && p.vmaf > h.vmaf)));
// rungs: on the hull, monotone, spaced, bounded
assert!(!rungs.is_empty() && rungs.len() <= 6 && rungs.iter().all(|r| hull.contains(r)));
assert!(rungs.windows(2).all(|w| w[1].kbps >= 1.5 * w[0].kbps && w[1].vmaf >= w[0].vmaf + 3.0));
assert_eq!(pick(&hull, 1.5, 3.0, 95.0, 3).len(), 3);
}
Compiled and run, asserts pass: the 18 probes give a 10-point hull and six rungs, from 270p at 150 kbit/s (VMAF 50) to 1080p at 6000 kbit/s (95); dominated probes such as 360p at 300 and 720p at 1200 drop out.
Complexity and constants
- Hull: O(n log n) for a few dozen probes; microseconds, not timed. The cost is the encoder.
- Encode work per rung is roughly pixels x frames at a fixed preset; 16:9 rungs of 1080/720/540/360/270 lines sum to about 1.9x one 1080p encode (arithmetic, assuming linear scaling).
- Probes add work: a 5 x 4 grid is 20 extra encodes of the title (faster preset or short clips cut this); per-shot repeats it per shot.
- Declared
BANDWIDTHmust match the measured peak (Apple: within 10 % for VOD, peak at most 200 % of average).
Efficiency
- Cost: per title, one decode plus probe and final encodes, bound by CPU (or encoder ASIC) time; output is duration x sum of rung bitrates (2000 s with the six rungs above: about 2.8 GB, arithmetic). Viewer egress scales with what is played, not stored.
- Levers:
- Per-title or per-shot ladder instead of fixed — fewer bits at equal quality (Netflix; magnitude not quoted here) — when content varies widely.
- HEVC instead of H.264 — Apple's tables give 4.5 vs 6 Mbit/s at 1080p and 2.4 vs 3 at 720p, 20-25 % less (Apple HLS spec) — where devices allow; costs encode time and licensing; AV1 saves more at far higher encode cost (no figure quoted).
- Cheap ladder first, upgrade by views — encode spend only where views earn it — long-tail catalogues.
- Chunked parallel encode — wall time about the longest chunk — long sources.
- One decode shared by all rungs — saves repeated decode and scale work — every ladder.
- Naive → standard → state of the art:
- One H.264 file · one rung, wrong for most viewers
- Fixed 6-9 rung H.264 ladder, constrained VBR · about N times the 1080p encode
- Per-shot hull + multi-codec, hot titles re-encoded harder · several times that, repaid by views
Implementation notes
- FFmpeg:
-g 60 -keyint_min 60 -sc_threshold 0 -force_key_frames "expr:gte(t,n_forced*2)"for 30 fps with 2 s keyframes in every rung (-sc_threshold 0disables libx264 scene cuts; other encoders spell this differently). This produced identical keyframe times (0.066, 2.066, 4.066 s, ...) in two rungs on this machine (FFmpeg 7.1.1). - HLS:
-f hls -hls_time 6 -hls_segment_type fmp4 -hls_flags independent_segments -var_stream_map "v:0,agroup:aud v:1,agroup:aud a:0,agroup:aud,name:audio" -master_pl_name master.m3u8. Audio goes in its own group; sharing one audio stream between variants fails with "Same elementary stream found more than once". - DASH muxer:
-seg_duration,-adaptation_sets "id=0,streams=v id=1,streams=a". One set of CMAF fMP4 segments can serve HLS and DASH; only manifests differ. - Poster:
-vf "thumbnail=150,scale=640:-2" -frames:v 1 poster.webp(most representative of 150 frames). Sprite:-vf "fps=1/2,scale=160:-2,tile=6x2" -frames:v 1 sprite.jpg, one request instead of many. Both ran here. - Keep the VMAF model and display size fixed across probes.
- Rate control: constrained VBR, for example
-maxrate1.5x and-bufsize2x the target (a common choice, not a standard); keep at least some H.264 rungs for compatibility, as Apple advises.
Pitfalls
- Per-rung scene-cut keyframes: boundaries differ, switching hitches. Disable or force.
- Open GOPs at segment starts: the segment cannot be decoded alone.
- Hull from too few probes: kinks look like crossovers; probe more densely near the knees.
- Mean-VMAF targets hide short bad scenes; check a low percentile.
- Declared
BANDWIDTHbelow real peaks: players overestimate and rebuffer. - Encoding rungs above the source's own quality wastes storage; cap at the source.
Variants and alternatives
In production
- Netflix runs per-title and per-shot ladders on VMAF; Apple publishes a fixed example ladder; FFmpeg, Shaka Packager and Bento4 implement the encode and packaging stages.
- Output sits in object storage behind a CDN.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| FFmpeg | C | FFmpeg/FFmpeg | brew install ffmpeg | — | — | — | — | encode, scale, HLS/DASH muxers, thumbnail/tile filters, libvmaf filter |
| x264 | C | videolan/x264 | brew install x264 | — | — | — | — | H.264 baseline-compatible rung; --keyint, --scenecut, --open-gop |
| x265 | C++ | videolan/x265 | brew install x265 | — | — | — | — | HEVC rungs |
| SVT-AV1 | C | AOMediaCodec/SVT-AV1 | brew install svt-av1 | — | — | — | — | AV1 rungs with parallel encoder |
| Shaka Packager | C++ | shaka-project/shaka-packager | prebuilt binaries on GitHub releases | — | — | — | — | HLS/DASH packaging, CMAF, DRM |
| Bento4 | C++ | axiomatic-systems/Bento4 | brew install bento4 | — | — | — | — | fMP4 fragmenting, HLS/DASH tools |
| libvmaf | C | Netflix/vmaf | brew install libvmaf | — | — | — | — | quality scoring for the probes |
Sources
- Apple, HTTP Live Streaming (HLS) authoring specification for Apple devices. spec
- R. Pantos, W. May, HTTP Live Streaming, RFC 8216, 2017. RFC
- A. Aaron, Z. Li, M. Manohara, J. De Cock, D. Ronca, Per-Title Encode Optimization, Netflix TechBlog, 2015 (blog; not fetchable from this environment).
- I. Katsavounidis, Dynamic optimizer: a perceptual video encoding optimization framework, Netflix TechBlog, 2018 (blog; not fetchable from this environment).
- ISO/IEC 23009-1 (DASH); ISO/IEC 23000-19 (CMAF).
- FFmpeg formats documentation (hls, dash muxers). docs