New features for media control

This commit is contained in:
Slowlife01
2025-03-16 21:04:08 +07:00
parent a69876325a
commit 0af85f8c03
5 changed files with 286 additions and 75 deletions

View File

@@ -14,6 +14,12 @@ class ZenMediaController {
supportedKeys = ['playpause', 'previoustrack', 'nexttrack'];
pipEligibilityMap = new Map();
mediaControllersMap = new Map();
_tabTimeout = null;
_controllerSwitchTimeout = null;
init() {
this.mediaTitle = document.querySelector('#zen-media-title');
this.mediaArtist = document.querySelector('#zen-media-artist');
@@ -31,49 +37,34 @@ class ZenMediaController {
this.onDeactivated = this._onDeactivated.bind(this);
window.addEventListener('TabSelect', (event) => {
if (this._currentBrowser) {
if (event.target.linkedBrowser.browserId === this._currentBrowser.browserId) {
gZenUIManager.motion
.animate(
this.mediaControlBar,
{
opacity: [1, 0],
y: [0, 10],
},
{
duration: 0.1,
}
)
.then(() => {
this.mediaControlBar.setAttribute('hidden', 'true');
});
} else if (this.mediaControlBar.hasAttribute('hidden')) {
this.mediaControlBar.removeAttribute('hidden');
window.requestAnimationFrame(() => {
this.mediaControlBar.style.height =
this.mediaControlBar.querySelector('toolbaritem').getBoundingClientRect().height + 'px';
this.mediaControlBar.style.opacity = 0;
gZenUIManager.motion.animate(
this.mediaControlBar,
{
opacity: [0, 1],
y: [10, 0],
},
{}
);
});
}
const linkedBrowser = event.target.linkedBrowser;
this.switchController();
gZenUIManager.updateTabsToolbar();
if (this._currentBrowser) {
if (linkedBrowser.browserId === this._currentBrowser.browserId) {
if (this._tabTimeout) {
clearTimeout(this._tabTimeout);
this._tabTimeout = null;
}
this.hideMediaControls();
} else {
this._tabTimeout = setTimeout(() => {
if (!this.mediaControlBar.hasAttribute('pip')) this.showMediaControls();
else this._tabTimeout = null;
}, 500);
}
}
});
window.addEventListener('TabClose', (event) => {
if (this._currentBrowser) {
if (event.target.linkedBrowser.browserId === this._currentBrowser.browserId) {
this.deinitMediaController(this._currentMediaController);
}
}
const linkedBrowser = event.target.linkedBrowser;
this.deinitMediaController(
linkedBrowser.browsingContext.mediaController,
true,
linkedBrowser.browserId === this._currentBrowser?.browserId,
true
);
});
window.addEventListener('DOMAudioPlaybackStarted', (event) => {
@@ -87,24 +78,46 @@ class ZenMediaController {
* Deinitializes a media controller, removing all event listeners and resetting state.
* @param {Object} mediaController - The media controller to deinitialize.
*/
deinitMediaController(mediaController) {
async deinitMediaController(mediaController, shouldForget = true, shouldOverride = true, shouldHide = true) {
if (!mediaController) return;
mediaController.removeEventListener('positionstatechange', this.onPositionstateChange);
mediaController.removeEventListener('playbackstatechange', this.onPlaybackstateChange);
mediaController.removeEventListener('supportedkeyschange', this.onSupportedKeysChange);
mediaController.removeEventListener('metadatachange', this.onMetadataChange);
mediaController.removeEventListener('deactivated', this.onDeactivated);
const retrievedMediaController = this.mediaControllersMap.get(mediaController.id);
this._currentMediaController = null;
this._currentBrowser = null;
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
if (this.tabObserver && shouldOverride) {
this.tabObserver.disconnect();
this.tabObserver = null;
}
gZenUIManager.motion
if (shouldForget) {
mediaController.removeEventListener('positionstatechange', this.onPositionstateChange);
mediaController.removeEventListener('playbackstatechange', this.onPlaybackstateChange);
mediaController.removeEventListener('supportedkeyschange', this.onSupportedKeysChange);
mediaController.removeEventListener('metadatachange', this.onMetadataChange);
mediaController.removeEventListener('deactivated', this.onDeactivated);
this.mediaControllersMap.delete(mediaController.id);
this.pipEligibilityMap.delete(retrievedMediaController.browser.browserId);
}
if (shouldOverride) {
this._currentMediaController = null;
this._currentBrowser = null;
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
if (shouldHide) await this.hideMediaControls();
this.mediaControlBar.removeAttribute('muted');
this.mediaControlBar.classList.remove('playing');
}
}
hideMediaControls() {
if (this.mediaControlBar.hasAttribute('hidden')) return;
return gZenUIManager.motion
.animate(
this.mediaControlBar,
{
@@ -119,8 +132,67 @@ class ZenMediaController {
this.mediaControlBar.setAttribute('hidden', 'true');
gZenUIManager.updateTabsToolbar();
});
this.mediaControlBar.removeAttribute('muted');
this.mediaControlBar.classList.remove('playing');
}
showMediaControls() {
const tab = gBrowser.getTabForBrowser(this._currentBrowser);
if (tab.hasAttribute('pictureinpicture')) return this.hideMediaControls();
if (!this.mediaControlBar.hasAttribute('hidden')) return;
this.updatePipButton();
this.mediaControlBar.removeAttribute('hidden');
window.requestAnimationFrame(() => {
this.mediaControlBar.style.height =
this.mediaControlBar.querySelector('toolbaritem').getBoundingClientRect().height + 'px';
this.mediaControlBar.style.opacity = 0;
gZenUIManager.updateTabsToolbar();
gZenUIManager.motion.animate(
this.mediaControlBar,
{
opacity: [0, 1],
y: [10, 0],
},
{}
);
});
}
setupMediaController(mediaController, browser) {
this._currentMediaController = mediaController;
this._currentBrowser = browser;
this.updatePipButton();
this.tabObserver = new MutationObserver((entries) => {
for (const entry of entries) {
if (entry.target.hasAttribute('pictureinpicture')) {
this.hideMediaControls();
this.mediaControlBar.setAttribute('pip', '');
} else {
const { selectedBrowser } = gBrowser;
if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
this.showMediaControls();
}
this.mediaControlBar.removeAttribute('pip');
}
}
});
this.tabObserver.observe(gBrowser.getTabForBrowser(browser), {
attributes: true,
attributeFilter: ['pictureinpicture'],
});
const positionState = mediaController.getPositionState();
this.mediaControllersMap.set(mediaController.id, {
controller: mediaController,
browser,
position: positionState.position,
duration: positionState.duration,
lastUpdated: Date.now(),
});
}
/**
@@ -128,12 +200,15 @@ class ZenMediaController {
* @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')) {
setupMediaControlUI(metadata, positionState) {
this.updatePipButton();
if (!this.mediaControlBar.classList.contains('playing') && this._currentMediaController.isPlaying) {
this.mediaControlBar.classList.add('playing');
}
this.mediaFocusButton.style.listStyleImage = `url(${this._currentBrowser.mIconURL})`;
const iconURL = this._currentBrowser.mIconURL || `page-icon:${this._currentBrowser.currentURI.spec}`;
this.mediaFocusButton.style.listStyleImage = `url(${iconURL})`;
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
@@ -158,10 +233,21 @@ class ZenMediaController {
this.updateMuteState();
if (!mediaController.isActive || this._currentBrowser?.browserId === browser.browserId) return;
const metadata = mediaController.getMetadata();
const positionState = mediaController.getPositionState();
this.mediaControllersMap.set(mediaController.id, {
controller: mediaController,
browser,
position: positionState.position,
duration: positionState.duration,
lastUpdated: Date.now(),
});
if (this._currentBrowser) this.switchController();
else {
this.deinitMediaController(this._currentMediaController);
this._currentMediaController = mediaController;
this._currentBrowser = browser;
this.setupMediaController(mediaController, browser);
this.setupMediaControlUI(metadata, positionState);
}
mediaController.addEventListener('positionstatechange', this.onPositionstateChange);
@@ -169,28 +255,31 @@ class ZenMediaController {
mediaController.addEventListener('supportedkeyschange', this.onSupportedKeysChange);
mediaController.addEventListener('metadatachange', this.onMetadataChange);
mediaController.addEventListener('deactivated', this.onDeactivated);
}
const metadata = mediaController.getMetadata();
const positionState = mediaController.getPositionState();
this.setupMediaControl(metadata, positionState);
updatePipEligibility(browser, isEligible) {
this.pipEligibilityMap.set(browser.browserId, isEligible);
}
/**
* @param {Event} event - The deactivation event.
*/
_onDeactivated(event) {
if (event.target === this._currentMediaController) {
this.deinitMediaController(event.target);
}
this.deinitMediaController(event.target, true, event.target.id === this._currentMediaController.id, true);
this.switchController();
}
/**
* 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);
_onPlaybackstateChange() {
if (this._currentMediaController?.isPlaying) {
this.mediaControlBar.classList.add('playing');
} else {
this.switchController();
this.mediaControlBar.classList.remove('playing');
}
}
/**
@@ -198,6 +287,7 @@ class ZenMediaController {
* @param {Event} event - The supported keys change event.
*/
_onSupportedKeysChange(event) {
if (event.target !== this._currentMediaController) return;
for (const key of this.supportedKeys) {
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
button.disabled = !event.target.supportedKeys.includes(key);
@@ -209,13 +299,61 @@ class ZenMediaController {
* @param {Event} event - The position state change event.
*/
_onPositionstateChange(event) {
if (event.target !== this._currentMediaController) return;
if (event.target !== this._currentMediaController) {
const mediaController = this.mediaControllersMap.get(event.target.id);
this.mediaControllersMap.set(event.target.id, {
...mediaController,
position: event.position,
duration: event.duration,
lastUpdated: Date.now(),
});
}
this._currentPosition = event.position;
this._currentDuration = event.duration;
this.updateMediaPosition();
}
switchController(force = false) {
let timeout = 3000;
if (this._controllerSwitchTimeout) {
clearTimeout(this._controllerSwitchTimeout);
this._controllerSwitchTimeout = null;
}
if (this.mediaControllersMap.size === 1) timeout = 0;
this._controllerSwitchTimeout = setTimeout(() => {
if (!this._currentMediaController?.isPlaying || force) {
const nextController = Array.from(this.mediaControllersMap.values())
.filter(
(ctrl) =>
ctrl.controller.isPlaying &&
gBrowser.selectedBrowser.browserId !== ctrl.browser.browserId &&
ctrl.controller.id !== this._currentMediaController?.id
)
.sort((a, b) => b.lastUpdated - a.lastUpdated)
.shift();
if (nextController) {
this.deinitMediaController(this._currentMediaController, false, true, false).then(() => {
this.setupMediaController(nextController.controller, nextController.browser);
const elapsedTime = Math.floor((Date.now() - nextController.lastUpdated) / 1000);
this.setupMediaControlUI(nextController.controller.getMetadata(), {
position: nextController.position + (nextController.controller.isPlaying ? elapsedTime : 0),
duration: nextController.duration,
});
this.showMediaControls();
});
}
}
this._controllerSwitchTimeout = null;
}, timeout);
}
/**
* Updates the media progress bar and time display.
*/
@@ -274,6 +412,9 @@ class ZenMediaController {
* @param {Event} event - The metadata change event.
*/
_onMetadataChange(event) {
if (event.target !== this._currentMediaController) return;
this.updatePipButton();
const metadata = event.target.getMetadata();
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
@@ -327,6 +468,18 @@ class ZenMediaController {
}
}
onControllerClose() {
this._currentMediaController?.pause();
this.switchController(true);
this.deinitMediaController(this._currentMediaController, true, true, true);
}
onMediaPip() {
this._currentBrowser.browsingContext.currentWindowGlobal
.getActor('PictureInPictureLauncher')
.sendAsyncMessage('PictureInPicture:KeyToggle');
}
updateMuteState() {
if (!this._currentBrowser) return;
if (this._currentBrowser._audioMuted) {
@@ -335,6 +488,12 @@ class ZenMediaController {
this.mediaControlBar.removeAttribute('muted');
}
}
updatePipButton() {
const isPipEligible = this.pipEligibilityMap.get(this._currentBrowser.browserId);
if (isPipEligible) this.mediaControlBar.setAttribute('can-pip', '');
else this.mediaControlBar.removeAttribute('can-pip');
}
}
window.gZenMediaController = new ZenMediaController();