4.2 KiB
ARCHITECTURE
One binary, three modes. packages/server (Relic) is embedded, not a separate product.
- Client: GUI Join, sync WS client (
web_socket_channelinapps/desktop, NOTpackages/core— core stays Flutter-free)- mpv. No server code runs.
- Server+Client (Host): GUI Host starts
packages/serverin-process, binds port, self-connects via loopback. Host opens ports, runs traffic. Clients open nothing outbound-only. - Headless:
--headless, same server package, no window/player init.
Server side: Relic (HTTP + built-in WS). Client side WS: web_socket_channel
(Relic has no client; server WS handling stays in Relic).
Media order (locked): links first (shared http(s) URL, Syncplay-style sync), file relay after (server-proxied bytes for the whitelist case).
Media paths (links vs file relay)
Two ways to watch, same sync engine, same Range endpoint:
- Links (primary, shipped): host shares an http(s) media URL
(e.g. a Jellyfin Download URL). Viewers stream it directly; the
server is sync-authority only. The URL travels as
media.urlin room broadcasts and as welcomemediaUrl(set to the shared URL when the server holds no file). This covers most sessions — proxying is usually unnecessary. - File relay (secondary, local-file half shipped): host serves
bytes from their own machine through the server's token-gated
Range endpoint
GET /m/<sid>/<token>/<name>. Viewers (and the host via loopback) play the server URL. This is the fallback for the whitelist case: viewers cannot reach the source, but the server/host can (e.g. Jellyfin allowlists the host, not the viewers). No server storage — bytes stream file→socket, config + logs only, never to disk.
Backends behind MediaSource (packages/core/lib/src/media_source.dart):
FileMediaSource (local file — SHIPPED as an abstraction, but the
live route serves SessionMedia.file directly via file.openRead;
nothing in packages/ references FileMediaSource today);
HttpSource (server fetches a remote URL and re-serves it —
DEFERRED, Phase 8). resolveMediaRequest stays
framework-agnostic so both ride the same tested Range table.
Deferred: remote-host upload tunnel (NOT active protocol)
If a VPS tier ever needs a remote host to push bytes to a server
it cannot serve from disk, the reserved design is the brief's WSS
tunnel: host dials out, JSON control frames
(hello/welcome+mediaUrl, open{id,start,end}, ack, cancel,
eof/err) plus binary [u32 BE reqId][payload] chunks —
chunk 256 KiB, credit window 8 (≈2 MiB in flight), max 16 concurrent
open, relay acks only after socket flush (real backpressure).
RelayConstants.chunkBytes/creditWindowChunks/maxConcurrentOpen
are reserved for this. No code exists; direct-serve covers v1
(host IS the server, opens ports itself).
On file relay, host playback goes through loopback like everyone
else's (http://127.0.0.1:<port>/…) — same code path as remote
viewers, so server bugs surface on the host screen first. (On
links the host plays the direct URL.) Deployment ladder,
cheapest first: host's own PC (LAN, or internet with port-forward) →
same binary --headless on a friend's always-on box → cheap VPS
(CGNAT escape; TLS/Caddy deferred with this tier).
Sync: heavily Syncplay-inspired + original readiness design, NOT 1:1
(audit done; deviations: clock never moves from reports, no
pause-on-leave, force-for-all, quorum gate — see HANDOFF §2).
Additive namespaced extensions only, proto bump + PROTOCOL.md same-commit.
No server storage: config + logs only. Media never touches disk.
Health reporting (standing rule)
Every failure must display. Nothing stalls silently.
- Server
/healthreturns machine-readable status: ok, room, viewer count, uptime, media, bytes served, version. The app reads it. - App surfaces all failure states in UI with cause + what-to-do: connecting/reconnecting, wrong secret (401), kicked (403), session gone (410), host offline / buffering (503), starvation ("waiting for X — N.N s buffered"). Backed by a structured status signal (severity + message), not bare strings.
- Errors log via
debugPrint+ stack trace at the boundary, then convert to a displayable state. No swallowed exceptions.