diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/video-player.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/video-player.js | 101 |
1 files changed, 70 insertions, 31 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js index a9d41b6..2c424fb 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js @@ -280,6 +280,64 @@ function formatClock(seconds) { } /** + * The player as a remote, while a television plays the film. + * + * Only what it is told: the receiver's position on the film's timeline (null + * until the receiver has answered), its state, and whether a seek is on its + * way. The scrubber reports a drag as it moves and a seek once let go. + */ +function CastRemote({ device, position, duration, playerState, moving, + onDrag, onSeek, onToggle, onStop }) { + const known = position !== null; + const paused = playerState === 'PAUSED'; + const busy = !known || moving || playerState === 'BUFFERING' || playerState === 'LOADING'; + const pct = known && duration > 0 ? Math.min(100, (position / duration) * 100) : 0; + return html` + <div class="video-remote"> + <div class="video-remote-head"> + <span class="video-remote-badge"><${Icon} name="cast" /></span> + <span class="video-remote-label">${t('cast.casting_to')}</span> + <span class="video-remote-device">${device}</span> + <span class="video-remote-state"> + ${busy ? html`<span class="spinner"></span>${' '}${t('cast.waiting')}` : ''} + </span> + </div> + <div class="video-remote-track"> + <input class="video-remote-seek" type="range" min="0" step="1" + style=${`--pct:${pct}%`} + max=${Math.max(1, Math.floor(duration))} + value=${Math.floor(position || 0)} + disabled=${!known || !(duration > 0)} + aria-label=${t('cast.seek')} + onInput=${(e) => onDrag(+e.target.value)} + onChange=${(e) => onSeek(+e.target.value)} /> + <div class="video-remote-times"> + <span>${known ? formatClock(position) : '–:––'}</span> + <span>${duration > 0 ? formatClock(duration) : '–:––'}</span> + </div> + </div> + <div class="video-remote-buttons"> + <button class="video-remote-skip" disabled=${!known} + onClick=${() => onSeek(position - 30)} + title=${t('cast.back30')} aria-label=${t('cast.back30')}> + <${Icon} name="skipback" /><span class="video-remote-skip-n">30</span></button> + <button class="video-remote-main ${paused ? 'is-paused' : ''}" disabled=${!playerState} + onClick=${onToggle} + title=${paused ? t('cast.play') : t('cast.pause')} + aria-label=${paused ? t('cast.play') : t('cast.pause')}> + <${Icon} name=${paused ? 'play' : 'pause'} /></button> + <button class="video-remote-skip" disabled=${!known} + onClick=${() => onSeek(position + 30)} + title=${t('cast.fwd30')} aria-label=${t('cast.fwd30')}> + <${Icon} name="skipfwd" /><span class="video-remote-skip-n">30</span></button> + </div> + <button class="video-remote-stop" onClick=${onStop}> + <${Icon} name="close" /> ${t('cast.stop')}</button> + </div> + `; +} + +/** * Where *this account on this device* last left off in a given file. * * localStorage rather than the node: it needs no protocol, no storage anyone @@ -1907,7 +1965,7 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { </div> ${(phase === 'streaming' || phase === 'loading') && html` - <div class="video-container"> + <div class="video-container ${remoteMode ? 'video-container-remote' : ''}"> <video ref=${videoRef} controls autoplay> ${subtitleUrl && html` <track key=${subtitleUrl} kind="subtitles" src=${subtitleUrl} @@ -1919,35 +1977,16 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { `} </video> ${remoteMode && html` - <div class="video-remote"> - <div class="video-remote-device"> - <${Icon} name="cast" /> ${t('cast.playing_on', { device: castDeviceName })} - </div> - <div class="video-remote-time"> - ${remoteShown === null || remoteTarget !== null - || (remote && (remote.playerState === 'BUFFERING' || remote.playerState === 'LOADING')) - ? html`<span class="spinner"></span>${' '}` : ''} - ${remoteShown === null ? t('cast.waiting') - : `${formatClock(remoteShown)} / ${formatClock(durationRef.current)}`} - </div> - <input class="video-remote-seek" type="range" min="0" step="1" - max=${Math.max(1, Math.floor(durationRef.current))} - value=${Math.floor(remoteShown || 0)} - disabled=${remoteShown === null || !(durationRef.current > 0)} - onInput=${(e) => setRemoteDrag(+e.target.value)} - onChange=${(e) => { setRemoteDrag(null); remoteSeek(+e.target.value); }} /> - <div class="video-remote-buttons"> - <button class="video-remote-btn" disabled=${remoteShown === null} - onClick=${() => remoteSeek(remoteShown - 30)} title="${t('cast.back30')}">−30 s</button> - <button class="video-remote-btn video-remote-main" disabled=${!remote} - onClick=${remoteToggle} - title="${remote && remote.playerState === 'PAUSED' ? t('cast.play') : t('cast.pause')}"> - <${Icon} name=${remote && remote.playerState === 'PAUSED' ? 'play' : 'pause'} /></button> - <button class="video-remote-btn" disabled=${remoteShown === null} - onClick=${() => remoteSeek(remoteShown + 30)} title="${t('cast.fwd30')}">+30 s</button> - </div> - <button class="video-remote-stop" onClick=${stopCast}>${t('cast.stop')}</button> - </div> + <${CastRemote} + device=${castDeviceName} + position=${remoteShown} + duration=${durationRef.current} + playerState=${remote ? remote.playerState : null} + moving=${remoteTarget !== null} + onDrag=${setRemoteDrag} + onSeek=${(to) => { setRemoteDrag(null); remoteSeek(to); }} + onToggle=${remoteToggle} + onStop=${stopCast} /> `} ${phase === 'loading' && html` <div class="video-loading"> @@ -1987,4 +2026,4 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) { * live. */ -export { VideoPlayer }; +export { VideoPlayer, CastRemote }; |