feat(player): add PWA-compatible stream casting (#33)
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>
This commit is contained in:
82
test/t33CastFeature.test.js
Normal file
82
test/t33CastFeature.test.js
Normal file
@ -0,0 +1,82 @@
|
||||
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`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user