#!/usr/bin/env bash
#
# build-ffmpeg.sh — build the ffmpeg/ffprobe binaries Snitt.app ships, from official source.
#
# ############################################################################
# #  LGPL ONLY. NEVER ADD --enable-gpl, --enable-nonfree OR --enable-version3.
# #
# #  Snitt ships these binaries inside a closed-source app. That is only legal
# #  under the LGPL, and only while the build stays LGPL:
# #
# #    * --enable-gpl      pulls in GPL code (libx264, libx265, most of the
# #                        postproc/filters marked GPL) and would force us to
# #                        release Snitt's own source. Do not add it.
# #    * --enable-nonfree  produces a binary that may not be redistributed AT
# #                        ALL. Do not add it.
# #    * --enable-version3 upgrades the licence to LGPL v3. Our shipped
# #                        LICENSE.md is LGPL 2.1. Do not add it without
# #                        changing that file too.
# #
# #  If you need a codec that only exists behind one of those flags, the answer
# #  is a VideoToolbox codec or a permissively-licensed library (BSD/ISC/MIT),
# #  not the flag. `verify_license` at the bottom of this script fails the build
# #  if a GPL/nonfree flag ever sneaks in.
# ############################################################################
#
# Reproducible end to end: fetch -> verify SHA-256 -> configure (flags in one
# array) -> build arm64 -> build x86_64 -> lipo -> strip -> print the licence
# banner out of the binary that was just built.
#
# Output: frontend/Snitt/vendor/ffmpeg/{ffmpeg,ffprobe}  (gitignored — ~85 MB)
#         frontend/Snitt/vendor/ffmpeg/VERSION           (gitignored, provenance)
# The Xcode "Bundle ffmpeg" script phase copies those into the .app and signs
# them. Missing binaries are a warning there, not an error, so a fresh clone
# still builds and falls back to a PATH/Homebrew ffmpeg.
#
# Usage:  scripts/build-ffmpeg.sh [--arm64-only] [--jobs N] [--keep-work]

set -euo pipefail

# ---------------------------------------------------------------------------
# The pin. Bumping ffmpeg = change these two lines and re-run.
# ---------------------------------------------------------------------------
# 8.1.2 and not the newer 9.0.x on purpose: `FFmpegRunner.version` is
# "ffmpeg/8.1" and every measurement the client has ever uploaded
# (`meta.ffmpeg_version`) was produced by an 8.1.x binary. silencedetect and
# scdet boundaries are inputs to server-side seam placement, so a major bump is
# a deliberate re-measure, not a packaging detail. 9.0.1 shipped 2026-08-12 —
# the day this was written — which is another reason to not be first.
FFMPEG_VERSION="8.1.2"
FFMPEG_SHA256="464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c"
FFMPEG_URL="https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"

# dav1d — the AV1 decoder, and NOT optional.
#
# ffmpeg has no native AV1 software decoder: its built-in "av1" decoder is a
# hwaccel shell, so an ffmpeg without dav1d decodes exactly zero AV1 frames.
# Measured on the first build of this script against a real corpus video
# (Ali Abdaal, Monthly Favourites #7): 0 of 19258 frames decoded, 43051 error
# lines, and the media-integrity gate correctly called the file "damaged" —
# a healthy file the app would have refused to import. YouTube serves AV1 for
# most modern uploads, so the A-side corpus is full of it.
#
# Licence: dav1d is BSD-2-Clause, which is LGPL-compatible and does NOT taint
# the build. (The GPL alternative — libaom's decoder is fine too, but dav1d is
# both faster and permissive, so there is no reason to go near anything else.)
DAV1D_VERSION="1.5.4"
DAV1D_SHA256="686616b7c69eb88d44459391ab25cac13b6647a3b288835c5784e71c1514a5c5"
DAV1D_URL="https://downloads.videolan.org/pub/videolan/dav1d/${DAV1D_VERSION}/dav1d-${DAV1D_VERSION}.tar.xz"

