refactor(nests): per-stream channel count + AudioBroadcastConfig (I4 prep)
Split the previously global `AudioFormat.CHANNELS = 1` into a `DEFAULT_CHANNELS` constant + per-call-site `channelCount` parameters so a single broadcast can advertise stereo Opus without forcing every mono call site to grow a new argument. Generalises the catalog factory to `MoqLiteHangCatalog.opus48k(name, channels)` with memoised JSON bytes per shape, threads a new `AudioBroadcastConfig(channelCount)` through `connectNestsSpeaker` / `connectReconnectingNestsSpeaker` / `MoqLiteNestsSpeaker`, and adds a `channelCount` parameter to `MediaCodecOpusEncoder`. Production behaviour is unchanged for mono callers (the new config defaults to mono); the listener side already discovers the channel count from the catalog via `NestViewModel.awaitAudioPipelineConfig`. No test or wire changes. Phase 1 of `nestsClient/plans/2026-05-06-i4-stereo-cross-stack-scenario.md`. The hang-interop test scaffolding (`HangInteropTest`, `runSpeakerToHangListen`, Rust `hang-listen` / `hang-publish`, `JvmOpusEncoder`) doesn't exist on this branch yet, so the I4 forward + reverse scenarios are deferred until the parent T16 plan lands. https://claude.ai/code/session_01EqJEADzH9yjSuoP5L9js8i
This commit is contained in:
+35
-3
@@ -25,7 +25,7 @@ import kotlin.test.assertEquals
|
||||
|
||||
class MoqLiteHangCatalogTest {
|
||||
@Test
|
||||
fun opusMono48kEmitsCanonicalHangShape() {
|
||||
fun opus48kMonoEmitsCanonicalHangShape() {
|
||||
// Byte-exact assertion: `:commons`'s `RoomSpeakerCatalogTest`
|
||||
// round-trips this same string against the parser. If either
|
||||
// side drifts from the kixelated/hang wire shape, both tests
|
||||
@@ -35,17 +35,49 @@ class MoqLiteHangCatalogTest {
|
||||
"\"codec\":\"opus\",\"container\":{\"kind\":\"legacy\"}," +
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":1,\"jitter\":20}}}}"
|
||||
val actual =
|
||||
MoqLiteHangCatalog.opusMono48k("audio/data").encodeJsonBytes().decodeToString()
|
||||
MoqLiteHangCatalog.opus48k("audio/data").encodeJsonBytes().decodeToString()
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun opus48kStereoEmitsTwoChannelHangShape() {
|
||||
// Stereo broadcasts (kixelated/moq web publisher with a stereo
|
||||
// AudioContext) declare numberOfChannels = 2. Listeners pick
|
||||
// this up via the catalog and configure their decoder + sink
|
||||
// for L/R interleaved PCM.
|
||||
val expected =
|
||||
"{\"audio\":{\"renditions\":{\"audio/data\":{" +
|
||||
"\"codec\":\"opus\",\"container\":{\"kind\":\"legacy\"}," +
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":2,\"jitter\":20}}}}"
|
||||
val actual =
|
||||
MoqLiteHangCatalog
|
||||
.opus48k("audio/data", numberOfChannels = 2)
|
||||
.encodeJsonBytes()
|
||||
.decodeToString()
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renditionKeyMatchesCallerSuppliedTrackName() {
|
||||
val actual =
|
||||
MoqLiteHangCatalog.opusMono48k("custom/track").encodeJsonBytes().decodeToString()
|
||||
MoqLiteHangCatalog.opus48k("custom/track").encodeJsonBytes().decodeToString()
|
||||
// The rendition map MUST be keyed on the caller-supplied track
|
||||
// name — the watcher uses this string verbatim as the
|
||||
// SUBSCRIBE.track on the audio subscription.
|
||||
assertEquals(true, actual.contains("\"custom/track\":{"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun opus48kJsonBytesMemoisesPerShape() {
|
||||
// Same shape → same byte array reference (fast-path on every
|
||||
// subsequent broadcast / hot-swap iteration).
|
||||
val a = MoqLiteHangCatalog.opus48kJsonBytes("audio/data", 1)
|
||||
val b = MoqLiteHangCatalog.opus48kJsonBytes("audio/data", 1)
|
||||
assertEquals(true, a === b)
|
||||
|
||||
// Different shape → different bytes; both stay cached.
|
||||
val stereo = MoqLiteHangCatalog.opus48kJsonBytes("audio/data", 2)
|
||||
assertEquals(true, a !== stereo)
|
||||
assertEquals(true, stereo === MoqLiteHangCatalog.opus48kJsonBytes("audio/data", 2))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user