The jam room server now lives in https://github.com/team-spotube/jam-server.
Remove jam-server/ and its deploy workflow, and point code comments, test
instructions, AGENTS.md and the self-hosting note (all locales) at the new
repository.
Security:
- Participants connect with a per-room key derived from a random per-install
secret; others only see an id derived from it, so nobody can reconnect as
someone else (including the host).
- "create" only works on a fresh code or for the room's own host, so empty
rooms can't be taken over.
- Per-socket message and queue budgets disconnect clients that flood; the
host throttles itself well under them.
- Bans also cover the device: a hashed digest of device traits combined with
the network, stored only as an opaque per-room value. Earlier members are
never caught by someone else's ban.
- Peer queue items and suggestions are sanitized (streaming tracks only, no
URLs, https covers); suggestions are credited to the server-attested sender;
local file paths go out as opaque ids. Names are cleaned server-side.
- Room codes and keys use a secure random source; the deploy workflow is
read-only.
Names:
- Participants get a stable generated name ("Mellow Otter") per install, and
can pick their own on the Jam screen or in Jam settings. The participant
list marks "(you)".
Also translate the Jam strings into all locales and add tests for the above.
- Add jam-server/: a Cloudflare Worker with one Durable Object per room code.
It tracks membership, host handoff and kicks/bans, keeps the queue for late
joiners, routes guest commands to the host and serves the room clock.
- Sync playback: devices estimate the server clock, the host sends position
anchors in server time, guests follow by nudging playback rate (seeking only
when far off) and stream the host's audio source when they share a plugin.
- Seek, play/pause, skip and jump apply to the whole room.
- Recover from network loss instead of crashing: socket failures reconnect
with backoff, silent sockets are detected, and an expired room is recreated
by its host.
- Keep cost and battery low: anchors only on change (plus host drift and a
60 s heartbeat), kept in memory rather than storage, free runtime keep-alives
once a minute, rare clock re-syncs, and no polling while paused or buffering.
- Remove the MQTT broker settings and libraries.
- Add sync unit tests, client/session integration tests (JAM_TEST_URL) and a
network-loss test, plus a deploy workflow for the server.
PlaybackService.onCreate runs on the main thread and called
runBlocking { getString(...) } for its notification strings. Compose
resources share one in-flight load per strings file (AsyncCache creates the
Deferred in the caller's scope); when App()'s stringResource started that
load on the main dispatcher first, blocking main to await it deadlocked and
the first frame never rendered. Timing-dependent, so mostly seen under the
debugger.
Create the notification channel immediately under the app name and update
its localized name/description from serviceScope. The short-lived
placeholder notification no longer carries localized text.
Downloads
- Show the real download folder: "Current folder: <path> (default)" in
settings instead of the misleading "default music folder", and a
"Saved to <path>" line on the Downloads screen
- Register finished downloads with Android's MediaStore (MediaScanner) so
music players and the Files app see them immediately
- Add tagged logs across the download path to diagnose format/quality
selection: raw NewPipe streams ([Streams]), every selection candidate
with its rank key and fallback warnings ([Resolve]), and per-download
enqueue prefs, resolved stream, HEAD/GET status + content type, segment
responses and written vs expected bytes ([Download])
Connect status
- Mobile Home keeps its Devices / Group Jam / Support icons (Jam now uses
the sound icon) and shows a slim banner only while remote-controlling a
device or in a Jam ("Playing on krtx", "Hosting · 3 listeners")
- Sidebar Devices / Group Jam rows get a live dot and status line
- New ConnectStatusViewModel feeds both
Remote control settings
- Extract "Allow remote control" and "Device name" into shared
remoteControlSettingCards, used by Playback settings and a new
"This device" section on the Devices screen
- Remove the unused "Enable Connect" setting and its strings
- Redesign RemoteControlScreen to match the expanded player: artwork sized
to fit, side-by-side queue panel on wide screens, proper connecting /
disconnected / error states, "Playing on <device>" subtitle
- Seek and volume sliders keep local drag state and only commit on release
(volume throttled while dragging) instead of sending a command per frame
- Redesign DevicesScreen with the settings card components, a connected
device card, error banner and empty state; ViewModels expose one UiState
- Fix tapping an already-connected device doing nothing; reopen its remote
screen instead, and switch devices by disconnecting first
- Fix the controller getting stuck in Connecting after the host denies a
request: Ktor never delivers Close frames to `incoming`, so detect the
close reason after the receive loop ends
- Fix a UI freeze when leaving the devices screen on desktop: run discovery
(and JmDNS's blocking close/resolve) on Dispatchers.IO
- Advertise the LAN address of the default-route interface on desktop;
dns-sd-kt otherwise picks any site-local address, e.g. a Docker bridge