# The app's deployment target. Both slices must be able to run on it.
export MACOSX_DEPLOYMENT_TARGET="15.0"

# configure bakes --prefix into the binary's own `-version` output, and we never
# run `make install`, so a real path here would only publish whoever's home
# directory built it and make the configuration line differ machine to machine.
# A fixed fake prefix keeps the banner byte-identical across build hosts.
FAKE_PREFIX="/opt/snitt/ffmpeg"

# ---------------------------------------------------------------------------
# The configure flags, in ONE array so the whole licensing/feature decision is
# readable in one place.
# ---------------------------------------------------------------------------
#
# What the app actually asks ffmpeg to do (audited Aug 12, 2026 across
# FFmpegRunner, MeasurementsExporter, MediaValidator, HeadlessRunner —
# MP4Renderer and SplicePopCheck use AVFoundation, not ffmpeg, so no video
# ENCODER is needed anywhere):
#
#   1. ffprobe -show_format -show_streams -print_format json      (any container)
#   2. ffmpeg -i X -vn -ac 1 -ar 16000 -c:a pcm_s16le out.wav     (ASR audio)
#   3. ffmpeg -i X -vn -ac 1 -ar 16000 -c:a aac -movflags +faststart out.m4a
#   4. ffmpeg -i X -an -vf scdet,metadata=print:file=- -f null -  (cut detection)
#   5. ffmpeg -i X -an -progress pipe:1 -f null -                 (decode integrity)
#   6. ffmpeg -i X -vn -af silencedetect -f null -                (silences)
#
# So: EVERY demuxer and EVERY decoder stays on (raw creator footage is whatever
# their camera wrote, and published downloads are whatever YouTube served), and
# the encoder/muxer/protocol sets are cut to exactly what those six lines need.
FFMPEG_CONFIGURE_FLAGS=(
  # -- licence: the absence of --enable-gpl / --enable-nonfree / --enable-version3
  #    is the whole point. See the banner at the top of this file.

  # -- nothing is auto-detected. Without this, configure silently links whatever
  #    Homebrew happens to have installed (openssl, dav1d, x264 …) and the build
  #    stops being reproducible — and could stop being LGPL.
  --disable-autodetect

  # -- system libraries we DO want, named explicitly. All ship with macOS, all
  #    are permissively licensed, all are needed for ordinary container/codec
  #    support (matroska header compression, mov/zlib atoms, png, subtitles).
  --enable-zlib
  --enable-bzlib
  --enable-iconv
  # --disable-autodetect turns the feature ON without running configure's probe,
  # and the probe is what would have appended -liconv. On macOS iconv lives in a
  # separate library, so without this the link fails on _iconv_open.
  --extra-libs=-liconv
  # NOT --enable-lzma: the macOS SDK ships liblzma the dylib but not lzma.h, so
  # configure cannot find it and enabling it would mean vendoring xz. What it
  # buys is LZMA-compressed TIFF and one Matroska header-compression mode —
  # neither of which any camera or YouTube download produces.

  # -- Apple hardware paths. VideoToolbox is the h264/hevc/prores/av1 hwaccel
  #    that makes a full-file decode of a 4K camera clip cheap; AudioToolbox
  #    adds Apple's AAC decoders.
  --enable-videotoolbox
  --enable-audiotoolbox

  # -- the one external library. BSD-2-Clause, statically linked, built by this
  #    same script from a pinned tarball. Without it AV1 does not decode AT ALL
  #    (see the DAV1D_VERSION comment above).
  --enable-libdav1d

  --enable-pthreads
  --enable-static
  --disable-shared          # self-contained binaries; no dylibs to relocate or sign

  # -- programs. ffplay needs SDL and we never play through ffmpeg.
  --enable-ffmpeg
  --enable-ffprobe
  --disable-ffplay
  --disable-sdl2

  # -- capture/display devices. The app never records; avdevice would drag in
  #    AVFoundation capture and CoreGraphics screen capture, both of which cost
  #    us a privacy prompt we have no use for.
  --disable-avdevice
  --disable-devices
  --disable-indevs
  --disable-outdevs

  # -- no wire protocols. Every input is a local file the user picked. This also
  #    means the shipped binary cannot open a URL, which is a nice property for
  #    something that gets pointed at untrusted media.
  --disable-network
  --disable-protocols
  --enable-protocol=file
  --enable-protocol=pipe
  --enable-protocol=fd

  # -- DECODE STAYS WIDE. Do not trim this. Every demuxer, every decoder, every
  #    parser and every bitstream filter stays enabled, because "raw footage" is
  #    whatever the creator's camera, phone, screen recorder or YouTube download
  #    produced, and a missing decoder is a file the user simply cannot import.

  # -- encode is narrow, because the app only ever writes audio through ffmpeg.
  #    wrapped_avframe and pcm_s16le are what the `null` muxer defaults to for
  #    video/audio, so `-f null -` (used by three of the six invocations above)
  #    does not work without them.
  --disable-encoders
  --enable-encoder=aac
  --enable-encoder=pcm_s16le
  --enable-encoder=pcm_s24le
  --enable-encoder=pcm_f32le
  --enable-encoder=wrapped_avframe
  --enable-encoder=rawvideo
  --enable-encoder=png
  --enable-encoder=mjpeg

  --disable-muxers
  --enable-muxer=null
  --enable-muxer=wav
  --enable-muxer=mov
  --enable-muxer=mp4
  --enable-muxer=ipod          # the .m4a container for the compressed ASR upload
  --enable-muxer=adts
  --enable-muxer=rawvideo
  --enable-muxer=image2
  --enable-muxer=pcm_s16le

  # -- docs. Nothing in the app reads them and they are half the install.
  --disable-doc
  --disable-htmlpages
  --disable-manpages
  --disable-podpages
  --disable-txtpages

  --disable-debug
  --enable-optimizations
)

