Merge remote-tracking branch 'origin/main' into codex/enigma2-integration
# Conflicts: # public/index.html
This commit is contained in:
@ -24,7 +24,7 @@ test('both subtitle menus select real HLS cues, discard stale tracks and preserv
|
||||
assert.equal(generated.status, 0, generated.stderr);
|
||||
|
||||
const root = path.join(__dirname, '../public');
|
||||
const scripts = ['/js/components/CaptionTracks.js', '/js/components/VideoPlayer.js', '/js/pages/WatchPage.js'];
|
||||
const scripts = ['/js/components/CaptionTracks.js', '/js/components/HlsAudioRecovery.js', '/js/components/VideoPlayer.js', '/js/pages/WatchPage.js'];
|
||||
const master = names => '#EXTM3U\n' + names.map(([name, extra, language = 'en'], index) =>
|
||||
`#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="${name}",LANGUAGE="${language}",DEFAULT=NO,AUTOSELECT=NO,${extra}URI="/sub${index}.m3u8"\n`).join('') +
|
||||
'#EXT-X-STREAM-INF:BANDWIDTH=100000,CODECS="avc1.42c00a",SUBTITLES="subs"\n/video.m3u8\n';
|
||||
@ -104,6 +104,11 @@ test('both subtitle menus select real HLS cues, discard stale tracks and preserv
|
||||
});
|
||||
assert.equal(await page.locator('#list button.active').count(), 1);
|
||||
}
|
||||
await page.evaluate(() => window.player.audioRecovery.requestRecovery('caption recovery fixture'));
|
||||
await page.waitForFunction(() => window.player.audioRecovery.lastRecovery > 0 && !window.player.audioRecovery.onAttached);
|
||||
await page.waitForFunction(() => Array.from(window.player.video.textTracks).some(track =>
|
||||
track.mode === 'showing' && Array.from(track.activeCues || []).some(cue => cue.text === 'Cue from /sub2.vtt')));
|
||||
assert.equal(await page.evaluate(() => window.player.video.paused), true);
|
||||
await page.locator('#list button').first().click();
|
||||
assert.equal(await page.evaluate(() => window.player.hls.subtitleTrack), -1);
|
||||
assert.equal(await page.evaluate(() => Array.from(window.player.video.textTracks).every(track => track.mode === 'disabled')), true);
|
||||
@ -162,6 +167,11 @@ test('both subtitle menus select real HLS cues, discard stale tracks and preserv
|
||||
return track.mode === 'showing' && track.cues?.length === 1 && track.cues[0].text === 'CEA caption';
|
||||
});
|
||||
|
||||
await page.evaluate(() => window.player.audioRecovery.requestRecovery('CEA recovery fixture'));
|
||||
await page.waitForFunction(() => window.player.audioRecovery.lastRecovery > 0 && !window.player.audioRecovery.onAttached);
|
||||
await page.waitForFunction(() => Array.from(window.player.video.textTracks).some(track =>
|
||||
track.label === 'English CC' && track.mode === 'showing' && track.cues?.length === 1 && track.cues[0].text === 'CEA caption'));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const player = window.player;
|
||||
player.stop();
|
||||
|
||||
121
test/hls-ad-break.test.js
Normal file
121
test/hls-ad-break.test.js
Normal file
@ -0,0 +1,121 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const http = require('node:http');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
test('Chromium plays program/ad/program audio with decoder resets at played boundaries',
|
||||
{ skip: process.env.NODECAST_BROWSER_TESTS !== '1' ? 'Browser tests enabled in CI' : false, timeout: 90000 }, async t => {
|
||||
const { chromium } = require('playwright');
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodecast-ad-audio-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
let binary = 'ffmpeg';
|
||||
if (spawnSync(binary, ['-version'], { stdio: 'ignore' }).status !== 0) binary = require('ffmpeg-static');
|
||||
const files = new Map();
|
||||
const manifest = ['#EXTM3U', '#EXT-X-TARGETDURATION:1', '#EXT-X-MEDIA-SEQUENCE:0'];
|
||||
for (let phase = 0; phase < 3; phase++) {
|
||||
const rate = phase === 1 ? 44100 : 48000;
|
||||
const channels = phase === 1 ? 2 : 6;
|
||||
const generated = spawnSync(binary, ['-hide_banner', '-loglevel', 'error', '-y',
|
||||
'-f', 'lavfi', '-i', 'color=c=black:s=160x90:r=10',
|
||||
'-f', 'lavfi', '-i', `sine=frequency=1000:sample_rate=${rate}`, '-t', '3',
|
||||
'-c:v', 'libx264', '-threads', '1', '-preset', 'ultrafast', '-g', '10',
|
||||
'-c:a', 'aac', '-ar', String(rate), '-ac', String(channels), '-b:a', '160k',
|
||||
'-f', 'hls', '-hls_time', '1', '-hls_list_size', '0',
|
||||
'-hls_segment_filename', path.join(dir, `phase${phase}-%d.ts`), path.join(dir, `phase${phase}.m3u8`)],
|
||||
{ encoding: 'utf8', timeout: 15000 });
|
||||
assert.equal(generated.status, 0, generated.stderr);
|
||||
if (phase) manifest.push('#EXT-X-DISCONTINUITY');
|
||||
const lines = fs.readFileSync(path.join(dir, `phase${phase}.m3u8`), 'utf8').trim().split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('#EXTINF:')) manifest.push(line);
|
||||
else if (line.endsWith('.ts')) { manifest.push('/' + line); files.set('/' + line, fs.readFileSync(path.join(dir, line))); }
|
||||
}
|
||||
}
|
||||
manifest.push('#EXT-X-ENDLIST');
|
||||
const root = path.join(__dirname, '../public');
|
||||
const scripts = ['/js/components/CaptionTracks.js', '/js/components/HlsAudioRecovery.js',
|
||||
'/js/components/VideoPlayer.js', '/js/pages/WatchPage.js'];
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, 'http://fixture.test');
|
||||
const send = (type, data) => { res.writeHead(200, { 'Content-Type': type }); res.end(data); };
|
||||
if (url.pathname === '/') return send('text/html', `<video></video><div id="ui"></div><div id="list"></div><script src="/hls.js"></script>${scripts.map(src => `<script src="${src}"></script>`).join('')}`);
|
||||
if (url.pathname === '/hls.js') return send('application/javascript', fs.readFileSync(require.resolve('hls.js/dist/hls.min.js')));
|
||||
if (scripts.includes(url.pathname)) return send('application/javascript', fs.readFileSync(path.join(root, url.pathname)));
|
||||
if (url.pathname === '/ads.m3u8') return send('application/vnd.apple.mpegurl', manifest.join('\n') + '\n');
|
||||
if (url.pathname === '/api/subtitle') return send('text/vtt', 'WEBVTT\n\n00:00:00.000 --> 00:00:09.000\nTest caption\n\n');
|
||||
if (files.has(url.pathname)) return send('video/mp2t', files.get(url.pathname));
|
||||
res.writeHead(404); res.end();
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
t.after(() => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }));
|
||||
const browser = await chromium.launch({ headless: true, args: ['--autoplay-policy=no-user-gesture-required'],
|
||||
executablePath: process.env.NODECAST_BROWSER_EXECUTABLE || undefined });
|
||||
t.after(() => browser.close());
|
||||
const base = `http://127.0.0.1:${server.address().port}`;
|
||||
for (const name of ['VideoPlayer', 'WatchPage']) {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
await page.goto(base);
|
||||
await page.evaluate(async ({ name, base }) => {
|
||||
const ui = document.getElementById('ui');
|
||||
const video = document.querySelector('video');
|
||||
const player = window.player = Object.create(window[name].prototype);
|
||||
Object.assign(player, { video, settings: {}, currentUrl: `${base}/ads.m3u8`, playbackGeneration: 1,
|
||||
captionsList: document.getElementById('list'), captionsMenu: ui, overlay: ui, nowPlaying: ui,
|
||||
stopTranscodeSession() {}, stopHistoryTracking() {}, saveProgress() {}, hideNowPlaying() {},
|
||||
updateTranscodeStatus() {}, updateNowPlaying() {}, showNowPlayingOverlay() {}, fetchEpgData() {},
|
||||
showError(message) { throw Error(message); } });
|
||||
const context = window.audioContext = new AudioContext();
|
||||
const analyser = context.createAnalyser();
|
||||
analyser.fftSize = 4096;
|
||||
context.createMediaElementSource(video).connect(analyser);
|
||||
analyser.connect(context.destination);
|
||||
await context.resume();
|
||||
window.measurements = { recoveries: [], tones: [], formats: [], played: [], errors: [] };
|
||||
let cc = 0;
|
||||
if (name === 'VideoPlayer') await player.play({ id: 'fixture', name: 'Ad fixture' }, `${base}/ads.m3u8`);
|
||||
else player.playHls(`${base}/ads.m3u8`);
|
||||
player.getCaptionTracks().setProbe({ subtitles: [{ index: 1, title: 'Extracted', language: 'en' }] }, 'http://provider.test/source');
|
||||
const hls = player.hls;
|
||||
const recover = hls.recoverMediaError.bind(hls);
|
||||
hls.recoverMediaError = () => { window.measurements.recoveries.push({ time: video.currentTime, cc }); recover(); };
|
||||
hls.on(Hls.Events.FRAG_CHANGED, (event, data) => { cc = data.frag.cc; window.measurements.played.push({ cc, time: video.currentTime }); });
|
||||
hls.on(Hls.Events.FRAG_PARSING_INIT_SEGMENT, (event, data) => {
|
||||
if (data.tracks.audio) window.measurements.formats.push({ cc: data.frag.cc, channels: data.tracks.audio.metadata.channelCount });
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) window.measurements.errors.push(data.details); });
|
||||
const bins = new Float32Array(analyser.frequencyBinCount);
|
||||
window.sampleTimer = setInterval(() => {
|
||||
if (video.paused || video.readyState < 3) return;
|
||||
analyser.getFloatFrequencyData(bins);
|
||||
let peak = 0;
|
||||
for (let i = 10; i < bins.length; i++) if (bins[i] > bins[peak]) peak = i;
|
||||
if (bins[peak] > -65) window.measurements.tones.push({ cc, time: video.currentTime,
|
||||
frequency: peak * context.sampleRate / analyser.fftSize,
|
||||
captions: Array.from(video.textTracks).some(track => track.mode === 'showing' && track.activeCues?.length > 0) });
|
||||
}, 100);
|
||||
}, { name, base });
|
||||
await page.waitForFunction(() => document.querySelectorAll('#list button').length === 2);
|
||||
await page.locator('#list button').nth(1).click();
|
||||
await page.waitForFunction(() => window.player.video.ended, null, { timeout: 30000 });
|
||||
const measured = await page.evaluate(() => { clearInterval(window.sampleTimer); return window.measurements; });
|
||||
assert.deepEqual(errors, [], name);
|
||||
assert.deepEqual(measured.errors, [], `${name}: ${JSON.stringify(measured)}`);
|
||||
assert.equal(measured.recoveries.length, 2, `${name}: ${JSON.stringify(measured)}`);
|
||||
assert.deepEqual(measured.recoveries.map(item => item.cc), [1, 2]);
|
||||
assert.ok(measured.recoveries[0].time >= 2.9 && measured.recoveries[1].time >= 5.9, 'recover at playback, not when ads prebuffer');
|
||||
for (const period of [0, 1, 2]) {
|
||||
assert.ok(measured.formats.some(format => format.cc === period && format.channels === (period === 1 ? 2 : 6)));
|
||||
const tones = measured.tones.filter(sample => sample.cc === period && sample.time % 3 > 0.5 && sample.time % 3 < 2.5);
|
||||
assert.ok(tones.length > 0, `${name}: missing audio in period ${period}: ${JSON.stringify(measured)}`);
|
||||
assert.ok(tones.every(sample => Math.abs(sample.frequency - 1000) < 100), `${name}: wrong pitch in period ${period}: ${JSON.stringify(tones)}`);
|
||||
assert.ok(tones.some(sample => sample.captions), `${name}: captions lost in period ${period}`);
|
||||
}
|
||||
await page.evaluate(() => { window.player.stop(); window.audioContext.close(); });
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
160
test/hls-audio-recovery.test.js
Normal file
160
test/hls-audio-recovery.test.js
Normal file
@ -0,0 +1,160 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const HlsAudioRecovery = require('../public/js/components/HlsAudioRecovery');
|
||||
const events = { FRAG_CHANGED: 'fragment', DESTROYING: 'destroying', MEDIA_ATTACHED: 'attached', MANIFEST_LOADING: 'manifest-loading' };
|
||||
const tick = () => new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
function fixture(t, chromium = true) {
|
||||
let current = true, now = 10000;
|
||||
const video = new EventEmitter();
|
||||
Object.assign(video, { currentTime: 83.5, paused: false, volume: 0.6, muted: true, playbackRate: 1.25,
|
||||
addEventListener: video.on.bind(video), removeEventListener: video.off.bind(video),
|
||||
play: async () => { video.paused = false; video.plays++; },
|
||||
pause: () => { video.paused = true; }, plays: 0 });
|
||||
const hls = new EventEmitter();
|
||||
hls.config = { startPosition: -1 };
|
||||
hls.positions = [];
|
||||
hls.recoverMediaError = () => { hls.positions.push(hls.config.startPosition); video.pause(); hls.emit(events.MEDIA_ATTACHED); };
|
||||
const recovery = new HlsAudioRecovery(hls, video, events, () => current, { chromium, now: () => now });
|
||||
t.after(() => recovery.dispose());
|
||||
return { recovery, hls, video, fragment: (cc, sn = 1) => hls.emit(events.FRAG_CHANGED, 'fragment', { frag: { cc, sn } }),
|
||||
advance: time => { now += time; }, supersede: () => { current = false; } };
|
||||
}
|
||||
|
||||
test('Chromium resets once at each played discontinuity, preserving time and playback settings', async t => {
|
||||
const f = fixture(t);
|
||||
f.fragment(0); f.fragment(0); f.fragment(0, 'initSegment');
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
f.fragment(1); f.fragment(1);
|
||||
assert.deepEqual(f.hls.positions, [], 'do not detach inside HLS event dispatch');
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, [83.5]);
|
||||
assert.equal(f.hls.config.startPosition, -1);
|
||||
assert.equal(f.video.paused, false);
|
||||
assert.deepEqual([f.video.volume, f.video.muted, f.video.playbackRate], [0.6, true, 1.25]);
|
||||
f.fragment(1);
|
||||
await tick();
|
||||
assert.equal(f.hls.positions.length, 1, 'reattachment does not create a recovery loop');
|
||||
f.advance(3000); f.video.currentTime = 89;
|
||||
f.fragment(2);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, [83.5, 89]);
|
||||
});
|
||||
|
||||
test('Firefox skips proactive discontinuity resets but supports requested media-error recovery', async t => {
|
||||
const f = fixture(t, false);
|
||||
f.fragment(0); f.fragment(1);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
assert.equal(f.recovery.requestRecovery('media error'), true);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, [83.5]);
|
||||
});
|
||||
|
||||
test('paused boundaries wait for playback and error recovery does not start paused video', async t => {
|
||||
const f = fixture(t);
|
||||
f.video.pause();
|
||||
f.fragment(0); f.fragment(1);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
f.video.paused = false; f.video.emit('playing');
|
||||
await tick();
|
||||
assert.equal(f.hls.positions.length, 1);
|
||||
f.advance(3000); f.video.pause();
|
||||
f.recovery.requestRecovery('media error');
|
||||
await tick();
|
||||
assert.equal(f.video.paused, true);
|
||||
assert.equal(f.video.plays, 1);
|
||||
});
|
||||
|
||||
test('superseded or destroyed players cancel queued recovery and late attachment callbacks', async t => {
|
||||
const f = fixture(t);
|
||||
f.fragment(0); f.fragment(1);
|
||||
f.supersede();
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
const other = fixture(t);
|
||||
other.hls.recoverMediaError = () => { other.video.pause(); };
|
||||
other.recovery.requestRecovery('media error');
|
||||
await tick();
|
||||
const late = other.hls.listeners(events.MEDIA_ATTACHED)[0];
|
||||
other.hls.emit(events.DESTROYING);
|
||||
late();
|
||||
assert.equal(other.video.plays, 0);
|
||||
assert.equal(other.hls.config.startPosition, -1);
|
||||
assert.equal(other.video.listenerCount('playing'), 0);
|
||||
assert.equal(other.hls.listenerCount(events.MEDIA_ATTACHED), 0);
|
||||
});
|
||||
|
||||
test('cooldown and retry budget bound frequent discontinuities and media errors', async t => {
|
||||
const f = fixture(t);
|
||||
assert.equal(f.recovery.requestRecovery('media error'), true);
|
||||
assert.equal(f.recovery.requestRecovery('media error'), true, 'coalesce simultaneous requests');
|
||||
await tick();
|
||||
assert.equal(f.recovery.requestRecovery('media error'), false);
|
||||
for (let i = 0; i < 2; i++) { f.advance(3000); f.recovery.requestRecovery('media error'); await tick(); }
|
||||
f.advance(3000);
|
||||
assert.equal(f.recovery.requestRecovery('media error'), false);
|
||||
assert.equal(f.hls.positions.length, 3);
|
||||
f.advance(30000);
|
||||
assert.equal(f.recovery.requestRecovery('media error'), true);
|
||||
await tick();
|
||||
assert.equal(f.hls.positions.length, 4);
|
||||
});
|
||||
|
||||
test('loading a new manifest cancels old recovery and establishes a fresh discontinuity baseline', async t => {
|
||||
const f = fixture(t);
|
||||
f.fragment(0); f.fragment(1);
|
||||
f.hls.emit(events.MANIFEST_LOADING);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
f.fragment(20);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, []);
|
||||
f.fragment(21);
|
||||
await tick();
|
||||
assert.deepEqual(f.hls.positions, [83.5]);
|
||||
});
|
||||
|
||||
for (const name of ['VideoPlayer', 'WatchPage']) {
|
||||
test(`${name} recovers append errors on the active instance and cancels superseded errors`, async () => {
|
||||
const window = {};
|
||||
class Hls extends EventEmitter {
|
||||
static Events = { ...events, ERROR: 'error', MANIFEST_PARSED: 'manifest',
|
||||
SUBTITLE_TRACKS_UPDATED: 'subtitles', SUBTITLE_TRACK_SWITCH: 'switch',
|
||||
NON_NATIVE_TEXT_TRACKS_FOUND: 'non-native', CUES_PARSED: 'cues' };
|
||||
static ErrorTypes = { MEDIA_ERROR: 'media', NETWORK_ERROR: 'network' };
|
||||
constructor(config) { super(); this.config = config; this.positions = []; }
|
||||
loadSource() {} attachMedia() {}
|
||||
destroy() { this.emit(events.DESTROYING); }
|
||||
recoverMediaError() { this.positions.push(this.config.startPosition); this.emit(events.MEDIA_ATTACHED); }
|
||||
}
|
||||
const file = name === 'VideoPlayer' ? 'components/VideoPlayer.js' : 'pages/WatchPage.js';
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js', file), 'utf8'), {
|
||||
window, Hls, HlsAudioRecovery, CaptionTracks: require('../public/js/components/CaptionTracks'), console
|
||||
});
|
||||
const player = Object.create(window[name].prototype);
|
||||
Object.assign(player, { playbackGeneration: 1, video: { currentTime: 42, paused: false,
|
||||
play: async () => {}, pause() {}, querySelectorAll: () => [] } });
|
||||
player.playHls('/api/transcode/session/playlist.m3u8');
|
||||
const active = player.hls;
|
||||
const error = { type: 'media', fatal: false, details: 'bufferAppendError' };
|
||||
active.emit('error', 'error', error);
|
||||
await tick();
|
||||
assert.deepEqual(active.positions, [42]);
|
||||
const obsoleteHandler = active.listeners('error')[0];
|
||||
active.emit('error', 'error', error);
|
||||
player.playbackGeneration++;
|
||||
player.playHls('/api/transcode/replacement/playlist.m3u8');
|
||||
obsoleteHandler('error', { ...error, fatal: true });
|
||||
await tick();
|
||||
assert.deepEqual(player.hls.positions, []);
|
||||
assert.equal(player.hls.config.startPosition, undefined);
|
||||
player.hls.destroy();
|
||||
});
|
||||
}
|
||||
@ -262,12 +262,12 @@ for (const name of ['VideoPlayer', 'WatchPage']) {
|
||||
test(`${name} ignores callbacks from an obsolete HLS instance`, () => {
|
||||
const window = {};
|
||||
class Hls extends EventEmitter {
|
||||
static Events = { MANIFEST_PARSED: 'manifest', ERROR: 'error', SUBTITLE_TRACKS_UPDATED: 'subtitles', SUBTITLE_TRACK_SWITCH: 'switch', NON_NATIVE_TEXT_TRACKS_FOUND: 'non-native', CUES_PARSED: 'cues' };
|
||||
static Events = { MANIFEST_PARSED: 'manifest', ERROR: 'error', SUBTITLE_TRACKS_UPDATED: 'subtitles', SUBTITLE_TRACK_SWITCH: 'switch', NON_NATIVE_TEXT_TRACKS_FOUND: 'non-native', CUES_PARSED: 'cues', FRAG_CHANGED: 'fragment', DESTROYING: 'destroying', MEDIA_ATTACHED: 'attached', MANIFEST_LOADING: 'manifest-loading' };
|
||||
static ErrorTypes = { NETWORK_ERROR: 'network', MEDIA_ERROR: 'media' };
|
||||
loadSource() {} attachMedia() {} destroy() { this.destroyed = true; }
|
||||
}
|
||||
const file = name === 'VideoPlayer' ? 'components/VideoPlayer.js' : 'pages/WatchPage.js';
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js', file), 'utf8'), { window, Hls, CaptionTracks: require('../public/js/components/CaptionTracks'), console: { log() {}, error() {} } });
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js', file), 'utf8'), { window, Hls, HlsAudioRecovery: require('../public/js/components/HlsAudioRecovery'), CaptionTracks: require('../public/js/components/CaptionTracks'), console: { log() {}, error() {} } });
|
||||
const player = Object.create(window[name].prototype);
|
||||
let plays = 0;
|
||||
player.video = { play: async () => { plays++; } };
|
||||
|
||||
@ -21,7 +21,7 @@ test('probe preserves every audio track index including unknown codecs', () => {
|
||||
|
||||
function playerFixture(name, settings, probe) {
|
||||
const window = { dispatchEvent() {} };
|
||||
const context = { window, CaptionTracks: require('../public/js/components/CaptionTracks'), VodDuration: require('../public/js/components/VodDuration'), console: { log() {}, warn() {}, error() {} },
|
||||
const context = { window, HlsAudioRecovery: require('../public/js/components/HlsAudioRecovery'), CaptionTracks: require('../public/js/components/CaptionTracks'), VodDuration: require('../public/js/components/VodDuration'), console: { log() {}, warn() {}, error() {} },
|
||||
CustomEvent: class {}, document: { getElementById() { return null; } },
|
||||
API: { settings: { get: async () => settings } }, fetch: probe };
|
||||
const file = name === 'VideoPlayer' ? 'components/VideoPlayer.js' : 'pages/WatchPage.js';
|
||||
|
||||
@ -16,7 +16,7 @@ test('VOD duration understands provider fields, clock strings and invalid values
|
||||
|
||||
test('Watch displays provider totals for sessions, browser totals for direct media, and resets between content', () => {
|
||||
const window = {};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js/pages/WatchPage.js'), 'utf8'), { window, VodDuration, CaptionTracks: require('../public/js/components/CaptionTracks') });
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../public/js/pages/WatchPage.js'), 'utf8'), { window, VodDuration, HlsAudioRecovery: require('../public/js/components/HlsAudioRecovery'), CaptionTracks: require('../public/js/components/CaptionTracks') });
|
||||
const watch = Object.create(window.WatchPage.prototype);
|
||||
let hidden;
|
||||
Object.assign(watch, { video: { duration: Infinity }, durationEl: {},
|
||||
|
||||
Reference in New Issue
Block a user