From ee9798fe3b88face2fa34f14874770f509c788a5 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 5 Oct 2026 02:29:27 +0200 Subject: feat(cast): music on the TV, the music bar as its remote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 4 +- docs/MESHBAY_DESIGN.md | 54 ++- docs/USERGUIDE.md | 27 +- .../app/src/main/assets/bridge/meshbay-bridge.js | 19 +- .../kotlin/org/meshbay/client/bridge/Channels.kt | 3 +- .../kotlin/org/meshbay/client/cast/CastChannels.kt | 50 ++- .../kotlin/org/meshbay/client/cast/CastControl.kt | 38 ++- .../kotlin/org/meshbay/client/cast/CastRelay.kt | 169 ++++++++-- .../kotlin/org/meshbay/client/save/SaveNames.kt | 1 + .../kotlin/org/meshbay/client/CastRelayTest.kt | 68 +++- packages/meshbay-client/src/cast-chromecast.js | 48 ++- packages/meshbay-client/src/cast-relay.js | 154 +++++++-- packages/meshbay-client/src/main.js | 32 +- packages/meshbay-client/src/preload.js | 14 +- .../src/meshbay_hub/static/cast-session.js | 59 +++- .../src/meshbay_hub/static/music-app.js | 3 + .../src/meshbay_hub/static/music-player.js | 212 +++++++++++- .../meshbay-hub/src/meshbay_hub/static/platform.js | 25 +- .../meshbay-hub/src/meshbay_hub/static/style.css | 2 + .../src/meshbay_hub/static/video-player.js | 1 + .../tests/harness/cast_session_probe.py | 166 +++++++-- packages/meshbay-hub/tests/test_android_cast.py | 8 +- packages/meshbay-hub/tests/test_cast_files.py | 372 +++++++++++++++++++++ packages/meshbay-hub/tests/test_cast_photos.py | 240 ------------- 24 files changed, 1332 insertions(+), 437 deletions(-) create mode 100644 packages/meshbay-hub/tests/test_cast_files.py delete mode 100644 packages/meshbay-hub/tests/test_cast_photos.py diff --git a/CLAUDE.md b/CLAUDE.md index 32ffc25..d5b66c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1033,7 +1033,7 @@ here are kept only where they are a rule about *editing* the code. | Where the hub is | `platform.js` — `hubBase()` | **the only file allowed to decide this** (§8.3) | | Downloads, decrypt pipeline | `file-utils.js`, `downloads.js`, `sw.js` | §8.5 | | Video player | `video-player.js` — `pump()` is the only place credit is granted | §8.5 | -| The television a session casts to, photos on it | `cast-session.js` | §11.4 | +| The television a session casts to; photos and music on it | `cast-session.js`, `music-player.js` | §11.4 | | Transfers widget | `transfers.js` | §5.5 | | Cross-group merge | `source-merge.js`, `group-name.js` | §9.11 | | Node page | `node-page.js` | §6.7 | @@ -1068,7 +1068,7 @@ when `ANDROID_HOME` is set (`test_android_shell.py`). | An invitation answered on the home page (accept, decline), in both engines | `packages/meshbay-hub/tests/harness/invitation_probe.py` | | A group owner's invited list and host approvals, in both engines | `packages/meshbay-hub/tests/harness/group_hosts_probe.py` | | A show's modal and the player, walked episode to episode | `packages/meshbay-hub/tests/harness/video_series_probe.py` | -| A television chosen once, then photos and a film sent to it, in both engines | `packages/meshbay-hub/tests/harness/cast_session_probe.py` | +| A television chosen once, then photos, a film and music sent to it, in both engines | `packages/meshbay-hub/tests/harness/cast_session_probe.py` | | A lazily loaded view whose module answers 404 (a tab older than the deploy) | `packages/meshbay-hub/tests/harness/lazy_failure_probe.py` | ## meshbay.org server (target state) diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 64364b0..5d33f5f 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -3423,27 +3423,49 @@ pauses the local element too, which otherwise goes on fetching for nobody. The copy-URL cast has no receiver to read and keeps the ordinary player. **A television is chosen for the session, not for one film** -(`cast-session.js`). Any cast button — Videos' toolbar, the Photos toolbar, a -photo album's bar, the lightbox, the player — sets it, and it is held in memory -only. A film opened +(`cast-session.js`). Any cast button — Videos', Photos' and Music's toolbars, a +photo album's bar, the lightbox, the video player, the music bar — sets it, and +it is held in memory only. A film opened while one is set starts its cast as a restart does: pending until the first segment, then the relay and `connect` (or `reload`, when the receiver is already connected), with the player drawn as the remote from the start. *Stop casting* clears it, from the player's remote or from any button. -**A photo is served at `/image`, one at a time** (`cast:image`). The page scales -it to fit 1920×1080, applies its EXIF orientation and re-encodes it as JPEG -before it leaves: a camera original is too large for a receiver to decode in good -time, and some formats it does not decode at all. The relay starts for it if -nothing else has, serves it behind the stream's token with a versioned address -(a receiver handed the same URL twice shows what it already has), and accepts -only `image/jpeg`, `image/png` or `image/webp`. **The shell, not the page, says -what the receiver loads:** the relay's photo URL is loaded as a picture -(`streamType: NONE`, photo metadata, no subtitles), any other relay URL as the -stream, and on Android no URL but the relay's own is accepted at all. A photo -does not start the foreground service, and its load answers as soon as the -receiver accepts it: a photo never reaches PLAYING. Only the newest photo asked -for is sent; paging quickly shows where the reader stopped. +**A photo or a music track is served whole at `/file`, one at a time** +(`cast:file:begin`, `:write`, `:end`), its cover at `/cover`. It crosses in +pieces — binary frames of a megabyte on Android — because a lossless track is +too large for one bridge message, and on Android it is written to the relay's +spool directory rather than held. The relay starts for it if nothing else has, +serves it behind the stream's token with a versioned address (a receiver handed +the same URL twice shows what it already has), honours byte ranges (a receiver +seeks in a track that way), and accepts only pictures and the audio types the +music player plays. **The shell, not the page, says what the receiver loads:** +the relay's file is loaded as what the relay was told it is — a photo as a +picture (`streamType: NONE`), a track as music (`BUFFERED`, with title, artist, +album and the relay's cover URL), at `startAt` seconds for a track picked up +mid-song — any other relay URL as the stream, and on Android no URL but the +relay's own is accepted at all. A file does not start the foreground service; a +photo's load answers as soon as the receiver accepts it, since a photo never +reaches PLAYING. + +**A photo is scaled before it leaves.** The page fits it to 1920×1080, applies +its EXIF orientation and re-encodes it as JPEG: a camera original is too large +for a receiver to decode in good time, and some formats it does not decode at +all. Only the newest photo asked for is sent; paging quickly shows where the +reader stopped. + +**With a television chosen, the music bar is its remote.** Each track, once +decrypted, goes to the relay with its cover instead of to the `