perf(nests): bound encoder CSD loop, single-alloc audio framing, cache catalog JSON

Three audit follow-ups, all in the audio publish hot path:

Audit-3: MediaCodecOpusEncoder.encode could busy-loop on a buggy
encoder that emits BUFFER_FLAG_CODEC_CONFIG on every dequeue. The
existing format-change path has a one-shot `formatChangeAbsorbed`
guard for exactly this reason; the new CSD-skip path didn't. Cap
consecutive CSD skips per encode call at MAX_CSD_SKIPS_PER_CALL=4
(generous: Codec2 emits 1 OpusHead or 2 OpusHead+OpusTags in
practice). On overshoot, log a warning and return ByteArray(0) —
the broadcaster's `if (opus.isEmpty()) continue` already handles
that contract.

Audit-6: NestMoqLiteBroadcaster's per-frame send path allocated
twice per Opus packet — once for the timestamp Varint and once
for the concatenated `varint+opus` payload. At 50 fps × N
speakers that's measurable young-gen pressure. Switch to the
same single-allocation pattern PublisherStateImpl.send already
uses: compute Varint.size(timestampUs), allocate the final
payload buffer once, write the varint directly into it via
Varint.writeTo, then copy the opus bytes. Drops audio-thread
allocations from 3/frame to 1/frame (the third was the wrap in
PublisherStateImpl.send, already optimised).

Audit-7: MoqLiteNestsSpeaker.startBroadcasting and
ReconnectingNestsSpeaker.runHotSwapIteration both encode the same
fixed catalog JSON on every call — once per broadcast start and
once per JWT-refresh hot-swap iteration. Cache the encoded bytes
on MoqLiteHangCatalog.Companion.OPUS_MONO_48K_AUDIO_DATA_JSON_BYTES
so the kotlinx.serialization run happens at class-init time and
both call sites read a static reference. Trivial perf, but
co-locates the "default Amethyst publish shape" in one place.

https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
Claude
2026-05-06 18:37:19 +00:00
parent ade8da3b5b
commit a94d12638f
5 changed files with 68 additions and 8 deletions
@@ -91,6 +91,13 @@ class MediaCodecOpusEncoder(
// without producing output, which would otherwise busy-spin at
// 100 Hz against the 10 ms dequeue timeout).
var formatChangeAbsorbed = false
// CSD-skip iteration cap. Codec2 stacks have been observed to
// emit OpusHead AND OpusTags as separate CSD buffers; some
// future stack might emit more. Cap the per-call CSD-skip
// count so a buggy encoder that emits CSD on every dequeue
// can't busy-loop the per-frame budget. 4 is generous: the
// expected case is 1 (OpusHead) or 2 (OpusHead + OpusTags).
var csdSkipsThisCall = 0
while (true) {
val outputIndex = codec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US)
when {
@@ -119,6 +126,20 @@ class MediaCodecOpusEncoder(
"MediaCodecOpusEncoder skipped ${bufferInfo.size}-byte CODEC_CONFIG (OpusHead/OpusTags) — not an audio frame"
}
}
csdSkipsThisCall += 1
if (csdSkipsThisCall >= MAX_CSD_SKIPS_PER_CALL) {
// A buggy encoder is emitting CSD on every
// dequeue; bail this call rather than
// busy-looping the per-frame budget.
// Returning empty is the existing "warmup"
// contract — broadcaster's
// `if (opus.isEmpty()) continue` handles it
// and the next encode call retries.
com.vitorpamplona.quartz.utils.Log.w("NestTx") {
"MediaCodecOpusEncoder hit MAX_CSD_SKIPS_PER_CALL=$MAX_CSD_SKIPS_PER_CALL; bailing this encode call (encoder may be misbehaving)"
}
return ByteArray(0)
}
// Don't return; loop to find the next output
// buffer (the next dequeue may be the actual
// first audio frame).
@@ -163,6 +184,16 @@ class MediaCodecOpusEncoder(
private const val DEQUEUE_TIMEOUT_US = 10_000L
private const val FRAME_DURATION_US = 20_000L
/**
* Per-encode-call cap on consecutive
* [MediaCodec.BUFFER_FLAG_CODEC_CONFIG] outputs we'll skip
* before bailing the call. Expected steady state is 0; the
* one-time startup cost is 1 (OpusHead) or 2 (OpusHead +
* OpusTags on Codec2). 4 leaves headroom for a future stack
* that emits more without uncapping the loop entirely.
*/
private const val MAX_CSD_SKIPS_PER_CALL: Int = 4
private fun buildFormat(bitrate: Int): MediaFormat =
MediaFormat
.createAudioFormat(