From eb0c8951196c637e44daf3c0617b131b997d5d2c Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: scrcpy-android: mirror an Android device over wireless ADB Native Java app for Android 12+ that mirrors another Android device over wireless ADB, forwarding video, audio, touch input, and clipboard. Bundles a pinned scrcpy-server.jar and the vendored libadb-android stack. Supports h264/h265/av1 video and raw/opus audio with in-app codec selection. No NDK, no Kotlin. Includes JVM unit tests and a Docker-based emulator e2e rig. --- test-rig/e2e.sh | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100755 test-rig/e2e.sh (limited to 'test-rig/e2e.sh') diff --git a/test-rig/e2e.sh b/test-rig/e2e.sh new file mode 100755 index 0000000..ae2a1a8 --- /dev/null +++ b/test-rig/e2e.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Single-emulator self-mirror E2E. +# +# Boot an AOSP API-35 emulator, root it, install the app, let it +# generate its keypair, copy that pubkey into /data/misc/adb/adb_keys +# so the app can connect to its own host's adbd over TCP, launch +# Mirror against 127.0.0.1:5555, wait for the first video frame in +# logcat, screencap, assert the frame is non-uniform. +# +# Runs inside the test image; orchestrated by ../test. + +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 +mkdir -p "$TMPDIR" + +log() { printf 'e2e: %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 + +# Clean stale lock files from a previously aborted run; otherwise the +# emulator refuses to start ("Running multiple emulators with the same AVD"). +AVD_DIR="$ANDROID_AVD_HOME/$AVD.avd" +rm -f "$AVD_DIR"/multiinstance.lock "$AVD_DIR"/hardware-qemu.ini.lock 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 (no window, no snapshot)' + emulator -avd "$AVD" -no-window -no-audio -no-snapshot -gpu swiftshader_indirect \ + -no-boot-anim -accel on >"$TMPDIR/emulator.log" 2>&1 & +fi + +log 'wait-for-device' +adb -s "$SERIAL" wait-for-device +log 'wait for boot' +until [ "$(adb -s "$SERIAL" shell getprop sys.boot_completed | tr -d '\r')" = 1 ]; do + sleep 2 +done + +# ---- 3. root (emulator adbd already listens on TCP 5555) ---- +log 'adb root' +adb -s "$SERIAL" root +adb -s "$SERIAL" wait-for-device + +# ---- 4. install ---- +log "install $APK" +adb -s "$SERIAL" install -r "$APK" >/dev/null + +# ---- 5. let the app generate its keypair ---- +log "launching Main once so Adb.java writes filesDir/adbkey" +adb -s "$SERIAL" shell am start -W -n "$PKG/invalid.lena.scrcpy.Main" >/dev/null +# Adb.java writes the keypair inside its constructor, before setContentView +sleep 1 + +# ---- 6. extract pubkey and authorise ---- +log 'reading filesDir/adbkey via run-as' +adb -s "$SERIAL" shell "run-as $PKG cat files/adbkey" > "$TMPDIR/adbkey.der" +if [ ! -s "$TMPDIR/adbkey.der" ]; then + log 'adbkey was empty - did Adb.java run?' + adb -s "$SERIAL" logcat -d -t 200 -s scrcpy-android >&2 || true + exit 1 +fi + +log 'computing android pubkey blob' +java "$ROOT/test-rig/Keygen.java" "$TMPDIR/adbkey.der" > "$TMPDIR/adb_keys.line" + +log 'appending to /data/misc/adb/adb_keys' +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" + +# adbd rereads /data/misc/adb/adb_keys on every AUTH challenge, +# so no daemon restart is necessary. Restarting it would put the +# host's adb client into 'offline' state and force a reconnect dance. + +# ---- 7. trigger Mirror ---- +log 'starting Mirror -> 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 + +# ---- 8. wait for first frame, fail on session give-up ---- +# Server's openAbstract budget (Server.OPEN_DEADLINE_MS) must stay +# shorter than this deadline; otherwise our retries finish before +# we've given the wire a chance. +deadline=$(( $(date +%s) + ${E2E_DEADLINE:-60} )) +ok= +while [ "$(date +%s)" -lt "$deadline" ]; do + line=$(adb -s "$SERIAL" logcat -d -s scrcpy-android | tail -200) + if printf '%s' "$line" | grep -q 'video frame n=1'; then + ok=1; break + fi + if printf '%s' "$line" | grep -q 'session: gave up'; then + log 'session gave up; logcat:' + printf '%s\n' "$line" | tail -50 >&2 + exit 1 + fi + sleep 1 +done +if [ -z "$ok" ]; then + log 'timed out waiting for video frame n=1' + log "full app log: $TMPDIR/logcat.scrcpy" + adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.scrcpy" 2>&1 || true + log "full system log: $TMPDIR/logcat.full" + adb -s "$SERIAL" logcat -d > "$TMPDIR/logcat.full" 2>&1 || true + log 'last 80 scrcpy-android lines:' + tail -80 "$TMPDIR/logcat.scrcpy" >&2 || true + exit 1 +fi +log 'video frame n=1 - proceeding to screencap' + +# Give the decoder a moment to actually render to the surface. +sleep 1 + +# ---- 9. screencap + pixhash ---- +adb -s "$SERIAL" exec-out screencap -p > "$TMPDIR/shot.png" +log "screencap $(wc -c <"$TMPDIR/shot.png") bytes" +java "$ROOT/test-rig/Pixhash.java" "$TMPDIR/shot.png" --min 5 + +# ---- 9b. mid-session disconnect -> auto-reconnect ---- +# Kill the scrcpy server on the target out from under the app. The video +# socket dies; Session.run() must detect it, rerun the bring-up ladder, +# and a fresh VideoStream must log 'video frame n=1' again. Clear logcat +# first so the only frame we can match is the post-reconnect one. +log 'forcing a mid-session disconnect (pkill scrcpy server on target)' +adb -s "$SERIAL" logcat -c +adb -s "$SERIAL" shell 'pkill -f com.genymobile.scrcpy.Server' || true + +rdeadline=$(( $(date +%s) + ${E2E_RECONNECT_DEADLINE:-60} )) +relink= +while [ "$(date +%s)" -lt "$rdeadline" ]; do + if adb -s "$SERIAL" logcat -d -s scrcpy-android | grep -q 'video frame n=1'; then + relink=1; break + fi + sleep 1 +done +if [ -z "$relink" ]; then + log 'timed out waiting for auto-reconnect (no fresh video frame n=1)' + adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.reconnect" 2>&1 || true + log "full app log: $TMPDIR/logcat.reconnect" + tail -80 "$TMPDIR/logcat.reconnect" >&2 || true + exit 1 +fi +log 'auto-reconnect ok - fresh video frame n=1 after link loss' + +# ---- 10. cleanup ---- +log "full app log: $TMPDIR/logcat.scrcpy" +adb -s "$SERIAL" logcat -d -s scrcpy-android > "$TMPDIR/logcat.scrcpy" 2>&1 || true +log 'adb emu kill' +adb -s "$SERIAL" emu kill || true + +log 'e2e: pass' -- cgit v1.2.3