fix(nestsClient): fix race + leaks + Android encoder/decoder bugs from second audit
A fresh audit caught a real race in the round-1 subscribe() rewrite plus several distinct bugs in the Android MediaCodec layer. **MoqLiteSession.subscribe() race regression** — the launched collector that reads the SubscribeResponse and watches for peer FIN ran cleanup (remove from subscriptionsBySubscribeId, close frames) before subscribe() itself reached the post-await registration. If the publisher FIN'd immediately after sending Ok, the collector exited first against an empty map; subscribe() then registered the subscription, leaving frames in the map with no live collector to ever close it on transport drop. Consumer hung forever — exactly the failure mode round-1 fix #3 was supposed to prevent. Fix: pre-register the subscription BEFORE launching the collector. Drop unused `ok` field from ListenerSubscription. **MoqLiteNestsSpeaker.startBroadcasting publisher leak** — if session.publish() succeeded but broadcaster.start() then threw (mic permission denied, AudioRecord allocation failure), the publisher was never closed and stayed registered as session.activePublisher, permanently blocking subsequent startBroadcasting calls. **runCatching{suspend close} swallowing CancellationException** — MoqLiteNestsSpeaker.close, MoqLiteBroadcastHandle.close, MoqLiteNestsListener.close all wrapped suspending closes in runCatching, breaking structured cancellation when the parent scope cancelled teardown. Replaced with explicit cancel-rethrowing try/catch. **MediaCodecOpusDecoder ArrayList<Short> boxing on every audio frame** — at 50 fps × 960 samples × N speakers ≈ 48 000 boxed Short allocations/sec/speaker on the audio hot path. Rewritten to write directly into a pre-sized ShortArray via ShortBuffer.get(dst, off, len). **MediaCodecOpusEncoder bugs** - KEY_AAC_PROFILE = AACObjectLC was being set on an audio/opus encoder. Meaningless for Opus; stricter Codec2 stacks on Android 13+ reject the configure() call with IllegalArgumentException and surface as DeviceUnavailable. Removed. - The drain loop's INFO_OUTPUT_FORMAT_CHANGED branch had no progress guard. A buggy encoder re-emitting FORMAT_CHANGED without producing output would busy-spin against the 10 ms dequeue timeout. Now absorbed at most once per encode call. Same guard added to the decoder. Adds regression test: - frames_flow_completes_when_peer_FINs_immediately_after_Ok 224 tests pass, 0 failures. Android target compiles clean.
This commit is contained in:
+32
@@ -413,6 +413,38 @@ class MoqLiteSessionTest {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun frames_flow_completes_when_peer_FINs_immediately_after_Ok() =
|
||||
runBlocking {
|
||||
// Race regression: an earlier shape of subscribe() pre-registered
|
||||
// the subscription AFTER awaiting the Ok response. If the peer
|
||||
// FIN'd between sending Ok and subscribe() resuming, the
|
||||
// collector's exit-cleanup ran first against an empty map, and
|
||||
// subscribe() then inserted the subscription — leaving the
|
||||
// frames channel registered with no live collector to ever close
|
||||
// it. This test forces the order by writing Ok and FIN before
|
||||
// the listener has a chance to consume them.
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
val peerJob =
|
||||
async {
|
||||
val (bidi, _) = nextSubscribeBidi(serverSide)
|
||||
bidi.write(MoqLiteCodec.encodeSubscribeOk(okFor(0L)))
|
||||
bidi.finish()
|
||||
}
|
||||
|
||||
val handle = session.subscribe("speakerZ", "audio/data")
|
||||
withTimeout(2_000) { peerJob.await() }
|
||||
|
||||
// frames flow must complete naturally regardless of whether
|
||||
// the FIN landed before or after subscribe()'s post-await
|
||||
// registration.
|
||||
withTimeout(2_000) { handle.frames.toList() }
|
||||
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unsubscribe_FINs_the_subscribe_bidi() =
|
||||
runBlocking {
|
||||
|
||||
Reference in New Issue
Block a user