Files
arr/scripts/make-probe-fixtures.sh
Miguel Palhas e852f42a9c feat(arr): name subtitle codecs and dispositions
SubtitleTrack now carries the codec, split text formats from bitmap
ones per DESIGN.md §15, plus the forced and SDH dispositions ffprobe
reports. Without the forced flag a file carrying only a forced track
read as satisfied for that language.
2026-08-24 22:32:01 +01:00

177 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regenerate the ffprobe fixture clips in crates/arr-probe/tests/fixtures.
#
# The clips are committed (a few KB each, DESIGN.md §12) so the test suite
# never needs ffmpeg. This script exists to document how they were made and to
# rebuild them if the set ever needs to change.
#
# The two Dolby Vision clips cannot be encoded directly: x265 refuses to write
# a DV stream without an RPU file. Instead a real HEVC clip is produced and the
# `dvcC`/`dvvC` configuration box is spliced into its sample entry, which is
# exactly what ffprobe reads to report `dv_profile`.
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
out="$root/crates/arr-probe/tests/fixtures"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$out"
# 1080p feature: three audio tracks covering all three §5.2 signals, plus
# three subtitle tracks — two plain text, one carrying §15's forced
# disposition. Three seconds, so the runtime sanity check has something to
# compare against.
printf '1\n00:00:00,000 --> 00:00:01,000\nhello\n\n' > "$work/en.srt"
printf '1\n00:00:00,000 --> 00:00:01,000\nolá\n\n' > "$work/pt.srt"
printf '1\n00:00:00,500 --> 00:00:01,500\nSIGN\n\n' > "$work/forced.srt"
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=black:s=1920x1080:r=24:d=3" \
-f lavfi -i "sine=frequency=440:duration=3" \
-f lavfi -i "sine=frequency=660:duration=3" \
-f lavfi -i "sine=frequency=880:duration=3" \
-i "$work/en.srt" -i "$work/pt.srt" -i "$work/forced.srt" \
-map 0:v -map 1:a -map 2:a -map 3:a -map 4:s -map 5:s -map 6:s \
-c:v libx264 -preset ultrafast -crf 51 -pix_fmt yuv420p \
-c:a aac -b:a 8k -c:s srt \
-metadata:s:a:0 language=eng -metadata:s:a:0 title="English" \
-metadata:s:a:1 language=por -metadata:s:a:1 title="Português (Brasil)" \
-metadata:s:a:1 handler_name="Portuguese (Brazil)" \
-metadata:s:a:2 language=pt-PT \
-metadata:s:s:0 language=eng -metadata:s:s:1 language=por \
-metadata:s:s:2 language=eng -disposition:s:2 forced \
"$out/movie-1080p.mkv"
# The extras reel trap: larger on disk than the feature (noise compresses
# badly) but a third of its runtime.
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "nullsrc=s=640x360:r=12:d=1,geq=random(1)*255:128:128" \
-an -c:v libx264 -preset ultrafast -crf 51 -pix_fmt yuv420p \
"$out/extras-360p.mkv"
# HDR10: bt2020 primaries and the PQ transfer, which is all ffprobe reports.
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=black:s=3840x2160:r=24:d=1" -an \
-vf "format=yuv420p10le,setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc" \
-c:v libx265 -preset ultrafast -crf 51 \
"$out/hdr10-2160p.mkv"
# HLG and SDR both use HEVC here so transfer function, rather than codec bit
# depth, is what distinguishes their probe results.
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=black:s=1280x720:r=24:d=1" -an \
-vf "format=yuv420p10le,setparams=color_primaries=bt2020:color_trc=arib-std-b67:colorspace=bt2020nc" \
-c:v libx265 -preset ultrafast -crf 51 \
"$out/hlg-720p.mkv"
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=black:s=1280x720:r=24:d=1" -an \
-vf "format=yuv420p10le" \
-c:v libx265 -preset ultrafast -crf 51 \
"$out/sdr-10bit-hevc-720p.mkv"
# HEVC base layer for the two Dolby Vision clips.
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=black:s=1280x720:r=24:d=1" -an \
-vf "format=yuv420p10le,setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc" \
-c:v libx265 -preset ultrafast -crf 51 -tag:v hvc1 \
"$work/base.mp4"
cat > "$work/dovi.py" <<'PY'
"""Splice a DolbyVisionConfigurationBox into an MP4 HEVC sample entry."""
import struct
import sys
CONTAINERS = {"moov", "trak", "mdia", "minf", "stbl", "stsd"}
def children(buf, start, end):
boxes = []
off = start
while off + 8 <= end:
size = struct.unpack(">I", buf[off : off + 4])[0]
if size == 0:
size = end - off
boxes.append((off, size, buf[off + 4 : off + 8].decode("latin1")))
off += size
return boxes
def first_child_offset(off, kind):
# stsd carries a version/flags word and an entry count before its children.
return off + (16 if kind == "stsd" else 8)
def find(buf, start, end, path):
for off, size, kind in children(buf, start, end):
if kind != path[0]:
continue
if len(path) == 1:
return off, size
return find(buf, first_child_offset(off, kind), off + size, path[1:])
return None
def configuration(profile, compatibility_id, level=6):
bits = (profile & 0x7F) << 41
bits |= (level & 0x3F) << 35
bits |= 1 << 34 # rpu_present_flag
bits |= 1 << 32 # bl_present_flag
bits |= (compatibility_id & 0xF) << 28
return bytes([1, 0]) + bits.to_bytes(6, "big") + bytes(16)
def grow_ancestors(buf, target, growth):
def walk(start, end):
for off, size, kind in children(buf, start, end):
if not off <= target < off + size:
continue
struct.pack_into(">I", buf, off, size + growth)
if kind in CONTAINERS:
walk(first_child_offset(off, kind), off + size)
return
walk(0, len(buf))
def main(source, destination, profile, compatibility_id):
buf = bytearray(open(source, "rb").read())
entry = find(buf, 0, len(buf), ["moov", "trak", "mdia", "minf", "stbl", "stsd", "hvc1"])
if entry is None:
sys.exit("no hvc1 sample entry")
off, size = entry
payload = configuration(profile, compatibility_id)
kind = b"dvvC" if profile == 5 else b"dvcC"
box = struct.pack(">I", 8 + len(payload)) + kind + payload
buf[off + 4 : off + 8] = b"dvh1"
buf[off + size : off + size] = box
grow_ancestors(buf, off, len(box))
open(destination, "wb").write(bytes(buf))
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]))
PY
python3 "$work/dovi.py" "$work/base.mp4" "$out/dv-profile5.mp4" 5 0
python3 "$work/dovi.py" "$work/base.mp4" "$out/dv-profile8-1.mp4" 8 1
# An audio file with embedded artwork. ffprobe reports the cover as a video
# stream, so this is what stops a soundtrack passing for a feature.
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "color=c=red:s=64x64:d=1" -frames:v 1 "$work/cover.jpg"
ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i "sine=frequency=440:duration=2" -i "$work/cover.jpg" \
-map 0:a -map 1:v -c:a aac -b:a 8k -c:v mjpeg -disposition:v attached_pic \
"$out/audio-with-cover.m4a"
# Not media at all: the thing a multi-file torrent is full of.
printf 'Release notes. Not a video file.\n' > "$out/notes.nfo"
ls -l "$out"