multi player / per user playing and translation
This commit is contained in:
+61
-37
@@ -8,9 +8,10 @@ can play selected files either on the host machine or on a discovered UPnP or
|
||||
Sonos renderer.
|
||||
|
||||
The application is designed as a personal, stateful desktop or LAN service. A
|
||||
single server-side player instance owns the active browser location, playlists,
|
||||
playback state, output selection, and audio backend. All connected browser
|
||||
windows observe and control that same instance.
|
||||
single server-side player instance owns shared libraries, discovery and output
|
||||
inventory. Each username owns its playlists, preferences and playback session.
|
||||
Different users can therefore play concurrently when they select different
|
||||
physical outputs.
|
||||
|
||||
## 2. System context
|
||||
|
||||
@@ -29,7 +30,7 @@ flowchart LR
|
||||
|
||||
The browser is a thin client: it renders a complete server-provided state
|
||||
snapshot and sends commands back to the server. The server is authoritative;
|
||||
there is no browser-side persistence or independent playback state.
|
||||
the browser stores neither playlists nor language preferences.
|
||||
|
||||
## 3. Runtime structure
|
||||
|
||||
@@ -42,7 +43,7 @@ flowchart TB
|
||||
Playlists[private/playlists.rkt<br/>durable playlist tabs]
|
||||
DLNAAdapter[private/dlna-playback.rkt<br/>playlist transition orchestration]
|
||||
Library[private/library.rkt<br/>filesystem and metadata]
|
||||
UI[public/index.html + styles.css + app.js<br/>browser UI]
|
||||
UI[public/index.html + styles.css + app.js + translate.js<br/>browser UI]
|
||||
Audio[racket-audio<br/>local backend]
|
||||
Discovery[racket-upnp + racket-sonos<br/>device discovery]
|
||||
DLNA[racket-audio-dlna<br/>transport, seeking and media publication]
|
||||
@@ -120,11 +121,12 @@ The mutable `player` structure is the aggregate root for:
|
||||
|
||||
- configured libraries and the current browser location;
|
||||
- playlist tabs, the selected tab, and their durably stored tracks;
|
||||
- discovered renderers and the selected renderer;
|
||||
- discovered renderers and per-renderer session ownership;
|
||||
- registered HTTP playback agents and their pending command queues;
|
||||
- the lazily created local or network backend;
|
||||
- transport state, current track, position, duration, audio properties, volume,
|
||||
and repeat mode;
|
||||
- a playback session per username, containing its selected renderer, lazy
|
||||
backend, current track, transport state, audio properties, volume and repeat
|
||||
mode;
|
||||
- one shared DLNA media-file server used by all network backends;
|
||||
- current error, discovery, and shutdown status;
|
||||
- synchronization primitives and the DLNA publication port.
|
||||
|
||||
@@ -140,19 +142,29 @@ written in one `keystore` transaction. `playlists-for-<username>` contains the
|
||||
ordered playlist GUIDs; each GUID key contains that playlist's name and tracks.
|
||||
Loading validates every stored track independently against all configured
|
||||
library roots, so one playlist can safely combine multiple libraries.
|
||||
`language-for-<username>` stores the user's selected interface language in the
|
||||
same keystore.
|
||||
|
||||
### 3.4 Playback backends
|
||||
|
||||
The player exposes local, UPnP, and Sonos outputs as renderers with a common
|
||||
logical interface, but dispatches explicitly based on backend kind.
|
||||
|
||||
The default `local` renderer uses `racket-audio`. Its backend is created on the
|
||||
first playback-related command, not at startup. State and end-of-track callbacks
|
||||
update the player and automatically advance the playlist. The UI's linear
|
||||
0-100 volume is squared before being sent to the local audio library to provide
|
||||
a more useful perceived volume curve.
|
||||
Each username has an independent playback pipeline. Separate outputs can be
|
||||
active concurrently. Outputs remain shared resources: selecting an output
|
||||
claims it for that session. If another session owns it, that backend is closed,
|
||||
the former session becomes stopped, and ownership transfers to the selecting
|
||||
user. This deliberately avoids renderer ACLs and durable reservations.
|
||||
|
||||
Network outputs use the small `private/dlna-playback.rkt` adapter. It owns only
|
||||
The optional `local` renderer uses `racket-audio`. It is included by default
|
||||
and can be omitted with `[player] local-output=false`. Its backend is created
|
||||
on the first playback-related command, not at startup. State and end-of-track
|
||||
callbacks update the player and automatically advance the playlist. The UI's
|
||||
linear 0-100 volume is squared before being sent to the local audio library to
|
||||
provide a more useful perceived volume curve.
|
||||
|
||||
Network outputs use the small `private/dlna-playback.rkt` adapter. All adapters
|
||||
share one `racket-audio-dlna` media-file server, while each adapter owns only
|
||||
the playlist transition state machine; all transport commands, seeking, HTTP
|
||||
publication, UPnP calls and cached renderer information are delegated to
|
||||
`racket-audio-dlna`. After starting a track, the adapter asks that package to
|
||||
@@ -168,9 +180,9 @@ separately and never trigger that fallback. A seek uses
|
||||
`dlna-player-seek-percentage!` directly; its synchronously updated cached
|
||||
position is pushed to the application state immediately.
|
||||
|
||||
Changing the selected renderer closes the existing backend and resets playback
|
||||
state. The replacement backend remains lazy and is created only when it is
|
||||
needed.
|
||||
Changing a session's selected renderer closes that session's existing backend
|
||||
and resets its playback state. The replacement backend remains lazy and is
|
||||
created only when needed.
|
||||
|
||||
Registered playback agents form a fourth renderer kind. An agent keeps the
|
||||
client/server direction unchanged: it registers and polls the server, while the
|
||||
@@ -200,7 +212,7 @@ thread so that the initial API call can return immediately. It:
|
||||
4. Represents each Sonos group as one logical renderer.
|
||||
5. Removes individual UPnP devices that are already members of those groups.
|
||||
6. Sorts the resulting outputs by display name and retains local output as the
|
||||
first option.
|
||||
first option when local output is enabled.
|
||||
|
||||
The `discovering` state lets polling clients show progress. Discovery failures
|
||||
are recorded in the shared player error field.
|
||||
@@ -215,6 +227,8 @@ serves static assets from [`public/`](public/) and exposes these API endpoints:
|
||||
| `GET` | `/api/state` | Return the complete current state; also refresh network-renderer information. |
|
||||
| `POST` | `/api/discover` | Start asynchronous discovery and return the current state. |
|
||||
| `POST` | `/api/command/:command` | Execute a command with its JSON request body and return the updated state. |
|
||||
| `GET` | `/api/preferences` | Return the current user's durable UI preferences. |
|
||||
| `POST` | `/api/preferences` | Persist the current user's UI language. |
|
||||
| `POST` | `/api/agent/register` | Register or refresh a polling playback agent. |
|
||||
| `POST` | `/api/agent/poll` | Accept agent state and acknowledgements and return its next command. |
|
||||
| `GET` | `/api/agent/media/:app-id/:token` | Download the track currently assigned to an agent. |
|
||||
@@ -234,6 +248,14 @@ header. Command failures are returned as HTTP 400 JSON responses with an
|
||||
selection;
|
||||
- implements keyboard actions and playlist drag-and-drop in the browser.
|
||||
|
||||
[`public/translate.js`](public/translate.js) follows the key-based translation
|
||||
model used by rktplayer. It supports Dutch, English, German, French and Spanish
|
||||
with English fallback. Browser preferences select the initial language; a
|
||||
manual selection is stored server-side per username and therefore follows the
|
||||
user across browsers. The native agent uses the equivalent
|
||||
[`private/translate.rkt`](private/translate.rkt) module and the operating-system
|
||||
language.
|
||||
|
||||
DOM signatures prevent rebuilding unchanged library, tab, and playlist
|
||||
collections on every poll. Playback status and other small values are updated
|
||||
on every render.
|
||||
@@ -309,16 +331,15 @@ sequenceDiagram
|
||||
|
||||
The application uses two semaphores with separate responsibilities:
|
||||
|
||||
- `command-lock` serializes commands and shutdown, preventing overlapping state
|
||||
transitions and backend operations.
|
||||
- `command-lock` serializes commands, state snapshots, agent transitions,
|
||||
discovery commits and shutdown, preventing overlapping backend operations.
|
||||
- `state-lock` protects short reads and mutations of the shared player fields
|
||||
performed by HTTP requests, audio callbacks, and the discovery thread.
|
||||
|
||||
Potentially slow discovery runs outside the state lock. Backend calls are also
|
||||
generally performed outside it, with their results committed in short locked
|
||||
sections. A state request is not serialized by `command-lock`; it may observe
|
||||
the last committed state while a command is performing external I/O, but its
|
||||
snapshot is internally protected by `state-lock`.
|
||||
sections. Audio callbacks use only the short-lived state lock and update the
|
||||
playback session captured when their backend was created.
|
||||
|
||||
The server module stores the player in a module-level `current-player` variable.
|
||||
This matches the intended one-player-per-process deployment, but it prevents
|
||||
@@ -333,6 +354,7 @@ file:
|
||||
- web listen address, defaulting to `127.0.0.1`;
|
||||
- web port, defaulting to `8080`;
|
||||
- DLNA media publication port, defaulting to `8734`;
|
||||
- optional server-local audio output under `[player]`, enabled by default;
|
||||
- named library root paths under `[libraries]` (the legacy semicolon-separated
|
||||
setting remains supported);
|
||||
- allowed 256-bit playback-agent IDs under `[playback-agents]`;
|
||||
@@ -342,9 +364,9 @@ file:
|
||||
Command-line network settings override INI values. Library paths from both
|
||||
sources are combined and de-duplicated.
|
||||
|
||||
Playlist tabs use the SQLite-backed `keystore` module at
|
||||
`data/playlists.keystore`. Output discovery, transport state, playback position
|
||||
and sessions still reset when the process restarts.
|
||||
Playlist tabs and per-user language preferences use the SQLite-backed
|
||||
`keystore` module at `data/playlists.keystore`. Output discovery, transport
|
||||
state, playback position and sessions still reset when the process restarts.
|
||||
|
||||
## 7. Security and operational boundaries
|
||||
|
||||
@@ -353,8 +375,9 @@ Authentication must be enabled before exposing it to an untrusted network, and
|
||||
a reverse proxy must provide HTTPS because session cookies are always marked
|
||||
`Secure`. Forwarded client addresses are security-sensitive configuration:
|
||||
only known reverse-proxy peers may be trusted, and the application port should
|
||||
remain firewalled from the internet. Playlists are isolated by username, while
|
||||
renderer selection and transport state remain shared.
|
||||
remain firewalled from the internet. Playlists, preferences, renderer selection
|
||||
and transport state are isolated by username. Physical outputs are shared and
|
||||
can be transferred between sessions.
|
||||
|
||||
Playback-agent registration and polling are authorized against a default-deny
|
||||
INI allowlist. Media URLs additionally contain an opaque per-track token. The
|
||||
@@ -371,18 +394,19 @@ operate on opaque indexes instead of sending paths directly. The DLNA backend
|
||||
must make a selected local file reachable by the network renderer, so its media
|
||||
port also needs to be accessible on the relevant trusted network.
|
||||
|
||||
There is no durable job queue or retry policy. Discovery and renderer failures
|
||||
are surfaced as shared UI errors, and renderer state is retried naturally by
|
||||
subsequent polling requests.
|
||||
There is no durable job queue or retry policy. Discovery failures are shared;
|
||||
playback failures belong to the affected user's session. Renderer state is
|
||||
retried naturally by subsequent polling requests.
|
||||
|
||||
## 8. Testing and extension points
|
||||
|
||||
Unit tests embedded in `private/library.rkt` cover root creation, filtering, and
|
||||
directory ordering. `private/playlists.rkt` tests transactional round trips,
|
||||
per-user GUID indexes, multiple libraries, deletion and library-boundary
|
||||
validation. Tests in `private/player.rkt`
|
||||
validation, and per-user language storage. Tests in `private/player.rkt`
|
||||
cover initial state, browser navigation, repeat mode, persistent playlist-tab
|
||||
operations, unknown commands, and clean shutdown. The current suite does not
|
||||
operations, concurrent user sessions, renderer takeover, unknown commands, and
|
||||
clean shutdown. The current suite does not
|
||||
exercise real audio devices, network discovery, DLNA renderers, HTTP routing,
|
||||
or browser behavior.
|
||||
|
||||
@@ -398,9 +422,9 @@ The main extension points are:
|
||||
|
||||
## 9. Architectural constraints and trade-offs
|
||||
|
||||
- **Per-user playlists, shared transport:** playlist collections are isolated
|
||||
by username, while the renderer and transport remain shared. A playlist
|
||||
command from another user explicitly takes over that shared player.
|
||||
- **Per-user pipelines, shared outputs:** playlist and transport state are
|
||||
isolated by username. Distinct outputs run concurrently; selecting an
|
||||
occupied output explicitly stops its previous owner and transfers it.
|
||||
- **Full-state snapshots:** a small and predictable client protocol, at the cost
|
||||
of repeatedly transferring all tracks and browser entries.
|
||||
- **One-second polling:** robust and dependency-free, but introduces periodic
|
||||
|
||||
Reference in New Issue
Block a user