Media Controller UI on Firefox tabs, driven by Firefox's internal MediaController API.
Warning
FX Media Controler uses Firefox AutoConfig to load privileged JavaScript at browser startup. It is not a WebExtension and does not run in the WebExtension sandbox — it runs with chrome-level access to Firefox internals.
Review autoconfig.js, autoconfig.cfg, and the built fx-media-controller.js before installing. Don't install this, or anything using AutoConfig, if you're not willing to read the code you're trusting with that access.
See Mozilla's AutoConfig docs for background.
- What this is
- Why not a WebExtension
- How it works
- Limitations
- Requirements
- Installation
- Troubleshooting
- Uninstallation
- Development
- Compatibility
- License
Firefox chrome-level customization that adds media playback controls to browser tabs using Firefox's internal MediaController, inspired by Firefox's native tab preview popups.
Built on Firefox's native media-control infrastructure, also used for media controls and OS-level media-key integration. No content scripts, no page injection.
WebExtensions can't do this. The MediaController API isn't exposed to the WebExtension API surface — it's chrome-only. A regular extension can react to media indirectly (e.g. the tab's audio icon) but can't query or drive playback state per tab the way this does, and can't render UI inside the tab strip itself.
Getting that requires chrome-privileged script, which for a non-extension means AutoConfig. That's the whole reason this project exists in this shape — not a design preference, a constraint of the API being used.
The trade-off: no WebExtension sandbox, no store review, no auto-update, manual install, and breakage risk on every Firefox internals change. Weigh that before installing.
flowchart TD
AC["autoconfig.cfg + autoconfig.js"] --> Idx["index.ts"]
subgraph UI["per tab"]
Tabs["tabs.ts"] --> Button["button.ts"]
Tabs --> Popup["popup.ts"]
end
Idx --> Browser["browser.ts"] --> Tabs
Button --> State["state.ts"]
Popup --> State
subgraph Media["media binding"]
Controller["controller.ts"] <--> FF["Firefox MediaController"]
Controller --> Position["position.ts"]
end
State --> Controller
index.ts— entry point loaded by AutoConfig. Initializes the current window, then watches for new ones (domwindowopened).browser.ts— wires up one browser window: tab open/close listeners, location-change listener, teardown on window unload.tabs.ts— per-tab lifecycle: creates/destroys the button, attaches hover events for the popup.button.ts— the tab-strip icon. Subscribes to tab media state, toggles play/pause on click.popup.ts— the hover popup (title, artwork, seek, track controls).state.ts—TabMediaState: per-tab pub/sub wrapper around a controller binding, one per tab, cached in aWeakMap.controller.ts— the actualMediaControllerbinding: event listeners, capability detection, state caching.position.ts— interpolates playback position between realpositionstatechangeevents (RAF loop), only while the popup is open.
- No volume control.
MediaControlKeyincludes'setvolume', butMediaControllerexposes no method to set volume — onlymute()/unmute(). Confirmed against the interface itself; not a bug in this project, a gap in the Firefox API. - No seek bar on sites that don't report position. Position data comes only from
navigator.mediaSession.setPositionState()calls made by the page. Sites that never call it (some players don't) show play/pause but no scrubber or time. - Only the main browser window is instrumented.
browser.tsbails out unlesslocation.hrefis exactlychrome://browser/content/browser.xhtml. Popup windows, PiP, and other chrome windows aren't covered. - Internal API, not a stable one.
MediaControlleris undocumented outside Mozilla's own source. Firefox releases can change or remove it without notice, per Mozilla's own caveat. - One AutoConfig entry point. Firefox only loads one
autoconfig.cfg. If you already use AutoConfig for something else, installing this without merging configs will break the other one.
- Firefox desktop (Windows, macOS, Linux)
- A Firefox installation you can write to (admin/root may be required)
- A Firefox profile with
chrome/customization enabled
Building from source additionally needs Node.js — see Development. Using a release build does not.
Close Firefox completely, then extract the release archive. You need:
autoconfig.js
autoconfig.cfg
fx-media-controller.js
fx-media-controller.css
Two destinations: the Firefox installation directory (AutoConfig files) and the Firefox profile (fx-media-controller files).
The folder containing the Firefox executable.
| OS | Typical path |
|---|---|
| Windows | C:\Program Files\Mozilla Firefox\ |
| macOS | /Applications/Firefox.app/Contents/Resources/ |
| Linux | /usr/lib/firefox/ or /usr/lib64/firefox/ |
Package-managed Linux builds vary — find wherever firefox (the binary) actually lives.
Copy autoconfig.cfg into that directory.
Copy autoconfig.js into <install dir>/defaults/pref/.
<Firefox installation>/
├── autoconfig.cfg
└── defaults/pref/autoconfig.js
Already using AutoConfig for something else? Don't overwrite
autoconfig.cfgblind — merge it. Firefox loads exactly one.
about:support → Profile Folder → Open Folder.
Inside the profile, create chrome/JS/ if it doesn't exist, then:
<Firefox profile>/
└── chrome/
├── JS/fx-media-controller.js
└── fx-media-controller.css
@import in userChrome.css is unreliable in some setups. Skip it — paste the contents of fx-media-controller.css directly into userChrome.css instead. If you don't have one yet, create chrome/userChrome.css and paste the styles in as-is.
If you'd rather try @import first:
@import url('fx-media-controller.css');Either way, set in about:config:
toolkit.legacyUserProfileCustomizations.stylesheets = true
Restart Firefox.
Open a tab with playing media. The Play/Pause button should appear on the tab and reflect playback state on click.
Button doesn't appear — confirm fx-media-controller.js is at <profile>/chrome/JS/, autoconfig.cfg is in the install directory, autoconfig.js is in defaults/pref/. Restart Firefox fully. Check the Browser Console for errors.
CSS not applied — confirm the pref is true, and confirm you pasted the styles into userChrome.css (not just @import, per step 5).
AutoConfig error — check file placement matches step 2 exactly, files copied unmodified, Firefox was fully closed while replacing them, no second AutoConfig config is present.
Worked, then broke after a Firefox update — updates can replace files in the install directory; re-check step 2. If files are intact and it still doesn't work, Firefox likely changed the internal API this depends on — see Limitations.
Close Firefox. Remove:
<Firefox installation>/autoconfig.cfg
<Firefox installation>/defaults/pref/autoconfig.js
<Firefox profile>/chrome/JS/fx-media-controller.js
<Firefox profile>/chrome/fx-media-controller.css
Remove the pasted styles (or the @import line) from userChrome.css — don't delete the file itself if it has other content.
npm run typecheck # tsc --noEmit
npm run lint # oxlint
npm run format # oxfmt
npm run check:deps # knip
npm run build # node build.mjs -> dist/npm run check runs typecheck, lint, format:check, and check:deps together. CI (.github/workflows/release.yml) runs on Node 24 and builds with FXMC_LOG_LEVEL=error.
Tied to Firefox internals, not a stable extension API — compatibility can change between Firefox releases without warning. Developed and tested against the version documented in the current release's notes.
MIT — see LICENSE.
