feat(audio-rooms): moq-lite speaker side end-to-end (phase 5c-speaker)
Production speaker path now runs on moq-lite, so connectNestsSpeaker
exchanges real moq-lite framing with the nostrnests reference relay.
Transport layer:
- WebTransportSession.incomingBidiStreams() — peer-initiated bidi
flow. moq-lite publishers receive Announce + Subscribe bidis from
the relay (rs/moq-lite/src/lite/publisher.rs:40 uses
Stream::accept(session)), so the abstraction grew the
accept-bidi-from-peer surface.
- WebTransportSession.openUniStream() — locally-opened uni stream
for group push (rs/moq-lite/src/lite/publisher.rs:338 uses
session.open_uni()).
- :quic WtPeerStreamDemux StrippedWtStream now carries optional
send/finish closures. The demux takes the QuicConnectionDriver
so wakeups fire after each app-level write on a peer-initiated
bidi.
- FakeWebTransport now exposes incomingBidiStreams + openUniStream
directly; the openPeerUniStream test helper went away (production
flow covers it).
Session layer:
- MoqLiteSession.publish(suffix) — claims a broadcast suffix and
lazily launches a relay→us bidi pump. ControlType=Announce reads
AnnouncePlease, replies Active(suffix). ControlType=Subscribe reads
body, replies SubscribeOk, registers the inbound subscription.
- MoqLitePublisherHandle — startGroup / send / endGroup / close
semantics. send opens a uni stream per group with DataType=0 +
GroupHeader and pushes varint(size)+payload frames. close emits
Announce(Ended) on every active announce bidi, FINs the uni.
Application layer:
- AudioRoomMoqLiteBroadcaster — sibling of AudioRoomBroadcaster but
drives MoqLitePublisherHandle (keeps IETF broadcaster intact for
its unit tests).
- MoqLiteNestsSpeaker — NestsSpeaker adapter, mirror of
MoqLiteNestsListener on the publish side.
- connectNestsSpeaker now opens a MoqLiteSession (no SETUP) and
returns MoqLiteNestsSpeaker.
Tests:
- 4 new MoqLiteSessionTest cases:
publisher_replies_to_announcePlease_with_active_announce,
publisher_acks_subscribe_and_pushes_group_data_on_uni_stream,
publisher_send_returns_false_when_no_inbound_subscriber,
publisher_close_emits_ended_announce.
Verified :commons:compileKotlinJvm + :amethyst:compilePlayDebugKotlin
both still compile against the swap.
Docs (plans + CLAUDE.md) refreshed to reflect speaker-side landing.
This commit is contained in:
+70
@@ -234,6 +234,11 @@ class QuicWebTransportSession(
|
||||
return QuicBidiStreamAdapter(s, state.driver)
|
||||
}
|
||||
|
||||
override suspend fun openUniStream(): WebTransportWriteStream {
|
||||
val s = state.openUniStream()
|
||||
return QuicUniWriteStreamAdapter(s, state.driver)
|
||||
}
|
||||
|
||||
override fun incomingUniStreams(): Flow<WebTransportReadStream> =
|
||||
flow {
|
||||
// Surface only unidirectional WT streams whose prefix bytes
|
||||
@@ -247,6 +252,18 @@ class QuicWebTransportSession(
|
||||
}
|
||||
}
|
||||
|
||||
override fun incomingBidiStreams(): Flow<WebTransportBidiStream> =
|
||||
flow {
|
||||
// Surface only peer-initiated bidi streams whose WT_BIDI_STREAM
|
||||
// prefix has been stripped by the demux. send/finish are
|
||||
// wired through the driver wakeup.
|
||||
state.incomingStrippedStreams.collect { stripped ->
|
||||
if (!stripped.isUnidirectional) {
|
||||
emit(StrippedWtBidiStreamAdapter(stripped))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendDatagram(payload: ByteArray): Boolean {
|
||||
state.sendDatagram(payload)
|
||||
return true
|
||||
@@ -292,9 +309,62 @@ private class QuicReadStreamAdapter(
|
||||
override fun incoming(): Flow<ByteArray> = stream.incoming
|
||||
}
|
||||
|
||||
/**
|
||||
* Write-only adapter over a locally-opened uni QUIC stream. The
|
||||
* underlying [QuicWebTransportSessionState.openUniStream] has already
|
||||
* pushed the WT framing prefix (0x54 + quarter session id), so the
|
||||
* caller's [write] payload goes straight onto the wire.
|
||||
*/
|
||||
private class QuicUniWriteStreamAdapter(
|
||||
private val stream: QuicStream,
|
||||
private val driver: com.vitorpamplona.quic.connection.QuicConnectionDriver,
|
||||
) : WebTransportWriteStream {
|
||||
override suspend fun write(chunk: ByteArray) {
|
||||
stream.send.enqueue(chunk)
|
||||
driver.wakeup()
|
||||
}
|
||||
|
||||
override suspend fun finish() {
|
||||
stream.send.finish()
|
||||
driver.wakeup()
|
||||
}
|
||||
}
|
||||
|
||||
/** Adapter for a WT peer-initiated uni stream whose prefix has been stripped. */
|
||||
private class StrippedWtReadStreamAdapter(
|
||||
private val stripped: com.vitorpamplona.quic.webtransport.StrippedWtStream,
|
||||
) : WebTransportReadStream {
|
||||
override fun incoming(): Flow<ByteArray> = stripped.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter for a peer-initiated bidi WT stream whose WT_BIDI_STREAM prefix
|
||||
* has been stripped. Routes [write] / [finish] through the demux's
|
||||
* driver-aware closures so application bytes actually leave the
|
||||
* connection.
|
||||
*/
|
||||
private class StrippedWtBidiStreamAdapter(
|
||||
private val stripped: com.vitorpamplona.quic.webtransport.StrippedWtStream,
|
||||
) : WebTransportBidiStream {
|
||||
init {
|
||||
check(!stripped.isUnidirectional) {
|
||||
"StrippedWtBidiStreamAdapter requires a bidi stream, got uni"
|
||||
}
|
||||
}
|
||||
|
||||
override fun incoming(): Flow<ByteArray> = stripped.data
|
||||
|
||||
override suspend fun write(chunk: ByteArray) {
|
||||
val send =
|
||||
stripped.send
|
||||
?: error("peer-initiated bidi stream has no send half — demux didn't wire one")
|
||||
send(chunk)
|
||||
}
|
||||
|
||||
override suspend fun finish() {
|
||||
val finish =
|
||||
stripped.finish
|
||||
?: error("peer-initiated bidi stream has no finish — demux didn't wire one")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user