feat(android): video decode pipeline — NDK AMediaCodec → SurfaceView

M4 Android stage 1 (video). Pull HEVC access units from the connector and render
them to the SurfaceView entirely in Rust (NDK AMediaCodec → ANativeWindow) — no
per-frame JNI, honoring the native-thread hot-path invariant.

- crates/punktfunk-android: decode.rs (one-in/one-out AMediaCodec loop; in-band
  VPS/SPS/PPS so no out-of-band csd; dims from NativeClient::mode). SessionHandle
  now holds an Arc<NativeClient> + the decode thread; nativeStartVideo/nativeStopVideo.
- clients/android: connect screen (host/port) + full-screen SurfaceView stream
  screen — surfaceCreated -> nativeStartVideo, leaving -> stop + close.

Verified live (Android emulator -> m3-host on the LAN box, ABI v2): QUIC handshake,
8-round clock-skew sync, HEVC decoder configured at 1280x720, and the data plane
delivered + fed all 299 access units (the punktfunk/1 NAT hole-punch worked through
the emulator's SLIRP). Real-pixel render is pending a non-synthetic source:
`m3-host --source synthetic` emits dummy transport payloads (not HEVC), so the
decoder correctly produces nothing; `--source virtual` (a compositor on the host)
is needed to verify decode-to-screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 02:03:32 +02:00
parent 7d7b5c8611
commit 2de6e762f6
7 changed files with 449 additions and 45 deletions
+4
View File
@@ -25,3 +25,7 @@ log = "0.4"
# `ndk` and Oboe/Opus audio later) is only pulled in for the real `*-linux-android` targets.
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.14"
# NDK bindings for the per-frame video path: AMediaCodec (HEVC hardware decode) + ANativeWindow
# (the SurfaceView surface). Links libmediandk/libnativewindow. Decode runs entirely in Rust — no
# per-frame JNI crossing (the "no async / native threads on the hot path" invariant).
ndk = { version = "0.9", features = ["media"] }