Skip to content

Global Playback 2026-FEB

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 via useGlobalAudioElement and useGlobalVideoElement; no DOM polling.
  • GlobalPlaybackControl: Fixed bar; reads context; renders MediaProgress for seekable items.
  • MediaProgress: Reads currentTime, duration, setSeekTo from context; no polling.
  • Data flow: Context state/actions drive renderer and bar; timeupdate and seek go through context; position is persisted in context’s setCurrentTime via packages/site/lib/playback-position.ts.

3. Context: state and actions

Reference: packages/site/contexts/GlobalPlaybackContext.tsx.

State

StateDescription
currentItemCurrently playing CarouselItem or null
playlistArray of CarouselItem
currentIndexIndex of current item in playlist
isPlayingPlay/pause
isMutedMute
volume0–1
currentTimePlayback position (seconds)
durationTotal duration (seconds)
seekToRequested seek time (seconds) or null
playbackErrorError message or null
isLoadingLoading flag
autoAdvanceEnabledAuto-advance preference (optional)
objectFitPreferenceImage fit: contain | cover (optional)

Actions

ActionDescription
playItem(item, items?, index?)Start playing item; optionally set playlist and index
addToPlaylist(items)Append items to playlist (no duplicates by id)
playNext / playPreviousMove to next/previous playable item in playlist
togglePlayToggle play/pause
setVolume / setMutedVolume and mute; persisted to localStorage
setCurrentTime / setDurationTime and duration; setCurrentTime persists via playback-position
setSeekTo(time | null)Request a seek; hooks apply to native elements
setPlaybackError / setIsLoadingError and loading state
clearPlaylistClear playlist and current item
shouldAutoPlay()One-shot: should current item auto-play (consumed by hooks)
setAutoAdvanceEnabled / setObjectFitPreferencePreferences; persisted to localStorage

Persistence

  • Volume, muted, auto-advance, object-fit: Stored in localStorage by the context.
  • Playback position per item: packages/site/lib/playback-position.tssavePosition(itemId, time) is called from context’s setCurrentTime; getStoredPosition(itemId) is used in audio/video hooks when the element loads.

4. Core components

  • GlobalMediaRendererpackages/site/components/global-media-renderer.tsx
    Renders a single hidden <audio> and <video>. Uses useGlobalAudioElement and useGlobalVideoElement; 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).

  • GlobalPlaybackControlpackages/site/components/global-playback-control.tsx
    Fixed bottom bar: prev/next/play-pause, volume (VerticalVolumeSlider), thumbnail, title, time. Renders MediaProgress when currentItem is seekable (audio or video). Hidden when there is no currentItem.


5. Hooks that drive native elements

  • useGlobalAudioElementpackages/site/hooks/use-global-audio-element.ts
    Sets audio.src, play/pause from isPlaying and shouldAutoPlay, applies seekTo, restores position once per track via getStoredPosition, reports time/duration/error via context callbacks, and calls playNext on ended.

  • useGlobalVideoElementpackages/site/hooks/use-global-video-element.ts
    Same pattern for video: src, play/pause, position restore, time/duration, playNext on ended. Note: This hook does not accept or apply seekTo; 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


7. Data type

  • CarouselItempackages/site/types/carousel.ts
    type: audio | video | youtube | vimeo | image | teaser; plus id, title, url / videoId, coverImage, etc. Playlist playNext / playPrevious in the context only advance when the item type is in ['audio', 'video', 'youtube', 'vimeo'].

8. Layout and provider wiring

packages/site/app/layout.tsx:

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

ConsumerUse of contextNotes
GlobalMediaRendererFull: state + setters for time, seek, error, loading, playNext, shouldAutoPlayDrives hidden audio/video.
GlobalPlaybackControlcurrentItem, isPlaying, volume, currentTime, duration, togglePlay, playNext/Previous, setVolume, setMutedBar UI only.
MediaProgresscurrentTime, duration, setSeekToSeek bar in global bar.
AudioPlayercurrentItem, currentTime, duration, playbackError, isLoading, playItem, setSeekToWhen active: shows context time/error/loading; when inactive: play calls playItem(item, playlist, playlistIndex). Receives isPlaying/togglePlay from parent.
ShowcaseplayItem, addToPlaylist, currentItem, currentIndex, isPlaying, togglePlay, etc.Passes playback state to MediaRenderer; syncs index with context; calls playItem/addToPlaylist.
AlbumShowcaseSame as ShowcaseSame pattern.
AudioPlaylistCarouselplayItem, addToPlaylist, currentItem, currentIndex, isPlaying, togglePlay, etc.Carousel of MediaRenderer; playItem on navigate/click; addToPlaylist(items) on mount.
AudioPlaylistViewerplayItem, addToPlaylistGrid/list/carousel container; addToPlaylist on mount; playItem on track click.
AudioPlaylistGridcurrentItemHighlight current item.
AudioPlaylistListcurrentItemHighlight current item.
PhotoobjectFitPreference, setObjectFitPreferenceCrop mode only; no playback.
  • MediaRenderer and VideoPlayer do not call useGlobalPlayback; they receive isPlaying, 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 setSeekTo for video, but useGlobalVideoElement does not accept or apply seekTo. 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.