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:
+62
-4
@@ -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).
|
||||
Reference in New Issue
Block a user