Link video/music playback to the OS's own media controls (Windows SMTC, macOS Now Playing, ...) #22

Merged
kyuno merged 1 commit from feature/media-session-integration into master 2026-09-17 23:43:32 +02:00
Owner

Summary

Asked for directly: link whatever's playing to Windows' own media popup. Implemented via the standard Media Session API (navigator.mediaSession), which the same code also surfaces to macOS' Now Playing widget and Android's notification for free, on whichever browser implements it (Chromium family — Firefox never has).

  • New mediaSession.ts: a thin wrapper (every export is a one-line feature-detection guard around the real browser call), shared by views/player/Player.ts (video) and musicBar/MusicBar.ts (music) — the two can never both be active at once, so there's no risk of them fighting over this single global.
  • Metadata/position/playback-state hook into each player's own existing single-source-of-truth functions (updatePlayPauseBtn/updateProgressBar in Player.ts, setPlayIcon/updatePositionDisplay in MusicBar.ts) rather than adding parallel call sites.
  • Play/pause/seek are wired once per party — the handlers close over local state and read it fresh on every OS-triggered call, so they keep working correctly through a live host handoff without needing to know it happened. A non-host's play/pause on the OS control routes through the exact same requestPause/requestResume the in-page request toolbar already uses (now factored out so both can call them); seeking is gated on isHost the same way the in-page seek bar already is (factored into a shared seekTo).
  • "Next" is handled separately: a registered nexttrack handler is what makes the OS show that control at all, so its presence tracks whatever the in-page equivalent already tracks (isHost && hasNextQueued for video's #nextBtn, music.current for the vote-skip button — clicking the OS's own "next" for music casts one skip vote, same as the in-page button).
  • Cleared entirely (clearMediaSession) on leaving a party/page; a lighter setMediaSessionIdle (metadata/state only) covers a listen party's queue running empty while the party itself stays open, since play/pause/seek stay meaningful the instant something plays again.

Test plan

  • npm run typecheck (client) — clean
  • npm run build (client) — clean
  • mediaSession.ts checked directly against the real API in a real Chromium browser: metadata resolves to a genuine MediaMetadata with the right fields and a resolved artwork URL, playback/position state apply without throwing, an invalid (non-positive/NaN) duration is silently skipped rather than reaching the call that would throw on it, action-handler registration doesn't throw with or without onSeek, and both setMediaSessionIdle/clearMediaSession correctly reset metadata to null and playback state to 'none'.
  • Player.ts's/MusicBar.ts's own wiring is one-line calls into those already-verified functions, at points each file already treats as its single source of truth for that piece of state — not independently exercised against a live party (would need mocking the entire socket layer for no more real signal than reading the call sites gives), and the actual OS-level popup itself isn't something this sandbox can display. Flagged the same way in docs/architecture.md — worth a real Windows/Chrome check once deployed.

🤖 Generated with Claude Code

## Summary Asked for directly: link whatever's playing to Windows' own media popup. Implemented via the standard Media Session API (`navigator.mediaSession`), which the same code also surfaces to macOS' Now Playing widget and Android's notification for free, on whichever browser implements it (Chromium family — Firefox never has). - New `mediaSession.ts`: a thin wrapper (every export is a one-line feature-detection guard around the real browser call), shared by `views/player/Player.ts` (video) and `musicBar/MusicBar.ts` (music) — the two can never both be active at once, so there's no risk of them fighting over this single global. - Metadata/position/playback-state hook into each player's own existing single-source-of-truth functions (`updatePlayPauseBtn`/`updateProgressBar` in Player.ts, `setPlayIcon`/`updatePositionDisplay` in MusicBar.ts) rather than adding parallel call sites. - Play/pause/seek are wired **once** per party — the handlers close over local state and read it fresh on every OS-triggered call, so they keep working correctly through a live host handoff without needing to know it happened. A non-host's play/pause on the OS control routes through the exact same `requestPause`/`requestResume` the in-page request toolbar already uses (now factored out so both can call them); seeking is gated on `isHost` the same way the in-page seek bar already is (factored into a shared `seekTo`). - "Next" is handled separately: a *registered* `nexttrack` handler is what makes the OS show that control at all, so its presence tracks whatever the in-page equivalent already tracks (`isHost && hasNextQueued` for video's `#nextBtn`, `music.current` for the vote-skip button — clicking the OS's own "next" for music casts one skip vote, same as the in-page button). - Cleared entirely (`clearMediaSession`) on leaving a party/page; a lighter `setMediaSessionIdle` (metadata/state only) covers a listen party's queue running empty while the party itself stays open, since play/pause/seek stay meaningful the instant something plays again. ## Test plan - [x] `npm run typecheck` (client) — clean - [x] `npm run build` (client) — clean - [x] `mediaSession.ts` checked directly against the real API in a real Chromium browser: metadata resolves to a genuine `MediaMetadata` with the right fields and a resolved artwork URL, playback/position state apply without throwing, an invalid (non-positive/`NaN`) duration is silently skipped rather than reaching the call that would throw on it, action-handler registration doesn't throw with or without `onSeek`, and both `setMediaSessionIdle`/`clearMediaSession` correctly reset metadata to `null` and playback state to `'none'`. - Player.ts's/MusicBar.ts's own wiring is one-line calls into those already-verified functions, at points each file already treats as its single source of truth for that piece of state — not independently exercised against a live party (would need mocking the entire socket layer for no more real signal than reading the call sites gives), and the actual OS-level popup itself isn't something this sandbox can display. Flagged the same way in docs/architecture.md — worth a real Windows/Chrome check once deployed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kyuno added 1 commit 2026-09-17 23:42:10 +02:00
Link video/music playback to the OS own media controls (Windows SMTC, macOS Now Playing, ...)
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
6582ad855b
New mediaSession.ts wraps the standard Media Session API - shared by
Player.ts and MusicBar.ts, which can never both be active at once so
there is no risk of them fighting over this one global. Metadata/
position/playback-state hook into each players own existing single
source of truth (updatePlayPauseBtn/updateProgressBar, setPlayIcon/
updatePositionDisplay) instead of adding parallel call sites. Play/
pause/seek are wired once and read isHost/conn fresh on every
OS-triggered call, so they keep working through a live host handoff;
a non-host play/pause routes through the same requestPause/
requestResume the request toolbar already uses, and seek is gated on
isHost the same way the in-page seek bar already is. Next is handled
separately since a registered nexttrack handler is what makes the OS
show that control at all - its presence tracks whatever the in-page
equivalent already tracks (isHost && hasNextQueued for video, a vote
skip for music). Cleared entirely on leaving a party/page, or just to
idle (metadata/state only) when a listen partys queue runs empty but
the party itself is still open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno merged commit 071184e3c6 into master 2026-09-17 23:43:32 +02:00
kyuno deleted branch feature/media-session-integration 2026-09-17 23:43:32 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/watchparty#22
No description provided.