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:
@@ -304,6 +304,32 @@ Validation: `gradle :app:assembleDebug :app:testDebugUnitTest` green
|
|||||||
(25/25, full --rerun-tasks rebuild, only two pre-existing warnings);
|
(25/25, full --rerun-tasks rebuild, only two pre-existing warnings);
|
||||||
`meson test` 5/5 unchanged (no C++ touched).
|
`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):
|
Still open (accepted, needs a device or a new test dep):
|
||||||
|
|
||||||
- Untested on device: `setOutputSurface` mid-session (surface switch) and
|
- Untested on device: `setOutputSurface` mid-session (surface switch) and
|
||||||
|
|||||||
@@ -110,11 +110,13 @@ class ReceiverPipeline(
|
|||||||
|
|
||||||
running = true
|
running = true
|
||||||
readerThread = Thread({ readLoop() }, "rtp-reader").also { it.start() }
|
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(
|
onStatus(
|
||||||
"Listening on $localIp (media :$udpPort, signaling :$signalingPort)\n" +
|
"Listening on $localIp (media :$udpPort, signaling :$signalingPort)\n" +
|
||||||
"Waiting for a sender… (fall back to: screencast --send --peer $localIp:$signalingPort)",
|
"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 {
|
val listener = object : NsdManager.RegistrationListener {
|
||||||
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
|
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
|
||||||
// Registered: the sender's mDNS browser should see it now.
|
// 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
|
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) = Unit
|
||||||
@@ -356,9 +359,14 @@ class ReceiverPipeline(
|
|||||||
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
||||||
}
|
}
|
||||||
try {
|
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
|
registrationListener = listener
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e(TAG, "mDNS registration failed", e)
|
||||||
onStatus("mDNS unavailable (${e.message}) — reach this receiver with --peer $localIp:$port")
|
onStatus("mDNS unavailable (${e.message}) — reach this receiver with --peer $localIp:$port")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,13 @@ existing signaling + RTP protocol; no C++ changes.
|
|||||||
depacketizer — 25 tests green)
|
depacketizer — 25 tests green)
|
||||||
- [x] 8.3 Signaling + NSD validated on device (offer → answer over the
|
- [x] 8.3 Signaling + NSD validated on device (offer → answer over the
|
||||||
network; mDNS registration via the API-36 RegistrationListener)
|
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
|
- [x] 8.4 MediaCodec decode + Surface render validated on device (in-band
|
||||||
SPS sizing, letterbox fit)
|
SPS sizing, letterbox fit)
|
||||||
- [x] 8.5 End-to-end on a Fairphone 6 (Android 16): streaming, letterboxed
|
- [x] 8.5 End-to-end on a Fairphone 6 (Android 16): streaming, letterboxed
|
||||||
|
|||||||
@@ -240,6 +240,16 @@ Platform quirks found while validating (Android 16 / API 36):
|
|||||||
|
|
||||||
- `android.permission.INTERNET` is required — NsdService rejects
|
- `android.permission.INTERNET` is required — NsdService rejects
|
||||||
registration without it.
|
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
|
- `DatagramSocket.localPort` gives the bound port; `.port` is -1 for
|
||||||
unconnected datagram sockets, and `.localAddress` is an `InetAddress`
|
unconnected datagram sockets, and `.localAddress` is an `InetAddress`
|
||||||
(Inet6Address on Android), not an `InetSocketAddress`.
|
(Inet6Address on Android), not an `InetSocketAddress`.
|
||||||
|
|||||||
Reference in New Issue
Block a user