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:
@@ -22,10 +22,8 @@ package com.vitorpamplona.nestsclient
|
||||
|
||||
import com.vitorpamplona.nestsclient.audio.AudioCapture
|
||||
import com.vitorpamplona.nestsclient.audio.OpusEncoder
|
||||
import com.vitorpamplona.nestsclient.moq.MoqSession
|
||||
import com.vitorpamplona.nestsclient.moq.MoqVersion
|
||||
import com.vitorpamplona.nestsclient.moq.SubscribeHandle
|
||||
import com.vitorpamplona.nestsclient.moq.TrackNamespace
|
||||
import com.vitorpamplona.nestsclient.transport.WebTransportException
|
||||
import com.vitorpamplona.nestsclient.transport.WebTransportFactory
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
@@ -163,12 +161,16 @@ private fun failedListener(state: MutableStateFlow<NestsListenerState>): NestsLi
|
||||
|
||||
/**
|
||||
* Speaker / host counterpart of [connectNestsListener]. Walks the same
|
||||
* HTTP → WebTransport → MoQ handshake; the difference is the post-setup
|
||||
* step is `announce(...)` (driven by [NestsSpeaker.startBroadcasting])
|
||||
* instead of `subscribe(...)`.
|
||||
* three-step HTTP → WebTransport → moq-lite session sequence the
|
||||
* listener does; the difference is downstream: [NestsSpeaker.startBroadcasting]
|
||||
* claims a broadcast suffix on the moq-lite session and starts pumping
|
||||
* Opus frames out as one uni stream per group.
|
||||
*
|
||||
* @param speakerPubkeyHex this user's pubkey hex, used as the MoQ track
|
||||
* name when we ANNOUNCE — listeners look us up by exactly that name.
|
||||
* @param speakerPubkeyHex this user's pubkey hex. Used as the moq-lite
|
||||
* broadcast suffix the relay routes to subscribers
|
||||
* (`MoqLiteSubscribe.broadcast == speakerPubkeyHex`); the JS reference
|
||||
* mints the same value via `Path.from(identity)`
|
||||
* (`@moq/publish/screen-B680RFft.js`).
|
||||
* @param captureFactory builds an [AudioCapture] (one per broadcast).
|
||||
* Android passes `{ AudioRecordCapture() }`.
|
||||
* @param encoderFactory builds an [OpusEncoder] (one per broadcast).
|
||||
@@ -228,28 +230,23 @@ suspend fun connectNestsSpeaker(
|
||||
|
||||
state.value = NestsSpeakerState.Connecting(NestsSpeakerState.Connecting.ConnectStep.MoqHandshake)
|
||||
|
||||
// moq-lite Lite-03 has NO setup message. Same logic as the listener
|
||||
// path — `supportedMoqVersions` retained for backward compat but
|
||||
// currently a no-op because version is selected by ALPN.
|
||||
val moq =
|
||||
try {
|
||||
MoqSession.client(webTransport, scope).also { it.setup(supportedMoqVersions) }
|
||||
com.vitorpamplona.nestsclient.moq.lite.MoqLiteSession
|
||||
.client(webTransport, scope)
|
||||
} catch (t: Throwable) {
|
||||
runCatching { webTransport.close(0, "moq setup failed") }
|
||||
state.value = NestsSpeakerState.Failed("MoQ handshake failed: ${t.message}", t)
|
||||
runCatching { webTransport.close(0, "moq-lite session init failed") }
|
||||
state.value = NestsSpeakerState.Failed("moq-lite session init failed: ${t.message}", t)
|
||||
return failedSpeaker(state)
|
||||
}
|
||||
|
||||
val negotiatedVersion =
|
||||
moq.selectedVersion ?: run {
|
||||
runCatching { moq.close() }
|
||||
state.value = NestsSpeakerState.Failed("MoQ session reported no negotiated version")
|
||||
return failedSpeaker(state)
|
||||
}
|
||||
|
||||
state.value = NestsSpeakerState.Connected(room, negotiatedVersion)
|
||||
return DefaultNestsSpeaker(
|
||||
state.value = NestsSpeakerState.Connected(room, MOQ_LITE_03_VERSION)
|
||||
return MoqLiteNestsSpeaker(
|
||||
session = moq,
|
||||
// Same single-segment shape as the listener path; see comment there.
|
||||
roomNamespace = TrackNamespace.of(room.moqNamespace()),
|
||||
speakerTrackName = speakerPubkeyHex.encodeToByteArray(),
|
||||
speakerPubkeyHex = speakerPubkeyHex,
|
||||
captureFactory = captureFactory,
|
||||
encoderFactory = encoderFactory,
|
||||
scope = scope,
|
||||
|
||||
Reference in New Issue
Block a user