Scroll scrub video helper asset

Read references/scroll-scrub.md first. Copy the fenced source to a temporary scroll-scrub-video.sh file and invoke it with bash.

#!/usr/bin/env bash
# Deterministic boundary-frame extraction and scroll-scrub encoding.

set -euo pipefail

usage() {
  echo "Usage:" >&2
  echo "  $0 bounds <input.mp4> <output-prefix>" >&2
  echo "  $0 desktop <input.mp4> <output.mp4>" >&2
  echo "  $0 mobile <input.mp4> <output.mp4>" >&2
  echo "  $0 poster <input.mp4> <output.png>" >&2
  exit 2
}

require_command() {
  command -v "$1" >/dev/null 2>&1 || {
    echo "Missing required command: $1" >&2
    exit 127
  }
}

require_input() {
  [ -f "$1" ] || {
    echo "Input file does not exist: $1" >&2
    exit 2
  }
}

ensure_parent() {
  local parent
  parent=$(dirname "$1")
  mkdir -p "$parent"
}

[ "$#" -eq 3 ] || usage
require_command ffmpeg

action=$1
input=$2
output=$3
require_input "$input"

case "$action" in
  bounds)
    ensure_parent "$output-first.png"
    ffmpeg -v error -y -ss 0 -i "$input" \
      -frames:v 1 -q:v 2 "$output-first.png"
    # Reverse buffers this short generated clip, making frame 1 the exact final
    # decoded frame instead of a timestamp approximation near the end.
    ffmpeg -v error -y -i "$input" -vf reverse \
      -frames:v 1 -q:v 2 "$output-last.png"
    ;;
  desktop)
    ensure_parent "$output"
    ffmpeg -v error -y -i "$input" -an \
      -vf "unsharp=5:5:0.8:5:5:0.0" \
      -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p \
      -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart "$output"
    ;;
  mobile)
    ensure_parent "$output"
    ffmpeg -v error -y -i "$input" -an \
      -vf "scale=-2:'min(720,ih)',unsharp=5:5:0.6:5:5:0.0" \
      -c:v libx264 -preset slow -crf 23 -pix_fmt yuv420p \
      -g 4 -keyint_min 4 -sc_threshold 0 -movflags +faststart "$output"
    ;;
  poster)
    ensure_parent "$output"
    ffmpeg -v error -y -ss 0 -i "$input" -frames:v 1 -q:v 2 "$output"
    ;;
  *)
    usage
    ;;
esac