diff --git a/src/browser/base/content/ZenUIManager.mjs b/src/browser/base/content/ZenUIManager.mjs
index f644915ca..1f84425ae 100644
--- a/src/browser/base/content/ZenUIManager.mjs
+++ b/src/browser/base/content/ZenUIManager.mjs
@@ -37,6 +37,8 @@ var gZenUIManager = {
window.addEventListener('TabClose', this.onTabClose.bind(this));
this.tabsWrapper.addEventListener('scroll', this.saveScrollbarState.bind(this));
+
+ gZenMediaController.init();
},
updateTabsToolbar() {
@@ -768,3 +770,252 @@ var gZenVerticalTabsManager = {
this._tabEdited = null;
},
};
+
+var gZenMediaController = {
+ _currentMediaController: null,
+ _currentBrowser: null,
+ _mediaUpdateInterval: null,
+
+ mediaTitle: null,
+ mediaArtist: null,
+ mediaControlBar: null,
+ mediaServiceIcon: null,
+ mediaServiceTitle: null,
+ mediaProgressBar: null,
+ mediaCurrentTime: null,
+ mediaDuration: null,
+ mediaFocusButton: null,
+
+ supportedKeys: ['playpause', 'previoustrack', 'nexttrack'],
+
+ init() {
+ this.mediaTitle = document.querySelector('#zen-media-title');
+ this.mediaArtist = document.querySelector('#zen-media-artist');
+ this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
+ this.mediaServiceIcon = document.querySelector('#zen-media-service-button > image');
+ this.mediaServiceTitle = document.querySelector('#zen-media-service-title');
+ this.mediaProgressBar = document.querySelector('#zen-media-progress-bar');
+ this.mediaCurrentTime = document.querySelector('#zen-media-current-time');
+ this.mediaDuration = document.querySelector('#zen-media-duration');
+ this.mediaFocusButton = document.querySelector('#zen-media-focus-button');
+ },
+
+ /**
+ * Deinitializes a media controller, removing all event listeners and resetting state.
+ * @param {Object} mediaController - The media controller to deinitialize.
+ */
+ deinitMediaController(mediaController) {
+ if (!mediaController) return;
+
+ mediaController.onpositionstatechange = null;
+ mediaController.onplaybackstatechange = null;
+ mediaController.onsupportedkeyschange = null;
+ mediaController.onmetadatachange = null;
+ mediaController.ondeactivated = null;
+
+ this._currentMediaController = null;
+ this._currentBrowser = null;
+
+ if (this._mediaUpdateInterval) {
+ clearInterval(this._mediaUpdateInterval);
+ this._mediaUpdateInterval = null;
+ }
+
+ this.mediaControlBar.setAttribute('hidden', 'true');
+ this.mediaControlBar.removeAttribute('muted');
+ this.mediaControlBar.classList.remove('playing');
+ },
+
+ /**
+ * Sets up the media control UI with metadata and position state.
+ * @param {Object} metadata - The media metadata (title, artist, etc.).
+ * @param {Object} positionState - The position state (position, duration).
+ */
+ setupMediaControl(metadata, positionState) {
+ if (!this.mediaControlBar.classList.contains('playing')) {
+ this.mediaControlBar.classList.add('playing');
+ }
+
+ this.mediaServiceTitle.textContent = this._currentBrowser._originalURI.displayHost;
+ this.mediaServiceIcon.src = this._currentBrowser.mIconURL;
+ this.mediaFocusButton.style.listStyleImage = `url(${this._currentBrowser.mIconURL})`;
+
+ this.mediaControlBar.removeAttribute('hidden');
+ this.mediaTitle.textContent = metadata.title || '';
+ this.mediaArtist.textContent = metadata.artist || '';
+
+ this._currentPosition = positionState.position;
+ this._currentDuration = positionState.duration;
+ this.updateMediaPosition();
+
+ for (const key of this.supportedKeys) {
+ const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
+ button.disabled = !this._currentMediaController.supportedKeys.includes(key);
+ }
+ },
+
+ /**
+ * @param {Object} mediaController - The media controller to activate.
+ * @param {Object} browser - The browser associated with the media controller.
+ */
+ activateMediaControls(mediaController, browser) {
+ if (this._currentBrowser?.browserId === browser.browserId) return;
+ else this._currentBrowser = browser;
+
+ mediaController.onpositionstatechange = this.onPositionstateChange.bind(this);
+ mediaController.onplaybackstatechange = this.onPlaybackstateChange.bind(this);
+ mediaController.onsupportedkeyschange = this.onSupportedKeysChange.bind(this);
+ mediaController.onmetadatachange = this.onMetadataChange.bind(this);
+ mediaController.ondeactivated = this.onDeactivated.bind(this);
+
+ if (this._currentMediaController === mediaController) return;
+ else this.deinitMediaController(this._currentMediaController);
+
+ this._currentMediaController = mediaController;
+
+ const metadata = mediaController.getMetadata();
+ const positionState = mediaController.getPositionState();
+
+ this.setupMediaControl(metadata, positionState);
+ },
+
+ /**
+ * @param {Event} event - The deactivation event.
+ */
+ onDeactivated(event) {
+ if (event.target === this._currentMediaController) {
+ this.deinitMediaController(event.target);
+ }
+ },
+
+ /**
+ * Updates playback state and UI based on changes.
+ * @param {Event} event - The playback state change event.
+ */
+ onPlaybackstateChange(event) {
+ this.mediaControlBar.classList.toggle('playing', event.target.isPlaying);
+ },
+
+ /**
+ * Updates supported keys in the UI.
+ * @param {Event} event - The supported keys change event.
+ */
+ onSupportedKeysChange(event) {
+ for (const key of this.supportedKeys) {
+ const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
+ button.disabled = !event.target.supportedKeys.includes(key);
+ }
+ },
+
+ /**
+ * Updates position state and UI when the media position changes.
+ * @param {Event} event - The position state change event.
+ */
+ onPositionstateChange(event) {
+ if (event.target !== this._currentMediaController) return;
+
+ this._currentPosition = event.position;
+ this._currentDuration = event.duration;
+ this.updateMediaPosition();
+ },
+
+ /**
+ * Updates the media progress bar and time display.
+ */
+ updateMediaPosition() {
+ if (this._mediaUpdateInterval) {
+ clearInterval(this._mediaUpdateInterval);
+ this._mediaUpdateInterval = null;
+ }
+
+ this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(this._currentPosition);
+ this.mediaDuration.textContent = this.formatSecondsToMinutes(this._currentDuration);
+ this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
+
+ if (this._currentMediaController?.isPlaying) {
+ this._mediaUpdateInterval = setInterval(() => {
+ if (this._currentMediaController?.isPlaying) {
+ this._currentPosition += 1;
+ if (this._currentPosition > this._currentDuration) {
+ this._currentPosition = this._currentDuration;
+ }
+ this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(this._currentPosition);
+ this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
+ } else {
+ clearInterval(this._mediaUpdateInterval);
+ this._mediaUpdateInterval = null;
+ }
+ }, 1000);
+ }
+ },
+
+ /**
+ * Formats seconds into a minutes:seconds string.
+ * @param {number} seconds - The time in seconds.
+ * @returns {string} Formatted time string.
+ */
+ formatSecondsToMinutes(seconds) {
+ const totalSeconds = Math.ceil(seconds);
+ const minutes = Math.floor(totalSeconds / 60);
+ const remainingSeconds = totalSeconds % 60;
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
+ },
+
+ /**
+ * Updates metadata in the UI.
+ * @param {Event} event - The metadata change event.
+ */
+ onMetadataChange(event) {
+ const metadata = event.target.getMetadata();
+ this.mediaTitle.textContent = metadata.title || '';
+ this.mediaArtist.textContent = metadata.artist || '';
+ },
+
+ onMediaPlayPrev() {
+ if (this._currentMediaController?.supportedKeys.includes('previoustrack')) {
+ this._currentMediaController.prevTrack();
+ }
+ },
+
+ onMediaPlayNext() {
+ if (this._currentMediaController?.supportedKeys.includes('nexttrack')) {
+ this._currentMediaController.nextTrack();
+ }
+ },
+
+ onMediaSeekDrag(event) {
+ this._currentMediaController?.pause();
+ const newTime = (event.target.value / 100) * this._currentDuration;
+ this.mediaCurrentTime.textContent = this.formatSecondsToMinutes(newTime);
+ },
+
+ onMediaSeekComplete(event) {
+ const newPosition = (event.target.value / 100) * this._currentDuration;
+ if (this._currentMediaController?.supportedKeys.includes('seekto')) {
+ this._currentMediaController.seekTo(newPosition);
+ this._currentMediaController.play();
+ }
+ },
+
+ onMediaFocus() {
+ this._currentMediaController?.focus();
+ },
+
+ onMediaMute() {
+ if (!this.mediaControlBar.hasAttribute('muted')) {
+ this._currentBrowser.mute();
+ this.mediaControlBar.setAttribute('muted', '');
+ } else {
+ this._currentBrowser.unmute();
+ this.mediaControlBar.removeAttribute('muted');
+ }
+ },
+
+ onMediaToggle() {
+ if (this.mediaControlBar.classList.contains('playing')) {
+ this._currentMediaController?.pause();
+ } else {
+ this._currentMediaController?.play();
+ }
+ },
+};
diff --git a/src/browser/base/content/navigator-toolbox-inc-xhtml.patch b/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
index f2be3c096..b2f5e92ae 100644
--- a/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
+++ b/src/browser/base/content/navigator-toolbox-inc-xhtml.patch
@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
-index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1c0d9968c 100644
+index 2a57e254c876589bf64bfbd5df188a2b435d8482..c4aecd9738baed13b8dd5687edc95695b0d5af37 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -73,7 +73,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
@@ -82,6 +82,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
#include titlebar-items.inc.xhtml
-
+#endif
++#include zen-media-controller.inc.xhtml
+#include zen-sidebar-icons.inc.xhtml
-
@@ -89,7 +90,7 @@ index a0a382643a2f74b6d789f3641ef300eed202d5e9..a962e155f1452362a2a35df89c8f56e1
diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml
index 33e923742..c5f6d0b17 100644
--- a/src/browser/base/content/zen-assets.inc.xhtml
+++ b/src/browser/base/content/zen-assets.inc.xhtml
@@ -26,6 +26,7 @@
+
# Scripts used all over the browser
diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn
index 2f20814ab..e024f82b9 100644
--- a/src/browser/base/content/zen-assets.jar.inc.mn
+++ b/src/browser/base/content/zen-assets.jar.inc.mn
@@ -49,6 +49,7 @@
content/browser/zen-styles/zen-rices.css (content/zen-styles/zen-rices.css)
content/browser/zen-styles/zen-branding.css (content/zen-styles/zen-branding.css)
content/browser/zen-styles/zen-welcome.css (content/zen-styles/zen-welcome.css)
+ content/browser/zen-styles/zen-media-controls.css (content/zen-styles/zen-media-controls.css)
content/browser/zen-styles/zen-panels/bookmarks.css (content/zen-styles/zen-panels/bookmarks.css)
content/browser/zen-styles/zen-panels/extensions.css (content/zen-styles/zen-panels/extensions.css)
diff --git a/src/browser/base/content/zen-media-controller.inc.xhtml b/src/browser/base/content/zen-media-controller.inc.xhtml
new file mode 100644
index 000000000..488aa44d4
--- /dev/null
+++ b/src/browser/base/content/zen-media-controller.inc.xhtml
@@ -0,0 +1,48 @@
+
\ No newline at end of file
diff --git a/src/browser/base/content/zen-styles/zen-media-controls.css b/src/browser/base/content/zen-styles/zen-media-controls.css
new file mode 100644
index 000000000..f1b86a8f2
--- /dev/null
+++ b/src/browser/base/content/zen-styles/zen-media-controls.css
@@ -0,0 +1,191 @@
+#zen-media-controls-toolbar {
+ --progress-height: 5px;
+ --button-spacing: 2px;
+
+ display: flex;
+ justify-content: space-between;
+ min-width: 0;
+ padding: 5px;
+ border-radius: 10px;
+ background: var(--zen-toolbar-element-bg) !important;
+ container-type: inline-size;
+
+ .toolbarbutton-1 {
+ border-radius: 5px;
+ color: white;
+ }
+
+ #zen-media-prev-button,
+ #zen-media-play-pause-button,
+ #zen-media-next-button {
+ margin: 0;
+ }
+
+ image.toolbarbutton-icon {
+ padding: 5px;
+ width: 26px;
+ height: 26px;
+ }
+
+ #zen-media-progress-bar {
+ appearance: none;
+ width: 100%;
+ height: var(--progress-height);
+ margin: 0 8px;
+ border-radius: 2px;
+ background-color: rgba(255, 255, 255, 0.2);
+ cursor: pointer;
+ transition: height 0.15s ease-out;
+
+ &::-moz-range-track {
+ background: var(--zen-colors-border);
+ border-radius: 999px;
+ height: var(--progress-height);
+ }
+
+ &::-moz-range-progress {
+ background: var(--zen-primary-color);
+ border-radius: 999px;
+ height: var(--progress-height);
+ }
+
+ &::-moz-range-thumb {
+ background: var(--zen-primary-color);
+ border: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ cursor: pointer;
+ }
+ }
+
+ &:hover {
+ .show-on-hover {
+ max-height: 50px;
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ #zen-media-focus-button {
+ width: 34px;
+ height: 26px;
+ align-self: center;
+ transition: opacity 0.2s ease, transform 0.2s ease;
+
+ @container (max-width: 185px) {
+ width: 0;
+ height: 0;
+ opacity: 0;
+ padding: 0;
+ transform: translateX(-20px);
+ }
+
+ @container (min-width: 185px) {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ toolbaritem {
+ flex-grow: 1;
+ padding: 0;
+ transition: padding 0.3s ease-out;
+ }
+
+ .show-on-hover {
+ padding-inline: 4px;
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+ transform: translateY(5px);
+ transition: max-height 0.35s ease-in-out,
+ opacity 0.25s ease-in-out,
+ transform 0.3s ease-in-out;
+ }
+
+ #zen-media-current-time,
+ #zen-media-duration {
+ margin: 0 0 0 1px;
+ }
+}
+
+@keyframes zen-media-controls-show {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+#zen-media-controls-toolbar {
+ &:not([hidden]) {
+ animation: zen-media-controls-show 0.2s ease-out;
+ display: flex;
+ }
+
+ &[hidden] {
+ display: none;
+ animation: none;
+ transition: none;
+ }
+}
+
+#zen-media-service-title,
+#zen-media-title,
+#zen-media-artist {
+ white-space: nowrap;
+ width: 0;
+ margin-left: 0;
+}
+
+#zen-media-service-title {
+ align-self: center;
+ font-size: 13px;
+ margin-bottom: 5px;
+ margin-left: 6px;
+}
+
+#zen-media-title,
+#zen-media-artist {
+ align-self: start;
+ margin-top: 5px;
+}
+
+#zen-media-title {
+ height: 16px;
+ font-size: 16px;
+ font-weight: bold;
+}
+
+#zen-media-artist {
+ height: 5px;
+ padding-bottom: 20px;
+}
+
+#zen-media-main-vbox,
+#zen-media-service-hbox,
+#zen-media-info-vbox,
+#zen-media-progress-hbox {
+ width: 100%;
+}
+
+#zen-media-main-vbox {
+ height: 100%;
+ justify-content: space-between;
+ overflow: hidden;
+}
+
+#zen-media-progress-hbox {
+ flex-grow: 1;
+ align-items: center;
+}
+
+#zen-media-controls-hbox {
+ align-items: flex-end;
+ justify-content: space-evenly;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+#zen-media-service-button image {
+ width: 20px;
+ height: 20px;
+}
\ No newline at end of file
diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css
index c8c69e4e8..f36e37502 100644
--- a/src/browser/themes/shared/zen-icons/icons.css
+++ b/src/browser/themes/shared/zen-icons/icons.css
@@ -1150,3 +1150,37 @@ menupopup > menuitem:is([type='checkbox']) .menu-iconic-left {
#sidebarRevampSeparator {
display: none !important;
}
+
+#zen-media-playpause-button {
+ list-style-image: url('media-play.svg') !important;
+}
+
+#zen-media-controls-toolbar.playing #zen-media-playpause-button {
+ list-style-image: url('media-pause.svg') !important;
+}
+
+#zen-media-nexttrack-button {
+ list-style-image: url('media-next.svg') !important;
+}
+
+#zen-media-previoustrack-button {
+ list-style-image: url('media-previous.svg') !important;
+}
+
+#zen-media-controls-toolbar[muted] #zen-media-mute-button {
+ list-style-image: url('media-mute.svg') !important;
+}
+
+#zen-media-mute-button {
+ list-style-image: url('media-unmute.svg') !important;
+}
+
+#zen-media-close-button {
+ list-style-image: url('close.svg') !important;
+}
+
+#zen-media-controls-toolbar:hover {
+ #zen-media-focus-button {
+ list-style-image: url('screen.svg') !important;
+ }
+}
diff --git a/src/browser/themes/shared/zen-icons/jar.inc.mn b/src/browser/themes/shared/zen-icons/jar.inc.mn
index fa20d2220..515c183f9 100644
--- a/src/browser/themes/shared/zen-icons/jar.inc.mn
+++ b/src/browser/themes/shared/zen-icons/jar.inc.mn
@@ -66,9 +66,11 @@
skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/lin/manage.svg)
skin/classic/browser/zen-icons/media-loop.svg (../shared/zen-icons/lin/media-loop.svg)
skin/classic/browser/zen-icons/media-mute.svg (../shared/zen-icons/lin/media-mute.svg)
+ skin/classic/browser/zen-icons/media-next.svg (../shared/zen-icons/lin/media-next.svg)
skin/classic/browser/zen-icons/media-pause.svg (../shared/zen-icons/lin/media-pause.svg)
skin/classic/browser/zen-icons/media-pip.svg (../shared/zen-icons/lin/media-pip.svg)
skin/classic/browser/zen-icons/media-play.svg (../shared/zen-icons/lin/media-play.svg)
+ skin/classic/browser/zen-icons/media-previous.svg (../shared/zen-icons/lin/media-previous.svg)
skin/classic/browser/zen-icons/media-speed.svg (../shared/zen-icons/lin/media-speed.svg)
skin/classic/browser/zen-icons/media-unmute.svg (../shared/zen-icons/lin/media-unmute.svg)
skin/classic/browser/zen-icons/menu-bar.svg (../shared/zen-icons/lin/menu-bar.svg)
@@ -198,9 +200,11 @@
skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/lin/manage.svg)
skin/classic/browser/zen-icons/media-loop.svg (../shared/zen-icons/lin/media-loop.svg)
skin/classic/browser/zen-icons/media-mute.svg (../shared/zen-icons/lin/media-mute.svg)
+ skin/classic/browser/zen-icons/media-next.svg (../shared/zen-icons/lin/media-next.svg)
skin/classic/browser/zen-icons/media-pause.svg (../shared/zen-icons/lin/media-pause.svg)
skin/classic/browser/zen-icons/media-pip.svg (../shared/zen-icons/lin/media-pip.svg)
skin/classic/browser/zen-icons/media-play.svg (../shared/zen-icons/lin/media-play.svg)
+ skin/classic/browser/zen-icons/media-previous.svg (../shared/zen-icons/lin/media-previous.svg)
skin/classic/browser/zen-icons/media-speed.svg (../shared/zen-icons/lin/media-speed.svg)
skin/classic/browser/zen-icons/media-unmute.svg (../shared/zen-icons/lin/media-unmute.svg)
skin/classic/browser/zen-icons/menu-bar.svg (../shared/zen-icons/lin/menu-bar.svg)
@@ -330,9 +334,11 @@
skin/classic/browser/zen-icons/manage.svg (../shared/zen-icons/lin/manage.svg)
skin/classic/browser/zen-icons/media-loop.svg (../shared/zen-icons/lin/media-loop.svg)
skin/classic/browser/zen-icons/media-mute.svg (../shared/zen-icons/lin/media-mute.svg)
+ skin/classic/browser/zen-icons/media-next.svg (../shared/zen-icons/lin/media-next.svg)
skin/classic/browser/zen-icons/media-pause.svg (../shared/zen-icons/lin/media-pause.svg)
skin/classic/browser/zen-icons/media-pip.svg (../shared/zen-icons/lin/media-pip.svg)
skin/classic/browser/zen-icons/media-play.svg (../shared/zen-icons/lin/media-play.svg)
+ skin/classic/browser/zen-icons/media-previous.svg (../shared/zen-icons/lin/media-previous.svg)
skin/classic/browser/zen-icons/media-speed.svg (../shared/zen-icons/lin/media-speed.svg)
skin/classic/browser/zen-icons/media-unmute.svg (../shared/zen-icons/lin/media-unmute.svg)
skin/classic/browser/zen-icons/menu-bar.svg (../shared/zen-icons/lin/menu-bar.svg)
diff --git a/src/browser/themes/shared/zen-icons/lin/media-next.svg b/src/browser/themes/shared/zen-icons/lin/media-next.svg
new file mode 100644
index 000000000..24fd40e36
--- /dev/null
+++ b/src/browser/themes/shared/zen-icons/lin/media-next.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/browser/themes/shared/zen-icons/lin/media-previous.svg b/src/browser/themes/shared/zen-icons/lin/media-previous.svg
new file mode 100644
index 000000000..d99e7ab53
--- /dev/null
+++ b/src/browser/themes/shared/zen-icons/lin/media-previous.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/dom/chrome-webidl/MediaController-webidl.patch b/src/dom/chrome-webidl/MediaController-webidl.patch
new file mode 100644
index 000000000..73a252539
--- /dev/null
+++ b/src/dom/chrome-webidl/MediaController-webidl.patch
@@ -0,0 +1,27 @@
+diff --git a/dom/chrome-webidl/MediaController.webidl b/dom/chrome-webidl/MediaController.webidl
+index 20f416d1c3b41798e0f90bbac5db40ed2a4ab000..06cb4c847fcfba964eeb93089613e293dc10bd87 100644
+--- a/dom/chrome-webidl/MediaController.webidl
++++ b/dom/chrome-webidl/MediaController.webidl
+@@ -20,6 +20,12 @@ enum MediaControlKey {
+ "stop",
+ };
+
++dictionary MediaControllerPositionState {
++ required double duration;
++ required double playbackRate;
++ required double position;
++};
++
+ /**
+ * MediaController is used to control media playback for a tab, and each tab
+ * would only have one media controller, which can be accessed from the
+@@ -36,6 +42,9 @@ interface MediaController : EventTarget {
+ [Throws]
+ MediaMetadataInit getMetadata();
+
++ [Throws]
++ MediaControllerPositionState getPositionState();
++
+ [Frozen, Cached, Pure]
+ readonly attribute sequence supportedKeys;
+
diff --git a/src/dom/media/mediacontrol/MediaController-cpp.patch b/src/dom/media/mediacontrol/MediaController-cpp.patch
new file mode 100644
index 000000000..562756783
--- /dev/null
+++ b/src/dom/media/mediacontrol/MediaController-cpp.patch
@@ -0,0 +1,30 @@
+diff --git a/dom/media/mediacontrol/MediaController.cpp b/dom/media/mediacontrol/MediaController.cpp
+index 3f08d24d4ed56bb72ed513ed602b2c8fa48afe7b..690d9abdb0ab8efc019dd606743b82504834faa0 100644
+--- a/dom/media/mediacontrol/MediaController.cpp
++++ b/dom/media/mediacontrol/MediaController.cpp
+@@ -51,6 +51,25 @@ void MediaController::GetSupportedKeys(
+ }
+ }
+
++void MediaController::GetPositionState(MediaControllerPositionState& aPositionState, ErrorResult& aRv) {
++ if (!IsActive() || mShutdown) {
++ LOG("Cannot get position state: controller is inactive or shut down");
++ aRv.Throw(NS_ERROR_NOT_AVAILABLE);
++ return;
++ }
++
++ Maybe currentPositionState = GetCurrentPositionState();
++ if (!currentPositionState) {
++ LOG("No position state available for controller %" PRId64, Id());
++ aRv.Throw(NS_ERROR_NOT_AVAILABLE);
++ return;
++ }
++
++ aPositionState.mDuration = currentPositionState->mDuration;
++ aPositionState.mPosition = currentPositionState->mLastReportedPlaybackPosition;
++ aPositionState.mPlaybackRate = currentPositionState->mPlaybackRate;
++}
++
+ void MediaController::GetMetadata(MediaMetadataInit& aMetadata,
+ ErrorResult& aRv) {
+ if (!IsActive() || mShutdown) {
diff --git a/src/dom/media/mediacontrol/MediaController-h.patch b/src/dom/media/mediacontrol/MediaController-h.patch
new file mode 100644
index 000000000..f2fec73c4
--- /dev/null
+++ b/src/dom/media/mediacontrol/MediaController-h.patch
@@ -0,0 +1,12 @@
+diff --git a/dom/media/mediacontrol/MediaController.h b/dom/media/mediacontrol/MediaController.h
+index 8fec9c59e38bc24b9ff6d30ddbaebff67107bc76..5e7f3634f9edef48d6f96b4900f82a7ebbd730be 100644
+--- a/dom/media/mediacontrol/MediaController.h
++++ b/dom/media/mediacontrol/MediaController.h
+@@ -90,6 +90,7 @@ class MediaController final : public DOMEventTargetHelper,
+ JS::Handle aGivenProto) override;
+ void GetSupportedKeys(nsTArray& aRetVal) const;
+ void GetMetadata(MediaMetadataInit& aMetadata, ErrorResult& aRv);
++ void GetPositionState(MediaControllerPositionState& aPositionState, ErrorResult& aRv);
+ IMPL_EVENT_HANDLER(activated);
+ IMPL_EVENT_HANDLER(deactivated);
+ IMPL_EVENT_HANDLER(metadatachange);
diff --git a/src/toolkit/actors/AudioPlaybackParent-sys-mjs.patch b/src/toolkit/actors/AudioPlaybackParent-sys-mjs.patch
new file mode 100644
index 000000000..a00527952
--- /dev/null
+++ b/src/toolkit/actors/AudioPlaybackParent-sys-mjs.patch
@@ -0,0 +1,17 @@
+diff --git a/toolkit/actors/AudioPlaybackParent.sys.mjs b/toolkit/actors/AudioPlaybackParent.sys.mjs
+index db682fd90b2bb5330497d2cf2158ff4cac6bbc47..c3eacff3b2215d29104216dd6086c486a86013e9 100644
+--- a/toolkit/actors/AudioPlaybackParent.sys.mjs
++++ b/toolkit/actors/AudioPlaybackParent.sys.mjs
+@@ -11,9 +11,12 @@ export class AudioPlaybackParent extends JSWindowActorParent {
+ }
+ receiveMessage(aMessage) {
+ const browser = this.browsingContext.top.embedderElement;
++ const mediaController = this.browsingContext.mediaController;
++
+ switch (aMessage.name) {
+ case "AudioPlayback:Start":
+ this._hasAudioPlayback = true;
++ browser.ownerGlobal.gZenMediaController.activateMediaControls(mediaController, browser);
+ browser.audioPlaybackStarted();
+ break;
+ case "AudioPlayback:Stop":