mirror of
https://github.com/zen-browser/desktop.git
synced 2025-10-06 09:56:36 +00:00
484 lines
16 KiB
JavaScript
484 lines
16 KiB
JavaScript
class ZenMediaController {
|
|
_currentMediaController = null;
|
|
_currentBrowser = null;
|
|
_mediaUpdateInterval = null;
|
|
|
|
mediaTitle = null;
|
|
mediaArtist = null;
|
|
mediaControlBar = null;
|
|
mediaProgressBar = null;
|
|
mediaCurrentTime = null;
|
|
mediaDuration = null;
|
|
mediaFocusButton = null;
|
|
mediaProgressBarContainer = null;
|
|
|
|
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');
|
|
this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
|
|
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');
|
|
this.mediaProgressBarContainer = document.querySelector('#zen-media-progress-hbox');
|
|
|
|
this.onPositionstateChange = this._onPositionstateChange.bind(this);
|
|
this.onPlaybackstateChange = this._onPlaybackstateChange.bind(this);
|
|
this.onSupportedKeysChange = this._onSupportedKeysChange.bind(this);
|
|
this.onMetadataChange = this._onMetadataChange.bind(this);
|
|
this.onDeactivated = this._onDeactivated.bind(this);
|
|
this.onPipModeChange = this._onPictureInPictureModeChange.bind(this);
|
|
|
|
window.addEventListener('TabSelect', (event) => {
|
|
const linkedBrowser = event.target.linkedBrowser;
|
|
this.switchController();
|
|
|
|
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) => {
|
|
const linkedBrowser = event.target.linkedBrowser;
|
|
if (!linkedBrowser?.browsingContext?.mediaController) return;
|
|
this.deinitMediaController(
|
|
linkedBrowser.browsingContext.mediaController,
|
|
true,
|
|
linkedBrowser.browserId === this._currentBrowser?.browserId,
|
|
true
|
|
);
|
|
});
|
|
|
|
window.addEventListener('DOMAudioPlaybackStarted', (event) => {
|
|
setTimeout(() => {
|
|
if (
|
|
this._currentMediaController?.isPlaying &&
|
|
this.mediaControlBar.hasAttribute('hidden') &&
|
|
!this.mediaControlBar.hasAttribute('pip')
|
|
) {
|
|
const { selectedBrowser } = gBrowser;
|
|
if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
|
|
this.showMediaControls();
|
|
}
|
|
}
|
|
}, 1000);
|
|
|
|
this.activateMediaControls(event.target.browsingContext.mediaController, event.target);
|
|
});
|
|
|
|
window.addEventListener('DOMAudioPlaybackStopped', () => this.updateMuteState());
|
|
}
|
|
|
|
async deinitMediaController(mediaController, shouldForget = true, shouldOverride = true, shouldHide = true) {
|
|
if (!mediaController) return;
|
|
|
|
const retrievedMediaController = this.mediaControllersMap.get(mediaController.id);
|
|
|
|
if (shouldForget) {
|
|
mediaController.removeEventListener('pictureinpicturemodechange', this.onPipModeChange);
|
|
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,
|
|
{
|
|
opacity: [1, 0],
|
|
y: [0, 10],
|
|
},
|
|
{
|
|
duration: 0.1,
|
|
}
|
|
)
|
|
.then(() => {
|
|
this.mediaControlBar.setAttribute('hidden', 'true');
|
|
gZenUIManager.updateTabsToolbar();
|
|
});
|
|
}
|
|
|
|
showMediaControls() {
|
|
if (this._currentMediaController.isBeingUsedInPIPModeOrFullscreen) return this.hideMediaControls();
|
|
if (!this.mediaControlBar.hasAttribute('hidden')) return;
|
|
|
|
this.updatePipButton();
|
|
const mediaInfoElements = [this.mediaTitle, this.mediaArtist];
|
|
for (const element of mediaInfoElements) {
|
|
element.removeAttribute('overflow'); // So we can properly recalculate the overflow
|
|
}
|
|
|
|
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],
|
|
},
|
|
{}
|
|
);
|
|
this.addLabelOverflows(mediaInfoElements);
|
|
});
|
|
}
|
|
|
|
addLabelOverflows(elements) {
|
|
for (const element of elements) {
|
|
const parent = element.parentElement;
|
|
if (element.scrollWidth > parent.clientWidth) {
|
|
element.setAttribute('overflow', '');
|
|
} else {
|
|
element.removeAttribute('overflow');
|
|
}
|
|
}
|
|
}
|
|
|
|
setupMediaController(mediaController, browser) {
|
|
this._currentMediaController = mediaController;
|
|
this._currentBrowser = browser;
|
|
|
|
this.updatePipButton();
|
|
|
|
const positionState = mediaController.getPositionState();
|
|
this.mediaControllersMap.set(mediaController.id, {
|
|
controller: mediaController,
|
|
browser,
|
|
position: positionState.position,
|
|
duration: positionState.duration,
|
|
lastUpdated: Date.now(),
|
|
});
|
|
}
|
|
|
|
setupMediaControlUI(metadata, positionState) {
|
|
this.updatePipButton();
|
|
|
|
if (!this.mediaControlBar.classList.contains('playing') && this._currentMediaController.isPlaying) {
|
|
this.mediaControlBar.classList.add('playing');
|
|
}
|
|
|
|
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 || '';
|
|
|
|
gZenUIManager.updateTabsToolbar();
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
activateMediaControls(mediaController, browser) {
|
|
this.updateMuteState();
|
|
this.switchController();
|
|
|
|
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.setupMediaController(mediaController, browser);
|
|
this.setupMediaControlUI(metadata, positionState);
|
|
}
|
|
|
|
mediaController.addEventListener('pictureinpicturemodechange', this.onPipModeChange);
|
|
mediaController.addEventListener('positionstatechange', this.onPositionstateChange);
|
|
mediaController.addEventListener('playbackstatechange', this.onPlaybackstateChange);
|
|
mediaController.addEventListener('supportedkeyschange', this.onSupportedKeysChange);
|
|
mediaController.addEventListener('metadatachange', this.onMetadataChange);
|
|
mediaController.addEventListener('deactivated', this.onDeactivated);
|
|
}
|
|
|
|
updatePipEligibility(browser, isEligible) {
|
|
this.pipEligibilityMap.set(browser.browserId, isEligible);
|
|
}
|
|
|
|
_onDeactivated(event) {
|
|
this.deinitMediaController(event.target, true, event.target.id === this._currentMediaController.id, true);
|
|
this.switchController();
|
|
}
|
|
|
|
_onPlaybackstateChange() {
|
|
if (this._currentMediaController?.isPlaying) {
|
|
this.mediaControlBar.classList.add('playing');
|
|
} else {
|
|
this.switchController();
|
|
this.mediaControlBar.classList.remove('playing');
|
|
}
|
|
}
|
|
|
|
_onSupportedKeysChange(event) {
|
|
if (event.target.id !== this._currentMediaController?.id) return;
|
|
for (const key of this.supportedKeys) {
|
|
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
|
|
button.disabled = !event.target.supportedKeys.includes(key);
|
|
}
|
|
}
|
|
|
|
_onPositionstateChange(event) {
|
|
if (event.target.id !== this._currentMediaController?.id) {
|
|
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).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);
|
|
}
|
|
|
|
updateMediaPosition() {
|
|
if (this._mediaUpdateInterval) {
|
|
clearInterval(this._mediaUpdateInterval);
|
|
this._mediaUpdateInterval = null;
|
|
}
|
|
|
|
if (this._currentDuration >= 900_000) return this.mediaControlBar.setAttribute('media-position-hidden', 'true');
|
|
else this.mediaControlBar.removeAttribute('media-position-hidden');
|
|
|
|
if (!this._currentDuration) return;
|
|
|
|
this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
|
|
this.mediaDuration.textContent = this.formatSecondsToTime(this._currentDuration);
|
|
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
|
|
|
|
this._mediaUpdateInterval = setInterval(() => {
|
|
if (this._currentMediaController?.isPlaying) {
|
|
this._currentPosition += 1;
|
|
if (this._currentPosition > this._currentDuration) {
|
|
this._currentPosition = this._currentDuration;
|
|
}
|
|
this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
|
|
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
|
|
} else {
|
|
clearInterval(this._mediaUpdateInterval);
|
|
this._mediaUpdateInterval = null;
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
formatSecondsToTime(seconds) {
|
|
if (!seconds || isNaN(seconds)) return '0:00';
|
|
|
|
const totalSeconds = Math.max(0, Math.ceil(seconds));
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60).toString();
|
|
const secs = (totalSeconds % 60).toString();
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${minutes.padStart(2, '0')}:${secs.padStart(2, '0')}`;
|
|
}
|
|
|
|
return `${minutes}:${secs.padStart(2, '0')}`;
|
|
}
|
|
|
|
_onMetadataChange(event) {
|
|
if (event.target.id !== this._currentMediaController?.id) return;
|
|
this.updatePipButton();
|
|
|
|
const metadata = event.target.getMetadata();
|
|
this.mediaTitle.textContent = metadata.title || '';
|
|
this.mediaArtist.textContent = metadata.artist || '';
|
|
}
|
|
|
|
_onPictureInPictureModeChange(event) {
|
|
if (event.target.id !== this._currentMediaController?.id) return;
|
|
if (event.target.isBeingUsedInPIPModeOrFullscreen) {
|
|
this.hideMediaControls();
|
|
this.mediaControlBar.setAttribute('pip', '');
|
|
} else {
|
|
const { selectedBrowser } = gBrowser;
|
|
if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
|
|
this.showMediaControls();
|
|
}
|
|
|
|
this.mediaControlBar.removeAttribute('pip');
|
|
}
|
|
}
|
|
|
|
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.formatSecondsToTime(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() {
|
|
if (!this._currentBrowser) return;
|
|
const sidebarId = this._currentBrowser.getAttribute('zen-sidebar-id');
|
|
if (sidebarId) gZenBrowserManagerSidebar.open(sidebarId);
|
|
else 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();
|
|
}
|
|
}
|
|
|
|
onControllerClose() {
|
|
this._currentMediaController?.pause();
|
|
this.switchController(true);
|
|
this.deinitMediaController(this._currentMediaController);
|
|
}
|
|
|
|
onMediaPip() {
|
|
this._currentBrowser.browsingContext.currentWindowGlobal
|
|
.getActor('PictureInPictureLauncher')
|
|
.sendAsyncMessage('PictureInPicture:KeyToggle');
|
|
}
|
|
|
|
updateMuteState() {
|
|
if (!this._currentBrowser) return;
|
|
if (this._currentBrowser._audioMuted) {
|
|
this.mediaControlBar.setAttribute('muted', '');
|
|
} else {
|
|
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();
|