feat(nests): emit catalog jitter hint matching Opus frame duration
hang.js's encoder publishes a `jitter` field (ms) in every audio rendition; the watcher uses it to size its jitter buffer. Without it the watcher falls back to a built-in default — works in practice today but means our publishes deviate from the canonical hang shape and the deviation could surface as audible buffer-sizing differences on a future watcher revision that takes the field as required. Add `jitter: 20` to MoqLiteHangCatalog.AudioRendition (matching the Opus frame cadence: 960 samples at 48 kHz = 20 ms; same value hang.js hard-codes at `js/publish/src/audio/encoder.ts:#runConfig`). Updates the byte-exact MoqLiteHangCatalogTest assertion and the parallel inline-literal in RoomSpeakerCatalogTest so both sides stay pinned. The commons-side `RoomSpeakerCatalog` parser doesn't surface the new field — `JsonMapper`'s `ignoreUnknownKeys = true` lets the watcher ingest it silently. Adding a typed `jitter` field there is a forward-compat task; not needed for interop with hang today. https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+6
-1
@@ -137,7 +137,7 @@ class RoomSpeakerCatalogTest {
|
||||
val emitted =
|
||||
"{\"audio\":{\"renditions\":{\"audio/data\":{" +
|
||||
"\"codec\":\"opus\",\"container\":{\"kind\":\"legacy\"}," +
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":1}}}}"
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":1,\"jitter\":20}}}}"
|
||||
val catalog = RoomSpeakerCatalog.parseOrNull(emitted.encodeToByteArray())
|
||||
assertNotNull(catalog)
|
||||
val rendition = catalog.primaryAudio()
|
||||
@@ -146,5 +146,10 @@ class RoomSpeakerCatalogTest {
|
||||
assertEquals("legacy", rendition.container?.kind)
|
||||
assertEquals(48_000, rendition.sampleRate)
|
||||
assertEquals(1, rendition.numberOfChannels)
|
||||
// `jitter` is intentionally not asserted on `rendition` — the
|
||||
// commons-side parser drops unknown fields via the JsonMapper's
|
||||
// `ignoreUnknownKeys = true`. Adding it to `RoomSpeakerCatalog`
|
||||
// is forward work; the byte-exact match in the literal above
|
||||
// already pins the wire shape.
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -40,6 +40,7 @@ import kotlinx.serialization.json.Json
|
||||
* "container": { "kind": "legacy" },
|
||||
* "sampleRate": 48000,
|
||||
* "numberOfChannels": 1,
|
||||
* "jitter": 20, // ms; matches one Opus frame
|
||||
* "bitrate": 32000 // optional
|
||||
* }
|
||||
* }
|
||||
@@ -67,6 +68,15 @@ internal data class MoqLiteHangCatalog(
|
||||
val container: Container,
|
||||
val sampleRate: Int,
|
||||
val numberOfChannels: Int,
|
||||
/**
|
||||
* Publisher-side cadence hint (ms). hang.js's watcher uses
|
||||
* this to size its jitter buffer — emit a real value rather
|
||||
* than relying on the watcher's fallback default. For Opus
|
||||
* this is the codec frame duration (20 ms at 48 kHz / 960
|
||||
* samples) and matches what hang.js's encoder emits at
|
||||
* `js/publish/src/audio/encoder.ts:#runConfig`.
|
||||
*/
|
||||
val jitter: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -124,9 +134,20 @@ internal data class MoqLiteHangCatalog(
|
||||
container = Container(kind = "legacy"),
|
||||
sampleRate = 48_000,
|
||||
numberOfChannels = 1,
|
||||
jitter = OPUS_FRAME_DURATION_MS,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* Opus frame duration in milliseconds — 960 samples / 48 kHz =
|
||||
* 20 ms. Matches
|
||||
* [com.vitorpamplona.nestsclient.audio.Audio.FRAME_SIZE_SAMPLES].
|
||||
* Published as the catalog `jitter` hint so hang.js's watcher
|
||||
* sizes its jitter buffer to one Opus frame, the natural
|
||||
* cadence of our encoder.
|
||||
*/
|
||||
private const val OPUS_FRAME_DURATION_MS: Int = 20
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class MoqLiteHangCatalogTest {
|
||||
val expected =
|
||||
"{\"audio\":{\"renditions\":{\"audio/data\":{" +
|
||||
"\"codec\":\"opus\",\"container\":{\"kind\":\"legacy\"}," +
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":1}}}}"
|
||||
"\"sampleRate\":48000,\"numberOfChannels\":1,\"jitter\":20}}}}"
|
||||
val actual =
|
||||
MoqLiteHangCatalog.opusMono48k("audio/data").encodeJsonBytes().decodeToString()
|
||||
assertEquals(expected, actual)
|
||||
|
||||
Reference in New Issue
Block a user