fix(android): harden the receiver after the review pass

- SignalingServer.send() never throws: a broken signaling connection
  drops the peer instead of killing the RTP reader thread via requestPli()
- H264Decoder: assign the codec before configure/start (no orphaned
  instance on failure); feed() reports input-queue timeouts so the
  pipeline requests a keyframe instead of dropping the frame silently
- Park offers that arrive without a surface as pendingOffer and configure
  on attachSurface(): a codec configured without a surface can never take
  one (setOutputSurface refuses it); the late configure sends a PLI
- Forget destroyed surfaces (onSurfaceTextureDestroyed -> detachSurface)
- Show the status overlay again on later messages
- Accumulate NAL bytes in ByteArrayOutputStreams, not boxed ArrayList<Int>
- Delete the dead NSD reflection fallback (ResolutionListener never
  existed; the classic API is present from API 16 through 36)
- Hold decoderLock around all MediaCodec calls (not thread-safe) and make
  requestPli thread-safe
- Update RUNBOOK quirks (NSD, setOutputSurface) and MEMORY notes
This commit is contained in:
2026-09-10 12:19:21 +02:00
parent bbe4f21a3b
commit c6722d164b
7 changed files with 297 additions and 134 deletions
+62 -4
View File
@@ -1,7 +1,9 @@
# Project Memory — screen_cast
Last updated: Phase 8 (Android receiver app) complete and validated on a
Fairphone 6; all prior phases done.
Fairphone 6; all prior phases done. The Android app's review findings were
fixed in a follow-up pass (same day) — see "Android app review" at the
bottom of this file.
## Project state
@@ -19,8 +21,11 @@ Fairphone 6; all prior phases done.
`MediaCodec.configure()` + `start()`; render via
`releaseOutputBuffer(render=true)`; C2 AVC needs a concrete size at
configure (in-band SPS reconfigures); `MediaFormat.format()`/
`KEY_MIME_TYPE` not public in API 36; NSD `RegistrationListener` replaced
`ResolutionListener` (reflection fallback for older devices);
`KEY_MIME_TYPE` not public in API 36; NSD: the classic
`registerService(info, flags, RegistrationListener)` API exists from API
16 through 36 (javap-verified on the android-36 SDK) — an old reflection
fallback targeting a never-existent "ResolutionListener" was removed as
dead code;
**`android._video-scaling`: C2 scales output to the Surface** → letterbox
by sizing the TextureView to the video aspect, NOT a transform matrix
(double scale). Sender-side: Hyprland + GTK portal's `--target monitor`
@@ -258,4 +263,57 @@ None.
- `CaptureSession::next_frame()` returns `nullopt` on stream error without
surfacing the reason (logged to stderr).
- Receiver ignores unknown packetization modes (STAP-A/MTAP/FU-B); senders
we control never emit them, but third-party interop would need support.
we control never emit them, but third-party interop would need support.
### Android app review (2026-09-10) — findings FIXED same day
Review pass (25 JVM tests re-run green; rtp/jitter/depacketizer verified
faithful to the C++ side source-to-source), followed by a fix pass that
landed all findings. No commit yet (user has not asked).
Fixed:
- `SignalingServer.send()` no longer throws (mirrors the C++ server, which
ignores write failures): a broken signaling TCP no longer kills the
`rtp-reader` thread via `requestPli()`; `peerOut` is dropped (closing the
socket) on write failure.
- `H264Decoder.configure()` assigns the created codec before configuring,
so a configure/start failure can no longer orphan the MediaCodec instance
(no finalizer; scarce native slots).
- Offer-before-surface no longer configures a ByteBuffer-mode decoder: the
offer is parked as `pendingOffer` and `attachSurface()` configures later
(a surface-less codec can never take one — `setOutputSurface` refuses
it, an IllegalStateException crash on the main thread). The late
configure sends a PLI (the sender only emits IDRs when asked).
- `ReceiverActivity.onSurfaceTextureDestroyed``pipeline.detachSurface()`;
the pipeline/decoder forget dead surfaces instead of configuring against
them (frames decode unrendered until the next attach).
- The status overlay reappears: `onStatus` sets `visibility = VISIBLE`
(it used to write into a GONE view after the first frame).
- `H264Decoder.feed()` returns false on input-queue timeout → the pipeline
requests a PLI instead of silently dropping the frame (no flush — the
codec is healthy).
- The depacketizer accumulates into `ByteArrayOutputStream`s instead of
boxed `ArrayList<Int>` (was ~MB/s of Integer allocations on keyframes).
- NSD reflection fallback deleted (dead code — see the quirk note above).
- Thread-safety: all codec calls now run under `decoderLock` (MediaCodec is
not thread-safe; feed/drain previously raced attachSurface);
`requestPli()` is thread-safe via `pliLock`.
Validation: `gradle :app:assembleDebug :app:testDebugUnitTest` green
(25/25, full --rerun-tasks rebuild, only two pre-existing warnings);
`meson test` 5/5 unchanged (no C++ touched).
Still open (accepted, needs a device or a new test dep):
- Untested on device: `setOutputSurface` mid-session (surface switch) and
the whole pendingOffer path; API-35 `detachOutputSurface()` could replace
the render-flag approach.
- No JVM tests for `SignalingMessage`/`SignalingServer` (would need the
`org.json:json` test dependency; android.jar stubs throw).
- Interop caveats by design: 2048-byte datagram buffer (our MTU is 1200),
RTP timestamps (90 kHz) fed as µs, unauthenticated offers (SRTP is a
future phase).
- Verified NOT a bug: reusing one `DatagramPacket` without resetting its
length — modern JVMs recv by buffer capacity (JDK-21 probe + the
successful on-device streaming confirm it).