261 lines
11 KiB
Markdown
261 lines
11 KiB
Markdown
# rkt-web-player
|
||
|
||
Een audioplayer met webinterface, gebouwd op de bestaande Racket-pakketten:
|
||
|
||
- `racket-audio` voor lokale weergave en metadata;
|
||
- `racket-upnp` en `racket-sonos` voor ontdekking van netwerkspelers;
|
||
- `racket-audio-dlna` voor het publiceren en afspelen van lokale bestanden;
|
||
- `racket-mimetypes`, `simple-ini` en `simple-log` voor ondersteunende taken.
|
||
|
||
## Starten
|
||
|
||
De gekoppelde ontwikkelpackages kunnen direct worden gebruikt:
|
||
|
||
```console
|
||
racket main.rkt D:\Muziek
|
||
```
|
||
|
||
Standaard opent de speler `http://127.0.0.1:8080/`. Iedere opgegeven map wordt
|
||
een afzonderlijke muziekbibliotheek. Bij het starten wordt alleen de inhoud van
|
||
de eerste rootmap gelezen; er vindt geen recursieve bibliotheekscan plaats.
|
||
|
||
```console
|
||
racket main.rkt --port 8090 --no-browser D:\Muziek D:\Podcasts
|
||
```
|
||
|
||
Met een klein INI-bestand zijn vaste defaults mogelijk:
|
||
|
||
```ini
|
||
[server]
|
||
listen-ip=127.0.0.1
|
||
port=8080
|
||
|
||
[player]
|
||
dlna-port=8734
|
||
local-output=true
|
||
|
||
[libraries]
|
||
muziek=D:\Muziek
|
||
podcasts=D:\Podcasts
|
||
|
||
[playback-agents]
|
||
7b4776ef27104e8eb9f7ea2c622ce76ca23de4260b0f94e6880d321017b32a0e4=true
|
||
|
||
[authentication]
|
||
trusted-proxies=127.0.0.0/8;::1/128
|
||
session-seconds=604800
|
||
|
||
[users]
|
||
hans=$argon2id$v=19$m=19456,t=2,p=1$...
|
||
```
|
||
|
||
Start dat bestand met `racket main.rkt --config rkt-web-player.ini`. Iedere key
|
||
onder `[libraries]` is de zichtbare bibliotheeknaam; de waarde is de lokale of
|
||
UNC-rootmap. Het oudere `[library] paths=D:\Muziek;D:\Podcasts` blijft eveneens
|
||
ondersteund. Playlisttabs worden standaard opgeslagen in de keystore
|
||
`data/playlists.keystore` binnen de geïnstalleerde map van rkt-web-player. Met
|
||
`playlist-keystore=...` onder `[player]` kan desgewenst een ander pad worden
|
||
gebruikt. Met `local-output=false` wordt de audio-uitvoer van de server zelf
|
||
niet als afspeelpunt aangeboden. Als nog geen netwerkspeler of playback agent
|
||
beschikbaar is, blijft de uitvoerselectie leeg totdat er een verschijnt.
|
||
|
||
Zodra `[users]` minstens één gebruiker bevat, moeten alle browserclients
|
||
inloggen, zowel lokaal als via internet. Wachtwoorden staan uitsluitend als
|
||
Argon2id-hash in de INI. Maak zo'n hash vanuit Racket:
|
||
|
||
```racket
|
||
#lang racket/base
|
||
|
||
(require rkt-web-player/users)
|
||
|
||
(displayln (make-password-hash "een lang en uniek wachtwoord"))
|
||
```
|
||
|
||
`trusted-proxies` bepaalt uitsluitend van welke directe peers de laatste
|
||
`X-Forwarded-For`-waarde wordt geaccepteerd. Laat die lijst zo klein mogelijk;
|
||
bij Apache op dezelfde machine zijn loopbackadressen voldoende. Zonder
|
||
gebruikers is authenticatie uitgeschakeld en blijft het oude gedrag behouden.
|
||
|
||
## Werking
|
||
|
||
De interface volgt de informatiearchitectuur van `rktplayer`, maar is
|
||
responsive en geschikt voor muis, toetsenbord en touch. Links staat de lazy
|
||
mappenbrowser met trackinformatie; rechts staan playlisttabs en de huidige
|
||
playlist. Een map wordt pas recursief gelezen wanneer die met **afspelen** of
|
||
**toevoegen** wordt gekozen. Trackmetadata wordt eveneens pas op dat moment
|
||
geladen. Bij de huidige track toont de webinterface embedded album-art uit de
|
||
audio-tags. Als die ontbreekt, worden naast het audiobestand ook `cover`,
|
||
`folder` en `front` met een JPEG- of PNG-extensie geprobeerd.
|
||
|
||
De webinterface praat met een kleine JSON-API onder `/api`. Lokale weergave
|
||
wordt pas geïnitialiseerd bij het eerste afspeelcommando. De knop naast de
|
||
uitvoerkeuze start UPnP-discovery; gevonden Sonos-zones worden als logische
|
||
groepen aangeboden en niet nogmaals als losse UPnP-renderers.
|
||
|
||
Voor UPnP- en Sonos-weergave publiceert de server ook de volgende playlisttrack
|
||
en stelt deze vooraf in als `NextAVTransportURI`. Een renderer die dit
|
||
ondersteunt kan daardoor zelf zonder serverronde naar de volgende track
|
||
overgaan. De server volgt de nieuwe URI en playlistindex. Renderers zonder
|
||
betrouwbare next-ondersteuning krijgen na het natuurlijke trackeinde een
|
||
servergestuurde fallback. Een expliciet stopcommando start nooit de volgende
|
||
track.
|
||
|
||
De bibliotheek links heeft de tabs **Mappen** en **Afspeellijsten**. Met
|
||
**Afspeellijst opslaan** boven de huidige playlist geef je de tab een naam en
|
||
bewaar je hem in de bibliotheek. **+** bij een bewaarde playlist opent of
|
||
selecteert zijn eigen tab; **▶** opent die tab en speelt de playlist af.
|
||
Een playlist die al open is, krijgt geen tweede tab. De inhoud van andere
|
||
tabs blijft behouden. Wijzigingen in een bewaarde playlist worden automatisch
|
||
opgeslagen, ook wanneer je de tab hernoemt. Het kruisje sluit een bewaarde tab;
|
||
de playlist blijft beschikbaar in de bibliotheek.
|
||
|
||
Playlisttabs kunnen worden toegevoegd, geselecteerd, hernoemd door dubbel te
|
||
klikken en gesloten. Tracks kunnen worden afgespeeld, verwijderd en met
|
||
drag-and-drop verplaatst. Tabnamen, tabvolgorde en alle tracklijsten worden na
|
||
iedere wijziging transactioneel opgeslagen. Alleen een gesloten tab die niet
|
||
in de bibliotheek is bewaard, verdwijnt ook uit de keystore. Voor iedere gebruiker
|
||
bevat `playlists-for-<username>` de geordende lijst met geopende playlist-GUIDs,
|
||
en `saved-playlists-for-<username>` de bewaarde playlists. Onder iedere
|
||
GUID-key staan de naam en tracks van die playlist. Bestaande tabs worden niet
|
||
automatisch aan de bibliotheek toegevoegd. Tracks uit verschillende
|
||
geconfigureerde libraries mogen in dezelfde playlist staan; ontbrekende of
|
||
buiten de libraries gelegen bestanden worden bij het laden overgeslagen.
|
||
Iedere aangemelde gebruiker heeft daarbij een eigen playlistverzameling. Als
|
||
authenticatie is uitgeschakeld, wordt de verzameling van `anonymous` gebruikt.
|
||
Iedere gebruiker heeft daarnaast een eigen afspeelpipeline met uitvoerkeuze,
|
||
transportstatus, huidige track, volume en herhaalmodus. Verschillende fysieke
|
||
afspeelpunten kunnen daardoor gelijktijdig voor verschillende gebruikers
|
||
spelen. De afspeelpunten zelf blijven gedeeld: kiest een gebruiker een reeds
|
||
bezet afspeelpunt, dan wordt de vorige pipeline op dat punt gestopt en neemt de
|
||
nieuwe gebruiker het over. Een oudere
|
||
`playlists-for-local`-verzameling blijft in de keystore staan, maar wordt niet
|
||
automatisch aan een gebruiker toegewezen.
|
||
|
||
De webinterface ondersteunt Nederlands, Engels, Duits, Frans, Spaans,
|
||
Italiaans, Zweeds, Noors, Fins en IJslands. Bij
|
||
het eerste bezoek wordt de voorkeurstaal van de browser gebruikt. Een keuze in
|
||
de taalselector wordt daarna per gebruiker als `language-for-<username>` in
|
||
`data/playlists.keystore` opgeslagen en geldt daardoor ook op andere apparaten.
|
||
Zonder authenticatie geldt dit voor de gebruiker `anonymous`.
|
||
|
||
De server luistert standaard alleen op localhost. Geef alleen bewust een
|
||
LAN-adres aan `--listen-ip`. Configureer gebruikersauthenticatie voordat de
|
||
webinterface via een publiek bereikbare reverse proxy wordt aangeboden.
|
||
|
||
Gebruikers krijgen na succesvolle aanmelding een willekeurige 256-bit
|
||
sessiecookie met `Secure`, `HttpOnly` en `SameSite=Strict`. De standaard
|
||
inactiviteitsduur is zeven dagen. Geldig gebruik verschuift de servertermijn;
|
||
halverwege de termijn wordt ook de browsercookie opnieuw voor zeven dagen
|
||
uitgegeven. Sessies worden niet over een serverherstart heen bewaard. Na vijf
|
||
mislukte pogingen vanaf hetzelfde clientadres wordt aanmelden vijf minuten
|
||
geblokkeerd. De `/api/agent/*`-routes gebruiken geen gebruikerssessie: daarvoor
|
||
blijft de afzonderlijke playback-agent-allowlist gelden.
|
||
|
||
## Windows playback agent
|
||
|
||
Een lichte playback agent kan op een Windows-laptop draaien en meldt zichzelf
|
||
via HTTP bij de centrale RKT Web Player aan. De agent opent geen inkomende
|
||
netwerkpoort. Hij pollt de server voor opdrachten en rapporteert daarbij zijn
|
||
actuele afspeelstatus.
|
||
|
||
Start vanuit de broncode:
|
||
|
||
```console
|
||
racket player-agent.rkt
|
||
```
|
||
|
||
Dezelfde GUI kan vanuit een ander Racket-programma worden gestart:
|
||
|
||
```racket
|
||
#lang racket/base
|
||
|
||
(require rkt-web-player/player-agent)
|
||
|
||
(run-player-agent)
|
||
```
|
||
|
||
De GUI bewaart de server-URL, de gekozen naam en een eenmalig gegenereerde
|
||
256-bit applicatie-ID in `rkt-web-player-agent.ini` in de gebruikersspecifieke
|
||
Racket-configuratiemap. Vul als server bijvoorbeeld `http://192.168.1.10:8080`
|
||
in. De naam wordt in dezelfde agent-GUI ingesteld. Na registratie verschijnt
|
||
de agent met die naam als uitvoer van type `AGENT`; de server volgt latere
|
||
naamswijzigingen bij registratie en polling. De server laat een agent alleen
|
||
toe wanneer zijn volledige applicatie-ID vooraf als key onder
|
||
`[playback-agents]` staat en de waarde niet `false` is. Een onbekende ID krijgt
|
||
HTTP 403 en wordt niet als uitvoerpunt aangemaakt. De agent toont in dat geval
|
||
zijn ID en meldt dat de serverbeheerder deze eerst aan de INI moet toevoegen.
|
||
De GUI en het systeemvak volgen automatisch de systeemtaal en ondersteunen
|
||
dezelfde tien talen als de webinterface.
|
||
|
||
Onder **Afspelen** toont de agent het playlistnummer, de huidige track en
|
||
bestandsnaam, afspeeltoestand, verstreken en totale tijd, bitdiepte,
|
||
samplefrequentie, kanaalaantal en decoderformaat.
|
||
|
||
Een geselecteerde track wordt volledig naar een tijdelijk bestand gedownload
|
||
voordat `racket-audio` de weergave start. Tijdens het afspelen stuurt de server
|
||
al een `prefetch`-opdracht voor de volgende playlisttrack. De agent bewaart
|
||
daardoor maximaal het huidige en het volgende bestand lokaal. Bij de overgang
|
||
opent de agent het vooraf opgehaalde bestand direct bij decoder-EOF. Daardoor
|
||
blijven polling en netwerkvertraging buiten het tijdkritische audiopad en kan
|
||
`racket-audio` de volgende track achter de nog uitspelende uitvoerbuffer zetten.
|
||
De server wordt bijgewerkt zodra het nieuwe music-id werkelijk hoorbaar is en
|
||
stuurt vervolgens de daaropvolgende prefetch. Tijdelijke bestanden worden
|
||
opgeruimd zodra ze niet meer nodig zijn en bij afsluiten van de agent.
|
||
|
||
### Systeemvak
|
||
|
||
De GUI gebruikt het Racket-pakket `racket-tray`. Minimaliseren en de
|
||
vensterknop verbergen de agent in het systeemvak. Het menu bevat
|
||
**RKT Web Player Agent openen** en **Afsluiten**; openen herstelt ook een
|
||
geminimaliseerd venster. De tray is onderdeel van de normale package-
|
||
dependencies en vereist geen SDL3-pakket of SDL3-libraries meer.
|
||
|
||
Windows en macOS gebruiken de native systeemvoorzieningen zonder aanvullende
|
||
runtime. Op Linux gebruikt `racket-tray` Ayatana AppIndicator voor GTK3. Op
|
||
Debian en Ubuntu kan die runtime zo worden geïnstalleerd:
|
||
|
||
```console
|
||
sudo apt install libayatana-appindicator3-1
|
||
```
|
||
|
||
### CLI playback agent
|
||
|
||
De headless agent gebruikt exact dezelfde polling-, download-, audio- en
|
||
gapless-prefetchkern als de GUI-agent:
|
||
|
||
```console
|
||
racket player-agent-cli.rkt
|
||
racket player-agent-cli.rkt --server https://muziek.example.nl --name "Werkkamer"
|
||
```
|
||
|
||
GUI en CLI gebruiken standaard hetzelfde gebruikersspecifieke
|
||
`rkt-web-player-agent.ini`, dus ook hetzelfde application-ID. Met `--config`
|
||
kan de CLI een afzonderlijk INI-bestand en daarmee een afzonderlijke identiteit
|
||
gebruiken. Bij het starten worden naam, server en application-ID afgedrukt;
|
||
stoppen gaat met `Ctrl+C`.
|
||
|
||
Vanuit Racket is de headless variant eveneens beschikbaar:
|
||
|
||
```racket
|
||
(require rkt-web-player/player-agent)
|
||
|
||
(run-player-agent-cli #:server-url "https://muziek.example.nl"
|
||
#:name "Werkkamer")
|
||
```
|
||
|
||
De allowlist voorkomt dat een onbekende agent zich als uitvoerpunt registreert
|
||
of opdrachten en tijdelijke media ontvangt. Het applicatie-ID is een gedeeld
|
||
toegangstoken en vervangt geen transportbeveiliging of gebruikersauthenticatie.
|
||
Gebruik de agent en server alleen op een vertrouwd LAN zolang de HTTP-server
|
||
geen TLS heeft.
|
||
|
||
## Controleren
|
||
|
||
```console
|
||
raco test private/users.rkt private/library.rkt private/player.rkt \
|
||
private/server.rkt private-player-agent/player-agent-config.rkt
|
||
node tests/player-state.test.mjs
|
||
node tests/playlist-library.test.mjs
|
||
raco setup --check-pkg-deps rkt-web-player
|
||
```
|