fix(audio-rooms): SubscribeResponse framing matches moq-lite-03
Lite-03's SubscribeResponse on the response side of a Subscribe bidi
is two pieces concatenated:
type varint (0 = Ok, 1 = Drop)
body size-prefixed bytes
The type discriminator sits OUTSIDE the body's size prefix. Earlier
drafts wrapped the whole thing in one outer size prefix; Lite-03 split
them. See `rs/moq-lite/src/lite/subscribe.rs::SubscribeResponse::encode`
(the `_` arm, which matches Lite-03+).
Our codec was producing — and reading — the older outer-wrapped form,
so against a Lite-03 relay we mis-parsed the type discriminator (`0`
for Ok) as a "size=0" outer prefix and surfaced a confusing
`MoqCodecException: truncated varint at offset=0 (remaining=0)` from
inside `decodeSubscribeResponse`. The first byte the relay sent was
the type, not a size, and our reader peeled it off as the outer length.
Fix touches three pieces:
* `encodeSubscribeOk` / `encodeSubscribeDrop` emit
`type + size_prefixed(body)` directly (no outer wrap), via a small
`prefixWithType` helper.
* `decodeSubscribeResponse` reads `type` then a length-prefixed body,
decodes the body in its own reader, and asserts the outer payload
is fully consumed.
* `readSubscribeResponseFromBidi` walks chunks into the buffer until
both the type varint and the size-prefixed body have arrived, then
re-emits the contiguous `[type][size][body]` slab so the decoder
parses it self-contained. Reuses the `EarlyExit` collector pattern
the publisher-inbound path already uses; avoids `Flow.iterator()`
which doesn't exist in kotlinx.coroutines.
Tests:
* `MoqLiteCodecTest::subscribe{Ok,Drop}_round_trips` no longer
`peelSizePrefix` the encoded bytes — the new wire form has no outer
prefix to strip; the bytes go straight through `decodeSubscribeResponse`.
* `MoqLiteSessionTest::publisher_acks_subscribe_and_pushes_group_data_on_uni_stream`
also drops `MoqLiteFrameBuffer().readSizePrefixed()` on the ack chunk
for the same reason.
Verified end-to-end against a bare-metal moq-relay 0.10.25 + moq-auth
(`-DnestsInteropExternal=true`):
* `NostrNestsRoundTripInteropTest::production_speaker_broadcasts_to_production_listener_via_real_relay`
passes — speaker → listener → 8 frames round-trip cleanly.
* `NostrNestsMultiPeerInteropTest::one_speaker_fans_out_to_two_listeners`
passes — same speaker reaches both listeners with the full frame
stream.
* The `listener_subscribed_before_announce_receives_late_frames` and
`two_speakers_in_same_room_deliver_independently_to_one_listener`
cases still fail with `subscribe stream FIN before reply` — those
are different relay-behavior issues (the v1 relay seems to FIN
subscribes against unannounced broadcasts and against a second
speaker on the same listener), unrelated to the codec.
This commit is contained in:
+5
-2
@@ -177,7 +177,10 @@ class MoqLiteCodecTest {
|
||||
startGroup = null,
|
||||
endGroup = null,
|
||||
)
|
||||
val payload = peelSizePrefix(MoqLiteCodec.encodeSubscribeOk(msg))
|
||||
// moq-lite-03 SubscribeResponse is `[type][size][body]` with the
|
||||
// type discriminator OUTSIDE the size prefix — the encoder no
|
||||
// longer emits an outer wrap, so feed the bytes through directly.
|
||||
val payload = MoqLiteCodec.encodeSubscribeOk(msg)
|
||||
val resp = MoqLiteCodec.decodeSubscribeResponse(payload)
|
||||
val ok = assertIs<MoqLiteCodec.SubscribeResponse.Ok>(resp)
|
||||
assertEquals(msg, ok.ok)
|
||||
@@ -186,7 +189,7 @@ class MoqLiteCodecTest {
|
||||
@Test
|
||||
fun subscribeDrop_round_trips() {
|
||||
val msg = MoqLiteSubscribeDrop(errorCode = 0x12L, reasonPhrase = "publisher gone")
|
||||
val payload = peelSizePrefix(MoqLiteCodec.encodeSubscribeDrop(msg))
|
||||
val payload = MoqLiteCodec.encodeSubscribeDrop(msg)
|
||||
val resp = MoqLiteCodec.decodeSubscribeResponse(payload)
|
||||
val drop = assertIs<MoqLiteCodec.SubscribeResponse.Dropped>(resp)
|
||||
assertEquals(msg, drop.drop)
|
||||
|
||||
+5
-5
@@ -300,12 +300,12 @@ class MoqLiteSessionTest {
|
||||
),
|
||||
)
|
||||
|
||||
// Step 2: we reply SubscribeOk.
|
||||
// Step 2: we reply SubscribeOk. moq-lite-03's SubscribeResponse
|
||||
// is `[type][size][body]` with the type discriminator OUTSIDE
|
||||
// the size prefix — no outer wrap to strip, the chunk itself
|
||||
// is the wire form decodeSubscribeResponse expects.
|
||||
val ackChunk = withTimeout(2_000) { subBidi.incoming().first() }
|
||||
val resp =
|
||||
MoqLiteCodec.decodeSubscribeResponse(
|
||||
MoqLiteFrameBuffer().apply { push(ackChunk) }.readSizePrefixed()!!,
|
||||
)
|
||||
val resp = MoqLiteCodec.decodeSubscribeResponse(ackChunk)
|
||||
assertIs<MoqLiteCodec.SubscribeResponse.Ok>(resp)
|
||||
|
||||
// Step 3: publisher pushes one frame, which opens a uni
|
||||
|
||||
Reference in New Issue
Block a user