All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
Two features requested together: a semi-transparent overlay showing who's
in the party, and a way for a non-host to ask the host to pause.
- server/src/socketHandler.js: new "requestPause" event (anyone, throttled
5s per member server-side via member.lastPauseRequestUtc - the real
guard, independent of whatever the client does) broadcasts
"pauseRequested" {userId, userName} to the whole party room, not just the
host, so everyone sees why playback is about to pause.
- client/js/party.js: connectParty() grows an onPauseRequested callback and
the returned connection a requestPause() method, same shape as the
existing next()/sendPlaystate().
- client/js/views/player.js: renderMembers() now also rebuilds a
#presenceOverlay of one circle per member (initials from userName,
colorForName() picks a deterministic background from a small palette by
hashing the name - same person, same color, no server-side assignment
needed). "Which circle is mine" compares userName against the auth
singleton embyApi.js already populates, matching how the rest of the
client avoids keying UI logic on userId directly. Your own circle, only
when you're not host, is clickable and calls requestPause() (client-side
cooldown too, purely to avoid a click that would visibly do nothing - the
server-side one is what actually matters). Receiving pauseRequested shows
a short auto-clearing banner and pulses the matching circle for everyone,
including the requester and the host.
- client/css/app.css: .presenceOverlay/.avatar (bottom-left, opacity .75 is
the "semi transparence" itself) and the clickable/sent/pulse states,
.pauseRequestBanner.
Verified with mocked members/pauseRequested Socket.IO events against the
real running local server (see session history): initials/colors render
correctly, the host's own circle never gets the clickable affordance,
rapid repeat clicks collapse to exactly one requestPause emission (three
synchronous clicks -> one new emitted event), a pauseRequested event
banners and pulses the right circle. Re-checked the pre-existing host path
(becomeHost, settings menu, next-episode button) still works unchanged -
this pass only touched renderMembers() and init()'s connectParty() call.
docs/architecture.md has the full writeup. Also fixed two roadmap
checkboxes in README.md that should have been checked off by the previous
commit (live lobby/host handoff/no join codes) but were missed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
127 lines
5.8 KiB
Markdown
127 lines
5.8 KiB
Markdown
# Watch Party for Emby
|
|
|
|
A standalone web app for watching the same title, in sync, with other users
|
|
of the same Emby server. It reads your Emby catalog and streams your Emby
|
|
media, but it's its own app with its own player — not an Emby plugin.
|
|
|
|
See [docs/architecture.md](docs/architecture.md) for the full design,
|
|
including why this replaced an earlier Emby-plugin attempt (still in
|
|
[src/WatchParty/](src/WatchParty/) for reference — see below).
|
|
|
|
## How it works
|
|
|
|
1. Sign in with your Emby account (just username/password — the server
|
|
address is fixed server-side, see Deploying below).
|
|
2. A sidebar lists your Emby libraries; the home page shows every ongoing
|
|
watch party live (host, how long it's been running, member count) plus a
|
|
"recently added" row per library, Emby-style. No join codes — if a party
|
|
is running, it's already on the list, updating in real time.
|
|
3. Pick a movie/episode tile and hit "Lancer une watch party" (or drill into
|
|
a Series' seasons/episodes first) — you become the host — or hit
|
|
"Rejoindre" on any party already listed on the home page.
|
|
4. The host's play/pause/seek drives everyone else's player automatically;
|
|
starting from an episode queues the rest of the series so the host can
|
|
just hit "Épisode suivant" instead of restarting a party each time. If
|
|
the host leaves, the party hands off to another member automatically
|
|
(their player unlocks live, no reload needed) — only closing once
|
|
everyone's gone.
|
|
5. Everyone present shows up as a small avatar overlay on the video (their
|
|
initials, on the player itself). If you're not the host, clicking your
|
|
own avatar asks the host to pause — everyone sees the request, including
|
|
the host, without needing chat.
|
|
|
|
Everything — catalog, images, video, sync — flows through this app's own
|
|
backend, which proxies whatever it needs from Emby server-side. The browser
|
|
never talks to Emby directly and never holds an Emby access token; it only
|
|
ever has a session cookie for this app.
|
|
|
|
Music isn't part of this yet — it needs its own player and a collaborative
|
|
playlist, different enough to be its own pass. The sidebar already lists a
|
|
Music library if you have one; the page just says so for now.
|
|
|
|
## Status
|
|
|
|
Login, and the rendering/navigation logic for the sidebar/home/library/series
|
|
pages, have been exercised end to end (login against the real Emby server
|
|
with intentionally-wrong credentials; the pages against mocked API
|
|
responses). See [docs/architecture.md](docs/architecture.md#assumptions-to-verify)
|
|
for exactly what that does and doesn't confirm — the real Emby response
|
|
shapes this now depends on (library `CollectionType`, seasons/episodes,
|
|
`PlaybackInfo`'s media streams) and actual HLS playback are still unverified.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
server/ Node.js backend: proxies ALL Emby access (auth, catalog,
|
|
images, HLS streaming) and coordinates party/room state +
|
|
realtime sync. The browser only ever talks to this.
|
|
src/index.js Express app + REST endpoints
|
|
src/embyAuth.js server-side login against Emby + session cookies
|
|
src/embyProxy.js generic Emby request/response proxying (incl. HLS)
|
|
src/socketHandler.js Socket.IO protocol (see comments in the file)
|
|
src/partyManager.js in-memory room state
|
|
|
|
client/ Static frontend (plain JS, ES modules, no build step).
|
|
Talks only to server/ - never to Emby directly.
|
|
js/shell.js persistent sidebar + #mainContent the router swaps
|
|
js/tiles.js poster-tile rendering shared by home.js and library.js
|
|
js/embyApi.js calls our own /api/* routes (auth, catalog, playback options)
|
|
js/party.js our own backend's party REST + Socket.IO client
|
|
js/views/ one file per screen: login, home, library, series, player
|
|
|
|
src/WatchParty/ The earlier Emby-plugin attempt. Builds fine and installs,
|
|
but Emby has no supported extension point for a player
|
|
button, and even menu integration required reverse-
|
|
engineering undocumented internals - see
|
|
docs/architecture.md for the full story. Kept for
|
|
reference; not the active direction.
|
|
|
|
docs/architecture.md full design doc for the standalone app
|
|
```
|
|
|
|
## Running locally
|
|
|
|
```bash
|
|
cd server
|
|
npm install
|
|
EMBY_SERVER_URL=https://your-emby-server npm start
|
|
```
|
|
|
|
Then open `http://localhost:3000`.
|
|
|
|
## Deploying
|
|
|
|
Build the Docker image from the **repo root** (it needs both `server/` and
|
|
`client/`):
|
|
|
|
```bash
|
|
docker build -f server/Dockerfile -t watchparty .
|
|
docker run -d --name watchparty -p 3000:3000 -e EMBY_SERVER_URL=https://theater.media.kyuno.fr watchparty
|
|
```
|
|
|
|
Then put it behind your existing reverse proxy like everything else
|
|
(`theater.media.kyuno.fr`, `git.dev.kyuno.fr`, ...), e.g.
|
|
`watchparty.kyuno.fr` → `http://<container>:3000`.
|
|
|
|
## Roadmap
|
|
|
|
- [x] Design + scaffold the standalone app
|
|
- [x] Proxy all Emby access server-side; Emby-themed UI (sidebar, carousels)
|
|
- [x] TV show navigation (Series → Seasons → Episodes) + episode auto-queue
|
|
- [x] Player language/subtitle/quality pickers
|
|
- [x] Socket.IO instead of raw WebSockets; fixed two real memory leaks
|
|
(dead-connection cleanup server-side, view teardown on navigation
|
|
client-side) — see docs/architecture.md
|
|
- [x] Live "watch parties en cours" list (host, elapsed time, member count),
|
|
live host handoff, no join codes
|
|
- [x] Presence overlay (avatars) + "request pause" in the player
|
|
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
|
|
- [ ] Music: dedicated player, artist/album browsing, collaborative playlist
|
|
- [ ] Tune transcoding target / support direct play when possible
|
|
|
|
No chat, no OIDC — deliberately out of scope (Emby-only auth was the
|
|
explicit choice; see docs/architecture.md).
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|