#!/usr/bin/env bash
# Builds a single App Preview MONTAGE per device size: ~4 s of each of the 7 curated
# presets, cut together into one ~28 s clip, transcoded to Apple's exact accepted
# App-Preview resolution. Replaces the single-preset preview.mp4.
#
# Requires the DEBUG boot-preset hook (PHYSARUM_BOOT_PRESET=<index>) in AppState — the
# script relaunches the app once per preset to capture it deterministically. Debug
# simulator builds include the hook; Release excludes it.
#
# Accepted App-Preview sizes (verified 2026-06-21): iPhone 886x1920, iPad 1200x1600, tvOS 1920x1080.
# Output: docs/appstore/app-previews/<device>/preview.mp4 (overwrites the single-preset one).
#
# Requires: Xcode + simulators (6.9" iPhone Pro Max, 13" iPad Pro, Apple TV 4K), ffmpeg.
# Run ONLY when no other xcodebuild is active (concurrent builds on the same project race).
set -euo pipefail
cd "$(dirname "$0")/.."
SCHEME="Physarum"
PROJ="apple/PhysarumApp/Physarum.xcodeproj"
BUNDLE_ID="de.jkaindl.physarum"
PREV="docs/appstore/app-previews"
NPRESETS=7 # the 7 curated factory slots (indices 0..6)
FORM=7 # seconds to let each preset's pattern form before recording
CLIP=4 # seconds KEPT per preset (ffmpeg -t) → 7 × 4 = 28 s montage
REC=7 # seconds RECORDED per preset; > CLIP so every device captures ≥ CLIP and
# gets trimmed to exactly CLIP. The tvOS simulator recorder has ~1.7 s
# startup latency (a 4 s window under-captured), so record long, trim uniform.
command -v ffmpeg >/dev/null || { echo "error: ffmpeg not found (brew install ffmpeg)" >&2; exit 1; }
( cd apple/PhysarumApp && xcodegen generate )
# args: name-pattern, dir-label, sdk, preview-W, preview-H
montage() {
local sim="$1" dir="$2" sdk="$3" pvW="$4" pvH="$5"
echo "==> $dir"
local udid
udid=$(xcrun simctl list devices available | grep -E "$sim" | grep -oiE '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' | head -1 || true)
[ -n "$udid" ] || { echo "skip: no simulator matching /$sim/"; return 0; }
xcrun simctl boot "$udid" 2>/dev/null || true
xcrun simctl bootstatus "$udid" -b
local dd="${TMPDIR:-/tmp}/physarum-montage-$dir"
xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" build >/dev/null
local app
app=$(find "$dd/Build/Products" -name 'Physarum.app' -path "*$sdk*" | head -1)
[ -n "$app" ] || { echo "skip: no .app built for $dir"; return 0; }
xcrun simctl install "$udid" "$app"
local tmp="${TMPDIR:-/tmp}/physarum-montage-clips-$dir"
rm -rf "$tmp"; mkdir -p "$tmp"
local list="$tmp/list.txt"; : > "$list"
local i
for i in $(seq 0 $((NPRESETS - 1))); do
echo " preset ${i} of $((NPRESETS - 1))"
local raw="$tmp/raw-$i.mov" attempt=0 dur=0
# The simulator recorder sporadically captures nothing (1 frame) on rapid relaunch+record
# cycles — validate each clip and retry until it has at least CLIP seconds of footage.
while :; do
attempt=$((attempt + 1))
SIMCTL_CHILD_PHYSARUM_BOOT_PRESET="$i" xcrun simctl launch --terminate-running-process "$udid" "$BUNDLE_ID" >/dev/null
sleep "$FORM"
xcrun simctl io "$udid" recordVideo --codec=h264 --force "$raw" &
local rec=$!
sleep "$REC"
kill -INT "$rec" 2>/dev/null || true
wait "$rec" 2>/dev/null || true
sleep 2 # let the recorder finish writing + settle before validating / next launch
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$raw" 2>/dev/null || echo 0)
if awk "BEGIN{exit !(${dur:-0} >= $CLIP)}"; then break; fi
echo " clip $i captured only ${dur}s — retry $attempt"
[ "$attempt" -ge 4 ] && { echo " WARNING: clip $i still short after $attempt tries"; break; }
done
# Normalise each clip to the exact App-Preview size (cover-scale, no stretch), 30 fps.
ffmpeg -y -loglevel error -i "$raw" \
-vf "scale=${pvW}:${pvH}:force_original_aspect_ratio=increase,crop=${pvW}:${pvH},fps=30" \
-t "$CLIP" -c:v libx264 -profile:v high -pix_fmt yuv420p -an "$tmp/clip-$i.mp4"
echo "file '$tmp/clip-$i.mp4'" >> "$list"
done
mkdir -p "$PREV/$dir"
# Re-encode the concatenation into one clean file (robust against per-clip param drift).
ffmpeg -y -loglevel error -f concat -safe 0 -i "$list" \
-c:v libx264 -profile:v high -pix_fmt yuv420p -movflags +faststart "$PREV/$dir/preview.mp4"
echo " wrote $PREV/$dir/preview.mp4 (montage of $NPRESETS presets)"
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,codec_name -show_entries format=duration \
-of default=noprint_wrappers=1 "$PREV/$dir/preview.mp4" | sed 's/^/ /'
}
# Optional device filter: pass dir-labels (e.g. `… appletv`) to capture a subset; no args = all.
WANT="${*:-all}"
want() { case " $WANT " in *" all "*|*" $1 "*) return 0 ;; *) return 1 ;; esac; }
want iphone-6.9 && montage "iPhone 1[5-9] Pro Max" "iphone-6.9" "iphonesimulator" 886 1920
want ipad-13 && montage "iPad Pro 13-inch" "ipad-13" "iphonesimulator" 1200 1600
want appletv && montage "Apple TV 4K" "appletv" "appletvsimulator" 1920 1080
echo "Done. Montage App previews in $PREV/*/preview.mp4 (review for taste)."