diff --git a/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md b/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md new file mode 100644 index 000000000..1f93f9b94 --- /dev/null +++ b/nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md @@ -0,0 +1,236 @@ +# QUIC stream cliff against nostrnests.com — investigation plan + +**Status:** open. Mitigated in production via group-packing +(`NestMoqLiteBroadcaster.framesPerGroup = 5`). Root cause not isolated. + +**Reproduces against:** `https://moq.nostrnests.com:4443` (production +relay). Not consistently reproducible against the local +`kixelated/moq` reference relay (the local harness shows the same +shape but with much higher run-to-run variance). + +## Symptom + +When the speaker pumps Opus frames at the production 20 ms cadence +and packs **one frame per moq-lite group** (= one client-initiated +QUIC unidirectional stream per Opus frame), the listener stops +receiving frames somewhere around the 99th frame. From there to +end-of-broadcast, **`MoqLitePublisherHandle.send` keeps returning +`true` for every subsequent frame**, so the application has no signal +that anything is wrong; the audio simply cuts out a few seconds in. + +The bug is invisible to: + +- `NestMoqLiteBroadcaster`'s outer `runCatching {…}.onFailure {…}` + (no exception is thrown on the silent-loss path). +- `MoqLitePublisherHandle.send`'s return value (always `true` past + the cliff). +- `QuicConnection`'s peer-cap check (`nextLocalUniIndex >= peerMaxStreamsUni` + doesn't fire because the relay raises `MAX_STREAMS_UNI` ahead of + our consumption — pre-fix tests confirmed `firstFalseSend = -1` + even at 1500 frames). + +## Sweep evidence (production, before mitigation) + +`NostrnestsProdAudioTransmissionTest` (commit +`2da18859`-era, before any QUIC changes), one row per scenario: + +| Scenario | streams opened | `received` | cliff | +|---|---|---|---| +| `sweep-frames50` | 50 | 50/50 | none ✅ | +| `sweep-baseline` 100 frames | 100 | 99/100 | last 1 | +| any cadence (5/10/40/80/200 ms) at 100 frames | 100 | 99/100 | last 1 | +| any payload (80 B / 1 KB / 4 KB) at 100 frames | 100 | 99/100 | last 1 | +| `sweep-frames200` | 200 | **99**/200 | hard at 99 | +| `sweep-frames400` | 400 | **99**/400 | hard at 99 | +| `sweep-30s` (1500 frames) | 1500 | **99**/1500 | hard at 99 | +| `sweep-120s` (6000 frames) | 6000 | 52/6000 | hard at 52 | +| `sweep-burst` (cadence 0) | 100 | 65/100 | hard at 65 | +| `sweep-payload-16kb` 100 frames × 16 KB | 100 | 49/100 | hard at 49 | +| **`sweep-frames-per-group-5`** | **20** groups | **100/100** | none ✅ | +| **`sweep-frames-per-group-20`** | **5** groups | **100/100** | none ✅ | +| **`sweep-frames-per-group-all`** | **1** group | **100/100** | none ✅ | + +The signal is unambiguous: **fewer streams → no loss, regardless of +frame count, payload, or duration**. The cliff scales with stream- +opening rate, not with byte volume or wall-clock time. + +The number "99" is suspicious — it's exactly QUIC's typical +`initial_max_streams_uni = 100` minus one. But: + +- Pre-mitigation `firstFalseSend = -1` for tests pumping 1500+ + streams says the cap WAS being raised dynamically. So the cliff + is NOT `openUniStream` rejecting past the local cap. +- A 16 KB payload variant cliffed at 49 streams (~800 KB total), + while 80-byte variants cliffed at 99 (~8 KB total). These are + inconsistent if the cause were a single fixed cap. +- The cliff is *consistent* across many scenarios at ~99 — too + consistent for plain network packet loss. + +## What we tried and what we learned + +### Attempt 1 — make `openUniStream` suspend instead of throw + +Branch / commit: `f0705e3a` on `claude/audio-transmission-tests-zKzlB`, +**reverted in `96a585a6`**. + +Rationale: the obvious failure mode is `peerMaxStreamsUni` being hit +without us ever waiting for the relay to extend it; we'd just throw +locally. So we'd: + +1. Make `QuicConnection.openBidiStream` / `openUniStream` suspend + on a `CompletableDeferred` notifier when the cap is hit, + re-acquire the lock and re-check on every wake. +2. Fire the notifier from `QuicConnectionParser` whenever an inbound + `MAX_STREAMS` frame raises a cap. +3. Emit `STREAMS_BLOCKED` (RFC 9000 §19.14) when an opener registers + itself blocked, so the peer knows we want more credit. +4. Wake blocked openers with `QuicConnectionClosedException` on + close so they don't hang. + +Unit tests for the suspend / wake / STREAMS_BLOCKED / close paths +pass (`PeerStreamLimitTest`). + +**Result against production:** the cliff didn't move. Same scenarios +still hit ~99 frames received, and the run-to-run variance got +*worse* (numbers swung between 19/100 and 99/100 on consecutive +identical baseline runs against the same relay). The fix also +introduced no observable change in the per-frame `tx i=… ok dt=…us` +traces (`firstFalseSend = -1` still on every test), which means the +suspend path isn't being triggered in production — the peer's cap +isn't what we're hitting. + +The fix is RFC-correct on its own (we should suspend rather than +throw, and we should send STREAMS_BLOCKED), but it does not address +the production cliff. Reverted to keep the tree clean while we +chase the real cause. + +### Attempt 2 — pack multiple frames per moq-lite group (shipping) + +Commit on `claude/audio-transmission-tests-zKzlB` (this commit). + +Rationale: `sweep_frames_per_group_*` was 100/100 across every +multi-frame-per-group variant. The cliff scales with the number of +new uni streams opened, not with anything else. Packing N Opus +frames per moq-lite group reduces the new-stream rate by N×. + +Default chosen: `framesPerGroup = 5` ≈ 100 ms of audio per group. +Stream-creation rate drops from 50/sec to 10/sec, well below the +cliff. + +Trade-off: a brand-new subscriber that attaches mid-broadcast picks +up at the next group boundary per moq-lite "from-latest" semantics. +With 5 frames/group the late-join initial gap is up to 100 ms +(perceptually inaudible). With 1 frame/group it was up to 20 ms. + +This is a **mitigation, not a fix.** Other workloads with a high +per-stream cost (file transfer, metadata fan-out, anything that +naturally wants a fresh stream per message) would still hit the +cliff if they exceed roughly 50 streams/second. + +## Hypotheses still on the table + +In rough order of likelihood, none confirmed: + +1. **Connection-level send credit** (`initial_max_data` / `MAX_DATA`). + If the relay's connection-level grant is ~8 KB and only extends + on `DATA_BLOCKED`, we'd cliff at ~99 frames × ~85 bytes = ~8.4 KB. + `:quic` parses inbound `MAX_DATA` (`QuicConnectionParser.kt:266`) + but never emits `DATA_BLOCKED` when our writer hits + `connBudget == 0` in `appendStreamFrames`. That asymmetry would + stall the producer with no signal. The 16 KB payload data point + (cliff at 49 × 16 KB ≈ 800 KB) doesn't match a single fixed + 8 KB cap, but it could match a different per-byte budget the + relay calibrates to its initial offer. Worth instrumenting. + +2. **Per-stream send credit** (`initial_max_stream_data_uni` / + `MAX_STREAM_DATA`). Each new uni stream gets its own credit + window from the peer's `initial_max_stream_data_uni`. If that's + small enough to require a `MAX_STREAM_DATA` on every stream and + the peer is conservative about issuing it, the writer would + buffer frames forever waiting for credit while `publisher.send` + reports success. `:quic` honours the per-stream credit + (`QuicConnectionWriter.kt:280`) but never emits + `STREAM_DATA_BLOCKED`. + +3. **Reference-relay default `MAX_STREAMS_UNI` extension policy.** + `kixelated/moq-rs` is built on Quinn; Quinn's stream-cap + extension is automatic when streams complete. If the production + nostrnests deployment uses a custom Quinn config that only + extends in response to `STREAMS_BLOCKED` (which we don't send + on the un-fixed code path), we'd starve. Attempt 1 added + STREAMS_BLOCKED emission and didn't shift the cliff, weak + evidence against this hypothesis. + +4. **Relay-side per-subscription buffer.** If the relay has a + per-subscriber forward buffer that's smaller than the total + in-flight uni-stream count, and it drops streams that don't + fit, the cliff would scale with stream count and be invisible + to the speaker. Hardest to investigate without relay logs. + +## Next steps to actually fix + +The investigation order I'd take, given the data: + +1. **Instrument outbound `appendStreamFrames`.** Log every iteration + where `connBudget <= 0` (connection-level credit exhausted) or + where a stream's `streamRemaining <= 0` (per-stream credit + exhausted). Re-run the prod sweep with this on. If the cliff + correlates with one of those, the hypothesis is confirmed and the + fix is to emit `DATA_BLOCKED` / `STREAM_DATA_BLOCKED` and on the + write path, suspend the writer until a fresh `MAX_DATA` / + `MAX_STREAM_DATA` arrives. + +2. **Capture the production peer transport parameters.** Dump + `tp.initialMaxData`, `tp.initialMaxStreamDataUni`, and + `tp.initialMaxStreamsUni` once at handshake time so we know + exactly what the relay grants. Compare against the cliff + thresholds. + +3. **Test with `:quic` flow-control diagnostics in place.** Re-run + `sweep_frames200` and `sweep_30s` — the cliff is rock-solid at + exactly 99 in production, so a single pass is enough to confirm + the hypothesis. + +4. **If it's not connection / stream data flow control,** instrument + the relay (file an issue on `nostrnests/nests` or `kixelated/moq`) + asking whether their relay imposes a per-subscriber stream + buffer that drops on overflow. + +## Re-running the sweep after a candidate fix + +The test infrastructure is already in place: + +```bash +./gradlew :nestsClient:jvmTest \ + --tests com.vitorpamplona.nestsclient.interop.NostrnestsProdAudioTransmissionTest \ + -DnestsProd=true -DnestsInteropDebug=true --rerun-tasks +grep -E "sub\[|missing=" \ + nestsClient/build/test-results/jvmTest/TEST-com.vitorpamplona.nestsclient.interop.NostrnestsProdAudioTransmissionTest.xml \ + | sort +``` + +Acceptance criterion: `sweep-30s` reports `received≈1500/1500` +without any `framesPerGroup` mitigation. That removes the only +remaining audio-quality tradeoff (the 100 ms late-join initial gap) +and lets us reset `DEFAULT_FRAMES_PER_GROUP` back to 1 to match the +JS reference broadcaster's wire shape. + +## Files / lines to touch when picking this up + +- `quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionWriter.kt` + — `appendStreamFrames` is where the writer skips streams with no + credit; instrument here. +- `quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnection.kt:160-165` + — `sendConnectionFlowCredit` / `sendConnectionFlowConsumed`. The + values to log + diff. +- `quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/TransportParameters.kt` + — `decode` is where peer TPs land at handshake. +- `quic/src/commonMain/kotlin/com/vitorpamplona/quic/connection/QuicConnectionParser.kt` + — currently parses `MAX_DATA`, `MAX_STREAM_DATA`, `MAX_STREAMS`, + `STREAMS_BLOCKED` (last one ignored). The reverse — + `DATA_BLOCKED` / `STREAM_DATA_BLOCKED` emission — would live in + `QuicConnectionWriter`. +- `nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/SendTraceScenario.kt` + — extend per-frame trace with `connBudget` / `streamRemaining` + if the writer instrumentation surfaces them via diagnostics on + `QuicConnection`. diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsSpeaker.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsSpeaker.kt index 1d6a36c5e..dfdf5836b 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsSpeaker.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/MoqLiteNestsSpeaker.kt @@ -52,6 +52,13 @@ class MoqLiteNestsSpeaker internal constructor( private val encoderFactory: () -> OpusEncoder, private val scope: CoroutineScope, private val mutableState: MutableStateFlow, + /** + * How many Opus frames to pack into one moq-lite group / QUIC uni + * stream. Forwarded to [NestMoqLiteBroadcaster.framesPerGroup] — + * see that field's kdoc for the production stream-cliff rationale. + * Defaults to [NestMoqLiteBroadcaster.DEFAULT_FRAMES_PER_GROUP]. + */ + private val framesPerGroup: Int = NestMoqLiteBroadcaster.DEFAULT_FRAMES_PER_GROUP, ) : NestsSpeaker { override val state: StateFlow = mutableState.asStateFlow() @@ -81,6 +88,7 @@ class MoqLiteNestsSpeaker internal constructor( encoder = encoderFactory(), publisher = publisher, scope = scope, + framesPerGroup = framesPerGroup, ) broadcaster.start() mutableState.value = diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsConnect.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsConnect.kt index c5155174b..d0e517881 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsConnect.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/NestsConnect.kt @@ -167,6 +167,11 @@ private fun failedListener(state: MutableStateFlow): NestsLi * Android passes `{ AudioRecordCapture() }`. * @param encoderFactory builds an [OpusEncoder] (one per broadcast). * Android passes `{ MediaCodecOpusEncoder() }`. + * @param framesPerGroup how many Opus frames to pack into one moq-lite + * group / QUIC uni stream. See + * [com.vitorpamplona.nestsclient.audio.NestMoqLiteBroadcaster.framesPerGroup] + * for the production stream-cliff rationale that motivates the + * default of 5. */ suspend fun connectNestsSpeaker( httpClient: NestsClient, @@ -177,6 +182,9 @@ suspend fun connectNestsSpeaker( speakerPubkeyHex: String, captureFactory: () -> AudioCapture, encoderFactory: () -> OpusEncoder, + framesPerGroup: Int = + com.vitorpamplona.nestsclient.audio + .NestMoqLiteBroadcaster.DEFAULT_FRAMES_PER_GROUP, ): NestsSpeaker { val state = MutableStateFlow( @@ -240,6 +248,7 @@ suspend fun connectNestsSpeaker( encoderFactory = encoderFactory, scope = scope, mutableState = state, + framesPerGroup = framesPerGroup, ) } diff --git a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt index 808f9bbe0..91319b5ba 100644 --- a/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt +++ b/nestsClient/src/commonMain/kotlin/com/vitorpamplona/nestsclient/audio/NestMoqLiteBroadcaster.kt @@ -41,7 +41,41 @@ class NestMoqLiteBroadcaster( private val encoder: OpusEncoder, private val publisher: MoqLitePublisherHandle, private val scope: CoroutineScope, + /** + * Number of consecutive Opus frames to pack into a single moq-lite + * group (= one QUIC unidirectional stream). The first call to + * [MoqLitePublisherHandle.send] in each group opens the stream; + * subsequent calls within the same group write additional + * varint-prefixed frames; [MoqLitePublisherHandle.endGroup] FINs + * the stream and the next [send] starts a fresh group. + * + * **Default = 5 (≈ 100 ms of audio per group).** This is a + * mitigation for a production loss cliff observed against + * `nostrnests.com`: at one-frame-per-group and 20 ms cadence the + * client opens 50 uni streams/second, and somewhere around the + * 99th stream the connection enters a state where new uni streams' + * data stops being delivered to the listener, even though + * `publisher.send` still returns true. Packing 5 frames per group + * reduces the stream-creation rate to 10/second, which the prod + * sweep (`NostrnestsProdAudioTransmissionTest.sweep_frames_per_group_5`) + * shows delivers 100/100 frames cleanly. See + * `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md`. + * + * Trade-off: a brand-new subscriber that attaches mid-broadcast + * picks up at the next group boundary per moq-lite "from-latest" + * semantics. With 5 frames per group, late joiners miss up to + * 100 ms of audio. With the original 1 frame per group it was + * up to 20 ms. Both are imperceptible for live audio rooms. + * + * Set to 1 to restore the original behaviour for testing or for + * deployments to relays that don't exhibit the stream-cliff bug. + */ + private val framesPerGroup: Int = DEFAULT_FRAMES_PER_GROUP, ) { + init { + require(framesPerGroup >= 1) { "framesPerGroup must be >= 1, got $framesPerGroup" } + } + private var job: Job? = null @Volatile private var stopped: Boolean = false @@ -67,6 +101,12 @@ class NestMoqLiteBroadcaster( } job = scope.launch { + // Counts frames written to the current moq-lite group. + // Reset to 0 immediately after each [endGroup] so the + // next [send] auto-starts a fresh group via + // [MoqLitePublisherHandle.send]'s "open on first frame" + // contract. + var framesInCurrentGroup = 0 try { while (true) { val pcm = capture.readFrame() ?: break @@ -87,23 +127,17 @@ class NestMoqLiteBroadcaster( } if (opus.isEmpty()) continue if (muted) continue - // One Opus frame per moq-lite group — mirrors the - // nests JS reference's audio publish path, and is - // load-bearing for the listener-survives-publisher- - // recycle invariant: a brand-new subscriber that - // attaches mid-broadcast (e.g. listener wrapper - // re-subscribing after a publisher cycle) gets the - // NEXT group's frames per moq-lite "from-latest" - // semantics. Without endGroup, the entire broadcast - // is one giant group and new subscribers wait - // indefinitely. The 20 ms cadence here means at - // most one frame of audio missed for any new - // subscriber. See - // `nestsClient/plans/2026-04-26-moq-lite-gap.md`'s - // "Group size: 1 frame per group" line. + // Pack [framesPerGroup] consecutive Opus frames + // into the same moq-lite group / QUIC uni stream + // before FINning. See the [framesPerGroup] kdoc + // for the production cliff this works around. runCatching { publisher.send(opus) - publisher.endGroup() + framesInCurrentGroup += 1 + if (framesInCurrentGroup >= framesPerGroup) { + publisher.endGroup() + framesInCurrentGroup = 0 + } }.onFailure { t -> if (t is CancellationException) throw t onError( @@ -115,6 +149,13 @@ class NestMoqLiteBroadcaster( ) } } + // EOF on the capture side. Flush whatever's in the + // open group so the relay sees its FIN and the + // listener doesn't sit on a half-delivered group + // buffer. Mirrors the per-group endGroup above. + if (framesInCurrentGroup > 0) { + runCatching { publisher.endGroup() } + } } catch (ce: CancellationException) { throw ce } catch (t: Throwable) { @@ -153,4 +194,15 @@ class NestMoqLiteBroadcaster( runCatching { encoder.release() } runCatching { publisher.close() } } + + companion object { + /** + * Default moq-lite group size = 5 Opus frames ≈ 100 ms of audio. + * Picked to keep the QUIC uni-stream creation rate under the + * production cliff observed against `nostrnests.com` while + * still giving late-joining subscribers a sub-100 ms initial + * audio gap. See [framesPerGroup] kdoc for the full rationale. + */ + const val DEFAULT_FRAMES_PER_GROUP: Int = 5 + } } diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsLateJoinInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsLateJoinInteropTest.kt index 49033cedf..60de07dcd 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsLateJoinInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsLateJoinInteropTest.kt @@ -90,6 +90,7 @@ class NostrNestsLateJoinInteropTest { speakerPubkeyHex = speakerSigner.pubKey, captureFactory = { capture }, encoderFactory = { InteropStubEncoder("FRAME-".encodeToByteArray()) }, + framesPerGroup = 1, ) } val broadcast = diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt index 01a47ea4d..280d30267 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMultiPeerInteropTest.kt @@ -354,6 +354,7 @@ class NostrNestsMultiPeerInteropTest { speakerPubkeyHex = signer.pubKey, captureFactory = { capture }, encoderFactory = { StubEncoder(encoderPrefix.encodeToByteArray()) }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(debugScope, "Connected", speaker.state.value) diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMuteInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMuteInteropTest.kt index 4ed587db7..dd1afd108 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMuteInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsMuteInteropTest.kt @@ -94,6 +94,7 @@ class NostrNestsMuteInteropTest { speakerPubkeyHex = speakerSigner.pubKey, captureFactory = { capture }, encoderFactory = { InteropStubEncoder("FRAME-".encodeToByteArray()) }, + framesPerGroup = 1, ) } val broadcast = diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingListenerInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingListenerInteropTest.kt index 88e296422..b196332d9 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingListenerInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingListenerInteropTest.kt @@ -119,6 +119,7 @@ class NostrNestsReconnectingListenerInteropTest { speakerPubkeyHex = pubkey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(scope, "Connected", speaker.state.value) @@ -246,6 +247,7 @@ class NostrNestsReconnectingListenerInteropTest { speakerPubkeyHex = pubkey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } val broadcast = speaker.startBroadcasting() @@ -496,6 +498,7 @@ class NostrNestsReconnectingListenerInteropTest { speakerPubkeyHex = pubkey, captureFactory = captureFactory, encoderFactory = { encoder }, + framesPerGroup = 1, ) }, ) diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingSpeakerInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingSpeakerInteropTest.kt index 2f82392d6..3021ccb67 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingSpeakerInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsReconnectingSpeakerInteropTest.kt @@ -278,6 +278,7 @@ class NostrNestsReconnectingSpeakerInteropTest { speakerPubkeyHex = pubkey, captureFactory = captureFactory, encoderFactory = { encoder }, + framesPerGroup = 1, ) }, ) diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt index e194b7e0f..fd4106c6e 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsRoundTripInteropTest.kt @@ -132,6 +132,7 @@ class NostrNestsRoundTripInteropTest { speakerPubkeyHex = pubkey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(scope, "Connected", speaker.state.value) diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsSpeakerCloseInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsSpeakerCloseInteropTest.kt index c52ccebb0..2e622abb5 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsSpeakerCloseInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsSpeakerCloseInteropTest.kt @@ -96,6 +96,7 @@ class NostrNestsSpeakerCloseInteropTest { speakerPubkeyHex = speakerSigner.pubKey, captureFactory = { capture }, encoderFactory = { InteropStubEncoder("FRAME-".encodeToByteArray()) }, + framesPerGroup = 1, ) } val broadcast = diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsUnsubscribeInteropTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsUnsubscribeInteropTest.kt index 1840f6b50..640d83925 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsUnsubscribeInteropTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrNestsUnsubscribeInteropTest.kt @@ -90,6 +90,7 @@ class NostrNestsUnsubscribeInteropTest { speakerPubkeyHex = speakerSigner.pubKey, captureFactory = { capture }, encoderFactory = { InteropStubEncoder("FRAME-".encodeToByteArray()) }, + framesPerGroup = 1, ) } val broadcast = diff --git a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrnestsProdAudioTransmissionTest.kt b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrnestsProdAudioTransmissionTest.kt index 94db1551a..cbc0c9366 100644 --- a/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrnestsProdAudioTransmissionTest.kt +++ b/nestsClient/src/jvmTest/kotlin/com/vitorpamplona/nestsclient/interop/NostrnestsProdAudioTransmissionTest.kt @@ -171,6 +171,7 @@ class NostrnestsProdAudioTransmissionTest { speakerPubkeyHex = signer.pubKey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(scope, "Connected", speaker.state.value) @@ -290,6 +291,7 @@ class NostrnestsProdAudioTransmissionTest { speakerPubkeyHex = hostSigner.pubKey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(scope, "Connected", speaker.state.value) @@ -408,6 +410,7 @@ class NostrnestsProdAudioTransmissionTest { speakerPubkeyHex = hostSigner.pubKey, captureFactory = { capture }, encoderFactory = { encoder }, + framesPerGroup = 1, ) } InteropDebug.assertSpeakerReached(scope, "Connected", speaker.state.value)