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:
blomios
2026-10-06 22:07:49 +02:00
parent d1934163ca
commit fd314f0a59
5 changed files with 275 additions and 2 deletions

View File

@ -35,6 +35,7 @@ class WatchPage {
this.timeTotal = document.getElementById('watch-time-total');
this.scrollHint = document.getElementById('watch-scroll-hint');
this.loadingSpinner = document.getElementById('watch-loading');
this.castService = null;
// Next episode
this.nextEpisodePanel = document.getElementById('watch-next-episode');
@ -153,6 +154,7 @@ class WatchPage {
// Picture-in-Picture
const pipBtn = document.getElementById('watch-pip');
pipBtn?.addEventListener('click', () => this.togglePictureInPicture());
this.initCastControl();
// Overflow Menu
const overflowBtn = document.getElementById('watch-overflow');
@ -240,6 +242,39 @@ class WatchPage {
});
}
initCastControl() {
const button = document.getElementById('watch-cast');
const status = document.getElementById('watch-cast-status');
if (!button || !this.video || typeof CastService === 'undefined') {
if (button) button.hidden = true;
if (status) status.textContent = 'Not supported on this browser';
return;
}
this.castService = new CastService(this.video);
this.castService.subscribe(state => {
const unsupported = state.type === 'unsupported';
button.hidden = unsupported;
button.disabled = !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'
: state.status === 'connecting' ? 'Opening device picker…'
: state.available ? 'Playback device available' : 'No playback devices available';
});
button.addEventListener('click', async event => {
event.stopPropagation();
try {
await this.castService.prompt();
} catch (error) {
if (status) status.textContent = 'Could not connect to a playback device.';
if (error?.name !== 'NotAllowedError' && error?.name !== 'AbortError') {
console.warn('[Watch] Device playback picker failed:', error?.message || error);
}
}
});
void this.castService.watchAvailability();
}
/**
* Main entry point - play content
* @param {Object} content - Movie or episode info