Phase 4 + WP3.2 of design/audio-quality-and-latency.md. **The defect.** Every client ring primed *up* to a target and clamped at a ceiling, and none walked the depth back *down*. Any transient — a Wi-Fi arrival burst, a host stall, or plain host-DAC-vs-client-DAC skew of a few dozen ppm — therefore added latency permanently, until an underrun happened to re-prime. Android, with no shed at all, converged on its 120 ms hard cap and stayed there for the rest of the session; that is the "audio latency is too high" report. Apple did shed, 40 ms in one go, which its own comment called "one audible blip". All four now share `punktfunk_core::audio::JitterPolicy`: depths in MILLISECONDS rather than device quanta (`3 x quantum` meant 15 ms at a 5 ms quantum and a silent 64 ms at a 20 ms one), a crossfaded 5 ms shed once the depth average has sat above target for 2 s of consumed audio, and de-prime hysteresis. Linux and Windows had never had that hysteresis — they still carried the `if ring.is_empty()` instant re-prime that Android identified as self-inflicted crackle, where one transient drain manufactured a whole target's worth of silence. Android's floor drops 40 -> 25 ms: the policy grows the target on the devices that actually underrun, instead of every device pre-paying for the worst one. The Windows ring moves from raw bytes to interleaved f32 so it can share the policy and the crossfade helper at all. Apple is the one client where the policy is hand-written in a second language, so it gets its own XCTest (`AudioRingDriftTests`). Verified here by compiling `AudioRing.swift` standalone against a simulation harness — +200 ppm for 5 minutes settles at 30 ms with zero silent callbacks, where the old ring would have ridden its 80 ms high-water mark. **WP3.2 — recovery lives in core, not in the clients.** The rebuilt frame is re-inserted into the demux queue in order, so every embedder (including any C-ABI consumer) gets a complete stream without knowing the `0xD2` plane exists, and their `AudioGapTracker` simply stops seeing the gap. `recovery_and_the_gap_tracker_agree` pins exactly that. For the same reason core advertises CLIENT_CAP_AUDIO_RED itself rather than making four embedders remember to. Verified: clippy --all-targets -D warnings and the full test suites for punktfunk-core, pf-client-core, punktfunk-host, pf-host-config under Linux/docker (163 + 61 tests); punktfunk-client-android `cargo ndk check` for aarch64 with the gate proven non-vacuous by a planted type error, and its 6 clippy findings confirmed IDENTICAL to the pristine file (all are the documented arm64-only artifacts); AudioRing.swift type-checked and simulated on macOS; fmt. The Windows client half (audio_wasapi.rs) is still not compile-verified anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
punktfunk — Android client (phone & TV)
The native Android app for streaming a punktfunk host to your phone, tablet, or Android TV. A Compose app that finds hosts on your network, pairs with a PIN, and streams at the display's own resolution — with hardware HEVC decode, HDR10, and controller support, built for both touch and the couch (D-pad / gamepad focus navigation).
Features
- Hardware decode — NDK
AMediaCodecHEVC →SurfaceView, including HDR10 (Main10 / BT.2020 PQ), with low-latency tuning and a live stats HUD. - Audio both ways — Opus + AAudio playback with a jitter ring, plus mic uplink to the host.
- Controller support — buttons + axes with rumble and HID feedback (lightbar / adaptive triggers); D-pad / gamepad focus navigation for TV and phone.
- Find hosts automatically — native mDNS discovery; first connect does a one-time SPAKE2 PIN pairing (or TOFU on trusted LANs), then reconnects on a Keystore-wrapped, pinned identity.
- Compose UI — Connect / Settings / Stream screens with Material You theming.
Built for arm64-v8a + armeabi-v7a + x86_64 — the 32-bit armeabi-v7a slice is what keeps the
app installable on the many 32-bit Google TV / Android TV streamers (Walmart onn. 4K, Chromecast with
Google TV, budget Amlogic boxes) that otherwise reject a 64-bit-only build as "not compatible".
Get it
Published to Google Play (Internal Testing) — join the beta via the Discord. Per-device setup and pairing: docs.punktfunk.unom.io/docs/install-client.
How it's built — Rust-heavy
Kotlin can't import the cbindgen C header the way Swift can, so a native bridge is unavoidable. We
write it in Rust and link punktfunk-core directly — so the Android client reuses the Linux
client's orchestration (audio jitter ring, VK keymap inverse, latency/skew math, capture state
machine, trust logic) instead of re-porting it into Kotlin.
| Side | Owns |
|---|---|
Rust (native/ → libpunktfunk_android.so) |
the JNI seam, NativeClient (QUIC control + UDP data plane), AnnexB → AMediaCodec decode (incl. HDR10), Opus + AAudio audio + mic, controller feedback, latency math, trust/pairing, mdns-sd discovery |
Kotlin (app/, kit/) |
Compose UI, SurfaceView lifecycle, input capture, the Wi-Fi MulticastLock + permission UX, Keystore identity |
The single seam is io.unom.punktfunk.kit.NativeBridge ⇄ Java_io_unom_punktfunk_kit_NativeBridge_*.
native/ Rust cdylib (workspace member) — links punktfunk-core directly
src/lib.rs crate doc · JNI_OnLoad · version probes
src/session/ session lifecycle: connect/pair + trust, plane start/stop, input shims
src/decode.rs AnnexB → AMediaCodec HEVC hardware decode → SurfaceView (incl. HDR10)
src/audio.rs · src/mic.rs Opus + AAudio playback / mic uplink
src/feedback.rs · src/stats.rs rumble + HID feedback; live video stats
src/discovery.rs native mdns-sd browse of the host's _punktfunk._udp advert
app/ :app — Compose UI: Connect / Settings / Stream (phone + TV)
kit/ :kit — NativeBridge · native mDNS discovery · Gamepad · Keymap · Keystore identity
Build & run
Prerequisites: Android SDK + NDK r30 (30.0.14904198), platforms;android-37.0,
build-tools;37.0.0, cmake;3.22.1 (builds libopus); JDK 21 (AGP 9.2 runs on JDK 17–21, not
a newer default); Rust with rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android and
cargo install cargo-ndk. Toolchain is pinned (AGP 9.2 · Gradle 9.4.1 · Kotlin 2.3.21 · Compose BOM
2026.05.01 · compileSdk 37 · minSdk 28).
Android Studio: open clients/android — it uses its bundled JBR 21, and the cargoNdk* task
builds the .so as part of the normal build.
CLI (point Gradle at JDK 21 if your machine default is newer):
export JAVA_HOME="$(/usr/libexec/java_home -v 21)" # or your Temurin 21 path
cd clients/android
./gradlew :app:assembleDebug # cargo-ndk cross-compiles libpunktfunk_android.so first
./gradlew :app:installDebug # onto a running emulator/device
# emulators from env setup: emulator -avd pf_phone | emulator -avd pf_tv
The debug APK lands in app/build/outputs/apk/debug/. Launch it, pick a host, pair, and stream.
Related
- Documentation — quick start, pairing, troubleshooting
- Project README — the host, the other clients, and how it all fits together