fix(nests): T13 reset Opus decoder on publisher boundary in NestPlayer
The reissuing-subscribe wrapper splices fresh-publisher frames into the
same `SharedFlow` that NestPlayer consumes, so across a JWT-refresh
hot-swap (speaker side) or a cliff-detector recycle (listener side)
the decoder receives a discontinuous frame stream while keeping its
Opus predictor state. Result: an audible warble at every publisher
cycle. Single-stream tests don't catch it because they never present
two distinct publishers.
Detect the boundary via `MoqObject.trackAlias` — the underlying
`MoqLiteSession.subscribe` assigns a fresh subscribeId per SUBSCRIBE,
which the listener wrapper surfaces verbatim as `trackAlias` on every
emitted object. A change between consecutive objects = new publisher.
Add an optional `decoderFactory: (() -> OpusDecoder)?` to NestPlayer.
When non-null, NestPlayer tracks `lastTrackAlias` and on a change
releases the current decoder and rebuilds via the factory. The
default `null` preserves the legacy single-decoder behaviour so
existing tests / callers that don't care about boundaries stand
unchanged.
NestViewModel.openSubscription wires the factory: a closure capturing
the catalog-derived `channelCount` so a rebuild reuses the SAME
channel layout — without that capture, a rebuild after a stereo-
publisher cycle would default to mono and silently downmix.
Two new NestPlayerTest cases pin the behaviour:
- `publisher_boundary_rebuilds_decoder_when_factory_provided`:
factory invoked twice (initial + boundary), first decoder
released on boundary, second released on stop.
- `publisher_boundary_no_op_when_factory_is_null`: legacy path
holds onto the same decoder across trackAlias changes (unchanged
semantics).
NestPlayer's first constructor parameter is now `initialDecoder`
(was `decoder`); positional-arg call sites are unchanged but the
named-arg call sites in the test suite are updated accordingly.
https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+19
-2
@@ -1177,7 +1177,16 @@ class NestViewModel(
|
||||
// the floor — wrap them in a nested try so any cancellation
|
||||
// or throw between here and slot.attach releases them
|
||||
// (audit round-2 VM #7).
|
||||
val decoder = decoderFactory(channelCount)
|
||||
// Per-subscription decoder factory closure: captures the
|
||||
// catalog-derived [channelCount] so a publisher-boundary
|
||||
// decoder rebuild (see [NestPlayer]'s `decoderFactory`
|
||||
// kdoc) reuses the SAME channel layout — without it, a
|
||||
// rebuild after a cliff-recycle would default to mono and
|
||||
// a stereo publisher would silently downmix on the new
|
||||
// decoder. The factory is also called for the initial
|
||||
// decoder so the construction path is uniform.
|
||||
val perSubscriptionDecoderFactory: () -> OpusDecoder = { decoderFactory(channelCount) }
|
||||
val decoder = perSubscriptionDecoderFactory()
|
||||
val player =
|
||||
try {
|
||||
playerFactory(channelCount)
|
||||
@@ -1195,7 +1204,7 @@ class NestViewModel(
|
||||
val isHushed = pubkey in _uiState.value.locallyHushed
|
||||
val roomPlayer =
|
||||
NestPlayer(
|
||||
decoder = decoder,
|
||||
initialDecoder = decoder,
|
||||
player = player,
|
||||
scope = viewModelScope,
|
||||
// ~100 ms of audio buffered before the AudioTrack
|
||||
@@ -1205,6 +1214,14 @@ class NestViewModel(
|
||||
// tuned in the audio-rooms audit; see NestPlayer
|
||||
// kdoc for details.
|
||||
prerollFrames = ROOM_PLAYER_PREROLL_FRAMES,
|
||||
// Trigger a decoder rebuild on every publisher
|
||||
// boundary (re-issuing wrapper spliced in a new
|
||||
// SUBSCRIBE → trackAlias changes). Without this,
|
||||
// Opus's predictor state from the prior
|
||||
// publisher's last frame is fed into the new
|
||||
// publisher's first frame and produces audible
|
||||
// warble at every cliff-recycle / hot-swap.
|
||||
decoderFactory = perSubscriptionDecoderFactory,
|
||||
)
|
||||
// Apply current mute + per-speaker hush state before play()
|
||||
// opens the device so the first frame respects them.
|
||||
|
||||
Reference in New Issue
Block a user