Add a browser-native casting service with Live and Watch player controls for Remote Playback and AirPlay-compatible devices. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.3 KiB
JavaScript
83 lines
3.3 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
const castServicePath = path.join(root, 'public', 'js', 'services', 'castService.js');
|
|
|
|
function loadCastService() {
|
|
assert.ok(fs.existsSync(castServicePath), 'CastService module should exist at public/js/services/castService.js');
|
|
return require(castServicePath);
|
|
}
|
|
|
|
test('CastService reports Remote Playback availability and prompts through video.remote', async () => {
|
|
const CastService = loadCastService();
|
|
let availabilityCallback;
|
|
let promptCalls = 0;
|
|
const video = {
|
|
remote: {
|
|
watchAvailability: async callback => {
|
|
availabilityCallback = callback;
|
|
callback(true);
|
|
return 7;
|
|
},
|
|
prompt: async () => { promptCalls += 1; }
|
|
}
|
|
};
|
|
|
|
const service = new CastService(video);
|
|
await service.watchAvailability();
|
|
assert.equal(service.state.available, true);
|
|
assert.equal(service.state.type, 'remote-playback');
|
|
availabilityCallback(false);
|
|
assert.equal(service.state.available, false);
|
|
|
|
await service.prompt();
|
|
assert.equal(promptCalls, 1);
|
|
});
|
|
|
|
test('CastService uses AirPlay picker when webkitShowPlaybackTargetPicker is available', async () => {
|
|
const CastService = loadCastService();
|
|
let airplayCalls = 0;
|
|
const video = {
|
|
webkitShowPlaybackTargetPicker: () => { airplayCalls += 1; }
|
|
};
|
|
|
|
const service = new CastService(video);
|
|
await service.watchAvailability();
|
|
assert.equal(service.state.available, true);
|
|
assert.equal(service.state.type, 'airplay');
|
|
|
|
await service.prompt();
|
|
assert.equal(airplayCalls, 1);
|
|
});
|
|
|
|
test('CastService stays unsupported without native casting APIs', async () => {
|
|
const CastService = loadCastService();
|
|
const service = new CastService({});
|
|
await assert.doesNotReject(() => service.watchAvailability());
|
|
await assert.doesNotReject(() => service.prompt());
|
|
assert.equal(service.state.available, false);
|
|
assert.equal(service.state.type, 'unsupported');
|
|
});
|
|
|
|
test('Live and Watch players expose cast controls and load CastService', () => {
|
|
const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8');
|
|
assert.match(html, /id="player-cast"/, 'Live player should expose a cast button');
|
|
assert.match(html, /id="watch-cast"/, 'Watch player should expose a cast button');
|
|
assert.match(html, /\/js\/services\/castService\.js/, 'index.html should load CastService before player pages');
|
|
});
|
|
|
|
test('PWA/service worker cache rules do not precache live stream endpoints', () => {
|
|
const publicDir = path.join(root, 'public');
|
|
const candidates = fs.readdirSync(publicDir)
|
|
.filter(name => /(?:service-worker|sw|workbox).*\.js$/i.test(name))
|
|
.map(name => path.join(publicDir, name));
|
|
for (const file of candidates) {
|
|
const text = fs.readFileSync(file, 'utf8');
|
|
assert.doesNotMatch(text, /\/api\/(?:proxy|transcode|live)/, `${path.basename(file)} should not precache stream API routes`);
|
|
assert.doesNotMatch(text, /\.m3u8|\.ts\b/, `${path.basename(file)} should not precache HLS playlists or segments`);
|
|
}
|
|
});
|