test+fix(nests): pin stripLegacyTimestampPrefix; primaryAudio filters to legacy
Two audit-pass gaps:
G1 — `MoqLiteNestsListener.Companion.stripLegacyTimestampPrefix`
is the entire receive-side wire-format converter for audio frames
(every web speaker's Opus packet flows through it) and had zero
unit tests. A regression here is invisible to users — silent decode
failure of every web speaker — and to interop tests, because both
sides agree on the same bug. New StripLegacyTimestampPrefixTest pins:
- all four QUIC varint-length tiers (1 / 2 / 4 / 8 bytes), one
test per tier with a representative timestamp value plus a
tag-byte assertion so the test fails loudly if Varint.encode's
tier-selection changes.
- empty-payload and malformed-payload fast paths (returns
payload unchanged, same instance — no allocation).
- round-trip against `NestMoqLiteBroadcaster`'s exact wire
shape (`varint(timestamp_us) + raw_opus_packet`) across five
timestamp values spanning all four tiers.
G6 — `RoomSpeakerCatalog.primaryAudio()` previously returned
`audio.renditions.values.firstOrNull()` with no container filter.
A future publisher that emits CMAF before legacy in iteration
order would surface the CMAF rendition, and our decoder pipeline
would then feed CMAF MOOF/MDAT bytes to its legacy
`varint(ts)+opus` parser and decode garbage. Filter to
`container.kind == Container.LEGACY_KIND` so:
- mixed CMAF+legacy publishers always surface the legacy entry
regardless of map iteration order
- CMAF-only publishers correctly return null (caller falls
back to "unknown codec / no audio")
- publishers that omit `container` entirely also return null —
treating "no container declared" as "legacy by default" would
silently mis-decode a CMAF-only publisher that just forgot
to spell out `container.kind`
LEGACY_KIND const lives on `Container.Companion` so call sites
share one canonical spelling. Three new test cases pin the new
filter behaviour (mixed iteration order, CMAF-only, missing
container). The pre-existing `toleratesUnknownKeys` test is
updated to declare a legacy container — the unknown-key
tolerance we're checking is about unrecognised siblings, not
container-presence.
https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+29
-6
@@ -106,15 +106,38 @@ data class RoomSpeakerCatalog(
|
||||
@Serializable
|
||||
data class Container(
|
||||
val kind: String? = null,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* The only `container.kind` value the Amethyst decoder
|
||||
* pipeline currently understands: each moq-lite frame is
|
||||
* `varint(timestamp_us) + raw_codec_payload`. Source:
|
||||
* `kixelated/moq/rs/hang/src/container/legacy.rs`.
|
||||
*
|
||||
* Other documented kinds — `"cmaf"` (MOOF/MDAT-fragmented
|
||||
* MP4) and a handful of in-flight experimental shapes —
|
||||
* would require a different decoder path and are
|
||||
* intentionally rejected by [primaryAudio].
|
||||
*/
|
||||
const val LEGACY_KIND: String = "legacy"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* First audio rendition, if any. The current single-Opus reality.
|
||||
* Map iteration order is the JSON insertion order (kotlinx.serialization
|
||||
* uses LinkedHashMap), so this picks the first rendition the
|
||||
* publisher declared rather than an arbitrary one.
|
||||
* First audio rendition with a [Container.LEGACY_KIND] container,
|
||||
* which is the only container layout the Amethyst decoder pipeline
|
||||
* understands today (`varint(timestamp_us) + opus_packet` per
|
||||
* frame). Returns null when no legacy rendition exists — the UI
|
||||
* surfaces "no audio info" rather than a CMAF rendition we'd try
|
||||
* to feed to our legacy decoder.
|
||||
*
|
||||
* Picks the first match in JSON iteration order
|
||||
* (kotlinx.serialization uses LinkedHashMap), so the publisher's
|
||||
* preferred legacy rendition wins. A future publisher that emits
|
||||
* CMAF-first then legacy still surfaces the legacy entry; CMAF-
|
||||
* only publishers correctly return null.
|
||||
*/
|
||||
fun primaryAudio(): AudioConfig? = audio?.renditions?.values?.firstOrNull()
|
||||
fun primaryAudio(): AudioConfig? = audio?.renditions?.values?.firstOrNull { it.container?.kind == Container.LEGACY_KIND }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
||||
+64
-2
@@ -90,9 +90,17 @@ class RoomSpeakerCatalogTest {
|
||||
@Test
|
||||
fun toleratesUnknownKeys() {
|
||||
// Forward-compat: future hang catalog revisions can add
|
||||
// fields without breaking older clients.
|
||||
// fields without breaking older clients. Container.kind is
|
||||
// declared as `legacy` because `primaryAudio()` filters to
|
||||
// that kind — the unknown-key tolerance we're testing here
|
||||
// is about unrecognized siblings, not about the container
|
||||
// requirement itself.
|
||||
val json =
|
||||
"""{"audio":{"renditions":{"audio/data":{"codec":"opus","extra":"future-only"}}},"video":{},"newTopLevel":true}"""
|
||||
"""{"audio":{"renditions":{"audio/data":{
|
||||
| "codec":"opus","container":{"kind":"legacy"},
|
||||
| "extra":"future-only"
|
||||
|}}},"video":{},"newTopLevel":true}
|
||||
""".trimMargin()
|
||||
val catalog = RoomSpeakerCatalog.parseOrNull(json.encodeToByteArray())
|
||||
assertNotNull(catalog)
|
||||
assertEquals("opus", catalog.primaryAudio()?.codec)
|
||||
@@ -116,6 +124,60 @@ class RoomSpeakerCatalogTest {
|
||||
assertNull(catalog.primaryAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun primaryAudioPicksLegacyEvenWhenCmafComesFirst() {
|
||||
// A future publisher may emit CMAF-first then legacy in the
|
||||
// renditions map. We only know how to decode the legacy
|
||||
// container today (varint(ts)+opus); CMAF (MOOF/MDAT) would
|
||||
// be fed bytes-as-Opus and decode to garbage. The filter
|
||||
// skips non-legacy renditions so the chosen entry is one we
|
||||
// can actually play.
|
||||
val mixed =
|
||||
"""{"audio":{"renditions":{
|
||||
| "video/cmaf":{"codec":"opus","container":{"kind":"cmaf"},"sampleRate":48000,"numberOfChannels":1},
|
||||
| "audio/data":{"codec":"opus","container":{"kind":"legacy"},"sampleRate":48000,"numberOfChannels":1}
|
||||
|}}}
|
||||
""".trimMargin()
|
||||
val catalog = RoomSpeakerCatalog.parseOrNull(mixed.encodeToByteArray())
|
||||
assertNotNull(catalog)
|
||||
val rendition = catalog.primaryAudio()
|
||||
assertNotNull(rendition, "expected the legacy rendition, not CMAF")
|
||||
assertEquals("legacy", rendition.container?.kind)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun primaryAudioReturnsNullForCmafOnlyPublisher() {
|
||||
// No legacy rendition → no decoder path. Returning null is
|
||||
// the right contract: the caller falls back to "unknown
|
||||
// codec / no audio" rather than feeding CMAF bytes to the
|
||||
// legacy decoder.
|
||||
val cmafOnly =
|
||||
"""{"audio":{"renditions":{"audio/cmaf":{
|
||||
| "codec":"opus","container":{"kind":"cmaf"},
|
||||
| "sampleRate":48000,"numberOfChannels":1
|
||||
|}}}}
|
||||
""".trimMargin()
|
||||
val catalog = RoomSpeakerCatalog.parseOrNull(cmafOnly.encodeToByteArray())
|
||||
assertNotNull(catalog)
|
||||
assertNull(catalog.primaryAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun primaryAudioReturnsNullWhenContainerKindIsMissing() {
|
||||
// Defensive: a malformed publisher that omits `container`
|
||||
// entirely must NOT be treated as legacy by default — we'd
|
||||
// be guessing the wire shape. Same null fallback as the
|
||||
// CMAF-only case.
|
||||
val noContainer =
|
||||
"""{"audio":{"renditions":{"audio/data":{
|
||||
| "codec":"opus","sampleRate":48000,"numberOfChannels":1
|
||||
|}}}}
|
||||
""".trimMargin()
|
||||
val catalog = RoomSpeakerCatalog.parseOrNull(noContainer.encodeToByteArray())
|
||||
assertNotNull(catalog)
|
||||
assertNull(catalog.primaryAudio())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingAudioReturnsNullPrimary() {
|
||||
// hang catalogs declaring only video MUST still parse without
|
||||
|
||||
Reference in New Issue
Block a user