fix(nests): T11 drop bestEffort=true on moq-lite group uni streams
`MoqLiteSession.openGroupStream` was opening each group's QUIC uni stream with `bestEffort = true`. `:quic`'s `SendBuffer.markLost` with that flag drops lost STREAM ranges WITHOUT retransmit AND WITHOUT `RESET_STREAM` (`quic/src/commonMain/kotlin/com/vitorpamplona/quic/ stream/SendBuffer.kt:300-309`). The peer's `ReceiveBuffer` ends up with chunks at `[0, P)` and `[Q, end)` and a permanent hole at `[P, Q)`; the application's `incoming` Flow parks on the hole forever. Web watchers (`@moq/hang` `Container.Consumer`) park their `Group.readFrame` until the relay's 30 s `MAX_GROUP_AGE` ages the broadcast queue out — manifesting as a 30 s silent dropout per lost packet on lossy networks (cellular, mobile WiFi, congested home routers). This is a real-world bug that's invisible on a clean LAN. The reference implementation (kixelated/moq-rs `Publisher::serve_group`, `rs/moq-lite/src/lite/publisher.rs:347-406`) writes to reliable QUIC streams with no `set_unreliable` call — `bestEffort` was Amethyst's private optimisation with no peer-side support. Drop it; let `:quic` retransmit lost ranges normally. A retransmit arriving 50–150 ms late still falls inside hang's default ~200 ms jitter buffer, so the cost is marginal extra bandwidth on retransmits and the win is no more silent dropouts on lossy networks. T11.2 — orthogonality check: stream-cliff fix is independent. Re-read `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md` and grep'd both the plan and `NestMoqLiteBroadcaster.kt` for `bestEffort` / `best_effort` — neither references it. The cliff fix is `framesPerGroup = 5/50` (cadence reduction); load-bearing on stream-creation RATE, not loss handling. Drop is safe. T11.3 (stream priority — newer groups drain first under congestion) deferred to a follow-up commit; hooking it into `:quic`'s send-frame loop is bigger than a one-line change and warrants its own task. The kixelated/moq-rs publisher uses `stream.set_priority(priority.current())` to bias the writer; without it, our drain order under congestion is FIFO across streams rather than newest-first, which can mean the listener catches up on a stale group when a fresh one is more useful. Doesn't block today — production audio rarely hits transport congestion at 1 group/sec cadence. https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+15
-8
@@ -1023,14 +1023,21 @@ class MoqLiteSession internal constructor(
|
||||
subscribeId: Long,
|
||||
sequence: Long,
|
||||
): com.vitorpamplona.nestsclient.transport.WebTransportWriteStream {
|
||||
// Group streams carry a single Opus packet. They're real-time
|
||||
// and best-effort — a STREAM frame arriving 200 ms late is
|
||||
// worse than useless because the listener has already moved
|
||||
// past that group's sequence number. Setting bestEffort=true
|
||||
// tells the underlying QUIC SendBuffer to drop lost ranges
|
||||
// instead of retransmitting them, bounding the bandwidth waste
|
||||
// we'd otherwise incur on a lossy uplink.
|
||||
val uni = transport.openUniStream(bestEffort = true)
|
||||
// Group streams use reliable QUIC delivery to match the
|
||||
// moq-lite reference (kixelated/moq-rs `serve_group` writes
|
||||
// reliable streams; bestEffort is not a moq-lite concept).
|
||||
// The previous shape opened with `bestEffort=true`, which
|
||||
// caused `:quic`'s `SendBuffer.markLost` to drop lost ranges
|
||||
// without retransmit AND without RESET_STREAM — leaving the
|
||||
// peer's stream-reassembly buffer permanently wedged at the
|
||||
// hole boundary. The watcher's `Group.readFrame` parks until
|
||||
// the relay's 30 s `MAX_GROUP_AGE` ages the broadcast queue
|
||||
// out, manifesting as a 30 s silent dropout per lost packet
|
||||
// on lossy networks. Reliable delivery costs marginal extra
|
||||
// bandwidth on retransmits (a lost STREAM range arriving 50–
|
||||
// 150 ms late still falls inside hang's default ~200 ms
|
||||
// jitter buffer) and avoids the dropout entirely.
|
||||
val uni = transport.openUniStream()
|
||||
uni.write(Varint.encode(MoqLiteDataType.Group.code))
|
||||
uni.write(MoqLiteCodec.encodeGroupHeader(MoqLiteGroupHeader(subscribeId, sequence)))
|
||||
return uni
|
||||
|
||||
Reference in New Issue
Block a user