# ---------------------------------------------------------------------------

ARM64_ONLY=0
KEEP_WORK=0
# Re-run the licence and capability gates against whatever is already in
# vendor/ffmpeg. Useful after tightening a check, and as a pre-release audit.
VERIFY_ONLY=0
JOBS="$(sysctl -n hw.ncpu)"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --arm64-only) ARM64_ONLY=1; shift ;;
    --keep-work)  KEEP_WORK=1; shift ;;
    --verify-only) VERIFY_ONLY=1; shift ;;
    --jobs)       JOBS="$2"; shift 2 ;;
    -h|--help)    sed -n '1,40p' "$0"; exit 0 ;;
    *) echo "unknown flag: $1" >&2; exit 2 ;;
  esac
done

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEST_DIR="${REPO_ROOT}/frontend/Snitt/vendor/ffmpeg"
WORK_DIR="${REPO_ROOT}/.build/ffmpeg"
SRC_DIR="${WORK_DIR}/ffmpeg-${FFMPEG_VERSION}"
DAV1D_SRC_DIR="${WORK_DIR}/dav1d-${DAV1D_VERSION}"

log()  { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }

# --- 0. the licence guard, run against the flags before anything is built ----
verify_flags_are_lgpl() {
  local flag
  for flag in "${FFMPEG_CONFIGURE_FLAGS[@]}"; do
    case "$flag" in
      --enable-gpl|--enable-nonfree|--enable-version3|--enable-libx264|--enable-libx265|--enable-libxvid|--enable-libfdk-aac|--enable-libsmbclient)
        die "FORBIDDEN FLAG '${flag}'. Snitt ships ffmpeg inside a closed-source app; that is only legal under the LGPL. Read the banner at the top of this script."
        ;;
    esac
  done
  log "licence pre-check: no GPL/nonfree flags in the configure array"
}

