'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const LivePlayer = require('./web/live-player.js'); function fixture() { let now = 100, serial = 0; const timeouts = new Map(), intervals = new Map(); const clock = { setTimeout(fn, ms) {const id=++serial;timeouts.set(id,{fn,ms});return id;}, clearTimeout(id) {timeouts.delete(id);}, setInterval(fn, ms) {const id=++serial;intervals.set(id,{fn,ms});return id;}, clearInterval(id) {intervals.delete(id);} }; const listeners = new Map(), indicators = {'.video-empty':{hidden:false},'.video-error':{hidden:true}}; const video = { currentTime:0,readyState:0,videoWidth:0,paused:false,ended:false,error:null, addEventListener(e,f) {listeners.set(e,f);},removeEventListener(e) {listeners.delete(e);}, emit(e) {listeners.get(e)?.();}, play() {this.paused=false;this.emit('play');return Promise.resolve();}, pause() {this.paused=true;this.emit('pause');},removeAttribute() {},load() {}, canPlayType() {return '';} }; const instances=[]; class Hls { static Events={ERROR:'error',MANIFEST_PARSED:'manifest'}; static isSupported() {return true;} constructor() {this.events={};instances.push(this);} on(e,f) {this.events[e]=f;} loadSource() {} attachMedia() {} destroy() {this.destroyed=true;} fail() {this.events.error(null,{fatal:true});} } const player=new LivePlayer(video,{querySelector:s=>indicators[s]},'/example',{Hls,clock,now:()=>now}); return {player,video,instances,timeouts,intervals,indicators, advance(ms) {now+=ms;player.watch();}, playing() {video.readyState=4;video.videoWidth=1920;video.emit('playing');}, retry() {const [id,t]=timeouts.entries().next().value;timeouts.delete(id);now+=t.ms;t.fn();} }; } test('readiness, recording status and labels do not change playback identity',()=>{ const camera={id:'one',enabled:true,ready:true,name:'A',recordingActive:false}; const original=LivePlayer.key([camera],'hd'); assert.equal(LivePlayer.key([{...camera,ready:false,name:'B',recordingActive:true}],'hd'),original); assert.notEqual(LivePlayer.key([camera],'compat'),original); }); test('fatal error reconnects only its own player with bounded backoff',()=>{ const a=fixture(),b=fixture(); for(let i=0;i<8;i++) { a.instances.at(-1).fail(); assert.equal(a.timeouts.size,1); assert.ok([...a.timeouts.values()][0].ms<=30000); a.retry(); } assert.equal(b.instances.length,1); a.player.destroy();b.player.destroy(); }); test('duplicate errors schedule only one retry',()=>{ const f=fixture();f.instances[0].fail();f.video.emit('error'); assert.equal(f.timeouts.size,1);f.player.destroy(); }); test('continuous playback does not restart during repeated health checks',()=>{ const f=fixture();f.playing(); for(let i=0;i<72;i++) {f.video.currentTime+=5;f.advance(5000);} assert.equal(f.instances.length,1);assert.equal(f.timeouts.size,0);f.player.destroy(); }); test('stalled playback automatically reconnects',()=>{ const f=fixture();f.playing();f.advance(30000); assert.equal(f.timeouts.size,1);f.retry();assert.equal(f.instances.length,2);f.player.destroy(); }); test('startup has enough time to wait for a source',()=>{ const f=fixture();f.advance(60000);assert.equal(f.timeouts.size,0); f.advance(6000);assert.equal(f.timeouts.size,1);f.player.destroy(); }); test('intentional pause is preserved and resume can recover a failed connection',()=>{ const f=fixture();f.playing();f.video.pause();f.instances[0].fail();f.advance(90000); assert.equal(f.instances.length,1);assert.equal(f.timeouts.size,0); f.video.play();assert.equal(f.instances.length,2);f.player.destroy(); }); test('destroy cancels timers and releases the old player',()=>{ const f=fixture();f.instances[0].fail();f.player.destroy(); assert.equal(f.timeouts.size,0);assert.equal(f.intervals.size,0);assert.equal(f.instances[0].destroyed,true); }); test('recovery before timeout cancels pending retry',()=>{ const f=fixture();f.instances[0].fail();f.playing(); assert.equal(f.timeouts.size,0);assert.equal(f.indicators['.video-error'].hidden,true);f.player.destroy(); }); test('error from an old HLS instance cannot restart its replacement',()=>{ const f=fixture();const old=f.instances[0];old.fail();f.retry();old.fail(); assert.equal(f.timeouts.size,0);assert.equal(f.instances.length,2);f.player.destroy(); });