Skip to content

feat: global media controls in sidebar#238

Draft
pip-owl wants to merge 12 commits intoMultiboxLabs:mainfrom
pip-owl:opencode/global-media-controls
Draft

feat: global media controls in sidebar#238
pip-owl wants to merge 12 commits intoMultiboxLabs:mainfrom
pip-owl:opencode/global-media-controls

Conversation

@pip-owl
Copy link
Contributor

@pip-owl pip-owl commented Mar 15, 2026

Summary

  • Adds a Global Media Controls widget to the sidebar that displays now-playing info (title, artist, artwork) extracted from web pages and provides play/pause, skip track, and mute controls without switching tabs
  • Uses a preload-based push approach (MutationObserver + contextBridge.executeInMainWorld()) to watch navigator.mediaSession metadata changes and send them to the main process via IPC — no polling from renderer
  • Each media tab gets its own card in the sidebar; cards for the currently focused tab are hidden to avoid redundancy

Changes

Area Files What
Design design/GLOBAL_MEDIA_CONTROLS.md Feature design document (source of truth)
Shared types tabs.ts, interfaces/browser/tabs.ts Added mediaTitle, mediaArtist, mediaArtwork, mediaPlaybackState to TabData; added mediaPlayPause/mediaNextTrack/mediaPreviousTrack to FlowTabsAPI
Main process tab.ts, tabs.ts (IPC), serialization.ts Media metadata properties on Tab class, IPC handlers for control commands + metadata updates, serialization of new fields
Preload index.ts setupMediaSessionWatcher() with MutationObserver + setTimeout throttling + 3s fallback poll + play/pause event listeners; monkey-patches navigator.mediaSession.setActionHandler() to capture handler refs for skip track support; media control bridge methods
Renderer global-media-controls.tsx (new), inner.tsx GlobalMediaControls component with per-tab MediaCard components, AnimatePresence animations, active tab filtering

Key Design Decisions

  • Preload push (not polling): MediaSession metadata is observed via MutationObserver + play/pause event listeners in the page context and pushed to main process via IPC, rather than polling with executeJavaScript
  • Action handler monkey-patching: navigator.mediaSession.setActionHandler() is monkey-patched in the preload to capture handler references, allowing skip track/play/pause to call the page's actual handlers directly instead of dispatching fake keyboard events
  • setTimeout over rAF: Uses setTimeout for debouncing because Chromium completely suspends requestAnimationFrame in background tabs (the exact use case for media controls)
  • Media element as ground truth: Playback state is derived from <video>/<audio> element's .paused property, not navigator.mediaSession.playbackState (many sites never update the latter)
  • Play/pause events sent immediately: Not debounced, for instant UI response when toggling playback

Notes

  • Phase 3 (hardware media keys) was cancelled — Chromium/OS handles this natively
  • Known TODO: MediaSession watcher currently only monitors the main frame, not iframes (e.g. YouTube embeds)

Add mediaPlayPause, mediaNextTrack, mediaPreviousTrack to FlowTabsAPI.
Add mediaTitle, mediaArtist, mediaArtwork, mediaPlaybackState to TabData.
Add mediaTitle, mediaArtist, mediaArtwork, mediaPlaybackState properties.
Add updateMediaMetadata() and clearMediaMetadata() methods that diff
incoming data and emit 'updated' only when values change.
Extend TabContentProperty union with the four new keys.
Add mute/unmute to tab context menu for awake tabs.
Add tabs:media-play-pause, tabs:media-next-track, tabs:media-previous-track
handlers that use executeJavaScript to control page media.
Add media-session:metadata-changed and media-session:metadata-cleared
listeners that receive push updates from the preload script.
Include mediaTitle, mediaArtist, mediaArtwork, mediaPlaybackState
in serializeTabForRenderer() so the sidebar UI can display them.
Add setupMediaSessionWatcher() that observes navigator.mediaSession
for metadata changes via MutationObserver + rAF throttling, with a
5-second fallback poll. Sends IPC messages to main process on change.
Add media control methods (play/pause, next, prev) to preload bridge.
Only activates on non-flow:// protocols (regular web pages).
Create GlobalMediaControls component with favicon, title/artist display,
play/pause, skip, and mute controls. Uses AnimatePresence for smooth
show/hide transitions. Derives primary media tab from audible/playing
state. Place component in sidebar between UpdateBanner and bottom row.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 15, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee1b7e91-48f0-4875-b080-f31d7cc1097a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

… for paused tabs

- Add play/pause event listeners (capture phase) in the preload MediaSession
  watcher so playback state updates immediately when media is toggled
- Derive playback state from the actual media element when
  navigator.mediaSession.playbackState is 'none'
- Include paused tabs in usePrimaryMediaTab() priority chain so the card
  stays visible after pausing, allowing the user to resume playback
- Update useMediaTabCount() to also count paused tabs
- Replace single-card design with per-tab MediaCard components so each
  playing/paused tab gets its own card in the sidebar
- Filter out the currently focused tab so the card only appears for
  background media tabs
- Extract MediaCard as a standalone memo'd component with its own state
…y/pause controls

- Monkey-patch navigator.mediaSession.setActionHandler() in the preload
  to capture handler references registered by the page, stored on
  window.__flowMediaActionHandlers
- Update next/prev track IPC handlers to call captured handlers directly
  instead of dispatching fake KeyboardEvents (which don't trigger
  MediaSession handlers)
- Add MediaSession play/pause action handler fallback for sites that use
  custom audio pipelines without standard <video>/<audio> elements
- Replace requestAnimationFrame with setTimeout for debouncing in the
  preload watcher (rAF is suspended in background tabs by Chromium)
- Derive playback state from media element .paused property as ground
  truth instead of relying on navigator.mediaSession.playbackState
- Send play/pause events immediately (not debounced) for instant UI
  response
- Reduce fallback poll interval from 5s to 3s
Implement new card-based UI with:
- Collapsed state: favicon + transport controls in a single row
- Expanded state (on hover): title row with favicon, title, close button
  slides in above the controls row
- Stacked card visual effect when multiple media tabs exist
- White/dark card backgrounds matching space theme
- Close tab button using flow.tabs.closeTab
- Max 5 media cards displayed
@iamEvanYT iamEvanYT force-pushed the main branch 2 times, most recently from ce3f35d to b2cbe93 Compare March 22, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants