feat(cast): add server-managed Google Cast playback (#34)

Add server-side Google Cast discovery and sessions, signed cast media URLs, browser device picker integration, and deployment documentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-06 22:40:17 +02:00
parent e6e481bf0e
commit 5ee07570e0
15 changed files with 1394 additions and 9 deletions

View File

@ -457,10 +457,23 @@ class VideoPlayer {
return;
}
this.castService = new CastService(this.video);
const serverAvailable = typeof ServerCastDevicePicker !== 'undefined'
&& typeof API !== 'undefined'
&& Boolean(API.cast);
this.serverCastPicker = serverAvailable ? new ServerCastDevicePicker({
document,
api: API.cast,
nativeCastService: this.castService,
mediaUrlProvider: () => this.getServerCastMediaPath(),
onState: state => {
if (status && state.status === 'connected') status.textContent = 'Playing on the selected device.';
else if (status && state.status === 'error') status.textContent = state.error || 'Could not connect to a playback device.';
}
}) : null;
this.castService.subscribe(state => {
const unsupported = state.type === 'unsupported';
button.hidden = unsupported;
button.disabled = !state.available || state.status === 'connecting';
button.hidden = unsupported && !serverAvailable;
button.disabled = serverAvailable ? false : (!state.available || state.status === 'connecting');
button.setAttribute('aria-pressed', String(state.status === 'connected'));
if (status) status.textContent = unsupported ? 'Not supported on this browser'
: state.status === 'connected' ? 'Playing on a device'
@ -470,7 +483,8 @@ class VideoPlayer {
button.addEventListener('click', async event => {
event.stopPropagation();
try {
await this.castService.prompt();
if (this.serverCastPicker) await this.serverCastPicker.open();
else await this.castService.prompt();
} catch (error) {
if (status) status.textContent = 'Could not connect to a playback device.';
if (error?.name !== 'NotAllowedError' && error?.name !== 'AbortError') {
@ -481,6 +495,20 @@ class VideoPlayer {
void this.castService.watchAvailability();
}
getServerCastMediaPath() {
const value = String(this.currentUrl || '');
if (!value) return null;
let path = value;
try {
const url = new URL(value, window.location.origin);
if (url.origin !== window.location.origin || url.search) return null;
path = url.pathname;
} catch { return null; }
const castable = /^\/api\/transcode\/[A-Za-z0-9_-]{1,128}\/stream\.m3u8$/.test(path)
|| /^\/api\/proxy\/xtream\/\d+\/stream\/[A-Za-z0-9._-]{1,128}\/(?:live|movie|series)$/.test(path);
return castable ? path : null;
}
/**
* Add the live quality control to the existing player overflow menu.
*/