// Regression checks for channel navigation, single start, controls and cancellation. const fs = require('node:fs'); const vm = require('node:vm'); const assert = require('node:assert/strict'); const listeners = {}, documentListeners = {}, elements = []; let clock = 0, interval, starts = 0, stops = 0, osd = 0, returned = 0, readyState = 0, remote = false; let policy = { IsEnigmaChannel: true, PreferH264: true }, policyCalls = 0, policyError = false; const profile = { DirectPlayProfiles: [{ Type: 'Video', VideoCodec: 'av1,h264' }], TranscodingProfiles: [{ Type: 'Video', Context: 'Streaming', VideoCodec: 'av1,h264', AudioCodec: 'aac', Protocol: 'hls', MaxWidth: 1920 }, { Type: 'Video', Context: 'Streaming', VideoCodec: 'av1', AudioCodec: 'aac', Protocol: 'hls' }, { Type: 'Audio', AudioCodec: 'aac', Protocol: 'http' }], CodecProfiles: [{ Type: 'Video', Codec: 'h264', Conditions: [{ Property: 'Width', Value: '1920' }] }] }; const pending = []; const location = { hash: '#/livetv', pathname: '/web/index.html', search: '', replace(value) { this.hash = value; } }; const document = { fullscreenElement: null, body: { append(element) { elements.push(element); } }, createElement() { return { style: {}, remove() { elements.splice(elements.indexOf(this), 1); } }; }, querySelector() { return { readyState, currentTime: 0 }; }, addEventListener(name, callback) { documentListeners[name] = callback; } }; const window = { ApiClient: { getCurrentUserId: () => 'user', serverId: () => 'server', getItem: async (user, id) => ({ Id: id, Type: 'TvChannel' }), getUrl: (path, params) => path + '?' + new URLSearchParams(params), ajax: async () => { ++policyCalls; if (policyError) throw Error('offline'); return policy; } }, addEventListener(name, callback) { listeners[name] = callback; } }; const context = { window, document, location, console, URLSearchParams, history: { replaceState(a, b, url) { location.hash = url.slice(url.indexOf('#')); } }, Date: { now: () => clock }, setTimeout, clearTimeout, setInterval(callback) { interval = callback; return callback; }, clearInterval(callback) { if (interval === callback) interval = null; } }; const manager = { getCurrentPlayer: () => ({ isLocalPlayer: !remote }), play() { ++starts; return new Promise(resolve => pending.push(resolve)); }, stop: async () => { ++stops; } }; const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }; (async () => { vm.runInNewContext(fs.readFileSync(__dirname + '/web/decoder-channel-playback.js', 'utf8'), context); const Plugin = await window.DecoderChannelPlayback(); const appHost = { getDeviceProfile: async () => profile }; const dependencies = { playbackManager: manager, appHost, toast() {}, loading: { hide() {} }, events: { on() {} }, appRouter: { showVideoOsd: async () => { ++osd; }, show: async () => { ++returned; location.hash = '#/livetv?tab=2'; } } }; new Plugin(dependencies); const channel = { Id: 'channel', Type: 'TvChannel', ServerId: 'server' }; const before = JSON.stringify(profile); const changed = await appHost.getDeviceProfile(channel, {}); assert.notEqual(changed, profile); assert.equal(JSON.stringify(profile), before, 'shared profile must not mutate'); assert.equal(changed.TranscodingProfiles.length, 2); assert.equal(changed.TranscodingProfiles[0].VideoCodec, 'h264'); assert.equal(changed.TranscodingProfiles[0].MaxWidth, 1920); assert.equal(changed.TranscodingProfiles[0].AudioCodec, 'aac'); assert.equal(JSON.stringify(changed.DirectPlayProfiles), JSON.stringify(profile.DirectPlayProfiles)); assert.equal(JSON.stringify(changed.CodecProfiles), JSON.stringify(profile.CodecProfiles)); let count = policyCalls; assert.equal(await appHost.getDeviceProfile({ Type: 'Movie' }, {}), profile); assert.equal(policyCalls, count); assert.equal(await appHost.getDeviceProfile({ ...channel, ServerId: 'other' }, {}), profile); assert.equal(policyCalls, count); remote = true; assert.equal(await appHost.getDeviceProfile(channel, {}), profile); assert.equal(policyCalls, count); remote = false; window.NativeShell = {}; assert.equal(await appHost.getDeviceProfile(channel, {}), profile); assert.equal(policyCalls, count); delete window.NativeShell; policy = { IsEnigmaChannel: false, PreferH264: true }; assert.equal(await appHost.getDeviceProfile(channel, {}), profile); policy = { IsEnigmaChannel: true, PreferH264: false }; assert.equal(await appHost.getDeviceProfile(channel, {}), profile); policyError = true; assert.equal(await appHost.getDeviceProfile(channel, {}), profile); policyError = false; policy = { IsEnigmaChannel: true, PreferH264: true }; count = policyCalls; await appHost.getDeviceProfile(channel, {}); assert.equal(policyCalls, count + 1, 'retry refreshes current policy'); const wrapped = appHost.getDeviceProfile; new Plugin(dependencies); assert.equal(appHost.getDeviceProfile, wrapped, 'one profile wrapper per appHost'); console.log('PASS scoped H264 policy, clone, audio/conditions/direct play preserved, movies/other sources/native/remote bypass, disabled/failing policy, retry and dedup'); assert.equal(location.hash, '#/livetv?tab=2'); location.hash = '#/livetv?tab=0'; listeners.hashchange(); assert.equal(location.hash, '#/livetv?tab=0'); const click = () => documentListeners.click({ button: 0, target: { closest: () => ({ closest: () => ({ dataset: { id: 'channel', serverid: 'server' } }) }) }, preventDefault() {}, stopImmediatePropagation() {} }); click(); click(); await flush(); assert.equal(starts, 1); assert.equal(elements[0].id, 'decoderPlaybackExit'); interval(); await flush(); assert.equal(osd, 0, 'OSD must wait for actual video metadata'); readyState=1;interval(); await flush(); assert.equal(osd, 1); listeners.keydown({ key: 'Escape', preventDefault() {}, stopImmediatePropagation() {} }); await flush(); assert.equal(stops, 1); assert.equal(returned, 1); assert.equal(elements.length, 0); click(); await flush(); assert.equal(starts, 2); pending[0](); await flush(); assert.equal(stops, 1, 'late old completion must not stop the new channel'); clock = 35000; interval(); await flush(); assert.equal(stops, 1, 'pending startup must survive 35 seconds'); assert.equal(returned, 1); assert.equal(elements.length, 1, 'cancel control remains available during slow startup'); clock = 45001; interval(); await flush(); assert.equal(stops, 2); assert.equal(returned, 2); assert.equal(elements.length, 0); pending[1](); await flush(); console.log('PASS default channels, explicit programmes, one start, native OSD, Escape, late completion and startup timeout'); })().catch(error => { console.error(error); process.exitCode = 1; });