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:
@ -251,10 +251,23 @@ class WatchPage {
|
||||
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'
|
||||
@ -264,7 +277,8 @@ class WatchPage {
|
||||
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') {
|
||||
@ -275,6 +289,20 @@ class WatchPage {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point - play content
|
||||
* @param {Object} content - Movie or episode info
|
||||
|
||||
Reference in New Issue
Block a user