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 / decoder-recovery.test.mjs
decoder-recovery.test.mjs 6.0K
1import assert from 'node:assert/strict';2import { readFile } from 'node:fs/promises';3import test from 'node:test';45import {6 DECODER_RECOVERY_REASONS,7 DecoderRecoveryState,8} from './decoder-recovery.mjs';910const pipeline = await readFile(new URL('./video-pipeline.mjs', import.meta.url), 'utf8');1112test('video pipeline uses the recovery state instead of removed legacy state', () => {13 assert.doesNotMatch(pipeline, /waitingForDecoderKeyframe/);14 assert.match(pipeline, /decoderRecovery\.reset\(\);/);15});1617test('decoder callbacks cannot mutate a replacement decoder session', () => {18 assert.match(19 pipeline,20 /output: \(frame\) => \{\s*if \(videoDecoder !== decoder\) \{\s*frame\.close\(\);\s*return;/,21 );22 assert.match(pipeline, /error: \(error\) => \{\s*if \(videoDecoder !== decoder\) return;/);23});2425test('a delivered discontinuity keyframe resets without requesting its replacement', () => {26 const requests = [];27 const recovery = new DecoderRecoveryState({28 initiallyRecovering: false,29 onKeyframeRequest: (reason) => requests.push(reason),30 });3132 assert.deepEqual(33 recovery.enter(DECODER_RECOVERY_REASONS.DISCONTINUITY, { requestKeyframe: false }),34 { entered: true, requested: false },35 );36 assert.equal(recovery.shouldDropFrame({ keyframe: true }), false);37 assert.equal(recovery.confirmKeyframeEnqueued(true), true);38 assert.deepEqual(requests, []);39 assert.match(40 pipeline,41 /enterDecoderRecovery\(DECODER_RECOVERY_REASONS\.DISCONTINUITY, \{\s*requestKeyframe: !keyframe,/,42 );43});4445test('drops deltas until a recovery keyframe is successfully enqueued', () => {46 const recovery = new DecoderRecoveryState();4748 assert.equal(recovery.recovering, true);49 assert.equal(recovery.shouldDropFrame({ keyframe: false }), true);50 assert.equal(recovery.shouldDropFrame({ keyframe: true }), false);51 assert.equal(recovery.confirmKeyframeEnqueued(false), false);52 assert.equal(recovery.recovering, true);53 assert.equal(recovery.shouldDropFrame({ keyframe: false }), true);54 assert.equal(recovery.confirmKeyframeEnqueued(true), true);55 assert.equal(recovery.recovering, false);56 assert.equal(recovery.shouldDropFrame({ keyframe: false }), false);57});5859test('coalesces keyframe requests throughout one recovery episode', () => {60 const requests = [];61 const recovery = new DecoderRecoveryState({62 initiallyRecovering: false,63 onKeyframeRequest: (reason) => requests.push(reason),64 });6566 assert.deepEqual(recovery.enter(DECODER_RECOVERY_REASONS.TRANSPORT_GAP), {67 entered: true,68 requested: true,69 });70 assert.deepEqual(recovery.enter(DECODER_RECOVERY_REASONS.DELIVERY_TIMEOUT), {71 entered: false,72 requested: false,73 });74 assert.deepEqual(recovery.enter(DECODER_RECOVERY_REASONS.FRONTEND_BACKPRESSURE), {75 entered: false,76 requested: false,77 });78 assert.deepEqual(requests, [DECODER_RECOVERY_REASONS.TRANSPORT_GAP]);7980 recovery.confirmKeyframeEnqueued(true);81 recovery.enter(DECODER_RECOVERY_REASONS.FRONTEND_BACKPRESSURE);82 assert.deepEqual(requests, [83 DECODER_RECOVERY_REASONS.TRANSPORT_GAP,84 DECODER_RECOVERY_REASONS.FRONTEND_BACKPRESSURE,85 ]);86});8788test('every supported failure reason notifies synchronously when entering recovery', () => {89 for (const reason of Object.values(DECODER_RECOVERY_REASONS)) {90 const requests = [];91 const recovery = new DecoderRecoveryState({92 initiallyRecovering: false,93 onKeyframeRequest: (requestedReason) => requests.push(requestedReason),94 });9596 const result = recovery.enter(reason);97 assert.deepEqual(result, { entered: true, requested: true });98 assert.deepEqual(requests, [reason]);99 assert.equal(recovery.recovering, true);100 }101});102103test('an initially waiting decoder requests immediately on its first loss signal', () => {104 const requests = [];105 const recovery = new DecoderRecoveryState({106 onKeyframeRequest: (reason) => requests.push(reason),107 });108109 assert.deepEqual(recovery.enter(DECODER_RECOVERY_REASONS.DISCONTINUITY), {110 entered: false,111 requested: true,112 });113 assert.equal(recovery.reason, DECODER_RECOVERY_REASONS.DISCONTINUITY);114 assert.deepEqual(requests, [DECODER_RECOVERY_REASONS.DISCONTINUITY]);115});116117test('synchronous keyframe failure stays coalesced until an explicit restart', () => {118 const requests = [];119 const recovery = new DecoderRecoveryState({120 initiallyRecovering: false,121 onKeyframeRequest: (reason) => requests.push(reason),122 });123124 recovery.enter(DECODER_RECOVERY_REASONS.DECODER_ERROR);125 recovery.confirmKeyframeEnqueued(false);126 recovery.enter(DECODER_RECOVERY_REASONS.DECODER_ERROR);127 assert.deepEqual(requests, [DECODER_RECOVERY_REASONS.DECODER_ERROR]);128129 assert.deepEqual(recovery.restart(DECODER_RECOVERY_REASONS.DECODER_RESET), {130 entered: true,131 requested: true,132 });133 assert.deepEqual(requests, [134 DECODER_RECOVERY_REASONS.DECODER_ERROR,135 DECODER_RECOVERY_REASONS.DECODER_RESET,136 ]);137});138139test('session reset clears request coalescing without notifying an inactive host', () => {140 const requests = [];141 const recovery = new DecoderRecoveryState({142 initiallyRecovering: false,143 onKeyframeRequest: (reason) => requests.push(reason),144 });145146 recovery.enter(DECODER_RECOVERY_REASONS.TRANSPORT_GAP);147 recovery.reset();148 assert.equal(recovery.recovering, true);149 assert.equal(recovery.reason, null);150 assert.equal(recovery.requestIssued, false);151 assert.deepEqual(requests, [DECODER_RECOVERY_REASONS.TRANSPORT_GAP]);152});153154test('rejects malformed reasons and frame decisions', () => {155 const recovery = new DecoderRecoveryState();156 assert.throws(() => recovery.enter('loss'), /unsupported decoder recovery reason/);157 assert.throws(158 () => recovery.enter(DECODER_RECOVERY_REASONS.DISCONTINUITY, { requestKeyframe: 'no' }),159 /requestKeyframe must be a boolean/,160 );161 assert.throws(() => recovery.restart(null), /unsupported decoder recovery reason/);162 assert.throws(() => recovery.shouldDropFrame({ keyframe: 1 }), /keyframe must be a boolean/);163 assert.throws(() => recovery.confirmKeyframeEnqueued('yes'), /succeeded must be a boolean/);164});