fix(android): pass PROTOCOL_DNS_SD so mDNS registration works

Android 16 added NsdManager.checkProtocol(): registerService with protocol
0 threw IllegalArgumentException: Unsupported protocol — silently, since
the old code swallowed the exception into a dead reflection fallback and
start() overwrote the failure status with the Listening line. mDNS never
actually advertised (the Phase 8 session streamed via --peer, masking it).

Also advertise after the listening status so a registration failure stays
visible, and log the exception and the registration success.

Validated live on the Fairphone 6: the desktop --discover lists the phone
and a full --send session decodes (in-band SPS 320x240 -> 2496x1040) and
renders.
This commit is contained in:
2026-09-10 12:35:00 +02:00
parent c6722d164b
commit 9a24933203
4 changed files with 53 additions and 2 deletions
+26
View File
@@ -304,6 +304,32 @@ 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).
### Discovery bug found and fixed on-device (2026-09-10, same day)
The user reported the desktop sender never discovered the phone.
Root cause (found live on the Fairphone 6): `registerService(info, 0,
listener)` — Android 16's `NsdManager.checkProtocol()` rejects protocol
`0` with `IllegalArgumentException: Unsupported protocol`. The old code
swallowed it into the dead reflection fallback, and `start()` posted the
"Listening…" status AFTER advertiseNsd, overwriting the failure text — so
mDNS never advertised and Phase 8's NSD validation was only ever "no
crash" (8.5 streamed via --peer, masking it).
Fixes in `ReceiverPipeline.advertiseNsd()`/`start()`:
- pass `NsdManager.PROTOCOL_DNS_SD`;
- advertise AFTER the listening status so a failure stays visible;
- `Log.e` the registration exception; `Log.i` on registered success.
On-device validation (adb, live): `mDNS registered:
screencast._screencast._tcp` in logcat; `dumpsys servicediscovery` shows
the active Advertiser (key diagnostic: `mClientRequests` empty == no
request ever issued); desktop `avahi-browse` and `screencast --discover`
list the phone at 192.168.178.29:5005; a 25s `--send --target monitor
--peer 192.168.178.29:5005` session decoded (in-band SPS reconfigured
320x240 → 2496x1040) and rendered (screencap mean brightness 0.51).
Note: with both the Pi and the phone on the LAN, plain `--send` refuses
(two receivers found) — target the phone with `--peer`.
Still open (accepted, needs a device or a new test dep):
- Untested on device: `setOutputSurface` mid-session (surface switch) and
@@ -110,11 +110,13 @@ class ReceiverPipeline(
running = true
readerThread = Thread({ readLoop() }, "rtp-reader").also { it.start() }
advertiseNsd(signalingPort)
// Advertise AFTER the listening status: a registration failure must
// not be overwritten by it (both post to the same overlay).
onStatus(
"Listening on $localIp (media :$udpPort, signaling :$signalingPort)\n" +
"Waiting for a sender… (fall back to: screencast --send --peer $localIp:$signalingPort)",
)
advertiseNsd(signalingPort)
}
/**
@@ -345,6 +347,7 @@ class ReceiverPipeline(
val listener = object : NsdManager.RegistrationListener {
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
// Registered: the sender's mDNS browser should see it now.
android.util.Log.i(TAG, "mDNS registered: ${serviceInfo.serviceName}.${SERVICE_TYPE}")
}
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) = Unit
@@ -356,9 +359,14 @@ class ReceiverPipeline(
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
}
try {
nsdManager.registerService(info, 0, listener)
// PROTOCOL_DNS_SD is mandatory on API 36: NsdManager.checkProtocol()
// rejects anything else (the historical 0 threw
// "IllegalArgumentException: Unsupported protocol", which the
// old code swallowed — mDNS never advertised on this phone).
nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, listener)
registrationListener = listener
} catch (e: Exception) {
android.util.Log.e(TAG, "mDNS registration failed", e)
onStatus("mDNS unavailable (${e.message}) — reach this receiver with --peer $localIp:$port")
}
}
+7
View File
@@ -114,6 +114,13 @@ existing signaling + RTP protocol; no C++ changes.
depacketizer — 25 tests green)
- [x] 8.3 Signaling + NSD validated on device (offer → answer over the
network; mDNS registration via the API-36 RegistrationListener)
**correction 2026-09-10**: registration had in fact never
succeeded (registerService passed protocol `0`, which API 36
rejects with "Unsupported protocol"; the swallowed failure was
hidden behind the "Listening…" status). Fixed with
`PROTOCOL_DNS_SD` + real on-device validation: desktop
`--discover` lists the phone and a live `--send` session
decodes and renders.
- [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
+10
View File
@@ -240,6 +240,16 @@ Platform quirks found while validating (Android 16 / API 36):
- `android.permission.INTERNET` is required — NsdService rejects
registration without it.
- `NsdManager.registerService` on Android 16 validates the protocol
argument (`NsdManager.checkProtocol`): the historical `0` throws
`IllegalArgumentException: Unsupported protocol` — pass
`NsdManager.PROTOCOL_DNS_SD`. This was silent for a long time: the app
swallowed the exception and the failure status was overwritten by the
"Listening…" line, so mDNS never advertised even though the UI looked
fine (discovery only worked via `--peer`). Diagnosed via
`adb shell dumpsys servicediscovery` (the client's `mClientRequests`
stays empty when no request was ever issued) plus logging the
exception.
- `DatagramSocket.localPort` gives the bound port; `.port` is -1 for
unconnected datagram sockets, and `.localAddress` is an `InetAddress`
(Inet6Address on Android), not an `InetSocketAddress`.