
A render that looks correct on your screen can still fail broadcast QC if the codec profile, color space, frame-rate rational, or filename is off by one character. Run an automated validation script on your rented Mac right before upload and you catch those failures before the delivery portal ever sees them. The $29 virtual Intel macOS plan (2 vCPU, 8 GiB RAM, 80 GiB disk) is enough to run the script because ffprobe only reads container headers; bigger hardware only matters if the same session also transcodes or renders.
Broadcast and streaming platforms reject deliverables for a short list of predictable reasons:
30/1 (true 30 fps) versus 30000/1001 (29.97). They look identical on a timeline and fail an exact-match spec check.yuv420p (8-bit 4:2:0) vs yuv422p10le (10-bit 4:2:2). HDR pipelines also need specific transfer-function and color-primaries metadata.SHW_S01E03_v2.mov will bounce show_S01E03_v2.mov immediately.Manual spot-checking one or two files misses regressions across a full batch. The script below checks every file in a folder against a machine-readable spec and fails loudly on the first problem.
ffprobe ships with every FFmpeg build and reads structured metadata from almost any media container. The Figment macOS image is clean, so install Homebrew and the packages once per rental from the desktop terminal reached through the browser console:
# One-time setup on the rented Mac (desktop terminal, reached through the browser console)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install ffmpeg # includes ffprobe
brew install jq # JSON parser used by the validation script
Per the ffprobe documentation, -of json -show_entries returns only the fields you ask for, and -select_streams v:0 narrows output to the first video track:
ffprobe -v quiet -of json \
-show_entries "stream=codec_name,width,height,r_frame_rate,pix_fmt" \
-select_streams v:0 \
deliverable.mov
For HDR deliverables, add color_space,color_transfer,color_primaries to -show_entries to check the EOTF and gamut tags your client requires (for example bt2020 primaries and smpte2084 transfer for HDR10).
For files that Spotlight has already indexed, mdls prints macOS metadata attributes. Content-type values use Apple's Uniform Type Identifiers:
mdls -name kMDItemPixelWidth -name kMDItemPixelHeight render_ep03.mov
mdls -name kMDItemContentType render_ep03.mov
# com.apple.quicktime-movie for .mov
# public.mpeg-4 for .mp4
mdls depends on Spotlight indexing, which may not have run on files you just copied in. Treat ffprobe as the authoritative source for per-stream checks and use mdls as a quick cross-check once the files have settled.
Encode the client's delivery requirements as a flat key=value config. The fps value must match ffprobe's r_frame_rate output exactly — it is a rational, not a decimal — and the naming_re must be a working extended regex for the real filename pattern.
# apple_tv_plus_spec.conf
# 4K ProRes 422 HQ, 23.976 fps, 10-bit 4:2:2
codec=prores
width=3840
height=2160
fps=24000/1001
pix_fmt=yuv422p10le
naming_re=^SHW_S[0-9]{2}E[0-9]{2}_v[0-9]+\.mov$
Common fps rationals: 24/1 (true 24), 24000/1001 (23.976), 30/1 (true 30), 30000/1001 (29.97), 60/1 (true 60), 60000/1001 (59.94). Run ffprobe once on a known-good render from your NLE and copy the string it prints — some tools write prores and some write prores_ks; use whichever your renders actually produce. Keep each value on a single line with no surrounding spaces; the loader below strips whitespace from every value.
Save as validate_delivery.sh, chmod +x, and run it against a batch folder. It is an unexecuted example — test it on a known-good and a known-bad file before putting it in front of a deadline.
#!/usr/bin/env bash
# validate_delivery.sh — pre-delivery QC for broadcast/streaming deliverables
# Usage: ./validate_delivery.sh <spec.conf> <media_folder>
# Requires: ffprobe (brew install ffmpeg), jq (brew install jq)
set -euo pipefail
SPEC="${1:?Usage: $0 <spec.conf> <media_folder>}"
FOLDER="${2:?Usage: $0 <spec.conf> <media_folder>}"
spec_val() { grep -E "^${1}=" "$SPEC" | cut -d= -f2- | tr -d '[:space:]'; }
EXPECTED_CODEC=$(spec_val codec)
EXPECTED_WIDTH=$(spec_val width)
EXPECTED_HEIGHT=$(spec_val height)
EXPECTED_FPS=$(spec_val fps)
EXPECTED_PIX=$(spec_val pix_fmt)
NAMING_RE=$(spec_val naming_re)
PASS=0; FAIL=0; FAIL_MSGS=()
check_file() {
local FILE="$1" BASE PROBE CODEC WIDTH HEIGHT R_FPS PIX
BASE="$(basename "$FILE")"
local -a ISSUES=()
if [[ -n "$NAMING_RE" ]] && ! [[ "$BASE" =~ $NAMING_RE ]]; then
ISSUES+=("naming: does not match /$NAMING_RE/")
fi
if ! PROBE=$(ffprobe -v quiet -of json \
-show_entries "stream=codec_name,width,height,r_frame_rate,pix_fmt" \
-select_streams v:0 "$FILE" 2>/dev/null); then
FAIL_MSGS+=("FAIL [unreadable] $BASE")
FAIL=$((FAIL + 1)); return 0
fi
CODEC=$(printf '%s' "$PROBE" | jq -r '.streams[0].codec_name // empty')
WIDTH=$(printf '%s' "$PROBE" | jq -r '.streams[0].width // empty')
HEIGHT=$(printf '%s' "$PROBE" | jq -r '.streams[0].height // empty')
R_FPS=$(printf '%s' "$PROBE" | jq -r '.streams[0].r_frame_rate // empty')
PIX=$(printf '%s' "$PROBE" | jq -r '.streams[0].pix_fmt // empty')
[[ "$CODEC" != "$EXPECTED_CODEC" ]] && ISSUES+=("codec=$CODEC want=$EXPECTED_CODEC")
[[ "$WIDTH" != "$EXPECTED_WIDTH" ]] && ISSUES+=("width=$WIDTH want=$EXPECTED_WIDTH")
[[ "$HEIGHT" != "$EXPECTED_HEIGHT" ]] && ISSUES+=("height=$HEIGHT want=$EXPECTED_HEIGHT")
[[ "$R_FPS" != "$EXPECTED_FPS" ]] && ISSUES+=("fps=$R_FPS want=$EXPECTED_FPS")
[[ "$PIX" != "$EXPECTED_PIX" ]] && ISSUES+=("pix_fmt=$PIX want=$EXPECTED_PIX")
if [[ ${#ISSUES[@]} -gt 0 ]]; then
local MSG
printf -v MSG '%s, ' "${ISSUES[@]}"
FAIL_MSGS+=("FAIL $BASE — ${MSG%, }")
FAIL=$((FAIL + 1))
else
printf 'PASS %s\n' "$BASE"
PASS=$((PASS + 1))
fi
}
shopt -s nullglob nocaseglob
FILES=()
for G in "$FOLDER"/*.mov "$FOLDER"/*.mp4 "$FOLDER"/*.mxf; do
FILES+=("$G")
done
if [[ ${#FILES[@]} -eq 0 ]]; then
printf 'No .mov/.mp4/.mxf files found in %s\n' "$FOLDER"; exit 1
fi
for f in "${FILES[@]}"; do check_file "$f"; done
printf '\n===== QC REPORT =====\nPASS: %d FAIL: %d\n\n' "$PASS" "$FAIL"
if [[ ${#FAIL_MSGS[@]} -gt 0 ]]; then
printf '%s\n' "${FAIL_MSGS[@]}"
exit 1
fi
exit 0
Exit code 1 means at least one file failed, so the script slots cleanly in front of an upload step or a Shortcuts automation. Exit code 0 means every .mov, .mp4, or .mxf in the folder passed every check. set -euo pipefail ensures a broken ffprobe call or an unset variable stops the run instead of silently reporting success. If you need audio checks too, call ffprobe a second time with -select_streams a:0 and compare codec_name, sample_rate, and channels the same way.
Add color metadata to both the spec file and the ffprobe query:
# Add to spec.conf
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020
# Then add to -show_entries in check_file():
# stream=codec_name,width,height,r_frame_rate,pix_fmt,color_space,color_transfer,color_primaries
# And compare the three new variables the same way.
Container tags are not a color-accurate preview. They prove the file carries the right metadata — which is what most automated QC tools reject on — but they do not replace a calibrated hardware monitor at sign-off.
Run this on the rented Mac in the minute before you upload. It pairs the script with the small checks a human still needs to do:
ffprobe the first and last file in the batch by hand and visually confirm the output before trusting the script on the rest../validate_delivery.sh spec.conf out/ and confirm FAIL: 0.mdls -name kMDItemContentType to confirm the UTI matches the container extension.du -sh out/*.mov against the per-episode file-size cap in the client spec.ls out/ | sort > /tmp/have.txt && diff -u manifest.txt /tmp/have.txt.spec.conf and the script in the project repo so the next version uses the same rules.The validation script reads container headers and runs shell tools — it does not decode or transcode. The $29 virtual Intel macOS plan is enough for probing a full folder of ProRes masters; ffprobe does not touch pixel data. Pick a bigger plan only if the same session also renders or transcodes:
Hardware class above the $29 VM is confirmed in the configuration quote — ask for the exact chip, RAM, and disk before you commit. Access to the VM is through the browser console; the desktop terminal is where you run every command in this guide, and the plan has no GPU acceleration or audio output, so transcodes that need those belong on an i3 or M-series host.