fix(demo): API35 capture recipe, reown unshallow, deferred recording, device match

Hardening from the first real end-to-end runs against the app:
- build-apk.sh: guard bitbox on the api.aar artifact (not the dir); unshallow
  the reown_flutter tarball so prepare_reown.sh can checkout its pinned commit
  (the tarball ships a shallow repo -> "unable to read tree"); generate reown
  uniffi bindings when absent.
- capture.sh boot: API 35 + `-gpu software -feature -Vulkan`. API 36.1's guest
  encoder is broken (screenrecord err=-38, screencap DMA assert) under both
  swiftshader_indirect and -gpu host; API 35 works.
- capture.sh: defer screenrecord until the app is foregrounded (flutter drive
  builds for minutes; the 180s cap must cover the scenario, not the build);
  match the target device by AVD name so a co-running emulator isn't hijacked;
  record to /data/local/tmp (headless /sdcard FUSE is flaky).
- README: document the API-35 requirement and reasoning.
This commit is contained in:
jwinterm
2026-08-02 21:29:40 -04:00
parent f64ee19b0c
commit 731adb84f1
3 changed files with 114 additions and 31 deletions
+21 -5
View File
@@ -94,12 +94,28 @@ no captions, no title/CTA cards, no music bed. `reel.py` also enforces
Apple's 15–30s duration window and will fail rather than emit an out-of-spec
preview.
## Store sizing
## The capture AVD (both details matter)
The harness creates its own AVD at exactly **1080×1920 (16:9)**. Do not point
it at the stock "Medium Phone" AVD: that is 1080×2400 = **2.22:1**, and Google
Play rejects phone screenshots above **2:1**. `marks.py shots` warns if the
frames it extracts violate that ratio.
Create it as **API 35**, at exactly **1080×1920**:
```bash
avdmanager create avd -n HashBags_Capture \
-k "system-images;android-35;google_apis;x86_64" -d pixel_6
# then in ~/.android/avd/HashBags_Capture.avd/config.ini:
# hw.lcd.width=1080 hw.lcd.height=1920 hw.lcd.density=420
# hw.ramSize=6144 showDeviceFrame=no
```
**Size:** the stock "Medium Phone" AVD is 1080×2400 = **2.22:1**, and Play
rejects phone screenshots above **2:1**. `marks.py shots` warns if extracted
frames violate that ratio.
**API level:** use 35, not 36. On the API 36.1 image the guest MediaCodec AVC
encoder is broken — `screenrecord` fails with `Encoder failed (err=-38)` at
every resolution and writes a 0-byte file, and `screencap` aborts on a
SwiftShader DMA assertion. Verified broken under `swiftshader_indirect` *and*
under `-gpu host` on a real NVIDIA GPU. API 35 + `-gpu software`
+ `-feature -Vulkan` (what `capture.sh boot` uses) works.
iOS App Store screenshots and the App Preview still have to come from a Mac —
see `store/SCREENSHOTS.md` for the fastlane snapshot path.
+12
View File
@@ -133,6 +133,18 @@ if [[ "$SKIP_DEPS" != "1" ]]; then
elif [[ -x scripts/android/build_reown_deps.sh ]]; then
echo "uniffi bindings missing -- generating via build_reown_deps.sh"
echo "(this needs the Rust/uniffi toolchain and is slow on a cold run)"
# The prebuilt tarball unpacks a SHALLOW git repo. prepare_reown.sh then
# does `git fetch -a && git checkout <pinned sha>`, which cannot reach that
# commit through a shallow history and dies with
# fatal: unable to read tree (8a6d79ef...)
# Unshallow in place rather than re-cloning: the tarball also carries
# prebuilt native artifacts that a bare clone would not have.
if [[ -d scripts/reown_flutter/.git ]] && \
[[ "$(git -C scripts/reown_flutter rev-parse --is-shallow-repository)" == "true" ]]; then
echo "unshallowing scripts/reown_flutter so the pinned commit is reachable"
git -C scripts/reown_flutter fetch --unshallow --tags || \
git -C scripts/reown_flutter fetch --depth=2147483647 --tags || true
fi
./scripts/android/build_reown_deps.sh || {
echo
echo "FATAL: could not generate the reown uniffi bindings."
+81 -26
View File
@@ -61,6 +61,9 @@ SCENARIOS=(core_store_set onboarding_create restore_from_seed swap_and_buy)
# On-device path for the in-flight recording.
DEV_CAPTURE=/data/local/tmp/hb-capture.mp4
# Package under capture — used to detect when the app is actually on screen.
APP_PKG="${DEMO_APP_PKG:-com.suchsoftware.hashwallet}"
log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; }
@@ -85,18 +88,40 @@ set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
# ---------------------------------------------------------------------------
# device helpers
# ---------------------------------------------------------------------------
# Wait up to `timeout` seconds for a device to appear. A freshly launched
# emulator takes appreciably longer than a fixed sleep to register with adb,
# and the port varies (5554, 5556, ...) depending on what else is running.
# Wait up to `timeout` seconds for OUR device to appear.
#
# Deliberately matches on AVD name rather than taking the first device adb
# lists: this box routinely has other capture emulators running (VeganIQ etc),
# and grabbing the wrong one silently records somebody else's app. Falls back
# to the sole device only when exactly one is attached.
detect_serial() {
[[ -n "$SERIAL" ]] && return 0
local timeout="${1:-90}" waited=0
local timeout="${1:-90}" waited=0 cand name
while (( waited < timeout )); do
SERIAL="$("$ADB" devices | awk '$2=="device" {print $1; exit}')"
[[ -n "$SERIAL" ]] && { log "device: $SERIAL"; return 0; }
while read -r cand; do
[[ -z "$cand" ]] && continue
name="$("$ADB" -s "$cand" emu avd name 2>/dev/null | head -1 | tr -d '\r')"
if [[ "$name" == "$AVD" ]]; then
SERIAL="$cand"; log "device: $SERIAL (avd $AVD)"; return 0
fi
done < <("$ADB" devices | awk '$2=="device" {print $1}')
# Exactly one device and it is not ours by name (physical phone, say) —
# take it, since there is no ambiguity to get wrong.
local all count
all="$("$ADB" devices | awk '$2=="device" {print $1}')"
count="$(printf '%s\n' "$all" | grep -c . || true)"
if [[ "$count" == "1" ]]; then
SERIAL="$(printf '%s\n' "$all" | head -1)"
log "device: $SERIAL (only device attached)"; return 0
fi
sleep 2; waited=$((waited + 2))
done
die "no device appeared within ${timeout}s. Run: tools/demo/capture.sh boot"
die "no device matching AVD '$AVD' appeared within ${timeout}s.
Attached devices:
$("$ADB" devices | tail -n +2)
Run 'tools/demo/capture.sh boot', or pass --serial <name> explicitly."
}
adbs() { "$ADB" -s "$SERIAL" "$@"; }
@@ -117,14 +142,21 @@ cmd_boot() {
log "a device is already booted"; detect_serial; return 0
fi
log "booting AVD $AVD (headless)"
# -feature -GLDMA: with swiftshader_indirect on a headless host, the guest
# graphics stack otherwise aborts on
# "Assertion failed: !rcEnc->featureInfo()->hasReadColorBufferDma"
# which kills screencap and screenrecord — i.e. exactly the two things this
# harness needs. Disabling the DMA colour-buffer path costs a little capture
# throughput and makes both work.
# These exact flags matter — headless capture is fussy and most combinations
# silently produce nothing:
#
# -gpu software swiftshader_indirect aborts the guest graphics stack
# with "Assertion failed: !rcEnc->featureInfo()
# ->hasReadColorBufferDma", killing screencap.
# -feature -Vulkan without it the guest MediaCodec AVC encoder fails with
# "Encoder failed (err=-38)" at every resolution, so
# screenrecord writes a 0-byte file.
#
# The AVD must also be an **API 35** image. On API 36.1 the encoder is broken
# regardless of GPU flags — verified against both swiftshader_indirect and
# -gpu host on a real NVIDIA GPU. Same recipe as VeganIQ_API35_Capture.
nohup "$EMULATOR_BIN" -avd "$AVD" -no-window -no-audio -no-boot-anim \
-gpu swiftshader_indirect -feature -GLDMA -no-snapshot-save \
-gpu software -feature -Vulkan -no-snapshot -no-snapshot-save \
> "$OUT_ROOT/emulator.log" 2>&1 &
sleep 5
detect_serial
@@ -193,19 +225,37 @@ cmd_record() {
fi
local send_addr="${DEMO_SEND_ADDRESS:-}"
local dev_epoch_start
dev_epoch_start="$(adbs shell date +%s%3N | tr -d '\r')"
log "starting screenrecord"
# /data/local/tmp, not /sdcard: the emulator's emulated-storage FUSE layer
# is flaky headless ("Transport endpoint is not connected"), and screenrecord
# then fails to open its output. /data/local/tmp is a plain filesystem that
# adb can always read and write.
# Arm the recorder to fire the moment the app hits the foreground, NOT now.
#
# `flutter drive` compiles an instrumented APK before it installs anything,
# which can take many minutes. screenrecord caps at 180s, so starting it here
# would burn the entire budget filming the launcher and stop before the app
# ever appeared. Poll for the package becoming the resumed activity, then
# start recording and stamp the device clock at that instant.
#
# /data/local/tmp, not /sdcard: the emulator's emulated-storage FUSE layer is
# flaky headless ("Transport endpoint is not connected") and screenrecord
# then cannot open its output.
adbs shell rm -f "$DEV_CAPTURE" || true
adbs shell screenrecord --size 1080x1920 --bit-rate 12000000 \
--time-limit 180 "$DEV_CAPTURE" &
local stamp_file="$RAW_DIR/$scenario.recstart"
rm -f "$stamp_file"
(
# Wait for the app to be foregrounded (cap the wait so a failed build does
# not leave this poller alive forever).
for _ in $(seq 1 900); do
if "$ADB" -s "$SERIAL" shell dumpsys activity activities 2>/dev/null \
| grep -q "topResumedActivity.*$APP_PKG"; then
"$ADB" -s "$SERIAL" shell date +%s%3N | tr -d '\r' > "$stamp_file"
"$ADB" -s "$SERIAL" shell screenrecord --size 1080x1920 \
--bit-rate 12000000 --time-limit 180 "$DEV_CAPTURE"
exit 0
fi
sleep 1
done
) &
local rec_pid=$!
sleep 2 # let the encoder actually start before the app launches
log "recorder armed — will start when $APP_PKG reaches the foreground"
log "driving scenario: $scenario"
set +e
@@ -230,9 +280,15 @@ cmd_record() {
log "stopping screenrecord"
adbs shell pkill -INT screenrecord || true
kill "$rec_pid" 2>/dev/null || true # in case it never armed
wait "$rec_pid" 2>/dev/null || true
sleep 3 # screenrecord needs a moment to finalise the moov atom
if [[ ! -s "$stamp_file" ]]; then
warn "the recorder never armed — $APP_PKG never reached the foreground."
warn "That usually means the build or install failed; see $RAW_DIR/$scenario.drive.log"
fi
adbs pull "$DEV_CAPTURE" "$RAW_DIR/$scenario.mp4" >/dev/null \
|| die "could not pull recording — did screenrecord start?"
adbs shell rm -f "$DEV_CAPTURE" || true
@@ -244,7 +300,6 @@ cmd_record() {
else
warn "no driver output — captions and screenshots will be unavailable"
fi
echo "$dev_epoch_start" > "$RAW_DIR/$scenario.recstart"
exit_demo_mode
if [[ $drive_rc -ne 0 ]]; then