aboutsummaryrefslogtreecommitdiff
path: root/test-rig/record.sh
blob: 82e31dc7d73b49f5cd777401650025fa22f8d1e6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/bin/sh
# Boot the emulator + Mirror, then capture frames via `screencap` and
# inject taps/keys through the app. Frames are PNGs in $TMPDIR/frames;
# the host side stitches them into /work/output.mp4 with ffmpeg.
#
# Why not `screenrecord`? On the AOSP x86_64 system image with a
# software GPU, screenrecord composes the system layer but misses
# SurfaceView's hardware layer - the mirror's decoded surface ends
# up as a black rectangle in the recording. screencap goes through
# a different capture path that does include the SurfaceView, as
# the pixhash check in ./test e2e already verifies.
#
# Source == target: every injected tap/key fires both locally and
# over the wire - visible in the nested mirror, which is the demo.

set -eu

ROOT=/work
# Debug applicationId (note the .debug suffix). Activity classes live in
# the invalid.lena.scrcpy namespace, which carries no suffix.
PKG=invalid.lena.scrcpy.debug
AVD=scrcpy-test
SERIAL=emulator-5554
APK=$ROOT/app/build/outputs/apk/debug/app-debug.apk
TMPDIR=$ROOT/.tools/e2e
FRAMES=$TMPDIR/frames
OUT=$ROOT/output.mp4
FPS=8
mkdir -p "$TMPDIR" "$FRAMES"
rm -f "$FRAMES"/*.png 2>/dev/null || true

log() { printf 'record: %s\n' "$*" >&2; }

# ---- 0. server jar ----
[ -f "$ROOT/app/src/main/assets/scrcpy-server.jar" ] || "$ROOT/scripts/update-server"

# ---- 1. build APK ----
log 'gradle :app:assembleDebug'
"$ROOT/gradlew" --no-daemon -q :app:assembleDebug

# ---- 2. AVD + boot ----
if ! avdmanager list avd | grep -q "Name: $AVD$"; then
    log "creating avd $AVD"
    echo no | avdmanager create avd -n "$AVD" -k 'system-images;android-35;default;x86_64' -d pixel >/dev/null
fi
AVD_DIR="$ANDROID_AVD_HOME/$AVD.avd"
rm -f "$AVD_DIR"/multiinstance.lock "$AVD_DIR"/hardware-qemu.ini.lock \
      "$AVD_DIR"/snapshot.lock.lock "$AVD_DIR"/read-snapshot.txt 2>/dev/null || true
rm -rf "$HOME/.android/avd/running" 2>/dev/null || true

if ! pgrep -f "emulator.*-avd $AVD" >/dev/null; then
    log 'booting emulator'
    emulator -avd "$AVD" -no-window -no-audio -no-snapshot -gpu swiftshader_indirect \
             -no-boot-anim -accel on >"$TMPDIR/emulator.log" 2>&1 &
fi
adb -s "$SERIAL" wait-for-device
until [ "$(adb -s "$SERIAL" shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do
    sleep 2
done

# ---- 3. root + install ----
log 'adb root'
adb -s "$SERIAL" root
adb -s "$SERIAL" wait-for-device
log "install $APK"
adb -s "$SERIAL" install -r "$APK" >/dev/null

# ---- 4. bootstrap adb_keys via the app's own keypair ----
adb -s "$SERIAL" shell am start -W -n "$PKG/invalid.lena.scrcpy.Main" >/dev/null
sleep 1
adb -s "$SERIAL" shell "run-as $PKG cat files/adbkey" > "$TMPDIR/adbkey.der"
[ -s "$TMPDIR/adbkey.der" ] || { log 'adbkey was empty'; exit 1; }
java "$ROOT/test-rig/Keygen.java" "$TMPDIR/adbkey.der" > "$TMPDIR/adb_keys.line"
adb -s "$SERIAL" push "$TMPDIR/adb_keys.line" /sdcard/adb_keys.line >/dev/null
adb -s "$SERIAL" shell "cat /sdcard/adb_keys.line >> /data/misc/adb/adb_keys \
                        && chown system:shell /data/misc/adb/adb_keys \
                        && chmod 640 /data/misc/adb/adb_keys \
                        && rm /sdcard/adb_keys.line"

# Dismiss the "Viewing full screen" system toast that pops up the
# first time an activity goes immersive - otherwise it covers the top
# strip of the mirror in our captures.
adb -s "$SERIAL" shell settings put global policy_control \
    'immersive.full=*' >/dev/null 2>&1 || true

# ---- 5. launch Mirror ----
log 'launching Mirror against 127.0.0.1:5555'
adb -s "$SERIAL" logcat -c
adb -s "$SERIAL" shell am start -n "$PKG/invalid.lena.scrcpy.Mirror" \
        --es host 127.0.0.1 --ei port 5555 >/dev/null

# ---- 6. wait for first frame ----
deadline=$(( $(date +%s) + ${E2E_DEADLINE:-120} ))
while [ "$(date +%s)" -lt "$deadline" ]; do
    if adb -s "$SERIAL" logcat -d -s scrcpy-android | tail -500 | grep -q 'video frame n=1'; then
        ok=1; break
    fi
    sleep 1
done
if [ "${ok:-}" != 1 ]; then
    log 'timed out waiting for first frame; full app log:'
    adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.record.scrcpy" 2>&1 || true
    tail -60 "$TMPDIR/logcat.record.scrcpy" >&2 || true
    exit 1
fi
log 'first frame ok - settling 5 s for the immersive toast to fade'
# The first immersive activity on a fresh AVD draws a top-of-screen
# "Viewing full screen" toast that fades after ~4 s. We deliberately
# do NOT try to dismiss it by injecting touch - anything at y ~= 0
# pulls the notification shade, anything near the bottom hits the nav
# bar, and either ends Mirror's foreground status. Wait it out instead.
sleep 5

# ---- 7. screencap loop in background while we inject events ----
# Mirror's overlay TextViews (target / frame counts / last event)
# live in the regular view hierarchy, so screencap captures them
# fine - the actual decoded video underneath stays black to capture
# but the overlay is the in-test proof.
log "capturing frames @ ${FPS} fps via screencap"
touch "$TMPDIR/recording"
(
    i=0
    while [ -f "$TMPDIR/recording" ]; do
        n=$(printf '%05d' "$i")
        adb -s "$SERIAL" exec-out screencap -p > "$FRAMES/f_$n.png" 2>/dev/null || true
        i=$((i + 1))
    done
) &
CAP_PID=$!
sleep 1

log 'taps'
adb -s "$SERIAL" shell input tap 540 960
sleep 0.6
adb -s "$SERIAL" shell input tap 270 480
sleep 0.6
adb -s "$SERIAL" shell input tap 810 1440
sleep 0.6

log 'swipe'
adb -s "$SERIAL" shell input swipe 200 1000 880 1000 250
sleep 0.6

log 'dpad keys'
for k in KEYCODE_DPAD_RIGHT KEYCODE_DPAD_LEFT KEYCODE_DPAD_DOWN KEYCODE_DPAD_UP; do
    adb -s "$SERIAL" shell input keyevent "$k"
    sleep 0.3
done

log 'alphanumerics'
for k in KEYCODE_S KEYCODE_C KEYCODE_R KEYCODE_C KEYCODE_P KEYCODE_Y; do
    adb -s "$SERIAL" shell input keyevent "$k"
    sleep 0.25
done

sleep 1
adb -s "$SERIAL" shell input tap 540 960
sleep 2

# ---- 8. stop capture, stitch ----
rm -f "$TMPDIR/recording"
wait "$CAP_PID" 2>/dev/null || true

count=$(ls "$FRAMES"/*.png 2>/dev/null | wc -l)
log "captured $count frames"
[ "$count" -gt 0 ] || { log 'no frames captured'; exit 1; }

log "stitching $FRAMES/*.png -> $OUT"
ffmpeg -y -framerate "$FPS" -pattern_type glob -i "$FRAMES/f_*.png" \
       -c:v libx264 -pix_fmt yuv420p -movflags +faststart "$OUT" \
       >/dev/null 2>&1

# ---- 9. cleanup ----
adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.record.scrcpy" 2>&1 || true
log "app log: $TMPDIR/logcat.record.scrcpy"
adb -s "$SERIAL" emu kill || true
log "wrote $(wc -c <"$OUT") bytes to $OUT"
log 'done'