The shell (lib/src/shell/) replaces the Home and session screens:
- Party strip: profile, Library / Schedule / Settings tabs, Join and
Host cards (hosting no longer takes media), status-ring avatar stack
with the member list, chat & activity panel, Leave.
- The real session player is mounted once and shown full, as a
draggable corner-snapping mini player, or fullscreen (window_manager,
party and toasts overlaid); the Video is never mounted twice.
- Profile popover with inline rename, colour and a crop picker (256 px
PNG), pushed live to the session; avatar toasts; waiting pill.
- Motion kit (fastEaseInToSlowEaseOut, fade-through, popovers,
mouse-drag/wheel horizontal scrolling).
- Library = the existing picker for now (restyled next round); Schedule
= the new SchedulePage; Settings = left nav over the existing panels.
Protocol 9 (packages/core, packages/server, sync client):
- {"t":"profile"} renames / changes the picture mid-session ("rename",
"avatar" feed actions); names are cleaned the same way at hello.
- state.members carry avatarHash; pictures are served once from
/a/<sid>/<token>/<hash> (PNG/JPEG/WebP/GIF by magic bytes only) and
cached by hash in the app (AvatarCache).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
278 lines
14 KiB
Markdown
278 lines
14 KiB
Markdown
# watch-together
|
||
|
||
Watch a video with friends, in sync, with the quality of a real mpv player.
|
||
Pick from your Jellyfin library, paste any direct video link, or pick a file on your own disk;
|
||
everyone's play, pause and seek stay together.
|
||
|
||
Why not Jellyfin SyncPlay: it runs in the browser and its sync is poor.
|
||
This is a desktop app that drives mpv.
|
||
|
||
Name: `watch-together` is a placeholder and the final name is undecided
|
||
(`sync-relay` is the working name). It appears in file names and the
|
||
update manifest until that is settled.
|
||
|
||
- Desktop only (Linux and Windows), always mpv. No web clients.
|
||
- No transcoding. Everyone plays the original file.
|
||
- Friends trust model: no accounts, no roles. A blank password means an open server.
|
||
|
||
How it fits together: [docs/HOW-IT-WORKS.md](docs/HOW-IT-WORKS.md).
|
||
|
||
## Install and update
|
||
|
||
From the Releases page of this repo:
|
||
|
||
- Linux: `watch-together-<version>-linux-x64.tar.gz`. Extract, run
|
||
`watch_together`. Needs mpv's library installed (`mpv` or `libmpv2`).
|
||
- Windows: `watch-together-<version>-windows-x64.zip`. Extract, run
|
||
`watch_together.exe`. mpv is bundled. New and not yet tried on a real
|
||
Windows desktop; reports welcome.
|
||
|
||
Update from the app: when a newer version exists, an update indicator
|
||
appears in the strip at the top of the window and opens the update card. "Update and restart" downloads,
|
||
verifies, applies in place and restarts; the install folder must be writable
|
||
by you. Settings has Check now and an auto-check toggle. From a terminal:
|
||
`watch_together --upgrade`, or the shipped `update.sh` / `update.bat`. A
|
||
release with a newer protocol is a required update: everyone on a server
|
||
must run the same protocol version, and a mismatch is refused with a message
|
||
telling you to update.
|
||
|
||
## The window
|
||
|
||
One window. Along the top, the strip: your avatar (click it for your
|
||
profile: name, picture with a crop editor, colour, and links to the
|
||
settings), the Library / Schedule / Settings tabs, and the party. Offline
|
||
the party side has Join and Host; in a server it shows everyone's avatar
|
||
with a ring for their state (green ready, amber buffering, grey not ready,
|
||
dimmed reconnecting; a crown on yours when you host, a tower on whoever
|
||
passes the video on). Click the avatars for the member list (force ready,
|
||
kick). Beside them the server and "ready n/m", the chat button with unread
|
||
count, and Leave. Empty strip space drags the window.
|
||
|
||
The player fills the window under the strip. Pick a tab and it shrinks to
|
||
a mini player in a corner: drag it anywhere and it snaps to the nearest
|
||
corner; double-click or ⤢ makes it big again, × stops watching (you stay in
|
||
the server, muted) and the strip's screen button brings it back. F or
|
||
double-click on the picture goes fullscreen (the strip hides, the party
|
||
shows in a corner while paused), Esc leaves it. While playback waits, a
|
||
pill under the picture says for whom ("Waiting for you · press Space when
|
||
ready"). Chat and activity open in a panel on the right (chat button or
|
||
Tab); joins, leaves, chat and other news also pop up bottom left with the
|
||
person's avatar.
|
||
|
||
Your name and picture change for everyone in the server right away; the
|
||
colour is yours only.
|
||
|
||
## Host from the app
|
||
|
||
Host in the strip: set a port and an optional password (blank = anyone can
|
||
join), then Host. The app starts a server inside itself and joins it; you
|
||
are the host and nobody can kick you. Nothing is up yet: pick what to
|
||
watch in the Library tab (below) and "Put it up for everyone". Leaving or
|
||
closing the app stops the server. Friends need to reach your machine: open
|
||
the TCP port on your router (or use a LAN or VPN address). Friends open
|
||
nothing. Ports you hosted on before are one click away in the Host card.
|
||
|
||
If you put up a local file, the server streams it to everyone from your
|
||
disk. Nothing is uploaded or stored anywhere.
|
||
|
||
## Library
|
||
|
||
What to watch: Jellyfin (browse your linked servers, see below), Link
|
||
(paste an http(s) URL; Ctrl+V outside a field pastes it in) or File (path
|
||
or Browse), then "Put it up for everyone" and the player opens. Offline the
|
||
library browses but nothing goes up until you join or host.
|
||
|
||
## Dedicated server
|
||
|
||
For an always-on box (no display needed): the release tarball also contains
|
||
`watch-together-server`, a small standalone server without the app. Run it,
|
||
then everyone (you included) uses Join.
|
||
|
||
```
|
||
./watch-together-server --port 8080 --secret <password>
|
||
```
|
||
|
||
| Flag | Default | Meaning |
|
||
|---|---|---|
|
||
| `--port`, `-p` | 8080 | TCP port, all IPv4 interfaces |
|
||
| `--secret` | empty | password; empty = open server (a warning is printed) |
|
||
| `--name` | `server` | name reported by `/health` |
|
||
| `--media` | none | local file to serve as the starting video |
|
||
| `--advertise-host` | none | host put in file URLs instead of the one each viewer dialled |
|
||
| `--config` | none | YAML or JSON file with the same settings (`port`, `secret`, `name`, `mediaPath`, `advertiseHost`); replaces the flags |
|
||
|
||
It prints `READY port=... name=...` once listening and stops cleanly on
|
||
Ctrl+C or SIGTERM. A dedicated server has no host: anyone can kick anyone.
|
||
To run it as a service, unpack the tarball to `/opt/watch-together` and use
|
||
[deploy/watch-together.service](deploy/watch-together.service) (copy it to
|
||
`/etc/systemd/system/`, edit the flags, `systemctl enable --now watch-together`).
|
||
|
||
## Join
|
||
|
||
Join in the strip. Servers you got into are listed first (up to 6), with
|
||
live status (online and how many are watching, or offline, plus "needs
|
||
update" when the server runs another protocol): click one to rejoin, x to
|
||
forget it. Or enter the host's address, port, and the password if there
|
||
is one, then Enter or Join. Your name and picture come from your profile
|
||
(your avatar at the top left).
|
||
|
||
## Settings
|
||
|
||
The Settings tab (Ctrl+,) has a page per topic: Profile; Player (hardware
|
||
decoding: auto, off or custom, the env var `SYNC_RELAY_HWDEC` overrides
|
||
it, applies on the next join or Reload video; "Experimental: mpv-style
|
||
controls", below; video shaders, see below); Jellyfin & integrations
|
||
(linked Jellyfin servers, below, and playing the next episode
|
||
automatically); Updates; Keyboard shortcuts (read-only); About (the
|
||
version).
|
||
|
||
Keys while the player fills the window: Space = ready / unready, Tab =
|
||
chat & activity, Left/Right = seek 10 s, Shift+Right = skip 1:30,
|
||
PgUp/PgDn = previous / next chapter (as in mpv), F = fullscreen, Esc =
|
||
close the chat or fullscreen, else ask to leave, Ctrl+R = reload video,
|
||
`<` / `>` or Shift+P / Shift+N = previous / next Jellyfin episode,
|
||
Ctrl+1..6 = Anime4K mode A, B, C, A+A, B+B, C+A, Ctrl+0 = shaders off.
|
||
Every seek, skip and chapter jump moves everyone.
|
||
|
||
Controls: the seek bar marks chapter starts with a diamond; hovering it
|
||
shows the time and the chapter title. The chapters button lists them
|
||
(current one checked; click to jump). "1:30" skips ahead 1:30. Audio and
|
||
Subtitles buttons (with the track count) list tracks as "Japanese · E-AC3
|
||
2.0 · title"; subtitles have Off.
|
||
|
||
Experimental mpv-style controls (Settings → Player, off by default,
|
||
switches right away): video controls modeled on mpv's uosc theme. A thick
|
||
timeline across the bottom shows the played part, the cached part ahead
|
||
(hatched), chapter starts as diamonds and the time; hovering it shows the
|
||
chapter title and the time there. With the controls hidden it stays as a
|
||
thin line. Above it flat buttons: ≡ menu, Subtitles and Audio (with the
|
||
track count), the shader badge (the active Anime4K mode, e.g. "A HQ"),
|
||
ready, chapters, 1:30, previous / next episode, change video, volume and
|
||
fullscreen; chat and back-to-library top right. Keys and skip buttons work
|
||
the same.
|
||
|
||
Skipping: during the opening a "Skip OP" button floats in the corner (it
|
||
jumps to the end of the opening), during the ending "Skip ED" (to the end
|
||
of the ending only; what comes after is yours to skip). The opening and
|
||
ending come from Jellyfin's intro / outro segments when your Jellyfin has
|
||
them (needs a segment plugin, e.g. Intro Skipper), else from chapter names
|
||
(OP, Opening, Intro / ED, Ending, Credits, Outro). Without an opening, on
|
||
anything longer than 20 minutes, "Skip 1:30" floats there during the first
|
||
6 minutes.
|
||
|
||
## Ports
|
||
|
||
| What | Default | Notes |
|
||
|---|---|---|
|
||
| Sync WebSocket and `/health` | TCP 8080 | plain `ws://`, no TLS |
|
||
| Local file streaming | same port | path `/m/...`, needs a session token |
|
||
|
||
Only the host needs the port reachable.
|
||
|
||
## Jellyfin
|
||
|
||
Settings > Jellyfin & integrations > Add server: type the address, then sign in with
|
||
Quick Connect (the app shows a code; approve it in Jellyfin under Profile >
|
||
Quick Connect) or your username and password. Several servers can be
|
||
linked; each shows its status, and a star picks the default. Sign out
|
||
revokes the app's sign-in on the server. If a sign-in expires the server
|
||
shows "Reconnect" instead of disappearing.
|
||
|
||
The Library's Jellyfin source is a library browser: Continue watching (with
|
||
time left), Next up and the latest per library on the start page; library
|
||
grids with sort (name, date added, release date, rating, random) and
|
||
Unwatched / Favorites filters; series pages with seasons, episode progress
|
||
and a "Next up" button; details with the file's tracks; search. Clicking a
|
||
movie or episode picks it. On wide windows a side pane shows details of
|
||
whatever is under the mouse.
|
||
|
||
Keys in the browser: arrows move, Enter picks or opens, I = details,
|
||
Esc / Backspace / Alt+Left = back (Esc first clears a search), / or Ctrl+F =
|
||
search.
|
||
|
||
Putting up a movie or episode sends only the item to the server (which
|
||
Jellyfin server and which item), never a link or your sign-in. Pasting a Jellyfin link (web page or stream link) does the
|
||
same when the app can reach that Jellyfin; otherwise it is shared as a
|
||
plain link.
|
||
|
||
Watching:
|
||
- Everyone with their own account on that Jellyfin server plays the file
|
||
directly with their own sign-in. Jellyfin tracks progress per account:
|
||
your watched state, Continue watching and resume point follow what you
|
||
saw (reported on start, every 10 s, on pause, resume, seek and track
|
||
change, and when it stops).
|
||
- Friends without an account watch through the server: whoever put the
|
||
item up passes it on automatically, with their own sign-in, which never
|
||
leaves their computer (only the current item, only to people in the
|
||
session). They show "via host" in the member list (click the avatars). When the app hosts,
|
||
or a dedicated server has its own login (`--jellyfin <file>`, see
|
||
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), Jellyfin relay), the server
|
||
fetches it itself instead.
|
||
- If nobody can pass it on (the person who put it up left), the app says
|
||
"No Jellyfin account here for ..." with a button to link one; it plays as
|
||
soon as you are signed in.
|
||
|
||
Episodes: the ⏮ / ⏭ buttons next to the controls (or `<` / `>`,
|
||
Shift+P / Shift+N) put up the previous or next episode for everyone. From
|
||
the ending (when it is known, see Skipping above; else in the last two
|
||
minutes) a "Next episode" button floats in the corner. When an
|
||
episode ends, a card shows what is next with Play next; with "Play the
|
||
next episode automatically" (Settings, on by default) it counts down 10 s
|
||
first, and Cancel stops it on your screen (seeking back stops it for
|
||
everyone). Anyone with an account can put the next one up; friends
|
||
watching via the host cannot.
|
||
|
||
Audio and subtitles: the Audio and Subtitles buttons in the controls pick tracks.
|
||
Your choice is remembered per series (and as a default) and the closest
|
||
match (language, then title, then codec and channels) is selected on the
|
||
next episode. This is per person; nobody else is affected.
|
||
|
||
"Change video" in the player's controls opens the Library tab (the player
|
||
goes mini): the library starts on the series you are watching with the
|
||
current season, else where you left it.
|
||
|
||
Anyone can put up a file from their own computer, also on a dedicated
|
||
server: it streams from your computer through the server to everyone (the
|
||
server keeps a few minutes in memory, so seeks and skipping the opening
|
||
are quick). Keep the app open while it plays. For a link you choose how
|
||
the others get it:
|
||
- Everyone fetches it (default): each app plays the link itself.
|
||
- Server fetches it: the server downloads it once for everyone, for links
|
||
that are slow or blocked for some. The server must be able to reach it,
|
||
and it must have a password (an open server refuses: anyone could make
|
||
it fetch addresses only it can reach).
|
||
- Through me: your computer downloads it and passes it on, for links only
|
||
you can open.
|
||
|
||
The last two need a link to a video file (not a page like YouTube) and
|
||
never show the link to anyone. Your upload has to beat the video's
|
||
bitrate: a 25-minute 2 GB episode is about 11 Mbit/s.
|
||
|
||
## Video quality and shaders
|
||
|
||
Each viewer can run mpv GLSL shaders on their own picture; nobody else is
|
||
affected. The app ships [Anime4K](https://github.com/bloc97/Anime4K) v4.0.1
|
||
(MIT) with the presets of its mpv guide: Mode A (1080p sources), B (720p),
|
||
C (480p), A+A, B+B, C+A, each as HQ (strong GPUs, GTX 1080 and up) or Fast
|
||
(the guide's low-end set). "Custom shaders" runs your own `.glsl` files.
|
||
|
||
Pick one with the ✨ button in the player's control bar (also in
|
||
fullscreen) or Ctrl+0..6; the change applies live and is saved, so the next
|
||
session starts with it. Settings → Player sets the default,
|
||
HQ or Fast (used by Ctrl+1..6) and the custom files. Shaders cost GPU time
|
||
every frame and need hardware video rendering: on media_kit's software
|
||
fallback they do nothing, and the app says so. A shader that is missing or
|
||
fails to load shows a toast and switches shaders off.
|
||
|
||
## Troubleshooting
|
||
|
||
- "host offline - check address and port": wrong address, or the port is not open.
|
||
- "wrong server password": password mismatch.
|
||
- "host runs a different version": update the app on both sides.
|
||
- Picture frozen but sound fine: Ctrl+R reloads the video; the app also does it automatically.
|
||
- Everyone waits at "waiting for X": X has not buffered 4 seconds yet or has not pressed Space.
|
||
- Windows: the build is new; if something is off there, say so.
|
||
|
||
For contributors: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md),
|
||
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/PROTOCOL.md](docs/PROTOCOL.md).
|