174 lines
6.5 KiB
Bash
Executable File
174 lines
6.5 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 two
|
|
# subtitle tracks. 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"
|
|
|
|
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" \
|
|
-map 0:v -map 1:a -map 2:a -map 3:a -map 4:s -map 5: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 \
|
|
"$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"
|