Zum Inhalt

scripts/shoot-appstore-media.sh

#!/usr/bin/env bash
# Captures App Store media from the simulator: a 25 s App Preview video per required
# device size (transcoded to Apple's exact accepted App-Preview resolution) PLUS extra
# still frames pulled from the same recording (the pattern at different moments).
#
# Apple App-Preview accepted resolutions (verified 2026-06-21):
#   iPhone 6.9"  -> 886 x 1920 (portrait)
#   iPad 13"     -> 1200 x 1600 (portrait)
#   Apple TV     -> 1920 x 1080 (landscape)
# Duration 15-30 s, <=30 fps, H.264 .mp4/.mov, up to 3 previews per localization.
#
# Output:
#   docs/appstore/app-previews/<device>/preview.mp4   (App-Preview, exact accepted size)
#   docs/appstore/screenshots/<device>/0{2,3}-*.png   (extra stills at screenshot size)
#
# 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"
SHOTS="docs/appstore/screenshots"
FORM_WAIT=10      # let the Metal pattern form before recording
REC_SECS=25       # recording length (within Apple's 15-30 s window)

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-WxH, screenshot-WxH
capture() {
  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-media-$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"
  xcrun simctl launch "$udid" "$BUNDLE_ID"
  sleep "$FORM_WAIT"

  mkdir -p "$PREV/$dir" "$SHOTS/$dir"
  local raw="${TMPDIR:-/tmp}/physarum-raw-$dir.mov"
  rm -f "$raw"
  echo "    recording ${REC_SECS}s…"
  xcrun simctl io "$udid" recordVideo --codec=h264 --force "$raw" &
  local rec=$!
  sleep "$REC_SECS"
  kill -INT "$rec" 2>/dev/null || true
  wait "$rec" 2>/dev/null || true

  # App Preview: cover-scale to the exact accepted size (no stretch), 30 fps, <=28 s, H.264.
  echo "    transcoding preview ${pvW}x${pvH}…"
  ffmpeg -y -loglevel error -i "$raw" \
    -vf "scale=${pvW}:${pvH}:force_original_aspect_ratio=increase,crop=${pvW}:${pvH},fps=30" \
    -t 25 -c:v libx264 -profile:v high -pix_fmt yuv420p -movflags +faststart \
    -an "$PREV/$dir/preview.mp4"

  # Extra stills at native (screenshot) resolution, the pattern at two more moments.
  ffmpeg -y -loglevel error -ss 5  -i "$raw" -frames:v 1 "$SHOTS/$dir/02-still.png"
  ffmpeg -y -loglevel error -ss 18 -i "$raw" -frames:v 1 "$SHOTS/$dir/03-still.png"

  echo "    wrote $PREV/$dir/preview.mp4 + 2 extra stills"
  # Report what we produced.
  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/^/      /'
}

capture "iPhone 1[5-9] Pro Max" "iphone-6.9" "iphonesimulator"   886 1920
capture "iPad Pro 13-inch"      "ipad-13"    "iphonesimulator"   1200 1600
capture "Apple TV 4K"           "appletv"    "appletvsimulator"  1920 1080

echo "Done. App previews in $PREV/*/preview.mp4 (review for taste; replace/re-record as desired)."