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:
Claude
2026-05-06 19:09:24 +00:00
parent a94d12638f
commit 75f572ba3d
3 changed files with 238 additions and 8 deletions
@@ -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 {
/**
@@ -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