cat / goq
clone

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

fix/release-build-and-order
goq / portal / audio-envelope.test.mjs
audio-envelope.test.mjs 5.6K
1import assert from 'node:assert/strict';2import test from 'node:test';34import {5 AUDIO_CHANNEL_PREFIX_LENGTH,6 AUDIO_ENVELOPE_HEADER_LENGTH,7 MAX_AUDIO_PAYLOAD_LENGTH,8 isCurrentAudioDelivery,9 isCurrentAudioGeneration,10 stageAudioTerminalState,11 takeAudioTerminalState,12 parseAudioEnvelope,13} from './audio-envelope.mjs';1415const PACKET_BASE = AUDIO_CHANNEL_PREFIX_LENGTH;1617function envelope({18 generation = 7n,19 deliveryId = 11n,20 flags = 1,21 channels = 2,22 sampleRate = 48000,23 frameSamples = 960,24 payload = Uint8Array.of(0xf8, 0xff, 0xfe),25 payloadLength = payload.length,26 sequence = 42n,27 captureTimestamp = 123456n,28 pts = 120000n,29} = {}) {30 const buffer = new ArrayBuffer(31 AUDIO_CHANNEL_PREFIX_LENGTH + AUDIO_ENVELOPE_HEADER_LENGTH + payload.length,32 );33 const bytes = new Uint8Array(buffer);34 const view = new DataView(buffer);35 bytes.set([0x53, 0x47, 0x41, 0x43]);36 view.setUint16(4, 1, false);37 view.setUint16(6, AUDIO_CHANNEL_PREFIX_LENGTH, false);38 view.setBigUint64(8, generation, false);39 view.setBigUint64(16, deliveryId, false);40 bytes.set([0x53, 0x47, 0x41, 0x31], PACKET_BASE);41 view.setUint16(PACKET_BASE + 4, 1, false);42 bytes[PACKET_BASE + 6] = AUDIO_ENVELOPE_HEADER_LENGTH;43 bytes[PACKET_BASE + 7] = 1;44 bytes[PACKET_BASE + 8] = flags;45 bytes[PACKET_BASE + 9] = channels;46 view.setUint32(PACKET_BASE + 12, sampleRate, false);47 view.setUint16(PACKET_BASE + 16, frameSamples, false);48 view.setUint32(PACKET_BASE + 20, payloadLength, false);49 view.setBigUint64(PACKET_BASE + 24, sequence, false);50 view.setBigUint64(PACKET_BASE + 32, captureTimestamp, false);51 view.setBigInt64(PACKET_BASE + 40, pts, false);52 bytes.set(payload, PACKET_BASE + AUDIO_ENVELOPE_HEADER_LENGTH);53 return buffer;54}5556test('parses the delivery prefix and bounded Opus envelope without copying payload', () => {57 const buffer = envelope();58 const packet = parseAudioEnvelope(buffer);59 assert.deepEqual(60 {61 generation: packet.generation,62 deliveryId: packet.deliveryId,63 codec: packet.codec,64 discontinuity: packet.discontinuity,65 channels: packet.channels,66 sampleRate: packet.sampleRate,67 frameSamples: packet.frameSamples,68 sequence: packet.sequence,69 captureTimestampMicros: packet.captureTimestampMicros,70 ptsMicros: packet.ptsMicros,71 },72 {73 generation: 7,74 deliveryId: 11,75 codec: 'opus',76 discontinuity: true,77 channels: 2,78 sampleRate: 48000,79 frameSamples: 960,80 sequence: 42,81 captureTimestampMicros: 123456,82 ptsMicros: 120000,83 },84 );85 new Uint8Array(buffer)[PACKET_BASE + AUDIO_ENVELOPE_HEADER_LENGTH] = 9;86 assert.equal(packet.data[0], 9);87});8889test('rejects malformed delivery and packet headers', () => {90 const mutations = [91 (bytes) => { bytes[0] = 0; },92 (bytes) => { bytes[5] = 2; },93 (bytes) => { bytes[7] = 23; },94 (bytes) => { bytes[PACKET_BASE] = 0; },95 (bytes) => { bytes[PACKET_BASE + 5] = 2; },96 (bytes) => { bytes[PACKET_BASE + 6] = 47; },97 (bytes) => { bytes[PACKET_BASE + 7] = 2; },98 (bytes) => { bytes[PACKET_BASE + 8] = 0x80; },99 (bytes) => { bytes[PACKET_BASE + 9] = 1; },100 (bytes) => { bytes[PACKET_BASE + 10] = 1; },101 (bytes) => { bytes[PACKET_BASE + 18] = 1; },102 ];103 for (const mutate of mutations) {104 const buffer = envelope();105 mutate(new Uint8Array(buffer));106 assert.throws(() => parseAudioEnvelope(buffer));107 }108 assert.throws(() => parseAudioEnvelope(envelope({ sampleRate: 44100 })));109 assert.throws(() => parseAudioEnvelope(envelope({ frameSamples: 480 })));110});111112test('rejects length mismatches and integers unsafe to represent in JavaScript', () => {113 assert.throws(() => parseAudioEnvelope(envelope({ payloadLength: 0 })));114 assert.throws(() => parseAudioEnvelope(envelope({ payloadLength: 2 })));115 assert.throws(() => parseAudioEnvelope(envelope({ payloadLength: MAX_AUDIO_PAYLOAD_LENGTH + 1 })));116 assert.throws(() => parseAudioEnvelope(new ArrayBuffer(117 AUDIO_CHANNEL_PREFIX_LENGTH + AUDIO_ENVELOPE_HEADER_LENGTH - 1,118 )));119 assert.throws(() => parseAudioEnvelope(new Uint8Array(envelope())));120 assert.throws(() => parseAudioEnvelope(envelope({ generation: 9007199254740992n })));121 assert.throws(() => parseAudioEnvelope(envelope({ deliveryId: 9007199254740992n })));122 assert.throws(() => parseAudioEnvelope(envelope({ sequence: 9007199254740992n })));123 assert.throws(() => parseAudioEnvelope(envelope({ captureTimestamp: 9007199254740992n })));124 assert.throws(() => parseAudioEnvelope(envelope({ pts: -1n })));125});126127test('matches deliveries only to the exact current safe generation', () => {128 const packet = parseAudioEnvelope(envelope({ generation: 91n }));129 assert.equal(isCurrentAudioDelivery(packet, 91), true);130 assert.equal(isCurrentAudioDelivery(packet, 90), false);131 assert.equal(isCurrentAudioDelivery(packet, null), false);132 assert.equal(isCurrentAudioDelivery(packet, Number.MAX_SAFE_INTEGER + 1), false);133 assert.equal(isCurrentAudioGeneration(91, 91), true);134 assert.equal(isCurrentAudioGeneration(undefined, undefined), false);135 assert.equal(isCurrentAudioGeneration(-1, -1), false);136});137138test('pre-result audio terminal states are bounded and replay exact generation', () => {139 const pending = [];140 for (let generation = 1; generation <= 6; generation++) {141 stageAudioTerminalState(pending, {142 generation,143 available: false,144 error: `ended-${generation}`,145 });146 }147 assert.equal(pending.length, 4);148 assert.deepEqual(pending.map((state) => state.generation), [6, 5, 4, 3]);149 assert.deepEqual(takeAudioTerminalState(pending, 6), {150 generation: 6,151 available: false,152 error: 'ended-6',153 });154 assert.deepEqual(pending, []);155});