test(nests): T16 Phase 4 — I13 long broadcast + I14 WebCodecs warmup
Adds the two browser-tier P0 scenarios deferred from Phase 4.C:
I13 (chromium_listener_long_broadcast_60s_tone_440):
- 60 s end-to-end Amethyst speaker -> Chromium @moq/lite + @moq/hang
Container.Legacy.Consumer; assert >= 50 s of decoded PCM, FFT
peak intact at 440 Hz, zero decoder errors.
- Spec asked for framesPerGroup = 50 against actual Container.Consumer.
Two constraints: (1) @moq/hang 0.2.4 doesn't export the high-level
Container.Consumer/Format API (Phase 4 uses Container.Legacy.Consumer
directly), (2) framesPerGroup = 50 against the local moq-relay
0.10.25 --auth-public minimal setup hits the per-subscriber forward
cliff per 2026-05-07-framespergroup-reconciliation.md. Pin 5
locally; production keeps 50.
- Catches what I1 forward (10 s) doesn't: Chromium WebTransport
MAX_STREAMS_UNI credit drift over 600+ uni streams, group-queue
eviction past MAX_GROUP_AGE = 30 s twice over, WebCodecs decoder
pacing/memory pressure at broadcast scale.
I14 (chromium_decoder_no_errors_through_warmup_window):
- Asserts Chromium AudioDecoder.error fires zero times during
a 10 s broadcast. Browser-tier mate of HangInteropTest's I11
(first_audio_frame_is_not_opus_codec_config); together they
cover T8 (BUFFER_FLAG_CODEC_CONFIG skip) on both reference paths.
- Deliberately no decoderOutputs floor: the Phase 4 harness has
a known cold-launch race that occasionally produces 0 frames
(per 2026-05-06-phase4-browser-harness-results.md). Since I14
is an absence assertion, vacuous-zero is safe — a T8 regression
would still trigger on whichever frames arrive in any green run.
- Note: JVM speaker uses libopus directly (no CSD prefix), so
this test path effectively asserts no spurious decode failures.
T8 itself is an Android-MediaCodecOpusEncoder fix; that path
isn't reachable from a JVM-host test.
Wire changes:
- listen.ts gains decoderOutputs + decoderErrors counters,
exposed via window.__decoderOutputs / __decoderErrors.
- tests/harness.spec.ts surfaces both in the meta JSON line.
- BrowserInteropTest.kt adds parseIntMetaFromStdout helper +
the two new scenarios.
Verification: all 4 BrowserInteropTest scenarios green in one
JVM run, no flake under -DnestsHangInterop=true -DnestsBrowserInterop=true.
This commit is contained in:
@@ -133,10 +133,21 @@ async function main() {
|
||||
const sampleRate = 48_000;
|
||||
const numberOfChannels = 1; // overwritten by catalog if available; default mono
|
||||
let warmed = 0;
|
||||
// I14 instrumentation. `decoderOutputs` counts every successful
|
||||
// `output()` callback (warmup frames included), `decoderErrors`
|
||||
// counts every WebCodecs `error()` callback. A T8 regression that
|
||||
// leaks `OpusHead` into a normal audio frame surfaces as either a
|
||||
// non-zero error count (decoder rejects the bytes) or — if Chromium
|
||||
// tolerates it — as the warmup window absorbing the stray frame
|
||||
// and the FFT peak shifting. The error counter catches case 1
|
||||
// deterministically; the FFT peak in I1 catches case 2.
|
||||
let decoderOutputs = 0;
|
||||
let decoderErrors = 0;
|
||||
|
||||
const decoder = new AudioDecoder({
|
||||
output: (data: AudioData) => {
|
||||
warmed++;
|
||||
decoderOutputs++;
|
||||
if (warmed <= 3) {
|
||||
// Mirror @moq/watch's 3-frame WebCodecs warmup skip.
|
||||
data.close();
|
||||
@@ -167,7 +178,10 @@ async function main() {
|
||||
}
|
||||
data.close();
|
||||
},
|
||||
error: (err) => console.error("[listen] AudioDecoder", err),
|
||||
error: (err) => {
|
||||
decoderErrors++;
|
||||
console.error("[listen] AudioDecoder", err);
|
||||
},
|
||||
});
|
||||
|
||||
decoder.configure({
|
||||
@@ -207,6 +221,8 @@ async function main() {
|
||||
|
||||
status(`done, frames=${framesDecoded}`);
|
||||
(window as any).__framesDecoded = framesDecoded;
|
||||
(window as any).__decoderOutputs = decoderOutputs;
|
||||
(window as any).__decoderErrors = decoderErrors;
|
||||
|
||||
// Flush any pending decoder output, then signal the WS server we're done.
|
||||
try {
|
||||
|
||||
@@ -49,6 +49,13 @@ test.describe("nests-browser-interop", () => {
|
||||
const meta = await page.evaluate(() => ({
|
||||
framesDecoded: (window as any).__framesDecoded,
|
||||
moqVersion: (window as any).__moqVersion,
|
||||
// I14 instrumentation: total WebCodecs `output()` callbacks
|
||||
// (warmup frames included) and total `error()` callbacks.
|
||||
// A T8 regression that leaks `OpusHead` into a normal audio
|
||||
// frame trips `decoderErrors` deterministically; the FFT
|
||||
// peak in I1 catches the silent-tolerance variant.
|
||||
decoderOutputs: (window as any).__decoderOutputs,
|
||||
decoderErrors: (window as any).__decoderErrors,
|
||||
}));
|
||||
// Always print a summary line — Kotlin parses this for follow-up
|
||||
// assertions (e.g. moq-lite-03 ALPN echo for I15).
|
||||
|
||||
Reference in New Issue
Block a user