Golden-File Testing for Transcode Output
This page sits beneath CI/CD and testing for media pipelines in the wider Pipeline Automation & Batch Processing layer, and it addresses the case where you want to catch any unintended change in a transcode — a shifted bitrate ladder, a subtly different picture, a re-timed audio track — but the output is never byte-for-byte identical, so a naïve file-diff test fails on every run.
Problem Framing
Transcoding is not deterministic at the byte level. The x264 and x265 encoders embed build identifiers and make thread-scheduling-dependent decisions; the MP4 muxer writes timestamps and padding that vary; even re-running the identical command on the same machine can produce files whose bytes differ while the decoded picture and sound are perceptually identical. So the obvious test is worthless:
$ md5sum candidate.mp4 golden.mp4
0f2e...a1 candidate.mp4
9b74...c3 golden.mp4 # different every run — this test is pure noise
Yet the risk it was trying to guard is real. A refactor of the encode ladder, a change from -preset medium to -preset fast, or a filter reordering can degrade quality or shift loudness without changing any field a codec contract pins. You need a test that answers “is this output meaningfully the same as the reference we blessed?” and tolerates the noise floor of encoder non-determinism while still failing on a real regression. That means comparing decoded signals and structured metadata against thresholds — not comparing bytes.
This is deliberately distinct from a contract test. A contract test asserts fixed properties from a spec (must be 48 kHz AAC in an MP4) with no reference file; the integration testing codec contracts workflow covers that. Golden-file testing asserts similarity to a known-good artifact, catching drift in properties no contract enumerates.
Solution Architecture
The comparison runs on three independent channels, each with its own tolerance, and the test fails if any channel breaches. Structured metadata (from ffprobe) is compared for exact equality on fields that must not move. The video signal is compared frame-against-frame with FFmpeg’s own PSNR and SSIM filters, gated on thresholds. The audio is decoded to raw PCM and compared by integrated loudness and a quantized hash, tolerating sample-level dither while catching a re-timed or re-gained track.
Splitting the comparison this way means a failure is diagnosable: a metadata mismatch points at a muxing or ladder change, a PSNR/SSIM drop points at the picture, and an audio breach points at loudness or timing — you know which layer moved before you open the file.
Implementation
1. Store the golden and a metadata sidecar
Commit the reference output alongside a JSON sidecar of its pinned metadata, so metadata comparison never has to decode the golden at test time and the expected values are reviewable in the diff.
# python 3.10+ numpy==1.26.4 soundfile==0.12.1
import json, subprocess, pathlib
def probe(path: str) -> dict:
out = subprocess.run(
["ffprobe", "-v", "error", "-show_streams", "-show_format",
"-of", "json", path], capture_output=True, text=True, check=True,
).stdout
return json.loads(out)
def golden_fields(path: str) -> dict:
m = probe(path)
v = next(s for s in m["streams"] if s["codec_type"] == "video")
a = next(s for s in m["streams"] if s["codec_type"] == "audio")
return {
"video": {"codec": v["codec_name"], "pix_fmt": v["pix_fmt"],
"width": v["width"], "height": v["height"],
"avg_frame_rate": v["avg_frame_rate"]},
"audio": {"codec": a["codec_name"], "sample_rate": a["sample_rate"],
"channels": a["channels"]},
"duration": round(float(m["format"]["duration"]), 2),
}
2. Compare structured metadata for exact fields
These fields must not drift, so compare them for equality. This is the cheap first channel and it runs without decoding a frame.
def test_metadata_matches_golden():
candidate = golden_fields("candidate.mp4")
expected = json.loads(pathlib.Path("tests/golden/720p.json").read_text())
assert candidate == expected, f"metadata drift: {candidate} != {expected}"
3. Compare the video signal with PSNR and SSIM
Run the candidate against the golden through FFmpeg’s psnr and ssim filters — FFmpeg does the frame alignment and math, printing a per-run average you parse and threshold. The filters are documented in the FFmpeg filters documentation.
import re
def compare_video(candidate: str, golden: str) -> tuple[float, float]:
proc = subprocess.run(
["ffmpeg", "-hide_banner", "-i", candidate, "-i", golden,
"-lavfi", "[0:v][1:v]psnr;[0:v][1:v]ssim", "-f", "null", "-"],
capture_output=True, text=True,
)
log = proc.stderr
psnr = float(re.search(r"average:([0-9.]+)", log).group(1))
ssim = float(re.search(r"All:([0-9.]+)", log).group(1))
return psnr, ssim
def test_video_within_quality_threshold():
psnr, ssim = compare_video("candidate.mp4", "tests/golden/720p.mp4")
assert psnr >= 42.0, f"PSNR {psnr} dB below 42 — picture regressed"
assert ssim >= 0.98, f"SSIM {ssim} below 0.98 — structural change"
A PSNR of 42 dB and SSIM of 0.98 sit above the encoder’s non-determinism floor — repeated correct runs comfortably clear them — while a real preset or filter regression drops well below. Raise the floors for archival masters, lower them for lossy proxy renditions.
4. Compare audio by decoded PCM
Byte-comparing AAC is noise, so decode both tracks to raw PCM and compare on two axes: integrated loudness (catches a gain or loudnorm change) and a quantized hash (catches a re-timed or truncated track while tolerating dither).
import numpy as np, soundfile as sf, hashlib, io
def decode_pcm(path: str) -> np.ndarray:
raw = subprocess.run(
["ffmpeg", "-v", "error", "-i", path, "-f", "wav", "-ac", "2",
"-ar", "48000", "pipe:1"], capture_output=True, check=True,
).stdout
data, _ = sf.read(io.BytesIO(raw), dtype="int16")
return data
def quantized_hash(pcm: np.ndarray) -> str:
# quantize to 8-bit to absorb sub-LSB dither, then hash the shape+values
q = (pcm >> 8).astype(np.int8)
return hashlib.sha256(q.tobytes()).hexdigest()
def test_audio_matches_golden():
cand, gold = decode_pcm("candidate.mp4"), decode_pcm("tests/golden/720p.mp4")
assert cand.shape == gold.shape, "audio length/channel drift"
assert quantized_hash(cand) == quantized_hash(gold), "audio content changed"
Verification
Confirm the golden tests pass on a correct build, then prove each channel fails on a matching regression so you know the thresholds are not slack.
# 1. Green across all three channels on the correct pipeline
pytest tests/golden -q
# expected: 3 passed
# 2. Prove the video channel bites — re-encode the candidate at a worse preset
ffmpeg -y -i src.mp4 -c:v libx264 -preset ultrafast -crf 34 candidate.mp4
pytest tests/golden/test_video_within_quality_threshold.py -q
# expected: FAILED ... PSNR 31.2 dB below 42
# 3. Prove the audio channel bites — nudge the gain by 3 dB
ffmpeg -y -i src.mp4 -af volume=3dB -c:v copy candidate.mp4
pytest tests/golden/test_audio_matches_golden.py -q
# expected: FAILED ... audio content changed
A healthy suite is three green channels on the blessed pipeline and a clean, well-labeled failure on each injected regression. If a deliberately worse encode still passes the PSNR gate, your threshold is too low — tighten it until the bad build fails but repeated good builds stay green.
Failure Modes & Edge Cases
| Edge case | Symptom | Remediation |
|---|---|---|
| Golden regenerated on every run | Test never catches drift, always green | Never set GOLDEN_UPDATE=1 in normal CI; regenerate only in a reviewed job |
| PSNR threshold too low | A visibly worse encode passes | Inject a bad preset and raise the floor until it fails; keep good runs > threshold |
| Frame-count mismatch | psnr filter aligns wrong frames, garbage score |
Assert nb_frames/duration equality first; a length change is its own failure |
| Encoder build bump | PSNR drifts 1–2 dB across the board | Pin FFmpeg; if an intended upgrade, re-bless goldens in a deliberate diff |
| Raw PCM hash never matches | Dither makes bit-exact audio impossible | Quantize before hashing (drop low bits); compare loudness + shape, not raw bytes |
| Golden files bloat the repo | Slow clones, large diffs | Keep clips short (2–5 s) and few; store large masters in LFS, not inline |
| Comparing VFR to CFR | SSIM tanks from timestamp misalignment | Force constant frame rate on both sides before comparing the picture |
Fields that must match a fixed specification regardless of any reference — the mandated codec, container brand, and sample rate — belong in a contract test rather than a golden; pair this workflow with integration testing codec contracts so spec violations and reference drift are caught by the layer suited to each.
FAQ
How do I update a golden file without hiding a real regression?
Treat a golden update as a code change, never as an automatic side effect of a failing test. Run a dedicated job with the update flag set, which regenerates the reference and its metadata sidecar, then open the resulting diff for review — including the PSNR and SSIM deltas from the old golden — so a human confirms the change is intended. A test that can silently rewrite its own expectation on red validates nothing, which is why the update path is isolated and reviewed.
What PSNR and SSIM thresholds should I start with?
Begin at 42 dB PSNR and 0.98 SSIM for a standard delivery rendition, then calibrate empirically: run the correct pipeline several times to find the non-determinism floor, and inject a deliberately worse encode to find the ceiling a regression trips. Set the threshold in the gap between them. Archival masters warrant tighter values, while lossy proxy or preview renditions can sit lower because a few dB of encoder noise is inaudible and invisible there.
Why quantize the audio before hashing instead of hashing the raw PCM?
Because decoded PCM from a lossy codec carries sub-LSB dither and rounding that changes the lowest bits every run, so a raw hash would never match even for identical content. Dropping the low bits before hashing absorbs that noise while still detecting a re-timed, re-gained, or truncated track, since those change the signal well above the quantization step. Pair the hash with an integrated-loudness check so a uniform gain shift is caught even if the quantized shape survives.
Related
- Up to the parent guide: CI/CD and testing for media pipelines
- Sibling technique on fixed specs: integration testing codec contracts
- The loudness reference these comparisons track: audio codec normalization workflows
- Pipeline overview: Pipeline Automation & Batch Processing