# --- 1. prerequisites --------------------------------------------------------
check_prerequisites() {
  command -v clang >/dev/null || die "clang not found — install the Xcode command line tools"
  command -v curl  >/dev/null || die "curl not found"
  command -v lipo  >/dev/null || die "lipo not found"
  if [[ "$ARM64_ONLY" -eq 0 ]] && ! command -v nasm >/dev/null; then
    die "nasm not found — the x86_64 slice needs it for the SIMD kernels. 'brew install nasm', or re-run with --arm64-only."
  fi
  command -v meson >/dev/null || die "meson not found — dav1d needs it. 'brew install meson ninja'."
  command -v ninja >/dev/null || die "ninja not found — dav1d needs it. 'brew install meson ninja'."
  command -v pkg-config >/dev/null || die "pkg-config not found — ffmpeg finds dav1d through it. 'brew install pkg-config'."
  xcrun --show-sdk-path >/dev/null || die "no macOS SDK"
}

# --- 2. fetch + verify -------------------------------------------------------
# fetch_and_unpack <url> <expected-sha256> <expected-dir>
# The SHA check is a hard stop, not a warning: an unverified tarball is the one
# way a GPL — or worse — payload could get into a binary we then sign and ship.
fetch_and_unpack() {
  local url="$1" expected="$2" dir="$3"
  local tarball="${WORK_DIR}/$(basename "$url")"

  mkdir -p "$WORK_DIR"
  if [[ -f "$tarball" ]]; then
    log "tarball already present: $(basename "$tarball")"
  else
    log "fetching ${url}"
    curl -fL --retry 3 --output "${tarball}.partial" "$url"
    mv "${tarball}.partial" "$tarball"
  fi

  local actual
  actual="$(shasum -a 256 "$tarball" | awk '{print $1}')"
  if [[ "$actual" != "$expected" ]]; then
    die "SHA-256 mismatch for $(basename "$tarball")
    expected: ${expected}
    actual:   ${actual}
  Refusing to build. Either the download is corrupt or the pin is wrong."
  fi
  log "SHA-256 verified: $(basename "$tarball")"

  rm -rf "$dir"
  tar -xf "$tarball" -C "$WORK_DIR"
  [[ -d "$dir" ]] || die "expected ${dir} after unpacking"
}

fetch_source() {
  fetch_and_unpack "$FFMPEG_URL" "$FFMPEG_SHA256" "$SRC_DIR"
  fetch_and_unpack "$DAV1D_URL"  "$DAV1D_SHA256"  "$DAV1D_SRC_DIR"
}

# --- 2b. dav1d, one architecture, static ------------------------------------
# Built into its own prefix and handed to ffmpeg's configure through
# PKG_CONFIG_PATH, so the ffmpeg build is otherwise untouched.
build_dav1d() {
  local arch="$1"
  local build_dir="${WORK_DIR}/dav1d-build-${arch}"
  local prefix
  prefix="$(dav1d_prefix "$arch")"
  local -a setup=()

  rm -rf "$build_dir" "$prefix"

  if [[ "$arch" != "$(uname -m)" ]]; then
    # meson cannot be told "-arch x86_64" through flags alone; it wants a cross
    # file naming the compiler and the host machine.
    local cross="${WORK_DIR}/meson-cross-${arch}.ini"
    cat > "$cross" <<EOF
[binaries]
c = ['clang', '-arch', '${arch}']
cpp = ['clang++', '-arch', '${arch}']
ar = 'ar'
strip = 'strip'
nasm = 'nasm'

[host_machine]
system = 'darwin'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'
EOF
    setup+=(--cross-file "$cross")
  fi

  log "building dav1d ${DAV1D_VERSION} for ${arch}"
  meson setup "$build_dir" "$DAV1D_SRC_DIR" \
    --prefix="$prefix" \
    --buildtype=release \
    --default-library=static \
    -Denable_tools=false \
    -Denable_tests=false \
    ${setup[@]+"${setup[@]}"} \
    > "${WORK_DIR}/dav1d-setup-${arch}.log" 2>&1 \
    || { tail -30 "${WORK_DIR}/dav1d-setup-${arch}.log" >&2; die "meson setup failed for dav1d ${arch}"; }

  ninja -C "$build_dir" install > "${WORK_DIR}/dav1d-build-${arch}.log" 2>&1 \
    || { tail -30 "${WORK_DIR}/dav1d-build-${arch}.log" >&2; die "dav1d build failed for ${arch}"; }

  [[ -f "${prefix}/lib/pkgconfig/dav1d.pc" ]] || die "dav1d ${arch}: no dav1d.pc in ${prefix}"
}

