fix(nestsclient): pack trackPriority + sequence into stream priority — moq-lite Lite-03 priority.rs
Per moq-lite Lite-03 (kixelated/moq `rs/moq-lite/src/lite/priority.rs`), `Publisher::serve_group` calls `priority.insert(track.priority, sequence)` and feeds the resulting position into `stream.set_priority`. Priority sorts first by `track.priority u8` (higher track = drains ahead under congestion), then by group `sequence` within a track (newer = drains ahead). Pre-fix we passed raw `sequence.toInt()` directly to setPriority, ignoring the per-track byte. For our single-Opus-track production case this was unobservable — newer-first ordering held by sequence monotonicity — but a future multi-track broadcast (audio + companion catalog / status track) would have starved the lower-rate track the moment audio's outbound queue got congested. Fix: add `trackPriority: Int = DEFAULT_TRACK_PRIORITY` parameter to `MoqLiteSession.publish()`, store on PublisherStateImpl, and bit-pack in `openGroupStream`: bits 31..24 trackPriority u8 (0..255) bits 23..0 sequence low 24 bits The 24-bit sequence window is ample (≈ 6 days at 1 group/sec, beyond which all newer groups within a single track tie — but they still beat older groups of any LOWER-priority track via the top byte). Production sessions cycle on JWT refresh every 9 min, so the wrap is defensive only. `DEFAULT_TRACK_PRIORITY = 0x80` matches the existing subscriber-side DEFAULT_PRIORITY midpoint, so all existing call sites keep their prior behavior. Test seam: `FakeWebTransport.openUniStream` now records the most- recent `setPriority` value via a shared `AtomicInteger` cell that the peer-side `FakeReadStream` exposes as `lastSetPriority`. Lets the new regression test verify the bit-pack formula on the actual peer-side read stream rather than peeking into private state. Regression test: `MoqLiteSessionTest.publisher_packs_trackPriority_and_sequence_into_setPriority_value`. Audit doc: nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md (M1). https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
This commit is contained in:
+63
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.nestsclient.moq.lite
|
||||
|
||||
import com.vitorpamplona.nestsclient.transport.FakeBidiStream
|
||||
import com.vitorpamplona.nestsclient.transport.FakeReadStream
|
||||
import com.vitorpamplona.nestsclient.transport.FakeWebTransport
|
||||
import com.vitorpamplona.quic.Varint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -399,6 +400,68 @@ class MoqLiteSessionTest {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publisher_packs_trackPriority_and_sequence_into_setPriority_value() =
|
||||
runBlocking {
|
||||
// Lite-03 audit M1: openGroupStream packs the publisher's
|
||||
// trackPriority byte into bits 31..24 and the group sequence
|
||||
// into bits 23..0 of the value passed to
|
||||
// WebTransportWriteStream.setPriority. This mirrors the
|
||||
// (track.priority, sequence) ordering kixelated's
|
||||
// PriorityHandle uses at `rs/moq-lite/src/lite/priority.rs`.
|
||||
// Verified on the *peer* side via FakeReadStream.lastSetPriority,
|
||||
// which the in-memory transport uses as a side-channel for
|
||||
// priority introspection.
|
||||
val (clientSide, serverSide) = FakeWebTransport.pair()
|
||||
val session = MoqLiteSession.client(clientSide, pumpScope)
|
||||
|
||||
// Use a non-default trackPriority so a regression that
|
||||
// accidentally drops it back to DEFAULT_TRACK_PRIORITY shows
|
||||
// up in the assertion.
|
||||
val publisher =
|
||||
session.publish(
|
||||
broadcastSuffix = "speakerPubkey",
|
||||
track = "audio/data",
|
||||
startSequence = 7L,
|
||||
trackPriority = 0xC0,
|
||||
)
|
||||
|
||||
val subBidi = serverSide.openBidiStream()
|
||||
subBidi.write(Varint.encode(MoqLiteControlType.Subscribe.code))
|
||||
subBidi.write(
|
||||
MoqLiteCodec.encodeSubscribe(
|
||||
MoqLiteSubscribe(
|
||||
id = 99L,
|
||||
broadcast = "speakerPubkey",
|
||||
track = "audio/data",
|
||||
priority = 0x80,
|
||||
ordered = true,
|
||||
maxLatencyMillis = 0L,
|
||||
startGroup = null,
|
||||
endGroup = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
withTimeout(2_000) { subBidi.incoming().first() }
|
||||
|
||||
assertEquals(true, publisher.send("opus-frame".encodeToByteArray()))
|
||||
publisher.endGroup()
|
||||
|
||||
val relayUni = withTimeout(2_000) { serverSide.incomingUniStreams().first() }
|
||||
val fakeRead = relayUni as FakeReadStream
|
||||
// Expected: (0xC0 shl 24) or (7 and 0x00FF_FFFF) =
|
||||
// 0xC000_0007 in unsigned terms = -1073741817 as Int.
|
||||
// Compute via the same formula so the test asserts the
|
||||
// CONTRACT, not a hex literal.
|
||||
val expected = ((0xC0 and 0xFF) shl 24) or (7 and 0x00FF_FFFF)
|
||||
assertEquals(expected, fakeRead.lastSetPriority, "trackPriority dominates the high byte; sequence fills the low 24 bits")
|
||||
|
||||
// Drain the stream so the cleanup path doesn't leak.
|
||||
relayUni.incoming().toList()
|
||||
publisher.close()
|
||||
session.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publisher_send_returns_false_when_no_inbound_subscriber() =
|
||||
runBlocking {
|
||||
|
||||
Reference in New Issue
Block a user