cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/architecture-review-findings
goq / portal / audio-pipeline.test.mjs
audio-pipeline.test.mjs 12.4K
1import assert from 'node:assert/strict';2import test from 'node:test';34import {5 AUDIO_SAMPLE_RATE,6 createAudioPipelineSession,7} from './audio-pipeline.mjs';8import {9 AUDIO_CHANNEL_PREFIX_LENGTH,10 AUDIO_ENVELOPE_HEADER_LENGTH,11} from './audio-envelope.mjs';1213const PACKET_BASE = AUDIO_CHANNEL_PREFIX_LENGTH;1415function envelope({16 generation = 7n,17 deliveryId = 11n,18 flags = 0,19 pts = 120000n,20} = {}) {21 const payload = Uint8Array.of(0xf8, 0xff, 0xfe);22 const buffer = new ArrayBuffer(23 AUDIO_CHANNEL_PREFIX_LENGTH + AUDIO_ENVELOPE_HEADER_LENGTH + payload.length,24 );25 const bytes = new Uint8Array(buffer);26 const view = new DataView(buffer);27 bytes.set([0x53, 0x47, 0x41, 0x43]);28 view.setUint16(4, 1, false);29 view.setUint16(6, AUDIO_CHANNEL_PREFIX_LENGTH, false);30 view.setBigUint64(8, generation, false);31 view.setBigUint64(16, deliveryId, false);32 bytes.set([0x53, 0x47, 0x41, 0x31], PACKET_BASE);33 view.setUint16(PACKET_BASE + 4, 1, false);34 bytes[PACKET_BASE + 6] = AUDIO_ENVELOPE_HEADER_LENGTH;35 bytes[PACKET_BASE + 7] = 1;36 bytes[PACKET_BASE + 8] = flags;37 bytes[PACKET_BASE + 9] = 2;38 view.setUint32(PACKET_BASE + 12, AUDIO_SAMPLE_RATE, false);39 view.setUint16(PACKET_BASE + 16, 960, false);40 view.setUint32(PACKET_BASE + 20, payload.length, false);41 view.setBigUint64(PACKET_BASE + 24, 42n, false);42 view.setBigUint64(PACKET_BASE + 32, 123456n, false);43 view.setBigInt64(PACKET_BASE + 40, pts, false);44 bytes.set(payload, PACKET_BASE + AUDIO_ENVELOPE_HEADER_LENGTH);45 return buffer;46}4748function harness({49 disconnecting = () => false,50 hasAudioDecoder = () => true,51 hasAudioWorklet = () => true,52} = {}) {53 const invokes = [];54 const updates = [];55 const avResets = [];56 const decoders = [];57 const worklets = [];58 const contexts = [];59 const chunks = [];6061 class FakeContext {62 constructor(options) {63 this.options = options;64 this.sampleRate = options.sampleRate;65 this.state = 'suspended';66 this.destination = {};67 this.modules = [];68 this.resumeCalls = 0;69 this.closeCalls = 0;70 this.audioWorklet = {71 addModule: async (url) => { this.modules.push(url); },72 };73 contexts.push(this);74 }7576 async resume() {77 this.resumeCalls++;78 this.state = 'running';79 }8081 async close() {82 this.closeCalls++;83 this.state = 'closed';84 }8586 getOutputTimestamp() {87 return { contextTime: 4, performanceTime: 1000 };88 }89 }9091 class FakeChannel {92 constructor(callback) {93 this.callback = callback;94 }95 }9697 const pipeline = createAudioPipelineSession({98 invokeCommand: async (command, args) => { invokes.push({ command, args }); },99 onUpdate: (session) => updates.push(session),100 resetAvSync: (...args) => {101 avResets.push(args);102 pipeline.resetSyncTelemetry(...args);103 },104 isDisconnecting: disconnecting,105 hasAudioDecoder,106 hasAudioWorklet,107 getAudioContextConstructor: () => FakeContext,108 probeDecoderSupport: async (config) => ({ supported: config.codec === 'opus' }),109 createDecoder: (callbacks) => {110 const decoder = {111 callbacks,112 state: 'configured',113 decodeQueueSize: 0,114 configured: null,115 decoded: [],116 closeCalls: 0,117 configure(config) { this.configured = config; },118 decode(chunk) { this.decoded.push(chunk); },119 close() { this.closeCalls++; this.state = 'closed'; },120 };121 decoders.push(decoder);122 return decoder;123 },124 createWorkletNode: (context, name, options) => {125 const node = {126 context,127 name,128 options,129 connected: null,130 disconnectCalls: 0,131 port: {132 messages: [],133 postMessage(message, transfers) { this.messages.push({ message, transfers }); },134 onmessage: null,135 onmessageerror: null,136 },137 connect(destination) { this.connected = destination; },138 disconnect() { this.disconnectCalls++; },139 onprocessorerror: null,140 };141 worklets.push(node);142 return node;143 },144 createEncodedChunk: (init) => {145 const chunk = { ...init };146 chunks.push(chunk);147 return chunk;148 },149 });150151 return {152 pipeline,153 FakeChannel,154 invokes,155 updates,156 avResets,157 decoders,158 worklets,159 contexts,160 chunks,161 };162}163164async function initializeAttempt(fixture) {165 fixture.pipeline.prepareForConnection();166 const primedSession = fixture.pipeline.session;167 fixture.pipeline.beginAttempt();168 const attempt = await fixture.pipeline.createAttemptChannel(fixture.FakeChannel, primedSession);169 return { ...attempt, primedSession };170}171172test('connection preparation synchronously primes WebKit audio and initializes bounded Opus resources', async () => {173 const fixture = harness();174 const initial = fixture.pipeline.session;175 initial.muted = true;176177 fixture.pipeline.prepareForConnection();178 const session = fixture.pipeline.session;179 assert.notEqual(session, initial);180 assert.equal(session.muted, true);181 assert.equal(session.acceptsNativeEvents, true);182 assert.equal(fixture.contexts.length, 1);183 assert.equal(fixture.contexts[0].resumeCalls, 1);184185 fixture.pipeline.beginAttempt();186 const attempt = await fixture.pipeline.createAttemptChannel(fixture.FakeChannel, session);187 assert.equal(attempt.session, session);188 assert.deepEqual(attempt.support, { supported: true, error: null });189 assert.equal(attempt.channel, session.channel);190 assert.equal(fixture.decoders[0].configured.codec, 'opus');191 assert.equal(fixture.decoders[0].configured.sampleRate, AUDIO_SAMPLE_RATE);192 assert.equal(fixture.worklets[0].name, 'sigil-audio-processor');193 assert.deepEqual(fixture.worklets[0].port.messages[0].message, { type: 'mute', muted: true });194 assert.equal(session.state, 'negotiating');195 assert.equal(session.stateDetail, 'Opus output ready');196});197198test('capability checks preserve the exact WebCodecs and AudioWorklet fallback errors', async () => {199 const missingDecoder = harness({ hasAudioDecoder: () => false });200 missingDecoder.pipeline.prepareForConnection();201 let session = missingDecoder.pipeline.beginAttempt();202 let attempt = await missingDecoder.pipeline.createAttemptChannel(203 missingDecoder.FakeChannel,204 session,205 );206 assert.deepEqual(attempt.support, {207 supported: false,208 error: 'WebCodecs AudioDecoder is unavailable',209 });210211 const missingWorklet = harness({ hasAudioWorklet: () => false });212 missingWorklet.pipeline.prepareForConnection();213 session = missingWorklet.pipeline.beginAttempt();214 attempt = await missingWorklet.pipeline.createAttemptChannel(missingWorklet.FakeChannel, session);215 assert.deepEqual(attempt.support, {216 supported: false,217 error: 'AudioWorklet is unavailable',218 });219});220221test('deliveries always acknowledge but only the current generation enters the bounded decoder', async () => {222 const fixture = harness();223 const attempt = await initializeAttempt(fixture);224 await fixture.pipeline.acceptConnectedResult({225 audio_generation: 7,226 audio_available: true,227 audio_error: null,228 }, attempt);229230 attempt.channel.callback(envelope({ generation: 8n, deliveryId: 80n }));231 attempt.channel.callback(envelope({ generation: 7n, deliveryId: 70n }));232 await Promise.resolve();233 assert.deepEqual(fixture.invokes.slice(0, 2), [234 { command: 'iroh_client_ack_audio', args: { generation: 8, deliveryId: 80 } },235 { command: 'iroh_client_ack_audio', args: { generation: 7, deliveryId: 70 } },236 ]);237 assert.equal(fixture.decoders[0].decoded.length, 1);238 assert.equal(fixture.chunks[0].timestamp, 120000);239 assert.equal(fixture.chunks[0].duration, 20000);240241 let audioDataClosed = false;242 fixture.decoders[0].callbacks.output({243 numberOfChannels: 2,244 numberOfFrames: 2,245 timestamp: 120000,246 copyTo: (channel, { planeIndex }) => channel.fill(planeIndex + 1),247 close: () => { audioDataClosed = true; },248 });249 const samples = fixture.worklets[0].port.messages.at(-1);250 assert.equal(samples.message.type, 'samples');251 assert.deepEqual([...samples.message.channels[0]], [1, 1]);252 assert.deepEqual([...samples.message.channels[1]], [2, 2]);253 assert.equal(samples.transfers.length, 2);254 assert.equal(audioDataClosed, true);255256 fixture.decoders[0].decodeQueueSize = 3;257 attempt.channel.callback(envelope({ generation: 7n, deliveryId: 71n }));258 assert.equal(fixture.decoders.length, 2);259 assert.equal(fixture.decoders[0].closeCalls, 1);260 assert.equal(attempt.session.decoderDropped, 3);261 assert.equal(fixture.decoders[1].decoded.length, 1);262 assert.equal(fixture.avResets.at(-1)[1].recoverRing, true);263 assert.deepEqual(fixture.worklets[0].port.messages.at(-1).message, {264 type: 'recover',265 avSyncEpoch: attempt.session.avSyncEpoch,266 });267});268269test('worklet telemetry is epoch-gated and projects signed audio/video skew', async () => {270 const fixture = harness();271 const attempt = await initializeAttempt(fixture);272 await fixture.pipeline.acceptConnectedResult({273 audio_generation: 7,274 audio_available: true,275 audio_error: null,276 }, attempt);277 const session = attempt.session;278 const worklet = fixture.worklets[0];279280 worklet.port.onmessage({ data: {281 type: 'stats',282 avSyncEpoch: session.avSyncEpoch + 1,283 bufferedFrames: 99,284 } });285 assert.equal(session.bufferedFrames, 0);286287 worklet.port.onmessage({ data: {288 type: 'stats',289 avSyncEpoch: session.avSyncEpoch,290 droppedFrames: 11,291 recoveryDiscardedFrames: 12,292 renderedMediaEndPtsMicros: 2_000_000,293 outputContextTimeEndSeconds: 4,294 bufferedFrames: 960,295 underflows: 2,296 underflowDurationMicros: 10_000,297 silentDurationMicros: 20_000,298 started: true,299 } });300 assert.equal(session.state, 'playing');301 assert.equal(session.workletDroppedFrames, 11);302 assert.equal(session.workletRecoveryDiscardedFrames, 12);303 assert.equal(session.bufferedFrames, 960);304 assert.equal(fixture.pipeline.sampleSkew(1_975_000, 1000), 25);305306 fixture.pipeline.resetSyncTelemetry(session, { recoverRing: true });307 assert.equal(session.audioTimeline, null);308 assert.deepEqual(worklet.port.messages.at(-1).message, {309 type: 'recover',310 avSyncEpoch: session.avSyncEpoch,311 });312 assert.equal(fixture.pipeline.sampleSkew(1_975_000, 1000), null);313});314315test('native terminal state is staged before connect and teardown isolates delayed callbacks', async () => {316 const fixture = harness();317 fixture.pipeline.prepareForConnection();318 fixture.pipeline.handleNativeState({ generation: 9, available: false, error: 'ended early' });319 assert.equal(fixture.pipeline.session.pendingTerminalStates.length, 1);320 const session = fixture.pipeline.beginAttempt();321 const attempt = await fixture.pipeline.createAttemptChannel(fixture.FakeChannel, session);322 const oldSession = attempt.session;323 const oldWorklet = fixture.worklets[0];324 await fixture.pipeline.acceptConnectedResult({325 audio_generation: 9,326 audio_available: true,327 audio_error: null,328 }, attempt);329 await Promise.resolve();330 assert.equal(oldSession.failed, true);331 assert.equal(oldSession.failureDetail, 'ended early');332 assert.deepEqual(fixture.invokes.at(-1), {333 command: 'iroh_client_stop_audio',334 args: { expectedGeneration: 9 },335 });336337 oldSession.muted = true;338 await fixture.pipeline.teardown();339 const successor = fixture.pipeline.session;340 assert.notEqual(successor, oldSession);341 assert.equal(successor.muted, true);342 assert.equal(fixture.contexts[0].closeCalls, 1);343 assert.equal(oldWorklet.disconnectCalls, 1);344 const updateCount = fixture.updates.length;345 oldWorklet.port.onmessage({ data: { type: 'accepted', id: 1 } });346 assert.equal(fixture.updates.length, updateCount);347});348349test('native stats and mute toggles update only the exact active generation', async () => {350 const fixture = harness();351 const attempt = await initializeAttempt(fixture);352 await fixture.pipeline.acceptConnectedResult({353 audio_generation: 7,354 audio_available: true,355 audio_error: null,356 }, attempt);357 fixture.pipeline.handleNativeStats({358 generation: 6,359 sequence_dropped_total: 99,360 frontend_dropped_total: 98,361 });362 assert.equal(attempt.session.transportDropped, 0);363 fixture.pipeline.handleNativeStats({364 generation: 7,365 sequence_dropped_total: 4,366 frontend_dropped_total: 5,367 });368 assert.equal(attempt.session.transportDropped, 4);369 assert.equal(attempt.session.frontendDropped, 5);370371 await fixture.pipeline.toggleMute();372 assert.equal(attempt.session.muted, true);373 assert.deepEqual(fixture.worklets[0].port.messages.at(-1).message, {374 type: 'mute',375 muted: true,376 });377});