docs: document the Android receiver app (Phase 8)

README: phone-as-second-screen section (build, install, sender usage,
multi-monitor caveat). RUNBOOK: Android build/test procedure, device-test
commands, and the API-36 platform quirks found during validation.
PHASES: Phase 8 milestones, all validated. MEMORY: current phase state
and the hard-won Android/portal lessons.
This commit is contained in:
2026-09-10 11:38:16 +02:00
parent 455ba6055d
commit bbe4f21a3b
4 changed files with 135 additions and 8 deletions
+24 -1
View File
@@ -102,6 +102,29 @@ where available. PLI + jitter validated on the desktop (induced-loss probe);
hardware decode validated as clean-fallback on the desktop, hardware path
pending a run on the Pi.
## Phase 8 — Android Receiver App
**Goal**: a native Android receiver app (Kotlin) so a phone can act as the
second receiver — screen on, USB-C DP-alt-mode to HDMI. The app speaks the
existing signaling + RTP protocol; no C++ changes.
- [x] 8.1 Gradle project builds (`android/`, AGP 9 built-in Kotlin, no
androidx; `gradle :app:assembleDebug`)
- [x] 8.2 Kotlin sources + JVM unit tests (rtp framing, jitter buffer,
depacketizer — 25 tests green)
- [x] 8.3 Signaling + NSD validated on device (offer → answer over the
network; mDNS registration via the API-36 RegistrationListener)
- [x] 8.4 MediaCodec decode + Surface render validated on device (in-band
SPS sizing, letterbox fit)
- [x] 8.5 End-to-end on a Fairphone 6 (Android 16): streaming, letterboxed
fullscreen render, PLI keyframe recovery on Wi-Fi loss
- [x] 8.6 Docs: README receiver section, RUNBOOK build/install/test,
PHASES + MEMORY updates
**Validation**: `gradle :app:testDebugUnitTest` green; live session
`--send --target window --peer <phone>:5005` rendered fullscreen on the
phone (and HDMI via DP-alt-mode) with loss recovery.
## Current phase
Phase 7Resilience and Polish.
Phase 8Android Receiver App (complete).
+49
View File
@@ -209,6 +209,55 @@ The headless equivalent runs as part of `meson test` (`udp loopback` test):
synthetic frames → encode → packetize → localhost UDP → depacketize →
decode, no portal or window involved.
## Android receiver app (Phase 8)
Toolchain: JDK 21, system `gradle` (no wrapper), Android SDK with android-36.
AGP 9 has **built-in Kotlin** — do not apply `org.jetbrains.kotlin.android`
and do not use `kotlinOptions {}`.
```sh
cd android
gradle :app:assembleDebug # -> app/build/outputs/apk/debug/app-debug.apk
gradle :app:testDebugUnitTest # 25 JVM tests (rtp, jitter, depacketizer)
```
Device test (validated on a Fairphone 6, Android 16 / API 36):
```sh
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n screen_cast.receiver/screen_cast.ReceiverActivity
# activity FQN is screen_cast.ReceiverActivity (namespace), not
# screen_cast.receiver.ReceiverActivity
adb shell uiautomator dump /sdcard/ui.xml && adb shell cat /sdcard/ui.xml # status text
adb exec-out screencap -p > phone.png # visual check
```
On the sender: `screencast --send --peer <phone-ip>:5005` (add `--target
window` to pick a specific window; the portal's monitor source picks the
first output on multi-monitor Hyprland/GTK-portal setups).
Platform quirks found while validating (Android 16 / API 36):
- `android.permission.INTERNET` is required — NsdService rejects
registration without it.
- `DatagramSocket.localPort` gives the bound port; `.port` is -1 for
unconnected datagram sockets, and `.localAddress` is an `InetAddress`
(Inet6Address on Android), not an `InetSocketAddress`.
- `MediaCodec`: pass the output Surface to `configure()` (`setOutputSurface`
is illegal after configure); call `start()`; render with
`releaseOutputBuffer(index, render=true)`; the C2 AVC decoder requires a
concrete width/height at configure (use a placeholder — the in-band SPS
reconfigures it).
- `MediaFormat.format()` / `KEY_MIME_TYPE` are not in the API-36 public
surface.
- NSD: API 36 replaced `ResolutionListener` registration with
`registerService(info, flags, RegistrationListener)`; the app falls back
to reflection on older devices.
- The C2 decoder scales its output to the Surface (`android._video-scaling`)
— letterbox by **sizing the TextureView to the video aspect** (centered
on a black window background), not with a transform matrix (double scale).
- mDNS does not cross subnets; `--peer` is the reliable path.
## Formatting
```sh