playlists and DLNA playback
This commit is contained in:
+57
-32
@@ -39,11 +39,13 @@ flowchart TB
|
||||
Server[private/server.rkt<br/>HTTP adapter]
|
||||
Users[private/users.rkt<br/>users, networks and sessions]
|
||||
Player[private/player.rkt<br/>application state and commands]
|
||||
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]
|
||||
Audio[racket-audio<br/>local backend]
|
||||
Discovery[racket-upnp + racket-sonos<br/>device discovery]
|
||||
DLNA[racket-audio-dlna<br/>network backend and media server]
|
||||
DLNA[racket-audio-dlna<br/>transport, seeking and media publication]
|
||||
AgentGUI[private/player-agent-gui.rkt<br/>GUI adapter]
|
||||
AgentCLI[player-agent-cli.rkt<br/>CLI adapter]
|
||||
AgentCore[private/player-agent-core.rkt<br/>polling and audio runtime]
|
||||
@@ -55,9 +57,11 @@ flowchart TB
|
||||
Server --> Users
|
||||
Server --> UI
|
||||
Player --> Library
|
||||
Player --> Playlists
|
||||
Player --> Audio
|
||||
Player --> Discovery
|
||||
Player --> DLNA
|
||||
Player --> DLNAAdapter
|
||||
DLNAAdapter --> DLNA
|
||||
AgentGUI --> AgentCore
|
||||
AgentCLI --> AgentCore
|
||||
AgentCore --> Server
|
||||
@@ -115,7 +119,7 @@ relative paths originated from directory listings below a configured root.
|
||||
The mutable `player` structure is the aggregate root for:
|
||||
|
||||
- configured libraries and the current browser location;
|
||||
- in-memory playlist tabs and the selected tab's tracks;
|
||||
- playlist tabs, the selected tab, and their durably stored tracks;
|
||||
- discovered renderers and the selected renderer;
|
||||
- registered HTTP playback agents and their pending command queues;
|
||||
- the lazily created local or network backend;
|
||||
@@ -130,9 +134,12 @@ required, and returns a complete JSON-compatible state snapshot. Commands cover
|
||||
library navigation, playlist/tab editing, output selection, transport, seeking,
|
||||
volume, and repeat mode.
|
||||
|
||||
Playlist tabs exist only in memory. Selecting, deleting, or creating a tab
|
||||
stops playback. Tracks are de-duplicated by normalized source path when they are
|
||||
appended.
|
||||
Selecting, deleting, or creating a tab stops playback. Tracks are de-duplicated
|
||||
by normalized source path when they are appended. Every playlist mutation is
|
||||
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.
|
||||
|
||||
### 3.4 Playback backends
|
||||
|
||||
@@ -145,15 +152,21 @@ 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 `racket-audio-dlna`. The backend controls the chosen media
|
||||
renderer and publishes local files over HTTP on the configured DLNA port and
|
||||
path. After starting a track, the server publishes the following track and sets
|
||||
it as the renderer's `NextAVTransportURI`. Network playback information is
|
||||
refreshed whenever a state snapshot is requested. A changed current URI
|
||||
promotes the prepared playlist index and immediately prepares its successor.
|
||||
If a renderer does not perform the prepared transition, a confirmed natural
|
||||
stop advances through a server-driven fallback. Explicit stops and failed play
|
||||
requests are tracked separately and never trigger that fallback.
|
||||
Network outputs use the small `private/dlna-playback.rkt` adapter. It 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
|
||||
publish the following track and set it as the renderer's
|
||||
`NextAVTransportURI`.
|
||||
|
||||
The adapter polls the package's cached renderer information independently of
|
||||
browser requests. A changed current URI promotes the prepared playlist index
|
||||
and immediately prepares its successor. If a renderer does not perform the
|
||||
prepared transition, a confirmed natural stop advances through a
|
||||
server-driven fallback. Explicit stops and failed play requests are tracked
|
||||
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
|
||||
@@ -269,16 +282,24 @@ sequenceDiagram
|
||||
participant B as Browser
|
||||
participant H as HTTP server
|
||||
participant P as Player
|
||||
participant A as DLNA playback adapter
|
||||
participant L as racket-audio-dlna
|
||||
participant D as DLNA renderer
|
||||
|
||||
loop Every second
|
||||
loop Browser state polling
|
||||
B->>H: GET /api/state
|
||||
H->>P: player-state->jsexpr
|
||||
P->>D: Query transport, position, track and volume
|
||||
D-->>P: Current renderer information
|
||||
P-->>H: Full state snapshot
|
||||
H-->>B: JSON response
|
||||
end
|
||||
|
||||
loop DLNA adapter polling
|
||||
L->>D: Poll transport and position
|
||||
D-->>L: Current renderer information
|
||||
A->>L: Read cached state
|
||||
L-->>A: DLNA info
|
||||
A-->>P: Update track, transport and time state
|
||||
end
|
||||
```
|
||||
|
||||
## 5. Concurrency and consistency
|
||||
@@ -311,15 +332,16 @@ file:
|
||||
- DLNA media publication port, defaulting to `8734`;
|
||||
- named library root paths under `[libraries]` (the legacy semicolon-separated
|
||||
setting remains supported);
|
||||
- allowed 256-bit playback-agent IDs under `[playback-agents]`.
|
||||
- allowed 256-bit playback-agent IDs under `[playback-agents]`;
|
||||
- the optional playlist-keystore override under `[player]`;
|
||||
- Argon2id user hashes, local networks, trusted proxies, and session timeout.
|
||||
|
||||
Command-line network settings override INI values. Library paths from both
|
||||
sources are combined and de-duplicated.
|
||||
|
||||
There is no database, migration process, user account, or persistent playlist
|
||||
store. Restarting the process resets playlists, output discovery, transport
|
||||
state, and all other mutable state.
|
||||
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.
|
||||
|
||||
## 7. Security and operational boundaries
|
||||
|
||||
@@ -353,10 +375,13 @@ subsequent polling requests.
|
||||
## 8. Testing and extension points
|
||||
|
||||
Unit tests embedded in `private/library.rkt` cover root creation, filtering, and
|
||||
directory ordering. Tests in `private/player.rkt` cover initial state, browser
|
||||
navigation, repeat mode, playlist-tab operations, unknown commands, and clean
|
||||
shutdown. The current suite does not exercise real audio devices, network
|
||||
discovery, DLNA renderers, HTTP routing, or browser behavior.
|
||||
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`
|
||||
cover initial state, browser navigation, repeat mode, persistent playlist-tab
|
||||
operations, unknown commands, and clean shutdown. The current suite does not
|
||||
exercise real audio devices, network discovery, DLNA renderers, HTTP routing,
|
||||
or browser behavior.
|
||||
|
||||
The main extension points are:
|
||||
|
||||
@@ -365,15 +390,14 @@ The main extension points are:
|
||||
dispatch, and state refresh in `private/player.rkt`;
|
||||
- add an API operation by defining its player command first and exposing it
|
||||
through the generic command endpoint;
|
||||
- add persistence behind playlist-tab and player initialization without
|
||||
changing the browser's snapshot-oriented protocol;
|
||||
- replace polling with server-pushed updates while keeping the current state
|
||||
snapshot as the synchronization model.
|
||||
|
||||
## 9. Architectural constraints and trade-offs
|
||||
|
||||
- **Single shared state:** simple coordination and UI synchronization, but no
|
||||
multi-user isolation or horizontal scaling.
|
||||
- **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.
|
||||
- **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
|
||||
@@ -381,8 +405,9 @@ The main extension points are:
|
||||
- **Lazy filesystem and backend initialization:** fast startup and low idle
|
||||
resource usage, while the first recursive selection or playback command can
|
||||
be comparatively slow.
|
||||
- **In-memory playlists:** minimal operational complexity, but no recovery after
|
||||
restart.
|
||||
- **Keystore playlists:** transactional recovery after restart without a custom
|
||||
database layer, with the SQLite-backed keystore remaining a single-node
|
||||
resource.
|
||||
- **Explicit backend branching:** easy to follow for the current small set of
|
||||
outputs, but adding renderer types touches several player functions rather
|
||||
than one formal backend interface.
|
||||
|
||||
Reference in New Issue
Block a user