Zum Inhalt

scripts/shoot-appstore-ui-video.sh

#!/usr/bin/env bash
# Records an App Store preview video that shows the app IN USE: a UI test (PhysarumUITests,
# PhysarumPreview scheme) taps through editor → randomize → library → capture while simctl
# records the simulator. The raw recording is trimmed (drop the build/launch settle) and
# cover-scaled to Apple's exact accepted preview size. Output kept separate from the existing
# preview.mp4 until reviewed: docs/appstore/app-previews/<device>/preview-ui.mp4
#
# DEFAULT trims TRIM=6 s off the front (build + app launch). After reviewing the result, adjust
# TRIM so the footage starts right as the pattern is alive. Usage: [TRIM=6] scripts/shoot-appstore-ui-video.sh [iphone-6.9 ipad-13]
# Requires: Xcode + simulators + ffmpeg. Run with NO other xcodebuild active.
set -euo pipefail
cd "$(dirname "$0")/.."

SCHEME="PhysarumPreview"
PROJ="apple/PhysarumApp/Physarum.xcodeproj"
PREV="docs/appstore/app-previews"
SCRATCH="${TMPDIR:-/tmp}"
# TRIM drops the build/launch settle off the front (the app becomes visible ~11s in). SPEED
# gently compresses the ~35s interaction window into Apple's <=30s limit (1.27x is barely
# perceptible). Re-calibrate both by inspecting the raw .mov if the test timing changes.
TRIM="${TRIM:-11}"
SPEED="${SPEED:-1.27}"

command -v ffmpeg >/dev/null || { echo "error: ffmpeg not found (brew install ffmpeg)" >&2; exit 1; }
( cd apple/PhysarumApp && xcodegen generate )

record_device() {
  local sim="$1" dir="$2" pvW="$3" pvH="$4" udid dd raw rec
  echo "==> $dir"
  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 sim /$sim/"; return 0; }
  xcrun simctl boot "$udid" 2>/dev/null || true
  xcrun simctl bootstatus "$udid" -b
  dd="$SCRATCH/physarum-uivideo-$dir"
  echo "  build-for-testing…"
  xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" build-for-testing >/dev/null 2>&1
  raw="$SCRATCH/physarum-uivideo-raw-$dir.mov"
  rm -f "$raw"
  echo "  recording while the UI test drives the app…"
  xcrun simctl io "$udid" recordVideo --codec=h264 --force "$raw" &
  rec=$!
  sleep 2   # let the recorder actually start before the test runner launches
  local t0 t1
  t0=$(date +%s)
  xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" test-without-building 2>&1 \
    | grep -iE "test case .* (passed|failed)|with [0-9]+ failure|error:" || true
  t1=$(date +%s)
  echo "  ui test ran for $((t1 - t0))s"
  kill -INT "$rec" 2>/dev/null || true
  wait "$rec" 2>/dev/null || true
  # Wait for simctl to finish flushing the .mov to disk before ffmpeg reads it (the cause of the
  # earlier 1.7s clip: ffmpeg ran while the file was still tiny). Poll until the size settles.
  local prev=-1 cur=0 i
  for i in $(seq 1 25); do
    cur=$(stat -f%z "$raw" 2>/dev/null || echo 0)
    [ "$cur" = "$prev" ] && [ "$cur" -gt 100000 ] && break
    prev=$cur; sleep 1
  done
  echo "  raw recording: $(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1 "$raw" 2>/dev/null)s, $(( cur / 1024 ))KB"
  mkdir -p "$PREV/$dir"
  echo "  transcoding ${pvW}x${pvH} (trim ${TRIM}s front, ${SPEED}x speed)…"
  local silent="$SCRATCH/physarum-uivideo-silent-$dir.mp4"
  ffmpeg -y -loglevel error -ss "$TRIM" -i "$raw" \
    -vf "setpts=PTS/${SPEED},scale=${pvW}:${pvH}:force_original_aspect_ratio=increase,crop=${pvW}:${pvH},fps=30" \
    -t 30 -c:v libx264 -profile:v high -pix_fmt yuv420p -movflags +faststart -an "$silent"
  # App Store Connect rejects a preview with NO audio track ("unsupported or corrupt audio
  # content"). Mux in a silent AAC stereo 48kHz track — video copied through, lossless.
  ffmpeg -y -loglevel error -i "$silent" -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=48000 \
    -c:v copy -c:a aac -b:a 256k -ar 48000 -shortest -movflags +faststart "$PREV/$dir/preview-ui.mp4"
  rm -f "$silent"
  echo "  wrote $PREV/$dir/preview-ui.mp4"
  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-ui.mp4" | sed 's/^/    /'
}

for d in ${@:-iphone-6.9}; do
  case "$d" in
    iphone-6.9) record_device "iPhone 1[5-9] Pro Max" "iphone-6.9" 886 1920 ;;
    ipad-13)    record_device "iPad Pro 13-inch"      "ipad-13"    1200 1600 ;;
    *) echo "unknown device: $d" >&2 ;;
  esac
done
echo "Done. Review $PREV/*/preview-ui.mp4 (then rename to preview.mp4 to use, or adjust TRIM and re-run)."