fix(nests): NestPlayer keeps existing decoder when boundary factory throws
Audit-1: the publisher-boundary rebuild path released the old decoder
BEFORE asking the factory for a replacement (`runCatching {
decoder.release() }; decoder = factory()`). If `factory()` threw —
MediaCodec contention, audio policy denial mid-session, etc. — the
released decoder stayed assigned to the field and every subsequent
`decoder.decode(payload)` would throw `IllegalStateException` with
no recovery path. The room would silently fail for that subscription
until torn down.
Build the replacement first; only release the old one and swap on
success. On factory failure, log and keep the existing decoder
running. Cross-publisher Opus predictor state is wrong for one
group but at least audio keeps playing.
Audit-4+5: companion test cleanup —
- Drop the `byteArrayOf(0x0A) + it` / `0x0B` dead expressions in
the FakeOpusDecoder closures of the existing boundary-rebuild
test. They allocated a ByteArray and concatenated, then threw
the result away.
- Drop the local `IntBox` helper and use
`java.util.concurrent.atomic.AtomicInteger` like the rest of
the codebase does (`MoqLiteSession`, `MoqLiteNestsListener`).
Same semantics, no new vocabulary.
New test pins the dangling-field fix:
`publisher_boundary_keeps_old_decoder_when_factory_throws` flows
two MoqObjects with different trackAliases through a NestPlayer
whose factory always throws; asserts both frames decode through
the original decoder and the original decoder is released exactly
once on `stop()`.
https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+27
-4
@@ -216,13 +216,36 @@ class NestPlayer(
|
||||
// side) or cliff-detector recycle (listener side).
|
||||
// The prior-trackAlias guard avoids a spurious
|
||||
// rebuild on the very first frame.
|
||||
//
|
||||
// Build the replacement BEFORE releasing the old
|
||||
// decoder so a factory failure (e.g. MediaCodec
|
||||
// contention, audio policy denial mid-session)
|
||||
// doesn't leave the field referencing a
|
||||
// released codec — every subsequent decode
|
||||
// would then throw `IllegalStateException` with
|
||||
// no recovery path. On factory failure, log
|
||||
// and keep using the existing decoder; cross-
|
||||
// publisher predictor state is wrong but at
|
||||
// least audio keeps playing.
|
||||
val factory = decoderFactory
|
||||
if (factory != null && lastTrackAlias != null && obj.trackAlias != lastTrackAlias) {
|
||||
com.vitorpamplona.quartz.utils.Log.d("NestPlay") {
|
||||
"NestPlayer publisher boundary: trackAlias $lastTrackAlias → ${obj.trackAlias}; rebuilding decoder"
|
||||
val replacement =
|
||||
runCatching { factory() }
|
||||
.onFailure { t ->
|
||||
if (t is CancellationException) throw t
|
||||
com.vitorpamplona.quartz.utils.Log.w("NestPlay") {
|
||||
"NestPlayer decoder factory threw on trackAlias " +
|
||||
"$lastTrackAlias → ${obj.trackAlias}; keeping old decoder " +
|
||||
"(${t::class.simpleName}: ${t.message})"
|
||||
}
|
||||
}.getOrNull()
|
||||
if (replacement != null) {
|
||||
com.vitorpamplona.quartz.utils.Log.d("NestPlay") {
|
||||
"NestPlayer publisher boundary: trackAlias $lastTrackAlias → ${obj.trackAlias}; rebuilding decoder"
|
||||
}
|
||||
runCatching { decoder.release() }
|
||||
decoder = replacement
|
||||
}
|
||||
runCatching { decoder.release() }
|
||||
decoder = factory()
|
||||
}
|
||||
lastTrackAlias = obj.trackAlias
|
||||
val pcm =
|
||||
|
||||
Reference in New Issue
Block a user