dav1d_prefix() { printf '%s/dav1d-install-%s' "$WORK_DIR" "$1"; }

# --- 3. build one architecture ----------------------------------------------
build_arch() {
  local arch="$1"
  local build_dir="${WORK_DIR}/build-${arch}"
  local -a extra=()

  rm -rf "$build_dir"
  mkdir -p "$build_dir"

  build_dav1d "$arch"
  local dav1d_prefix
  dav1d_prefix="$(dav1d_prefix "$arch")"
  # ONLY our own prefix: an empty PKG_CONFIG_LIBDIR stops pkg-config falling back
  # to Homebrew's /opt/homebrew/lib/pkgconfig, which is where an accidental
  # GPL-licensed dependency would come from.
  export PKG_CONFIG_PATH="${dav1d_prefix}/lib/pkgconfig"
  export PKG_CONFIG_LIBDIR="${dav1d_prefix}/lib/pkgconfig"

  if [[ "$arch" != "$(uname -m)" ]]; then
    extra+=(
      --enable-cross-compile
      --arch=x86_64
      --cpu=x86-64
      --target-os=darwin
      --cc="clang -arch x86_64"
      --cxx="clang++ -arch x86_64"
      --host-cc=clang
      --extra-cflags="-arch x86_64"
      --extra-ldflags="-arch x86_64"
      --x86asmexe=nasm
    )
  else
    extra+=(--cc=clang --cxx=clang++)
  fi

  log "configuring ${arch}"
  (
    cd "$build_dir"
    "${SRC_DIR}/configure" \
      --prefix="$FAKE_PREFIX" \
      --pkg-config-flags=--static \
      "${FFMPEG_CONFIGURE_FLAGS[@]}" \
      "${extra[@]}" \
      > configure.log 2>&1 || { tail -40 configure.log >&2; die "configure failed for ${arch} (full log: ${build_dir}/configure.log)"; }
  )

  log "building ${arch} with -j${JOBS} (this takes a few minutes)"
  ( cd "$build_dir" && make -j"$JOBS" > build.log 2>&1 ) \
    || { tail -40 "${build_dir}/build.log" >&2; die "make failed for ${arch} (full log: ${build_dir}/build.log)"; }

  for tool in ffmpeg ffprobe; do
    [[ -f "${build_dir}/${tool}" ]] || die "${arch}: ${tool} was not produced"
  done
  log "${arch} done: $(cd "$build_dir" && ls -lh ffmpeg ffprobe | awk '{print $9"="$5}' | tr '\n' ' ')"
}

