Emby Server plugin: watch party feature (session sync via Emby's remote-control commands) + companion web control panel
Find a file
Nicolas b6019bd211
All checks were successful
Publier l'image Docker / publish (push) Successful in 7s
Live lobby, host handoff reflected live, remove join codes, fix player scroll
Three requested changes plus one found-in-passing bug fix:

1. Home page's "watch parties en cours" is now a live Socket.IO
   subscription (connectLobby -> "lobby" room), not a one-shot fetch -
   updates the instant anything changes anywhere (create/join/leave).
   Host name + member count come from the snapshot; elapsed time ticks
   purely client-side from a createdUtc timestamp (no point pushing a
   recomputed duration every second for every viewer). home.js returns
   this subscription from its router cleanup, same leak-prevention
   pattern as the player's socket.

2. Host handoff is now live end to end, not just correct in the data
   model. PartyManager.leave already promoted another member and
   destroyed an empty party (that part predates this change) - what
   was missing was any way for an already-open follower player to find
   out it became host. applyLeave (shared by the socket leave/
   disconnect handlers and the REST leave endpoint, so this and the
   lobby broadcast only live in one place) now emits a personal
   "hostChanged" to the new host's own socket; player.js's becomeHost()
   unlocks controls live. becomeHost is written to tolerate being
   called more than once safely (addEventListener no-ops on a repeat
   identical listener; the heartbeat setInterval is explicitly guarded
   since it has no such idempotence - calling it twice would leak the
   first interval, the exact bug class the previous pass fixed).

3. No more join codes - PartyManager no longer generates or tracks
   one. The live lobby list is the only discovery mechanism now; a
   code copied out of band doesn't add anything on top of a list
   that's already always current. POST /api/parties/:partyId/join
   replaces :joinCode; the "Rejoindre avec un code" UI is gone.

Found while touching this: the home page's "Rejoindre" button used to
navigate to the player WITHOUT ever calling the join endpoint first -
it loaded fine (GET /api/parties/:id never checked membership) but the
realtime "join" was silently rejected server-side (not an actual
member), so sync never worked for anyone joining this way. Fixed to
await joinParty() before navigating, matching how the library grid's
tiles already did it.

Also: .playerWrap was width:100%/height:100vh inside .mainContent's own
padding, so the two stacked and made the page taller than the
viewport - an outer scrollbar despite the video already being full-
height. Changed to position:fixed/inset:0 so it fills the real
viewport regardless of ancestor padding.

Verified live against a real running server (see session history):
lobby subscription renders host/member-count/ticking-elapsed correctly
from a faked "parties" push (createdUtc 2:05 in the past rendered
"2:06" after ~1s more, proving the client-side tick is real, not
static); the join-then-navigate fix calls the join endpoint before
navigating; .playerWrap computes to position:fixed with all four
insets at 0.

docs/architecture.md has the full writeup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:47:12 +02:00
.forgejo/workflows Add Forgejo Actions pipeline to build/publish the Docker image 2026-09-13 20:59:55 +02:00
.github/workflows Scaffold Emby Watch Party plugin 2026-09-13 14:02:43 +02:00
client Live lobby, host handoff reflected live, remove join codes, fix player scroll 2026-09-13 23:47:12 +02:00
docs Live lobby, host handoff reflected live, remove join codes, fix player scroll 2026-09-13 23:47:12 +02:00
server Live lobby, host handoff reflected live, remove join codes, fix player scroll 2026-09-13 23:47:12 +02:00
src/WatchParty Fix the real crash: partyPage.html was missing data-controller 2026-09-13 20:17:14 +02:00
.gitignore Untrack .claude/ (local tooling config, not project source) 2026-09-13 20:53:31 +02:00
docker-compose.yml Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
LICENSE Scaffold Emby Watch Party plugin 2026-09-13 14:02:43 +02:00
README.md Live lobby, host handoff reflected live, remove join codes, fix player scroll 2026-09-13 23:47:12 +02:00

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 for the full design, including why this replaced an earlier Emby-plugin attempt (still in 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.

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 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

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/):

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

  • Design + scaffold the standalone app
  • Proxy all Emby access server-side; Emby-themed UI (sidebar, carousels)
  • TV show navigation (Series → Seasons → Episodes) + episode auto-queue
  • Player language/subtitle/quality pickers
  • 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
  • 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.