Viewers who cannot reach the media get it through the server: a file on
any member's computer, a link passed on by a member ("Through me") or
fetched by the server ("Server fetches it", password-protected servers
only), and Jellyfin items for guests without an account, sourced by
whoever put them up with their own sign-in (or a pasted link's key),
which never leaves their machine. Fixes guests on a dedicated server
without a Jellyfin login being blocked.
The source dials /r/.../source (works behind NAT) and answers pulls with
an adaptive in-flight window (2 x rate x min RTT, 256 KiB..8 MiB). The
server keeps a memory-only buffer of 1 MiB slices sized like a viewer's
mpv buffer (pinned head/tail, 64 MiB back, 256 MiB ahead), fills it with
one sequential pull at a time (blocked viewers first, newest request
only), fetches nothing while nobody watches through it, and drops it on
the next swap or stop. Measured with mpv through shaped links
(tool/relay_e2e.dart): 400 Mbit/s seeks ~0.03 s; 40 Mbit/s no stalls,
3 viewers cost the source ~1x the bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Host/swap send refs only; pasted Jellyfin links become refs; the host
hands the relay its own app-obtained credential.
- Each viewer resolves with their own account (token in a header) or
via the relay, else a clear blocked card with Link account.
- Per-account progress reporting (Playing/Progress/Stopped), 401 marks
the account expired.
- Previous/next episode in the controls (< > / Shift+P/N), floating
Next episode pill near the end, auto-advance countdown (one
designated swapper), track carry-over per series.
- Swap panel uses the shared MediaPicker; browser location remembered
per server and opens on the current series.
- Fix: states are applied one at a time (a stale pause could land
after a newer play).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Media can be a Jellyfin ref {serverId, itemId, mediaSourceId, title,
subtitle, runtimeSecs}; the server never stores or broadcasts a URL
or token for it.
- Optional server-side Jellyfin credential (serve map / --jellyfin
file) enables GET /j/<sid>/<token>/<itemId>: range-faithful proxy of
the active item only, exact client header fields, upstream aborted on
disconnect, bytes counted. welcome.relayUrl / state.relay advertise it.
- Members report jf access (own account vs relay).
- /m now sends the real Content-Type (Relic overrode it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- User seeks are intercepted at the player (SyncPlayer), previewed
locally, sent once 300 ms after the drag ends; own echoes are matched
by seekSeq and never re-applied; the landed check can't reapply a
stale target. 1 s position poll removed.
- ping/pong clock offset (min-RTT ClockSync); positions use server time.
- Reports carry duration; buffer gate is min(4 s, remaining - 2 s);
new finished transport instead of a HELD stall at the end.
- cache/readahead 150 s, 256 MiB demuxer cache, pins read back.
- Resumed reports put a stale ready member back in the set; hello.resume
takes over the old member after an unclean drop (no ghost).
- Docs: roadmap rendering findings (HW path works; shaders verified).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- serve(Map config, events) is the one server entrypoint: the app
hosts it via Isolate.spawn (crash surfaced on the Host card, logs
forwarded as [server]); bin/server.dart is the dedicated server,
compiled into the bundle as watch-together-server. --headless gone.
- Host identity via a per-hosting hostKey in hello, replacing the
loopback heuristic.
- Rename room -> server across code, files and wire (t:"state",
/health name); protocol bumped to 5.
- CI compiles the server binary; isolate smoke tests in server and
desktop suites. Docs updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reject non-finite numerics at protocol parse and in the engine
(a 1e400 seek became Infinity and broke every broadcast).
- Reject empty swaps; sanitize URL-looking media names.
- Blank password = open server; defaults no longer 'change-me'.
- Loopback peers (the in-process host) can't be kicked.
- /health no longer exposes media/transport; file URLs use the
host the client dialled instead of 127.0.0.1.
- Count served bytes as streamed, not as requested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>