# --- 4. combine + strip ------------------------------------------------------
combine() {
  mkdir -p "$DEST_DIR"
  local tool
  for tool in ffmpeg ffprobe; do
    local -a slices=("${WORK_DIR}/build-arm64/${tool}")
    [[ "$ARM64_ONLY" -eq 0 ]] && slices+=("${WORK_DIR}/build-x86_64/${tool}")

    if [[ ${#slices[@]} -gt 1 ]]; then
      log "lipo ${tool} (${#slices[@]} slices)"
      lipo -create "${slices[@]}" -output "${DEST_DIR}/${tool}"
    else
      cp "${slices[0]}" "${DEST_DIR}/${tool}"
    fi

    # -S drops debug symbols, -x drops non-global symbols. Signing happens later
    # (in the Xcode copy phase), so stripping here invalidates nothing.
    strip -S -x "${DEST_DIR}/${tool}"
    chmod 755 "${DEST_DIR}/${tool}"
  done
}

# --- 5. prove what we built --------------------------------------------------
verify_built_binaries() {
  local tool banner
  local built_version=""

  for tool in ffmpeg ffprobe; do
    local path="${DEST_DIR}/${tool}"
    [[ -x "$path" ]] || die "${path} missing or not executable"

    # The binary must be able to run on THIS machine to be inspected; the
    # x86_64 slice is checked structurally by lipo instead.
    banner="$("$path" -hide_banner -version 2>&1)"

    local config_line
    config_line="$(printf '%s\n' "$banner" | grep '^configuration:' || true)"
    [[ -n "$config_line" ]] || die "${tool}: no configuration line in -version output"

    if printf '%s' "$config_line" | grep -q -- '--enable-gpl'; then
      die "${tool} WAS BUILT WITH --enable-gpl. This binary must not ship. ${config_line}"
    fi
    if printf '%s' "$config_line" | grep -q -- '--enable-nonfree'; then
      die "${tool} WAS BUILT WITH --enable-nonfree. This binary must not ship."
    fi
    if printf '%s' "$config_line" | grep -q -- '--enable-version3'; then
      die "${tool} was built with --enable-version3 (LGPL v3). Our shipped LICENSE.md is LGPL 2.1."
    fi

    # ffmpeg prints its own verdict; "LGPL version 2.1 or later" is the string
    # we need and "GPL" appearing anywhere here is a failure.
    local license_line
    license_line="$("$path" -hide_banner -L 2>&1 | head -3 | tr '\n' ' ')"
    printf '%s' "$license_line" | grep -qi 'GNU Lesser General Public License' \
      || die "${tool}: -L did not report the LGPL. Got: ${license_line}"
    printf '%s' "$license_line" | grep -qi 'GNU General Public License version 2' \
      && die "${tool}: -L reports the GPL. This binary must not ship."

    built_version="$(printf '%s\n' "$banner" | head -1 | awk '{print $3}')"
    [[ "$built_version" == "$FFMPEG_VERSION" ]] \
      || die "${tool} reports version '${built_version}', expected '${FFMPEG_VERSION}'"

    printf '\n'
    log "${tool}"
    printf '    version : %s\n' "$(printf '%s\n' "$banner" | head -1)"
    printf '    archs   : %s\n' "$(lipo -archs "$path")"
    printf '    size    : %s\n' "$(du -h "$path" | awk '{print $1}')"
    printf '    licence : %s\n' "$(printf '%s' "$license_line" | tr -s ' ')"
    printf '    config  : %s\n' "$config_line"
  done

  # Line one is parsed by BundledTools.bundledVersion for the About panel.
  cat > "${DEST_DIR}/VERSION" <<EOF
ffmpeg ${FFMPEG_VERSION}
source   ${FFMPEG_URL}
sha256   ${FFMPEG_SHA256}
dav1d    ${DAV1D_VERSION} (BSD-2-Clause, static)
dav1d_source ${DAV1D_URL}
dav1d_sha256 ${DAV1D_SHA256}
built    $(date -u '+%Y-%m-%dT%H:%M:%SZ')
archs    $(lipo -archs "${DEST_DIR}/ffmpeg")
licence  LGPL 2.1 or later (no --enable-gpl, no --enable-nonfree, no --enable-version3)
recipe   scripts/build-ffmpeg.sh
EOF
  log "wrote ${DEST_DIR}/VERSION"
}


# --- 5b. prove it can still do the job --------------------------------------
# Trimming a build is how you accidentally ship an ffmpeg that opens a file and
# decodes nothing. That already happened once here: the first build of this
# script had no dav1d, and ffmpeg's built-in "av1" decoder is a hwaccel shell,
# so a real corpus video decoded 0 of 19258 frames and the media-integrity gate
# called a healthy file corrupt. Every capability the app depends on is asserted
# by name from here on.
verify_capabilities() {
  local ffmpeg="${DEST_DIR}/ffmpeg"
  local ffprobe="${DEST_DIR}/ffprobe"
  local decoders filters muxers encoders demuxers item

  decoders="$("$ffmpeg" -hide_banner -decoders 2>/dev/null)"
  filters="$("$ffmpeg" -hide_banner -filters 2>/dev/null)"
  muxers="$("$ffmpeg" -hide_banner -muxers 2>/dev/null)"
  encoders="$("$ffmpeg" -hide_banner -encoders 2>/dev/null)"
  demuxers="$("$ffmpeg" -hide_banner -demuxers 2>/dev/null)"

  # AV1 must go through dav1d. Checking for the word "av1" is not enough — the
  # useless hwaccel-only decoder is called that too.
  printf '%s' "$decoders" | grep -q 'libdav1d' \
    || die "no libdav1d decoder. AV1 files will decode zero frames and be reported as corrupt."

  # The codecs raw creator footage and YouTube downloads actually arrive in.
  for item in h264 hevc prores dnxhd mpeg4 vp8 vp9 mjpeg aac mp3 opus vorbis flac pcm_s16le; do
    printf '%s' "$decoders" | grep -qE "^ *[A-Za-z.]+ +${item} " \
      || die "decoder '${item}' is missing — that is a file class the user cannot import."
  done

  for item in mov,mp4,m4a,3gp,3g2,mj2 matroska,webm avi mpegts wav mp3 flac ogg; do
    printf '%s' "$demuxers" | grep -qE "^ *[A-Za-z.]+ +${item} " \
      || die "demuxer '${item}' is missing."
  done

  # The three filters the app names explicitly, plus the ones ffmpeg inserts for
  # -ac/-ar.
  for item in scdet metadata silencedetect aresample aformat anull null format scale; do
    printf '%s' "$filters" | grep -qE "^ *[A-Za-z.]+ +${item} " \
      || die "filter '${item}' is missing — one of the six ffmpeg invocations will fail."
  done

  for item in aac pcm_s16le wrapped_avframe; do
    printf '%s' "$encoders" | grep -qE "^ *[A-Za-z.]+ +${item} " \
      || die "encoder '${item}' is missing."
  done

  for item in null wav mp4 ipod; do
    printf '%s' "$muxers" | grep -qE "^ *[A-Za-z.]+ +${item} " \
      || die "muxer '${item}' is missing."
  done

  # ffprobe has to answer the exact question C0 asks it.
  "$ffprobe" -v error -print_format json -show_format -show_streams "$ffmpeg" >/dev/null 2>&1 \
    || true   # a Mach-O is not media; we only care that the option parsing works
  "$ffprobe" -h >/dev/null 2>&1 || die "ffprobe cannot even print help"

  log "capability check passed (dav1d AV1 decoder present, all named codecs/filters/muxers built)"
}

# --- run ---------------------------------------------------------------------
verify_flags_are_lgpl
if [[ "$VERIFY_ONLY" -eq 1 ]]; then
  verify_capabilities
  verify_built_binaries
  exit 0
fi
check_prerequisites
fetch_source
build_arch arm64
if [[ "$ARM64_ONLY" -eq 0 ]]; then
  build_arch x86_64
else
  warn "--arm64-only: the shipped app will not run ffmpeg on Intel Macs."
fi
combine
verify_capabilities
verify_built_binaries

if [[ "$KEEP_WORK" -eq 0 ]]; then
  rm -rf "${WORK_DIR}/build-arm64" "${WORK_DIR}/build-x86_64" "${WORK_DIR}/install-arm64" "${WORK_DIR}/install-x86_64" "$SRC_DIR"
fi

printf '\n'
log "binaries are in ${DEST_DIR} — rebuild the app to bundle them"
