Emby Server plugin: watch party feature (session sync via Emby's remote-control commands) + companion web control panel
Find a file
Nicolas 6ff9aa29e2 Pivot to a standalone app - Emby plugin hit a hard wall
No plugin API exists for adding a button to Emby's player or item
detail page (confirmed by an Emby developer on their own forum, and
by research since). The only way to reach that would be patching
Emby's shipped web client files directly - outside the plugin,
breaks on every image update, needs manual reapplication. Not worth
the ongoing cost, per discussion.

New direction: a standalone app with its own player. Reads Emby's
catalog/streams directly from the browser (no plugin, no menu
integration fights); a small Node backend only coordinates party
membership and relays playback state over WebSocket - it never
touches a media byte. Auth is Emby-only, as chosen: the browser signs
in against Emby directly and this backend validates whatever token it
gets handed by asking Emby itself (GET /Users/{userId}), never seeing
a password.

Control model changed too: now that we own the player, host controls
playback and everyone else follows (event-driven play/pause/seek +
a 3s heartbeat, drift-corrected on the follower side) - the old
plugin's "everyone keeps their own controls, shared reference
position" was specifically a concession to nudging pre-existing Emby
app sessions we didn't control, which no longer applies.

- server/: Express + ws backend (party lifecycle, WebSocket protocol,
  Emby token validation)
- client/: plain-JS/ES-modules frontend (login, search, item detail,
  party player) - no build step
- Smoke-tested locally end to end against a stubbed Emby URL: server
  starts, routing/auth-gating works, error handling degrades
  gracefully (network failure, invalid token) - see the session
  transcript for the actual browser test run
- docs/architecture.md rewritten around the new design, including why
  the plugin was abandoned and a fresh "assumptions to verify" list
  (CORS between this app's origin and Emby's is the biggest unknown)
- src/WatchParty/ (the Emby plugin) kept as-is for reference, no
  longer the active direction

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:48:55 +02:00
.claude Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
.github/workflows Scaffold Emby Watch Party plugin 2026-09-13 14:02:43 +02:00
client Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
docs Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
server Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +02:00
src/WatchParty Fix the real crash: partyPage.html was missing data-controller 2026-09-13 20:17:14 +02:00
.gitignore Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +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 Pivot to a standalone app - Emby plugin hit a hard wall 2026-09-13 20:48:55 +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.
  2. Search your Emby catalog for a title.
  3. Start a party (you become the host) or join one with a 6-character code.
  4. The host's play/pause/seek drives everyone else's player automatically, over a small backend that only ever coordinates timing — actual video always streams directly from your Emby server to each viewer's browser.

Status

Freshly scaffolded and smoke-tested (server starts, routes render, error handling works — see git history) with a stubbed Emby server. Not yet run against a real Emby server. The biggest open question is CORS: the browser calls Emby directly from a different origin, which needs Emby (or its reverse proxy) to answer that cross-origin requests are allowed — see docs/architecture.md.

Project layout

server/           Node.js backend: party/room coordination + WebSocket sync.
                  Never touches media bytes - only validates Emby tokens
                  (by asking Emby itself) and relays playback state.
  src/index.js       Express app + REST endpoints
  src/wsHandler.js   WebSocket protocol (see comments in the file)
  src/partyManager.js  in-memory room state
  src/embyAuth.js    token validation against your Emby server

client/           Static frontend (plain JS, ES modules, no build step).
                  Talks directly to Emby for catalog/images/streaming, and
                  to server/ only for party coordination.
  js/embyApi.js      Emby REST calls (auth, search, images, stream URLs)
  js/party.js        our own backend's REST + WebSocket client
  js/views/          one file per screen (login, browse, item, 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
  • Smoke-test locally (routing, error handling) without a real Emby server
  • Verify against the real Emby server (CORS is the main risk - see above)
  • Try a full party end-to-end with two accounts
  • TV show navigation (seasons/episodes) beyond flat search
  • 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.