diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/cast-session.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/cast-session.js | 59 |
1 files changed, 44 insertions, 15 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/cast-session.js b/packages/meshbay-hub/src/meshbay_hub/static/cast-session.js index 007722c..9f93919 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/cast-session.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/cast-session.js @@ -11,10 +11,14 @@ import * as platform from './platform.js'; // in the lightbox is shown there. In memory only: a television chosen // yesterday may be off today, and choosing it again is one tap. // -// The receiver is the default media receiver, which shows a film or a -// picture and nothing else; the interface itself stays on this device. +// The receiver is the default media receiver, which shows one film, one +// picture or one track at a time and nothing else; the interface itself stays +// on this device. `owner` says which of the three the television is showing +// now ('video', 'photo', 'music'), so that the music bar stops treating the +// receiver as its own once a film or a photo has taken it. let current = null; +let owner = null; const listeners = new Set(); function notify() { @@ -23,7 +27,15 @@ function notify() { export const castSession = { get device() { return current; }, + get owner() { return owner; }, get supported() { return Boolean(platform.capabilities.lanCast && platform.cast.available); }, + /** Whether a whole file (a photo, a track) can be sent: an older shell cannot. */ + get canSendFiles() { return this.supported && platform.cast.canSendFiles; }, + /** The television now shows what `who` sends it. */ + claim(who) { + owner = who; + notify(); + }, choose(device) { current = device ? { id: device.id, name: device.name } : null; notify(); @@ -32,6 +44,7 @@ export const castSession = { forget() { this.choose(null); }, /** Stop casting: the television goes back to its idle screen. */ async end() { + owner = null; this.choose(null); photoGen++; await platform.cast.chromecastDisconnect().catch(() => {}); @@ -49,6 +62,27 @@ export function useCastDevice() { return device; } +// ── A file on the television ───────────────────────────────────────────────── +// +// A photo or a music track, whole, handed to the relay and loaded on the +// session's television — connected if it is not yet, reloaded if it is. The +// relay, not the page, tells the receiver what the file is; the page only +// names it (title, artist, album, cover) and says where in it to begin. + +export async function loadFile({ owner: who, startAt = 0, ...file }) { + const device = current; + if (!device || !castSession.canSendFiles) throw new Error('No television to cast to'); + castSession.claim(who); + const sent = await platform.cast.file(file); + if (!sent) throw new Error('The cast relay did not take the file'); + const status = await platform.cast.status(); + if (status && status.chromecast && status.chromecast.connected) { + await platform.cast.chromecastReload({ mediaUrl: sent.url, startAt }); + } else { + await platform.cast.chromecastConnect({ deviceId: device.id, mediaUrl: sent.url, startAt }); + } +} + // ── A photo on the television ──────────────────────────────────────────────── // // Scaled to fit 1920×1080 and re-encoded as JPEG before it leaves: a camera's @@ -87,18 +121,10 @@ let photoChain = Promise.resolve(); export function showPhoto(blob) { const gen = ++photoGen; photoChain = photoChain.then(async () => { - const device = current; - if (gen !== photoGen || !device || !platform.cast.canShowImages) return; + if (gen !== photoGen || !current || !castSession.canSendFiles) return; const bytes = await scaledForTv(blob); if (gen !== photoGen) return; - const shown = await platform.cast.image({ bytes, type: 'image/jpeg' }); - if (!shown || gen !== photoGen) return; - const status = await platform.cast.status(); - if (status && status.chromecast && status.chromecast.connected) { - await platform.cast.chromecastReload({ mediaUrl: shown.url }); - } else { - await platform.cast.chromecastConnect({ deviceId: device.id, mediaUrl: shown.url }); - } + await loadFile({ owner: 'photo', bytes, type: 'image/jpeg' }); }).catch((err) => console.warn('[cast] photo not shown:', err)); return photoChain; } @@ -111,9 +137,12 @@ export function showPhoto(blob) { // toolbar. `cls` and `iconCls` are the button's and its icon's own classes, // so it matches its neighbours. // `onChosen` runs after a television is picked, for the view that has -// something to show on it straight away. +// something to show on it straight away. `up` opens the menu above the +// button, for one at the bottom of the window. -export function CastButton({ variant = 'page', cls = 'tb-btn tb-btn-icon', iconCls = '', onChosen }) { +export function CastButton({ + variant = 'page', cls = 'tb-btn tb-btn-icon', iconCls = '', onChosen, up = false, +}) { const device = useCastDevice(); const [open, setOpen] = useState(false); const [devices, setDevices] = useState([]); @@ -143,7 +172,7 @@ export function CastButton({ variant = 'page', cls = 'tb-btn tb-btn-icon', iconC if (!castSession.supported) return null; const overlay = variant === 'overlay'; - const menuCls = overlay ? 'cast-picker' : 'ctx-menu cast-menu'; + const menuCls = `${overlay ? 'cast-picker' : 'ctx-menu cast-menu'}${up ? ' cast-menu-up' : ''}`; const itemCls = overlay ? 'cast-picker-item' : 'ctx-menu-item'; const title = device ? `${t('cast.casting_to')} ${device.name}` : t('cast.start'); |