cat / goq
clone

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

fix/portal-dist-prebuild
goq / portal / video-pipeline.test.mjs
video-pipeline.test.mjs 6.0K
1import assert from 'node:assert/strict';2import test from 'node:test';34import {5 MAX_DECODE_QUEUE_SIZE,6 createVideoPipelineSession,7} from './video-pipeline.mjs';89const h264Keyframe = () => new Uint8Array([10 0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f,11 0, 0, 0, 1, 0x68, 0xee, 0x3c, 0x80,12 0, 0, 0, 1, 0x09, 0xf0,13 0, 0, 0, 1, 0x65, 0x88, 0x84,14]);1516const h264Delta = () => new Uint8Array([17 0, 0, 0, 1, 0x09, 0xf0,18 0, 0, 0, 1, 0x41, 0x9a,19]);2021function harness() {22 let clock = 10;23 const decoders = [];24 const chunks = [];25 const keyframeRequests = [];26 const audioResets = [];27 const formats = [];28 const draws = [];29 const canvas = { width: 0, height: 0 };30 const context = {31 drawImage: (...args) => draws.push(args),32 };3334 class FakeDecoder {35 constructor(callbacks) {36 this.callbacks = callbacks;37 this.decodeQueueSize = 0;38 this.decoded = [];39 this.configureCalls = [];40 this.closeCalls = 0;41 decoders.push(this);42 }4344 configure(config) { this.configureCalls.push(config); }45 decode(chunk) { this.decoded.push(chunk); }46 close() { this.closeCalls++; }47 }4849 const pipeline = createVideoPipelineSession({50 canvas,51 context,52 requestKeyframe: (reason) => keyframeRequests.push(reason),53 resetAudioSync: (...args) => audioResets.push(args),54 sampleAudioSkew: () => null,55 onFormatChanged: (format) => formats.push(format),56 now: () => clock,57 createDecoder: (callbacks) => new FakeDecoder(callbacks),58 createEncodedChunk: (init) => {59 const chunk = { ...init };60 chunks.push(chunk);61 return chunk;62 },63 requestFrame: () => 1,64 cancelFrame: () => {},65 setTimer: () => 2,66 cancelTimer: () => {},67 });6869 return {70 pipeline,71 canvas,72 decoders,73 chunks,74 keyframeRequests,75 audioResets,76 formats,77 draws,78 advance(milliseconds = 1) { clock += milliseconds; },79 };80}8182function processH264Keyframe(subject, overrides = {}) {83 subject.pipeline.processFramePayload({84 width: 1280,85 height: 800,86 codec: 'h264',87 keyframe: true,88 codecConfig: true,89 sequence: 1,90 pts_micros: 1_000,91 discontinuity: false,92 ...overrides,93 }, h264Keyframe());94}9596test('configured H.264 keyframes commit format only after decode enqueue', () => {97 const subject = harness();98 processH264Keyframe(subject);99100 assert.equal(subject.decoders.length, 1);101 assert.equal(subject.decoders[0].configureCalls.length, 1);102 assert.deepEqual(subject.decoders[0].configureCalls[0], {103 codec: 'avc1.64001f',104 codedWidth: 1280,105 codedHeight: 800,106 description: subject.decoders[0].configureCalls[0].description,107 optimizeForLatency: true,108 });109 assert.deepEqual(subject.pipeline.format, {110 codec: 'h264', width: 1280, height: 800, epoch: 1,111 });112 assert.deepEqual(subject.formats, [subject.pipeline.format]);113 assert.equal(subject.chunks.length, 1);114 assert.equal(subject.chunks[0].type, 'key');115 assert.deepEqual([...subject.chunks[0].data], [0, 0, 0, 3, 0x65, 0x88, 0x84]);116 assert.deepEqual(subject.keyframeRequests, []);117 const stats = subject.pipeline.snapshot();118 assert.equal(stats.receivedFrames, 1);119 assert.equal(stats.decoderInputFrames, 1);120 assert.equal(stats.droppedFrames, 0);121 assert.equal(stats.recovering, false);122 assert.equal(stats.decoderQueueCapacity, MAX_DECODE_QUEUE_SIZE);123 assert.equal(stats.presenterQueueDepth, 0);124 assert.equal(stats.presenterQueueCapacity, 2);125});126127test('unsupported codecs are dropped before decoder construction', () => {128 for (const codec of ['h265', 'av1']) {129 const subject = harness();130 subject.pipeline.processFramePayload({131 width: 1280,132 height: 800,133 codec,134 keyframe: true,135 codecConfig: true,136 sequence: 1,137 pts_micros: 2_000,138 discontinuity: false,139 }, h264Keyframe());140141 assert.equal(subject.decoders.length, 0);142 assert.equal(subject.chunks.length, 0);143 assert.deepEqual(subject.pipeline.format, {144 codec: 'h264', width: 0, height: 0, epoch: 0,145 });146 assert.equal(subject.pipeline.snapshot().droppedFrames, 1);147 }148});149150test('stale decoder callbacks cannot mutate a replacement decoder session', () => {151 const subject = harness();152 processH264Keyframe(subject);153 const stale = subject.decoders[0];154155 processH264Keyframe(subject, { sequence: 2, discontinuity: true, pts_micros: 2_000 });156 assert.equal(subject.decoders.length, 2);157 const staleFrame = { timestamp: 1_000, closeCalls: 0, close() { this.closeCalls++; } };158 stale.callbacks.output(staleFrame);159 stale.callbacks.error(new Error('stale decoder error'));160161 assert.equal(staleFrame.closeCalls, 1);162 assert.equal(subject.pipeline.snapshot().decoderOutputFrames, 0);163 assert.deepEqual(subject.keyframeRequests, []);164 assert.equal(subject.pipeline.format.epoch, 2);165});166167test('decode queue overload enters one bounded recovery episode and drops deltas', () => {168 const subject = harness();169 processH264Keyframe(subject);170 subject.decoders[0].decodeQueueSize = MAX_DECODE_QUEUE_SIZE;171 subject.pipeline.processFramePayload({172 width: 1280,173 height: 800,174 codec: 'h264',175 keyframe: false,176 codecConfig: false,177 sequence: 2,178 pts_micros: 2_000,179 }, h264Delta());180181 assert.deepEqual(subject.keyframeRequests, ['frontend-backpressure']);182 assert.equal(subject.decoders[0].closeCalls, 1);183 const stats = subject.pipeline.snapshot();184 assert.equal(stats.receivedFrames, 2);185 assert.equal(stats.decoderInputFrames, 1);186 assert.equal(stats.droppedFrames, 1);187 assert.equal(stats.recovering, true);188 assert.equal(stats.decoderQueueDepth, 0);189});190191test('reset closes decoding state but preserves the last dimensions and codec quirk', () => {192 const subject = harness();193 processH264Keyframe(subject);194 subject.pipeline.reset();195196 assert.equal(subject.decoders[0].closeCalls, 1);197 assert.deepEqual(subject.pipeline.format, {198 codec: 'h264', width: 1280, height: 800, epoch: 0,199 });200 const stats = subject.pipeline.snapshot();201 assert.equal(stats.receivedFrames, 0);202 assert.equal(stats.decoderInputFrames, 0);203 assert.equal(stats.recovering, true);204});