aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
diff options
context:
space:
mode:
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.js101
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 };