Fast, statistically honest video quality analysis: a Go library and a CLI that take a video and give you technical metrics, a VMAF score and a per-title adaptive streaming ladder (H.264, HEVC, AV1) as fast as the reliability you ask for allows.
- Technical analysis in seconds: bitrate, peaks and GOP structure without decoding. Then one decode fanned out to SI/TI (ITU-T P.910), shot detection, black and frozen segments, letterbox/pillarbox, luma levels and camera motion (each shot static, pan, tilt, zoom, tracking or handheld, with a shake measure).
- Audio QC at no extra wall time: every audio track measured while the
video decodes: loudness per ITU-R BS.1770-5 (integrated, range, true
peak, EBU Tech 3341/3342 conformant, within 0.01 LU of ffmpeg's
ebur128) against EBU R 128, ATSC A/85 or a streaming target; silence, muted channels, clipping, DC offset and phase problems (audio).
- VMAF with a confidence interval: short clips sampled across shots until
the 95% interval is narrower than your target. The intervals really cover
the truth 95% of the time, measured by replaying thousands of runs.
--exactscores every frame, bit-exact with Netflix'svmaftool, at 8 and 10 bits. Or pick a fixed budget (--sample 2%,--sample 1/scene).
- Beyond VMAF, on the same frames: XPSNR (a pure-Go port matching ffmpeg's filter), CAMBI banding, PSNR, PSNR-HVS, SSIM, MS-SSIM, CIEDE2000 or the whole AV2 CTC set, each with its own confidence interval, and VMAF per viewing device (phone, TV, 4K).
- Per-title ladders, verified: probe encodes of a representative digest,
rate-quality curves per resolution, their upper envelope, rungs one
just-noticeable difference apart, then a real encode of every rung. It lands
on the exhaustive optimum (−0.04 VMAF, −0.7% bitrate on average) in a
fraction of the time. Impose the shape (--rungs 1080,720,540), probe adaptively where the rungs are uncertain, add per-shot rungs, or let AV1 synthesise film grain; verified rungs are checked for banding and for VMAF/XPSNR disagreements.
- HDR aware: HDR10/HLG checked, MaxCLL/MaxFALL measured, wPSNR and ΔE ITP next to VMAF, 10-bit ladders carrying the HDR10 metadata (HDR).
- See what was measured: --overlay annotated.mp4burns the analysis into a copy of the video, frame by frame: timecode, bitrate, shots, camera motion, SI/TI, levels, VMAF of each scored frame and a timeline (annotated videos).
- A terminal UI you'll enjoy: a live dashboard with progress, ETA and panels that show VMAF converging and probes landing on a braille chart. There is also an interactive wizard, plus JSON and self-contained HTML reports.
Docker (linux/amd64, linux/arm64): qc, libvmaf 3.2.1 with its models and
ffmpeg with x264, x265 and SVT-AV1, nothing else to install. Mount your
videos on /data:
docker run --rm -v "$PWD:/data" ghcr.io/eko/qc vmaf reference.mov encode.mp4
docker run --rm -v "$PWD:/data" ghcr.io/eko/qc ladder source.mov -c av1 --html ladder.htmlHomebrew (macOS, Linux):
brew install eko/tap/qcPrebuilt binaries (Linux amd64/arm64, static; macOS arm64): on every release, with libvmaf and the VMAF models built in; only ffmpeg is needed (install).
From source: Go (see go.mod) with cgo, ffmpeg and ffprobe with
libx264, libx265 and libsvtav1, libvmaf ≥ 3.2.1 with its models and
pkg-config:
brew install ffmpeg libvmaf pkgconf # macOS; Debian/Ubuntu: see docs/install.md
go install github.com/eko/qc/cmd/qc@latestqc version --check checks the installation. Linux prerequisites, the image
contents and troubleshooting: docs/install.md.
qc # interactive wizard
qc run source.mov --codecs h264,av1 --html report.html # analysis + ladders
qc run encode.mp4 -r source.mov # + VMAF against the source
qc analyze video.mp4 [--fast] # technical analysis (--fast: no decoding)
qc analyze video.mp4 --loudness-target atsc # audio checked against ATSC A/85 (default: EBU R 128)
qc vmaf reference.mov distorted.mp4 [--exact] # VMAF ± 95% CI, or every frame
qc vmaf reference.mov distorted.mp4 --sample 5% # fixed budget (or 2/scene), one pass, CI reported
qc vmaf reference.mov distorted.mp4 --exact --overlay annotated.mp4 # + a copy with per-frame VMAF burnt in
qc ladder source.mov -c av1 --encode-bit-depth 10 # per-title Main10 AV1 ladder
qc ladder source.mov --rungs 1080,720,540,360 --top-vmaf 93 # impose the rungs, bitrates computed
qc ladder source.mov -c av1 --encode-ladder renditions/ # + the renditions, checked on the whole titleEvery command accepts -o report.json, --html report.html and -f json.
See the CLI reference.
Apple M2 Max, real 1080p25 H.264 sources:
How these numbers were obtained, and how to reproduce them on your own content: docs/validation.md.
- Install
- How it works
- Architecture
- Technical analysis
- VMAF engine
- HDR: detection, light levels, HDR metrics, HDR ladders
- Audio: loudness (BS.1770, EBU R 128, ATSC A/85) and defects
- Ladder engine
- Validation
- CLI
- NVIDIA GPUs: NVDEC decoding, NVENC ladders and CUDA VMAF with --gpu
- Annotated videos: the analysis burnt into a copy of the video with --overlay
- Innovation landscape and roadmap: beyond VMAF
dec := decode.NewFFmpeg("ffmpeg", 0) // decode.WithHWAccel(decode.HWAccelAuto): VideoToolbox segments on macOS; HWAccelCUDA: NVDEC
analyzer := analysis.New(logger,
probe.NewFFprobe("ffprobe"),
bitstream.NewFFprobeReader("ffprobe"),
dec,
quality.NewMeter(dec, libvmaf.NewEngine()),
)
report, err := analyzer.Analyze(ctx, "video.mp4", analysis.Options{})
cmp, err := analyzer.Compare(ctx, "reference.mov", "encode.mp4", analysis.CompareOptions{})
ffmpeg := encode.NewFFmpeg("ffmpeg") // encodes, digests, grain measurements, renditions
engine := ladder.NewEngine(analyzer, ffmpeg, ffmpeg,
ladder.WithGrainLab(ffmpeg), ladder.WithRenditionEncoder(ffmpeg))
res, err := engine.Build(ctx, "source.mov", ladder.Options{Codec: "av1"})Every options struct has a useful zero value, except that ladder.Options
needs its Codec. To go further:
cmp, err := analyzer.Compare(ctx, "reference.mov", "encode.mp4", analysis.CompareOptions{
Quality: quality.Options{
Precision: 0.25, // or Exact: true, or Sample below
Metrics: []string{quality.MetricXPSNR, quality.MetricCAMBI},
Devices: []string{vmaf.DevicePhone, vmaf.Device4K},
},
})
xpsnrY, _ := cmp.VMAF.Metric(quality.SeriesXPSNRY) // mean and 95% CI, like VMAF
phone, _ := cmp.VMAF.Device(vmaf.DevicePhone)
banding := cmp.VMAF.Banding // segments where CAMBI > 5
// A fixed budget instead of a precision; the analysed encode gives its shot
// cuts to per-scene budgets and is not inspected again.
budget, _ := quality.ParseSample("2/scene") // or "5%"
cmp, err = analyzer.Compare(ctx, "reference.mov", "encode.mp4", analysis.CompareOptions{
Quality: quality.Options{Sample: budget},
Distorted: report,
})
// An imposed AV1 ladder, probed adaptively, with checked rungs.
res, err = engine.Build(ctx, "source.mov", ladder.Options{
Codec: "av1",
Constraints: ladder.Constraints{Resolutions: []int{1080, 720, 540, 360}, TopVMAF: 93},
Probing: ladder.ProbingAdaptive,
FilmGrain: ladder.FilmGrainAuto,
Metrics: []string{quality.MetricXPSNR, quality.MetricCAMBI},
})
banded := ladder.BandedRungs(res.Rungs) // rungs capped by banding
conflicts := ladder.RankConflicts(res.Rungs) // VMAF and XPSNR disagree
// Per-shot rungs: one CRF per shot at an equal rate-quality slope.
res, err = engine.Build(ctx, "source.mov", ladder.Options{Codec: "hevc", PerShot: true})
for shot, cell := range res.ShotLadder(0) { // the top rung, shot by shot
fmt.Println(res.ShotInterval(shot).Start, cell.CRF, cell.PredictedBitrate)
}
if top := res.Rungs[0].PerShot; top != nil {
fmt.Println(top.PooledBitrate(res.Shots)) // its bitrate over the whole title
}
// The renditions: every rung (and per-shot version) encoded on the whole
// title, each checked against the source.
renditions, err := engine.Encode(ctx, "source.mov", res, ladder.RenditionOptions{
Dir: "renditions",
Check: &quality.Options{Precision: 0.5},
})
for _, rd := range renditions {
vmafPredicted, bitratePredicted := res.Prediction(rd)
fmt.Println(rd.Path, rd.Bitrate, bitratePredicted, rd.Checked.VMAFLabel(), vmafPredicted)
}On an NVIDIA GPU, check first: NVDEC falls back to the CPU by itself, but an
NVENC ladder would fail at its first encode. CUDA VMAF needs a binary built
with -tags cuda against a CUDA libvmaf (libvmaf.CUDABuilt), and a model
with CUDA features (VMAF v1, the default, has none):
err = nvidia.Check(ctx, "ffmpeg", nvidia.Requirements{
HWAccel: decode.HWAccelCUDA, // the decoder's decode.WithHWAccel
Codecs: []string{"hevc"}, // ladders on NVENC
})
res, err = engine.Build(ctx, "source.mov", ladder.Options{
Codec: "hevc",
Encoder: encode.HardwareNVENC, // no per-shot rungs nor film grain synthesis
Backend: vmaf.BackendAuto, // CUDA VMAF when the model and the build allow it
})
fmt.Println(cmp.VMAF.GPUSummary()) // e.g. "NVDEC decoding (cuda) · VMAF features on CUDA"quality/xpsnr also works on its own, on decoded frames, and matches
ffmpeg's xpsnr filter. Runnable examples are on
pkg.go.dev for analysis,
quality, quality/xpsnr, quality/hdr, ladder, pipeline, nvidia,
overlay and vmaf/libvmaf. To run everything with progress hooks, use
pipeline.Runner as described in
docs/architecture.md.
Contributions are welcome. See CONTRIBUTING.md: make check
runs what CI runs, and changes to the sampler or the ladder engine come with
their validation numbers.