VulpesOS/gaia/apps/system/js/media_playback.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

284 lines
8.9 KiB
JavaScript
Executable file

/* global IACHandler, Service */
'use strict';
function MediaPlaybackWidget(container, options) {
this.container = container;
this.nowPlaying = container.querySelector('.media-playback-nowplaying');
this.controls = container.querySelector('.media-playback-controls');
this.track = container.querySelector('.track');
this.previousButton = container.querySelector('.previous');
this.playPauseButton = container.querySelector('.play-pause');
this.nextButton = container.querySelector('.next');
this.previousButton.addEventListener(
'contextmenu', this.handleContextmenu.bind(this)
);
this.nextButton.addEventListener(
'contextmenu', this.handleContextmenu.bind(this)
);
this.container.addEventListener('touchend', this.handleTouchend.bind(this));
// When prev/next button releases, we need to know user simply clicking on it
// or holding on and perform fastseeking. Keeping this state to know which
// command should issue.
this.isFastSeeking = false;
window.addEventListener('iac-mediacomms', this.handleMessage.bind(this));
// When SCO status changes, we need to adjust the ui of the playback controls
window.addEventListener(
'bluetoothprofileconnectionchange', this.handleBluetoothChange.bind(this)
);
// Listen to the headphoneschange event for monitoring the audio routing.
var acm = navigator.mozAudioChannelManager;
if (acm) {
acm.addEventListener(
'headphoneschange', this.handleHeadphonesChange.bind(this)
);
}
this.audioRouting = 'speaker';
if (options && options.nowPlayingAction === 'openapp') {
this.nowPlaying.addEventListener('click', this.openMediaApp.bind(this));
}
// Listen for when the music app is terminated. We know which app to look
// for because we got it from the "appinfo" message. Then we hide the Now
// Playing container. XXX: This is a gigantic hack, stemming from
// <https://bugzilla.mozilla.org/show_bug.cgi?id=915880>. If you're thinking
// about doing something similar, step away from your keyboard immediately.
window.addEventListener('appterminated', function(event) {
if (event.detail.origin === this.origin) {
this.hidden = true;
}
}.bind(this));
}
MediaPlaybackWidget.prototype = {
get hidden() {
return this.container.hidden;
},
set hidden(value) {
this.container.hidden = value;
},
handleMessage: function mpw_handleMessage(event) {
var message = event.detail;
switch (message.type) {
case 'appinfo':
this.updateAppInfo(message.data);
break;
case 'nowplaying':
this.updateNowPlaying(message.data);
break;
case 'status':
this.updatePlaybackStatus(message.data);
break;
}
},
handleHeadphonesChange: function mpw_handleHeadphonesChange(event) {
this.handleAudioRouteChange(event, 'wired');
},
handleBluetoothChange: function mpw_handleBluetoothChange(event) {
var name = event.detail.name;
var connected = event.detail.connected;
if (name === 'sco') {
this.container.classList.toggle('disabled', connected);
} else if (name === 'a2dp') {
this.handleAudioRouteChange(event, 'bluetooth');
}
},
// Because currently the mozAudioChannelManager does not provide any api for
// querying the active headphones/headset(audio routing), so to fit the ux
// requirement, we have to monitor the wired headphones and bluetooth headset
// statuses in system, then decide if we want to pause the music player after
// one of the headphones/headset is disconnected.
// We should move this logic back to shared/js/media/remote_controls.js since
// the remote logics should be handled in remote controls module.
handleAudioRouteChange: function mpw_handleAudioRouteChange(event, reason) {
var isWiredHeadphonesConnected = false;
var isBluetoothHeadsetConnected = false;
if (reason === 'wired') {
isWiredHeadphonesConnected = event.target.headphones;
} else {
isBluetoothHeadsetConnected = event.detail.connected;
}
// Save the audio routing when one of the headphones/headset is connected.
if (isWiredHeadphonesConnected || isBluetoothHeadsetConnected) {
this.audioRouting = isWiredHeadphonesConnected ? 'wired' : 'bluetooth';
} else {
// Check if it's disconnecting the active headphones/headset.
// If so, then send pause command via IAC to notify the music app.
if (reason === this.audioRouting) {
this.sendCommand('pause');
} else if (reason !== 'wired' && reason !== 'bluetooth') {
throw Error('Not audio route changed from wired or bluetooth!');
}
isWiredHeadphonesConnected = navigator.mozAudioChannelManager &&
navigator.mozAudioChannelManager.headphones;
isBluetoothHeadsetConnected =
Service.query('Bluetooth.isA2DPProfileConnected');
// Save the correct audio routing for next unplugged/disconnected event
if (isWiredHeadphonesConnected) {
this.audioRouting = 'wired';
} else if (isBluetoothHeadsetConnected) {
this.audioRouting = 'bluetooth';
} else {
this.audioRouting = 'speaker';
}
}
},
updateAppInfo: function mpw_updateAppInfo(info) {
if (!info) {
return;
}
this.origin = info.origin;
},
updateNowPlaying: function mpw_updateNowPlaying(metadata) {
if (!metadata) {
return;
}
var title = metadata.title.trim();
var artist = metadata.artist.trim();
if (title || artist) {
this.track.removeAttribute('data-l10n-id');
this.track.textContent= '';
if (title) {
var titleNode = document.createTextNode(title);
this.track.appendChild(titleNode);
}
if (title && artist) {
var emDashNode = document.createTextNode(' — '); // Using a &mdash;
var artistNode = document.createTextNode(artist);
var artistBdiNode = document.createElement('bdi');
this.track.appendChild(emDashNode);
artistBdiNode.appendChild(artistNode);
this.track.appendChild(artistBdiNode);
}
else if (artist) { // but no title
this.track.appendChild(document.createTextNode(artist));
}
} else {
this.track.setAttribute('data-l10n-id', 'UnknownTrack');
}
},
updatePlaybackStatus: function mp_updatePlaybackStatus(status) {
switch (status.playStatus) {
case 'PLAYING':
// WORKAROUND: Sometimes we can still get the playing event after music
// app terminated, so we check the music app existence when playback is
// hidden initially before showing. Remove it once Bug 915880 fixed.
var origin = this.origin;
if (this.hidden && !Service.query('AppWindowManager.getApp', origin)) {
return;
}
this.hidden = false;
this.playPauseButton.dataset.icon = 'pause';
this.playPauseButton.setAttribute('data-l10n-id',
'mediaPlaybackPause');
break;
case 'PAUSED':
this.hidden = false;
this.playPauseButton.dataset.icon = 'play';
this.playPauseButton.setAttribute('data-l10n-id',
'mediaPlaybackPlay');
break;
case 'STOPPED':
this.hidden = true;
break;
case 'mozinterruptbegin':
this.hidden = true;
break;
}
},
openMediaApp: function mp_openMediaApp(event) {
if (this.origin) {
var evt = new CustomEvent('displayapp', {
bubbles: true,
cancelable: true,
detail: Service.query('getApp', this.origin)
});
window.dispatchEvent(evt);
}
},
sendCommand: function mp_sendCommand(command) {
var port = IACHandler.getPort('mediacomms');
if (port) {
port.postMessage({command: command});
}
},
handleContextmenu: function mp_handleContextmenu(event) {
var command = null;
switch (event.target) {
case this.previousButton:
command = 'rewindstart';
break;
case this.nextButton:
command = 'fastforwardstart';
break;
}
if (command) {
this.isFastSeeking = true;
this.sendCommand(command);
}
},
handleTouchend: function mp_handleTouchend(event) {
var command = null;
switch (event.target) {
case this.previousButton:
if (this.isFastSeeking) {
this.isFastSeeking = false;
command = 'rewindend';
} else {
command = 'prevtrack';
}
break;
case this.nextButton:
if (this.isFastSeeking) {
this.isFastSeeking = false;
command = 'fastforwardend';
} else {
command = 'nexttrack';
}
break;
case this.playPauseButton:
// The play/pause indicator will get set once the music app replies
// with its "mode" message, but this will make us appear speedier.
event.target.classList.toggle('is-paused');
command = 'playpause';
break;
}
if (command) {
this.sendCommand(command);
}
}
};