Global Playback — Architecture (2026-FEB)
Current-state documentation of the Global Playback system and its related components and consumers. For past or proposed changes, see docs/Plans/ (e.g. Global Playback Architecture Improvements.md, Global Media Renderer.md).
1. Overview and purpose
- Single global playback: Only one audio/video stream plays at a time across the app.
- Persistent bottom bar: NPR/SoundCloud-style bar fixed at the bottom. Playback survives navigation and view switches (e.g. carousel vs grid/list) because the actual
<audio>and<video>elements live in one place: GlobalMediaRenderer.
2. Architecture diagram
- GlobalPlaybackProvider (root) provides context.
- GlobalMediaRenderer: Hidden
<audio>and<video>; driven by context viauseGlobalAudioElementanduseGlobalVideoElement; no DOM polling. - GlobalPlaybackControl: Fixed bar; reads context; renders MediaProgress for seekable items.
- MediaProgress: Reads
currentTime,duration,setSeekTofrom context; no polling. - Data flow: Context state/actions drive renderer and bar;
timeupdateand seek go through context; position is persisted in context’ssetCurrentTimevia packages/site/lib/playback-position.ts.
3. Context: state and actions
Reference: packages/site/contexts/GlobalPlaybackContext.tsx.
State
| State | Description |
|---|---|
currentItem | Currently playing CarouselItem or null |
playlist | Array of CarouselItem |
currentIndex | Index of current item in playlist |
isPlaying | Play/pause |
isMuted | Mute |
volume | 0–1 |
currentTime | Playback position (seconds) |
duration | Total duration (seconds) |
seekTo | Requested seek time (seconds) or null |
playbackError | Error message or null |
isLoading | Loading flag |
autoAdvanceEnabled | Auto-advance preference (optional) |
objectFitPreference | Image fit: contain | cover (optional) |
Actions
| Action | Description |
|---|---|
playItem(item, items?, index?) | Start playing item; optionally set playlist and index |
addToPlaylist(items) | Append items to playlist (no duplicates by id) |
playNext / playPrevious | Move to next/previous playable item in playlist |
togglePlay | Toggle play/pause |
setVolume / setMuted | Volume and mute; persisted to localStorage |
setCurrentTime / setDuration | Time and duration; setCurrentTime persists via playback-position |
setSeekTo(time | null) | Request a seek; hooks apply to native elements |
setPlaybackError / setIsLoading | Error and loading state |
clearPlaylist | Clear playlist and current item |
shouldAutoPlay() | One-shot: should current item auto-play (consumed by hooks) |
setAutoAdvanceEnabled / setObjectFitPreference | Preferences; persisted to localStorage |
Persistence
- Volume, muted, auto-advance, object-fit: Stored in
localStorageby the context. - Playback position per item: packages/site/lib/playback-position.ts —
savePosition(itemId, time)is called from context’ssetCurrentTime;getStoredPosition(itemId)is used in audio/video hooks when the element loads.
4. Core components
-
GlobalMediaRenderer — packages/site/components/global-media-renderer.tsx
Renders a single hidden<audio>and<video>. UsesuseGlobalAudioElementanduseGlobalVideoElement; syncs volume to both elements. When the current item is YouTube or Vimeo, clears native elements (iframe is handled by the consuming component, e.g. MediaRenderer). -
GlobalPlaybackControl — packages/site/components/global-playback-control.tsx
Fixed bottom bar: prev/next/play-pause, volume (VerticalVolumeSlider), thumbnail, title, time. Renders MediaProgress whencurrentItemis seekable (audio or video). Hidden when there is nocurrentItem.
5. Hooks that drive native elements
-
useGlobalAudioElement — packages/site/hooks/use-global-audio-element.ts
Setsaudio.src, play/pause fromisPlayingandshouldAutoPlay, appliesseekTo, restores position once per track viagetStoredPosition, reports time/duration/error via context callbacks, and callsplayNexton ended. -
useGlobalVideoElement — packages/site/hooks/use-global-video-element.ts
Same pattern for video: src, play/pause, position restore, time/duration,playNexton ended. Note: This hook does not accept or applyseekTo; global bar seek for video is written to context by MediaProgress but is not applied to the video element in this hook (see Known gaps).
6. Supporting components and libs
-
MediaProgress — packages/site/components/media-progress.tsx
Progress bar used in the global bar. UsescurrentTime,duration, andsetSeekTofrom context; click-to-seek via getSeekTimeFromProgressClick. -
playback-position — packages/site/lib/playback-position.ts
getStoredPosition(itemId),savePosition(itemId, time). Context saves on time updates; audio/video hooks restore on load. -
image-fit-preference — packages/site/lib/image-fit-preference.ts
Stored in context asobjectFitPreference; used by Photo and carousel for crop mode (contain/cover).
7. Data type
- CarouselItem — packages/site/types/carousel.ts
type:audio|video|youtube|vimeo|image|teaser; plusid,title,url/videoId,coverImage, etc. PlaylistplayNext/playPreviousin the context only advance when the item type is in['audio', 'video', 'youtube', 'vimeo'].
8. Layout and provider wiring
- GlobalPlaybackProvider wraps the app (inside TooltipProvider).
- Inside the provider: PageTransitionLoader wraps main content (Navigation, main, Footer) and then GlobalMediaRenderer, GlobalPlaybackControl, and PlaybackShortcutsButtonSlot.
- PlaybackShortcutsContext is separate (nested inside GlobalPlaybackProvider); it controls whether the keyboard-shortcuts button is shown when a playback-shortcuts consumer (e.g. Photo, AlbumShowcase) is mounted.
9. Consumers and usage patterns
Every file that uses useGlobalPlayback or is the GlobalPlaybackProvider:
| Consumer | Use of context | Notes |
|---|---|---|
| GlobalMediaRenderer | Full: state + setters for time, seek, error, loading, playNext, shouldAutoPlay | Drives hidden audio/video. |
| GlobalPlaybackControl | currentItem, isPlaying, volume, currentTime, duration, togglePlay, playNext/Previous, setVolume, setMuted | Bar UI only. |
| MediaProgress | currentTime, duration, setSeekTo | Seek bar in global bar. |
| AudioPlayer | currentItem, currentTime, duration, playbackError, isLoading, playItem, setSeekTo | When active: shows context time/error/loading; when inactive: play calls playItem(item, playlist, playlistIndex). Receives isPlaying/togglePlay from parent. |
| Showcase | playItem, addToPlaylist, currentItem, currentIndex, isPlaying, togglePlay, etc. | Passes playback state to MediaRenderer; syncs index with context; calls playItem/addToPlaylist. |
| AlbumShowcase | Same as Showcase | Same pattern. |
| AudioPlaylistCarousel | playItem, addToPlaylist, currentItem, currentIndex, isPlaying, togglePlay, etc. | Carousel of MediaRenderer; playItem on navigate/click; addToPlaylist(items) on mount. |
| AudioPlaylistViewer | playItem, addToPlaylist | Grid/list/carousel container; addToPlaylist on mount; playItem on track click. |
| AudioPlaylistGrid | currentItem | Highlight current item. |
| AudioPlaylistList | currentItem | Highlight current item. |
| Photo | objectFitPreference, setObjectFitPreference | Crop mode only; no playback. |
- MediaRenderer and VideoPlayer do not call
useGlobalPlayback; they receiveisPlaying,togglePlay, etc. as props from parents (Showcase, AlbumShowcase, AudioPlaylistCarousel) that use context. - PlaybackShortcutsContext is separate from Global Playback; it only controls when the keyboard-shortcuts button is visible (when a consumer such as Photo or AlbumShowcase is mounted).
10. Known gaps / limitations
- Video seek from the global bar: MediaProgress calls
setSeekTofor video, butuseGlobalVideoElementdoes not accept or applyseekTo. Audio seek is fully wired; video seek in the bar does not move the video element. - YouTube/Vimeo: Playback is via iframe in the consuming component (e.g. MediaRenderer). The global bar shows metadata and controls (prev/next, play/pause) but the actual embed is not in GlobalMediaRenderer; native
<audio>/<video>are cleared when the current item is YouTube or Vimeo.