aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/icon.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css190
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-player.js101
-rw-r--r--packages/meshbay-hub/tests/test_cast_discovery.py3
14 files changed, 254 insertions, 74 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/icon.js b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
index c80258c..4fa4357 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/icon.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/icon.js
@@ -73,6 +73,10 @@ const ICON_PATHS = {
'chevron-left': ['M14.5 6l-6 6 6 6'],
'chevron-right': ['M9.5 6l6 6-6 6'],
close: ['M6 6l12 12M18 6L6 18'],
+ // A circle nearly closed, its arrow at the top pointing the way it turns;
+ // the remote writes the seconds inside.
+ skipback: ['M12 4.5a8 8 0 1 1-6.93 4', 'M14.5 2l-2.5 2.5 2.5 2.5'],
+ skipfwd: ['M12 4.5a8 8 0 1 0 6.93 4', 'M9.5 2l2.5 2.5-2.5 2.5'],
chat: ['M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'],
folder: ['M3.5 6.6a1 1 0 0 1 1-1h4.2l2 2.4h7.8a1 1 0 0 1 1 1v9.4a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1z'],
'bell-off': ['M18 9a6 6 0 0 0-12 0c0 6-2.5 7.5-2.5 7.5h17S18 15 18 9',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 17f41fe..bdc4dd5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -390,7 +390,8 @@ export default {
'cast.copied': 'URL kopiert',
'cast.scanning': 'Suche nach Geräten…',
'cast.no_devices': 'Keine Geräte gefunden',
- 'cast.playing_on': 'Wiedergabe auf {device}',
+ 'cast.casting_to': 'Wiedergabe auf',
+ 'cast.seek': 'Position im Film',
'cast.waiting': 'Warten auf den Fernseher…',
'cast.back30': '30 Sekunden zurück',
'cast.fwd30': '30 Sekunden vor',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index b9a4b29..658f388 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -388,7 +388,8 @@ export default {
'cast.copied': 'URL copied',
'cast.scanning': 'Scanning for devices…',
'cast.no_devices': 'No devices found',
- 'cast.playing_on': 'Playing on {device}',
+ 'cast.casting_to': 'Casting to',
+ 'cast.seek': 'Position in the film',
'cast.waiting': 'Waiting for the television…',
'cast.back30': 'Back 30 seconds',
'cast.fwd30': 'Forward 30 seconds',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 2b22c00..9e73017 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -388,7 +388,8 @@ export default {
'cast.copied': 'URL copiada',
'cast.scanning': 'Buscando dispositivos…',
'cast.no_devices': 'No se encontraron dispositivos',
- 'cast.playing_on': 'Reproduciendo en {device}',
+ 'cast.casting_to': 'Transmitiendo a',
+ 'cast.seek': 'Posición en la película',
'cast.waiting': 'Esperando al televisor…',
'cast.back30': 'Retroceder 30 segundos',
'cast.fwd30': 'Avanzar 30 segundos',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index fb6eb80..5d18d41 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -389,7 +389,8 @@ export default {
'cast.copied': 'URL copiée',
'cast.scanning': "Recherche d'appareils…",
'cast.no_devices': 'Aucun appareil trouvé',
- 'cast.playing_on': 'Lecture sur {device}',
+ 'cast.casting_to': 'Diffusion sur',
+ 'cast.seek': 'Position dans le film',
'cast.waiting': 'En attente de la télévision…',
'cast.back30': 'Reculer de 30 secondes',
'cast.fwd30': 'Avancer de 30 secondes',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index d9de9bb..c4d2acc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -389,7 +389,8 @@ export default {
'cast.copied': 'URL copiato',
'cast.scanning': 'Ricerca dispositivi…',
'cast.no_devices': 'Nessun dispositivo trovato',
- 'cast.playing_on': 'In riproduzione su {device}',
+ 'cast.casting_to': 'Trasmissione su',
+ 'cast.seek': 'Posizione nel film',
'cast.waiting': 'In attesa del televisore…',
'cast.back30': 'Indietro di 30 secondi',
'cast.fwd30': 'Avanti di 30 secondi',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 542840f..fe52bfa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -386,7 +386,8 @@ export default {
'cast.copied': 'URLをコピーしました',
'cast.scanning': 'デバイスを検索中…',
'cast.no_devices': 'デバイスが見つかりません',
- 'cast.playing_on': '{device} で再生中',
+ 'cast.casting_to': 'キャスト先',
+ 'cast.seek': '再生位置',
'cast.waiting': 'テレビを待っています…',
'cast.back30': '30秒戻る',
'cast.fwd30': '30秒進む',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index a6b9487..90b71f0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -390,7 +390,8 @@ export default {
'cast.copied': 'URL gekopieerd',
'cast.scanning': 'Apparaten zoeken…',
'cast.no_devices': 'Geen apparaten gevonden',
- 'cast.playing_on': 'Speelt af op {device}',
+ 'cast.casting_to': 'Casten naar',
+ 'cast.seek': 'Positie in de film',
'cast.waiting': 'Wachten op de televisie…',
'cast.back30': '30 seconden terug',
'cast.fwd30': '30 seconden vooruit',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 131f8a4..8c31cf3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -401,7 +401,8 @@ export default {
'cast.copied': 'URL skopiowany',
'cast.scanning': 'Wyszukiwanie urządzeń…',
'cast.no_devices': 'Nie znaleziono urządzeń',
- 'cast.playing_on': 'Odtwarzanie na {device}',
+ 'cast.casting_to': 'Przesyłanie do',
+ 'cast.seek': 'Pozycja w filmie',
'cast.waiting': 'Czekam na telewizor…',
'cast.back30': '30 sekund wstecz',
'cast.fwd30': '30 sekund do przodu',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index c6a0888..2f951ee 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -390,7 +390,8 @@ export default {
'cast.copied': 'URL copiada',
'cast.scanning': 'Procurando dispositivos…',
'cast.no_devices': 'Nenhum dispositivo encontrado',
- 'cast.playing_on': 'Reproduzindo em {device}',
+ 'cast.casting_to': 'Transmitindo para',
+ 'cast.seek': 'Posição no filme',
'cast.waiting': 'Aguardando a TV…',
'cast.back30': 'Voltar 30 segundos',
'cast.fwd30': 'Avançar 30 segundos',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index b568db1..ea02545 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -383,7 +383,8 @@ export default {
'cast.copied': '链接已复制',
'cast.scanning': '正在搜索设备…',
'cast.no_devices': '未找到设备',
- 'cast.playing_on': '正在 {device} 上播放',
+ 'cast.casting_to': '投放到',
+ 'cast.seek': '播放位置',
'cast.waiting': '正在等待电视…',
'cast.back30': '后退 30 秒',
'cast.fwd30': '前进 30 秒',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index dc121ab..e1e152e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -2086,8 +2086,12 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
/* The player as a remote while a television plays the film. Opaque, over
the local picture, which keeps running underneath only to pace the
- download. */
+ download; the container is given room for it on a portrait phone, where
+ the picture alone would be a strip. */
+.video-container-remote { min-height: min(72vh, 560px); }
+
.video-remote {
+ --remote-accent: #23b1f0;
position: absolute;
inset: 0;
z-index: 2;
@@ -2095,67 +2099,191 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
flex-direction: column;
align-items: center;
justify-content: center;
- gap: 18px;
- padding: 16px;
- background: #0b1222;
- border-radius: 4px;
+ gap: 32px;
+ padding: 24px 20px;
+ border-radius: 12px;
+ background:
+ radial-gradient(120% 70% at 50% 0%, rgba(35, 177, 240, 0.16) 0%, rgba(35, 177, 240, 0) 60%),
+ #0a1224;
color: #e2e8f0;
- min-height: 260px;
+ overflow: hidden;
}
-.video-remote-device {
+.video-remote-head {
display: flex;
- gap: 8px;
+ flex-direction: column;
align-items: center;
- font-size: 1.05rem;
+ gap: 6px;
text-align: center;
+ max-width: 100%;
+}
+
+.video-remote-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 64px;
+ height: 64px;
+ margin-bottom: 8px;
+ border-radius: 50%;
+ background: rgba(35, 177, 240, 0.14);
+ box-shadow: 0 0 0 1px rgba(35, 177, 240, 0.35), 0 0 32px rgba(35, 177, 240, 0.25);
+ color: var(--remote-accent);
+ font-size: 30px;
+}
+
+.video-remote-label {
+ font-size: 0.75rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: #94a3b8;
+}
+
+.video-remote-device {
+ font-size: 1.35rem;
+ font-weight: 600;
+ color: #f8fafc;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.video-remote-time {
+.video-remote-state {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 1.2em;
+ font-size: 0.85rem;
+ color: #94a3b8;
+}
+
+.video-remote-track { width: min(100%, 560px); }
+
+.video-remote-times {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 8px;
+ font-size: 0.85rem;
font-variant-numeric: tabular-nums;
color: #94a3b8;
}
+/* The track is drawn by hand so the part already played is filled: `--pct`
+ is set inline from the position. */
.video-remote-seek {
- width: min(100%, 640px);
+ --pct: 0%;
+ -webkit-appearance: none;
+ appearance: none;
+ display: block;
+ width: 100%;
+ height: 28px;
+ margin: 0;
+ background: transparent;
+ cursor: pointer;
+}
+.video-remote-seek:disabled { cursor: default; opacity: 0.5; }
+.video-remote-seek::-webkit-slider-runnable-track {
+ height: 6px;
+ border-radius: 3px;
+ background: linear-gradient(to right, var(--remote-accent) var(--pct), rgba(148, 163, 184, 0.25) var(--pct));
+}
+.video-remote-seek::-moz-range-track {
+ height: 6px;
+ border-radius: 3px;
+ background: linear-gradient(to right, var(--remote-accent) var(--pct), rgba(148, 163, 184, 0.25) var(--pct));
+}
+.video-remote-seek::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 18px;
+ height: 18px;
+ margin-top: -6px;
+ border: 0;
+ border-radius: 50%;
+ background: #ffffff;
+ box-shadow: 0 0 0 4px rgba(35, 177, 240, 0.35), 0 2px 6px rgba(0, 0, 0, 0.4);
+}
+.video-remote-seek::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ border: 0;
+ border-radius: 50%;
+ background: #ffffff;
+ box-shadow: 0 0 0 4px rgba(35, 177, 240, 0.35), 0 2px 6px rgba(0, 0, 0, 0.4);
}
.video-remote-buttons {
display: flex;
- gap: 16px;
align-items: center;
+ gap: 36px;
}
-.video-remote-btn {
- min-width: 64px;
- min-height: 44px;
- padding: 8px 14px;
- border: 1px solid #334155;
- border-radius: 999px;
- background: #1e293b;
- color: inherit;
- font: inherit;
+.video-remote-skip,
+.video-remote-main {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ border-radius: 50%;
cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ transition: transform 0.1s ease, background-color 0.15s ease;
}
+.video-remote-skip:active:not(:disabled),
+.video-remote-main:active:not(:disabled) { transform: scale(0.92); }
+.video-remote-skip:disabled,
+.video-remote-main:disabled { opacity: 0.35; cursor: default; }
-.video-remote-btn:disabled { opacity: 0.4; cursor: default; }
+.video-remote-skip {
+ width: 56px;
+ height: 56px;
+ background: transparent;
+ color: #e2e8f0;
+ font-size: 44px;
+}
+.video-remote-skip:hover:not(:disabled) { background: rgba(148, 163, 184, 0.12); }
+.video-remote-skip .icon { stroke-width: 1.4; }
+.video-remote-skip-n {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ transform: translateY(-38%);
+ font-size: 12px;
+ font-weight: 700;
+ text-align: center;
+ pointer-events: none;
+}
.video-remote-main {
- min-width: 56px;
- min-height: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
+ width: 76px;
+ height: 76px;
+ background: var(--remote-accent);
+ color: #04121f;
+ font-size: 34px;
+ box-shadow: 0 8px 24px rgba(35, 177, 240, 0.35);
}
+.video-remote-main:hover:not(:disabled) { background: #4cc3f5; }
+.video-remote-main .icon { stroke-width: 2.6; }
+/* The triangle is a closed path: filled, it reads as play at a glance. */
+.video-remote-main.is-paused .icon path { fill: currentColor; }
+.video-remote-main.is-paused .icon { transform: translateX(2px); }
.video-remote-stop {
- border: 0;
- background: none;
- color: #fca5a5;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 20px;
+ border: 1px solid rgba(148, 163, 184, 0.35);
+ border-radius: 999px;
+ background: transparent;
+ color: #cbd5e1;
font: inherit;
+ font-size: 0.9rem;
cursor: pointer;
- text-decoration: underline;
}
+.video-remote-stop:hover { border-color: #fca5a5; color: #fecaca; }
/* Where the film was picked up again. Sits over the picture rather than
pushing it down, and clears itself once the viewer is watching. */
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 };
diff --git a/packages/meshbay-hub/tests/test_cast_discovery.py b/packages/meshbay-hub/tests/test_cast_discovery.py
index 8bbe3c7..8d156ff 100644
--- a/packages/meshbay-hub/tests/test_cast_discovery.py
+++ b/packages/meshbay-hub/tests/test_cast_discovery.py
@@ -145,9 +145,8 @@ def test_the_player_is_a_remote_while_casting():
def test_every_locale_names_the_remote_controls():
from spa_source import STATIC
- keys = ["cast.playing_on", "cast.waiting", "cast.back30", "cast.fwd30", "cast.play", "cast.pause"]
+ keys = ["cast.casting_to", "cast.seek", "cast.waiting", "cast.back30", "cast.fwd30", "cast.play", "cast.pause"]
for f in sorted((STATIC / "locales").glob("*.js")):
text = f.read_text(encoding="utf-8")
for k in keys:
assert f"'{k}':" in text, f"{f.name} lacks {k}"
- assert "{device}" in (STATIC / "locales" / "ja.js").read_text(encoding="utf-8")