fix(nests): self-audit pass — two real bugs + robustness + tests
Audit of the four prior commits found two genuine regressions and two
robustness gaps in the new code paths.
Bug A: NestForegroundService.networkCallback dropped the publish for
the most important scenario it was added to handle. The earlier guard
`if (previous != null && previous != network)` suppressed both the
registration-time first onAvailable AND the legitimate WiFi-loss-
then-cellular-available path. On a WiFi → cellular handover the
sequence is `onLost(wifi)` (clears currentDefaultNetwork to null)
followed by `onAvailable(cellular)` — which then looks identical to
the registration callback to the guard, so no publish fires and the
QUIC session sits on the dead socket until PTO. Replaced the implicit
"previous == null" suppression with an explicit `seenInitialNetwork`
flag that's set true on the first onAvailable and never cleared, so
post-onLost reconnects publish correctly.
Bug B: requestAudioFocus result handling was too permissive. The
shape `if (result == AUDIOFOCUS_REQUEST_FAILED) TransientLoss else
Granted` falls through to Granted on the runCatching exception path
(`result == null`) and on AUDIOFOCUS_REQUEST_DELAYED (= 2) — meaning
audio plays over an active call when the OS hasn't actually released
focus. Switched to a strict `if (result == AUDIOFOCUS_REQUEST_GRANTED)`
check; everything else (FAILED, DELAYED, exception) starts the VM
muted.
Robustness: NestViewModel.openSubscription's onError callback used to
swallow every AudioException, which fit the per-packet decoder-error
case but turned PlaybackFailed and DeviceUnavailable from a deferred
beginPlayback into a permanent "Connecting…" spinner on the speaker
tile. Now we discriminate by AudioException.Kind: decoder/encoder
errors stay swallowed (Opus PLC papers them over), but PlaybackFailed
and DeviceUnavailable roll the slot back so a future reconcile can
retry.
Pre-roll ordering swap + tests: NestPlayer.play used to call
beginPlayback BEFORE flushing the pre-roll buffer, leaving a
microsecond window where the AudioTrack hardware was playing against
an empty buffer. AudioTrack MODE_STREAM explicitly supports write()
before play(), so flush-then-beginPlayback is the textbook pattern
and what the fix now does. Three regression tests cover:
- pre-roll defers beginPlayback until threshold is met (and the
flush-then-begin ordering is observable)
- partial pre-roll flushes when the upstream flow ends early
- empty flow doesn't begin playback at all
The FakeAudioPlayer grows beginPlaybackCount + queuedAtBeginPlayback
fields so the tests can assert ordering directly.
This commit is contained in:
+27
-1
@@ -34,6 +34,7 @@ import com.vitorpamplona.nestsclient.NestsRoomConfig
|
||||
import com.vitorpamplona.nestsclient.NestsSpeaker
|
||||
import com.vitorpamplona.nestsclient.NestsSpeakerState
|
||||
import com.vitorpamplona.nestsclient.audio.AudioCapture
|
||||
import com.vitorpamplona.nestsclient.audio.AudioException
|
||||
import com.vitorpamplona.nestsclient.audio.AudioPlayer
|
||||
import com.vitorpamplona.nestsclient.audio.NestPlayer
|
||||
import com.vitorpamplona.nestsclient.audio.OpusDecoder
|
||||
@@ -1015,7 +1016,32 @@ class NestViewModel(
|
||||
val instrumented = handle.objects.onEach { onSpeakerActivity(pubkey) }
|
||||
roomPlayer.play(
|
||||
instrumented,
|
||||
onError = { /* swallow per-packet decoder errors */ },
|
||||
onError = { err ->
|
||||
// Per-packet decoder errors are noise — Opus PLC
|
||||
// papers over a single bad frame. Swallow those.
|
||||
// PlaybackFailed (or DeviceUnavailable from a
|
||||
// deferred [AudioPlayer.beginPlayback], or the
|
||||
// audio-priority dispatcher dying mid-stream) is
|
||||
// terminal: the device is wedged for THIS
|
||||
// subscription. Roll the slot back so a future
|
||||
// reconcile can retry rather than leave a
|
||||
// perma-spinning speaker tile.
|
||||
when (err.kind) {
|
||||
AudioException.Kind.DecoderError,
|
||||
AudioException.Kind.EncoderError,
|
||||
-> {
|
||||
Unit
|
||||
}
|
||||
|
||||
AudioException.Kind.PlaybackFailed,
|
||||
AudioException.Kind.DeviceUnavailable,
|
||||
-> {
|
||||
if (activeSubscriptions[pubkey] === slot) {
|
||||
activeSubscriptions.remove(pubkey)?.let { closeSubscription(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLevel = { onAudioLevel(pubkey, it) },
|
||||
)
|
||||
slot.attach(handle, roomPlayer, player)
|
||||
|
||||
Reference in New Issue
Block a user