cat / goq
clone

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

release/portal-packaging
goq / portal / stream-runtime.test.mjs
stream-runtime.test.mjs 12.3K
1import test from 'node:test';2import assert from 'node:assert/strict';34import { FRAME_CHANNEL_CAPACITY } from './frame-session.mjs';5import { createStreamRuntime } from './stream-runtime.mjs';67function frameEnvelope({ sequence = 11, ptsMicros = 22, payload = [0x65] } = {}) {8 const buffer = new ArrayBuffer(40 + payload.length);9 const bytes = new Uint8Array(buffer);10 const view = new DataView(buffer);11 bytes.set([0x53, 0x47, 0x46, 0x52]);12 bytes[4] = 1;13 bytes[5] = 1;14 bytes[6] = 0b00000101;15 view.setUint16(8, 1280, false);16 view.setUint16(10, 800, false);17 view.setUint32(12, payload.length, false);18 view.setBigUint64(16, BigInt(sequence), false);19 view.setBigUint64(24, 0xffffffffffffffffn, false);20 view.setBigInt64(32, BigInt(ptsMicros), false);21 bytes.set(payload, 40);22 return buffer;23}2425function videoSnapshot(overrides = {}) {26 return {27 decoderQueueDepth: 1,28 decoderQueueCapacity: 2,29 presenterQueueDepth: 1,30 presenterQueueCapacity: 2,31 droppedFrames: 3,32 presentationDroppedFrames: 4,33 decodePercentiles: { p95: 5 },34 presentPercentiles: { p95: 6 },35 recovering: false,36 ...overrides,37 };38}3940function subject({ connected = false } = {}) {41 const events = [];42 const video = {43 processFramePayload(payload, data = null) {44 events.push({ type: 'process', payload, data });45 },46 reset() { events.push({ type: 'video-reset' }); },47 teardown() { events.push({ type: 'video-teardown' }); },48 snapshot(at) {49 events.push({ type: 'video-snapshot', at });50 return videoSnapshot();51 },52 };53 const publisher = {54 start(generation, available) {55 events.push({ type: 'publisher-start', generation, available });56 },57 stop() { events.push({ type: 'publisher-stop' }); },58 publish(snapshot) {59 events.push({ type: 'publisher-publish', snapshot });60 return true;61 },62 };63 const logger = {64 warn(...args) { events.push({ type: 'warn', args }); },65 error(...args) { events.push({ type: 'error', args }); },66 };67 const runtime = createStreamRuntime({68 invokeCommand(command, args) {69 events.push({ type: 'invoke', command, args });70 return Promise.resolve(true);71 },72 videoPipeline: video,73 isConnected: () => connected,74 disconnect: async () => { events.push({ type: 'disconnect' }); },75 adaptiveFeedbackPublisher: publisher,76 logger,77 });78 return { runtime, events, video, publisher };79}8081function activate(runtime, session, generation = 7, overrides = {}) {82 runtime.activateFrameSession(session, {83 generation,84 adaptiveFeedbackAvailable: true,85 adaptiveFeedbackError: null,86 ...overrides,87 });88}8990test('connection lifecycle resets stream state while close preserves the adaptive error quirk', () => {91 const { runtime, events } = subject();92 runtime.prepareConnection();93 const session = runtime.openFrameSession();94 activate(runtime, session, 7, {95 adaptiveFeedbackAvailable: false,96 adaptiveFeedbackError: 'not supported',97 });98 assert.equal(runtime.snapshot().adaptiveFeedbackError, 'not supported');99100 runtime.closeFrameSession(session);101 assert.equal(session.closing, true);102 assert.equal(runtime.generation, null);103 assert.equal(runtime.snapshot().adaptiveFeedbackAvailable, false);104 assert.equal(runtime.snapshot().adaptiveFeedbackError, 'not supported');105106 runtime.prepareConnection();107 const reset = runtime.snapshot(123);108 assert.equal(reset.adaptiveFeedbackError, null);109 assert.equal(reset.transportDroppedFrames, 0);110 assert.equal(reset.streamPathMode, 'unknown');111 assert.ok(events.some((event) => event.type === 'video-reset'));112113 runtime.prepareDisconnect();114 assert.equal(runtime.snapshot().adaptiveFeedbackError, null);115 runtime.teardown();116 assert.ok(events.some((event) => event.type === 'video-teardown'));117});118119test('closing a partially constructed attempt falls back to the active frame session', () => {120 const { runtime } = subject();121 runtime.prepareConnection();122 const session = runtime.openFrameSession();123124 runtime.closeFrameSession();125126 assert.equal(session.closing, true);127 assert.equal(runtime.activeSession, null);128 assert.equal(runtime.generation, null);129});130131test('activation flushes pre-connect binary acknowledgments', () => {132 const { runtime, events } = subject();133 runtime.prepareConnection();134 const session = runtime.openFrameSession();135 assert.equal(runtime.handleBinaryFrame(frameEnvelope(), session), true);136137 events.length = 0;138 activate(runtime, session, 7);139 assert.deepEqual(events.map((event) => event.type), [140 'invoke',141 'publisher-start',142 ]);143 assert.equal(events[0].args.generation, 7);144});145146test('a current pending frame error aborts activation after acknowledgments', () => {147 const { runtime, events } = subject();148 runtime.prepareConnection();149 const session = runtime.openFrameSession();150 runtime.handleBinaryFrame(frameEnvelope(), session);151 runtime.handleFrameError({ generation: 9, error: 'native failed' });152 events.length = 0;153154 assert.throws(() => activate(runtime, session, 9), /native failed/);155 assert.deepEqual(events.map((event) => event.type), ['invoke']);156 assert.equal(runtime.generation, 9);157});158159test('active binary delivery acknowledges before decode and malformed delivery still acknowledges', () => {160 const { runtime, events } = subject({ connected: true });161 runtime.prepareConnection();162 const session = runtime.openFrameSession();163 activate(runtime, session, 5);164 events.length = 0;165166 assert.equal(runtime.handleBinaryFrame(frameEnvelope({ sequence: 31 }), session), true);167 assert.deepEqual(events.slice(0, 2).map((event) => event.type), ['invoke', 'process']);168 assert.equal(events[0].args.generation, 5);169 assert.equal(events[1].payload.sequence, 31);170 assert.ok(events[1].data instanceof Uint8Array);171172 events.length = 0;173 assert.equal(runtime.handleBinaryFrame(new ArrayBuffer(1), session), false);174 assert.equal(session.failed, true);175 assert.match(session.failureDetail, /shorter than its header/);176 assert.deepEqual(events.map((event) => event.type), ['error', 'disconnect', 'invoke']);177 assert.equal(events[2].args.generation, 5);178});179180test('pre-connect frame errors are bounded and stale active errors are ignored', () => {181 const { runtime, events } = subject({ connected: true });182 runtime.prepareConnection();183 const session = runtime.openFrameSession();184 for (let index = 0; index < FRAME_CHANNEL_CAPACITY; index++) {185 assert.equal(runtime.handleFrameError({ generation: 20 + index, error: `${index}` }), true);186 }187 assert.equal(runtime.handleFrameError({ generation: 99, error: 'overflow' }), false);188 assert.equal(session.failed, true);189 assert.ok(events.some((event) => event.type === 'disconnect'));190191 runtime.prepareConnection();192 const next = runtime.openFrameSession();193 activate(runtime, next, 30);194 assert.equal(runtime.handleFrameError({ generation: 29, error: 'stale' }), false);195 assert.equal(next.failed, false);196});197198test('frame stats keep legacy FPS fallbacks and reset to exact defaults', () => {199 const { runtime } = subject();200 runtime.prepareConnection();201 const session = runtime.openFrameSession();202 activate(runtime, session, 4);203 assert.equal(runtime.handleFrameStats({ generation: 3, fps: 1 }), false);204 assert.equal(runtime.handleFrameStats({205 generation: 4,206 stats_version: 3,207 sequence_dropped_total: 6,208 frontend_dropped_total: 7,209 transport_object_dropped_total: 5,210 transport_late_object_dropped_total: 2,211 frontend_queue_dropped_total: 4,212 frontend_resync_dropped_total: 3,213 frontend_queue_depth: 1,214 frontend_queue_peak: 2,215 frontend_queue_capacity: 4,216 frontend_resync_episode_total: 1,217 frontend_resync_duration_ms_total: 12,218 frontend_resync_duration_ms_max: 12,219 frontend_resync_active: true,220 frontend_resync_duration_ms_current: 2,221 transport_interval_sample_count: 0,222 frontend_ipc_send_duration_sample_count: 0,223 timing_window_ms: 1000,224 timing_sample_capacity: 120,225 path_mode: 'future-mode-is-kept-verbatim',226 path_rtt_ms: 8.5,227 sequence: 41,228 fps: 44,229 }), true);230 const snapshot = runtime.snapshot();231 assert.equal(snapshot.transportDroppedFrames, 6);232 assert.equal(snapshot.transportObjectDroppedFrames, 5);233 assert.equal(snapshot.transportLateObjectDroppedFrames, 2);234 assert.equal(snapshot.frontendDroppedFrames, 7);235 assert.deepEqual(snapshot.frontendQueueStats, { depth: 1, peak: 2, capacity: 4 });236 assert.equal(snapshot.frontendResyncStats.active, true);237 assert.equal(snapshot.streamPathMode, 'future-mode-is-kept-verbatim');238 assert.equal(snapshot.streamRttMs, 8.5);239 assert.equal(snapshot.lastMediaSequence, 41);240 assert.equal(snapshot.lastTransportFps, 44);241 assert.equal(snapshot.lastFrontendSendFps, 44);242243 runtime.prepareConnection();244 const reset = runtime.snapshot();245 assert.equal(reset.lastTransportFps, 0);246 assert.equal(reset.lastFrontendSendFps, 0);247 assert.equal(reset.frontendQueueStats, null);248});249250test('adaptive diagnostics reject stale generations and stop failed feedback', () => {251 const { runtime, events } = subject();252 runtime.prepareConnection();253 const session = runtime.openFrameSession();254 activate(runtime, session, 12);255 const decision = {256 generation: 12,257 decision: {258 decision_id: 1,259 report_id: 2,260 target_kbps: 8000,261 floor_kbps: 2000,262 ceiling_kbps: 12000,263 state: 'decrease',264 reasons: ['queue'],265 applied: false,266 },267 };268 assert.equal(runtime.handleAdaptiveDecision({ ...decision, generation: 11 }), false);269 assert.equal(runtime.handleAdaptiveDecision(decision), true);270 assert.equal(runtime.snapshot().adaptiveDecision.target_kbps, 8000);271 assert.equal(runtime.handleAdaptiveFeedbackState({ generation: 11, available: false }), false);272 assert.equal(runtime.handleAdaptiveFeedbackState({ generation: 12, available: false }), true);273 assert.equal(runtime.snapshot().adaptiveFeedbackAvailable, false);274 assert.equal(runtime.snapshot().adaptiveFeedbackError, 'feedback stream closed');275 assert.ok(events.some((event) => event.type === 'publisher-stop'));276277 assert.equal(runtime.handleAdaptiveDecision({ generation: 12, decision: null }), false);278 assert.equal(events.at(-1).type, 'warn');279});280281test('adaptive feedback preserves bounded queue fallbacks and pressure signals', () => {282 const { runtime, events } = subject();283 runtime.prepareConnection();284 const session = runtime.openFrameSession();285 activate(runtime, session, 6);286 runtime.handleFrameStats({287 generation: 6,288 stats_version: 2,289 sequence_dropped_total: 10,290 frontend_dropped_total: 11,291 frontend_resync_active: true,292 frontend_resync_episode_total: 1,293 frontend_resync_duration_ms_total: 3,294 frontend_resync_duration_ms_max: 3,295 frontend_resync_duration_ms_current: 1,296 fps: 60,297 sequence: 99,298 });299 events.length = 0;300 const video = videoSnapshot({ recovering: false });301 assert.equal(runtime.publishAdaptiveFeedback(video), true);302 const published = events[0].snapshot;303 assert.deepEqual(published, {304 lastSequence: 99,305 frontendQueueDepth: 0,306 frontendQueueCapacity: 4,307 decoderQueueDepth: 1,308 decoderQueueCapacity: 2,309 presenterQueueDepth: 1,310 presenterQueueCapacity: 2,311 transportDroppedTotal: 10,312 frontendDroppedTotal: 11,313 decoderDroppedTotal: 3,314 presenterDroppedTotal: 4,315 transportDeliveryP95Ms: null,316 decodeLatencyP95Ms: 5,317 presentationLatencyP95Ms: 6,318 resyncActive: true,319 });320});321322test('adaptive feedback projects the bounded presenter queue and overwrite counter', () => {323 const { runtime, events } = subject();324 runtime.prepareConnection();325 const session = runtime.openFrameSession();326 activate(runtime, session, 6);327 events.length = 0;328329 assert.equal(runtime.publishAdaptiveFeedback(videoSnapshot({330 presenterQueueDepth: 2,331 presenterQueueCapacity: 2,332 presentationDroppedFrames: 3,333 })), true);334 assert.equal(events[0].snapshot.presenterQueueDepth, 2);335 assert.equal(events[0].snapshot.presenterQueueCapacity, 2);336 assert.equal(events[0].snapshot.presenterDroppedTotal, 3);337338 events.length = 0;339 assert.equal(runtime.publishAdaptiveFeedback(videoSnapshot({340 presenterQueueDepth: 0,341 presenterQueueCapacity: 2,342 presentationDroppedFrames: 3,343 })), true);344 assert.equal(events[0].snapshot.presenterQueueDepth, 0);345 assert.equal(events[0].snapshot.presenterQueueCapacity, 2);346 assert.equal(events[0].snapshot.presenterDroppedTotal, 3);347});