diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 02:29:27 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 02:29:27 +0200 |
| commit | ee9798fe3b88face2fa34f14874770f509c788a5 (patch) | |
| tree | f63e9506882863a885a907796eb977ec69137718 /packages/meshbay-client/src/cast-chromecast.js | |
| parent | 243bcf017b4957237b3ddf556bd0a37296eb8798 (diff) | |
| download | meshbay-ee9798fe3b88face2fa34f14874770f509c788a5.tar.gz | |
feat(cast): music on the TV, the music bar as its remote
A cast button in Music's toolbar and in the music bar. With a television
chosen, each decrypted track goes to the relay with its cover — found as the
album card finds it — and plays there as music with its title, artist and
album; the bar's play, pause, seek, previous and next drive the receiver, its
clock is the receiver's, and the end of a track there moves the queue on.
A film or a photo taking the television pauses the bar; stopping the cast
carries the track on locally.
Photos and tracks now share one path: a whole file sent to the relay in
pieces (binary frames on Android, written to disk there), served at /file
with byte ranges and its cover at /cover, and loaded as what the relay says
it is. cast:image is gone; cast:chromecast:seek is new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src/cast-chromecast.js')
| -rw-r--r-- | packages/meshbay-client/src/cast-chromecast.js | 48 |
1 files changed, 37 insertions, 11 deletions
diff --git a/packages/meshbay-client/src/cast-chromecast.js b/packages/meshbay-client/src/cast-chromecast.js index 4c8ddf0..8c6c9c6 100644 --- a/packages/meshbay-client/src/cast-chromecast.js +++ b/packages/meshbay-client/src/cast-chromecast.js @@ -42,17 +42,30 @@ const TEXT_TRACK_STYLE = Object.freeze({ /** * What to hand `player.load()` for a stream, with or without subtitles, or - * for a photo. + * for a whole file the relay serves. * * `streamType: 'LIVE'` because the relay has no beginning to seek back to — * the film's own timeline lives on this side, and a seek restarts the relay. - * A photo is not a stream at all. + * A file is the relay's to describe (`file`: its type and what to show with + * it), never the page's: a photo is not a stream at all, and a music track is + * an ordinary buffered file the receiver seeks in itself. */ -function mediaFor(mediaUrl, subtitle, contentType) { - if (contentType && contentType.startsWith('image/')) { - return { contentId: mediaUrl, contentType, streamType: 'NONE', +function mediaFor(mediaUrl, subtitle, file) { + if (file && file.contentType.startsWith('image/')) { + return { contentId: mediaUrl, contentType: file.contentType, streamType: 'NONE', metadata: { metadataType: 4 } }; // PhotoMediaMetadata } + if (file) { + const meta = file.meta || {}; + return { + contentId: mediaUrl, contentType: file.contentType, streamType: 'BUFFERED', + metadata: { + metadataType: 3, // MusicTrackMediaMetadata + title: meta.title || '', artist: meta.artist || '', albumName: meta.album || '', + images: meta.cover ? [{ url: meta.cover }] : [], + }, + }; + } const media = { contentId: mediaUrl, contentType: 'video/mp4', @@ -73,11 +86,14 @@ function mediaFor(mediaUrl, subtitle, contentType) { return media; } -function loadOptionsFor(subtitle) { - return { +function loadOptionsFor(subtitle, startAt) { + const options = { autoplay: true, activeTrackIds: subtitle && subtitle.url ? [TEXT_TRACK_ID] : [], }; + // Where in a track to begin: a cast started mid-song carries on from there. + if (startAt > 0) options.currentTime = startAt; + return options; } class CastChromecast { @@ -145,7 +161,7 @@ class CastChromecast { } } - async connect(deviceId, mediaUrl, subtitle, contentType) { + async connect(deviceId, mediaUrl, subtitle, file, startAt) { const device = this._devices.get(deviceId); if (!device) throw new Error(`Unknown device: ${deviceId}`); @@ -185,7 +201,7 @@ class CastChromecast { debug(`[cast-chromecast] loading with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`); const status = await new Promise((resolve, reject) => { - player.load(mediaFor(mediaUrl, subtitle, contentType), loadOptionsFor(subtitle), + player.load(mediaFor(mediaUrl, subtitle, file), loadOptionsFor(subtitle, startAt), (err, s) => { if (err) return reject(err); resolve(s); @@ -197,12 +213,12 @@ class CastChromecast { return { deviceName: device.name, playerState: status.playerState }; } - async reload(mediaUrl, subtitle, contentType) { + async reload(mediaUrl, subtitle, file, startAt) { if (!this._player) throw new Error('Not connected'); debug(`[cast-chromecast] reloading stream on "${this._connectedDevice?.name}"` + ` with ${subtitle?.url ? 'subtitles' : 'no subtitles'}`); const status = await new Promise((resolve, reject) => { - this._player.load(mediaFor(mediaUrl, subtitle, contentType), loadOptionsFor(subtitle), + this._player.load(mediaFor(mediaUrl, subtitle, file), loadOptionsFor(subtitle, startAt), (err, s) => { if (err) return reject(err); resolve(s); @@ -256,6 +272,16 @@ class CastChromecast { return { playerState: status && status.playerState }; } + /** Where in a track the receiver should be; a film seeks by restarting the relay instead. */ + async seek(seconds) { + if (!this._player) throw new Error('Not connected'); + const status = await new Promise((resolve, reject) => { + this._player.seek(Math.max(0, Number(seconds) || 0), (err, s) => (err ? reject(err) : resolve(s))); + }); + this._noteStatus(status); + return { playerState: status && status.playerState }; + } + async play() { if (!this._player) throw new Error('Not connected'); const status = await new Promise((resolve, reject) => { |