VulpesOS/gaia/shared/js/media/remote_controls.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

379 lines
11 KiB
JavaScript
Executable file

/* globals BluetoothHelper */
'use strict';
/**
* Media Remote Controls
*
* For media players, like music and video apps should support the capabilities
* to receive and interact with the remote media commands from the connected
* device/client, it might be a bluetooth device that supports A2DP and AVRCP,
* or a remote client, such as the controls in Lock Screen/Utility tray, which
* in the system app and uses the IAC.
*
* Since both AVRCP and IAC use system message to communication, this module
* is designed for handling all the remote commands from system message. With
* this module, players can just set the command listeners to the corresponding
* commands, they might be the common media actions that a player should have,
* so a player that uses this module doesn't need to write duplicate logic on
* handling the same commands from the different requesters, and doesn't need
* to care about where the requests are from.
*
* More, this module also handle the bluetooth states because to receive the
* remote request or send the metadata and play status depends on the bluetooth
* availability and the connection of A2DP, the player that uses the module
* will automatically receive the requests or send the play information to the
* remote client when the bluetooth states change.
*
*/
// Commands for AVRCP.
var AVRCP = {
PLAY_PRESS: 'media-play-button-press',
PLAY_RELEASE: 'media-play-button-release',
PAUSE_PRESS: 'media-pause-button-press',
PAUSE_RELEASE: 'media-pause-button-release',
PLAY_PAUSE_PRESS: 'media-play-pause-button-press',
PLAY_PAUSE_RELEASE: 'media-play-pause-button-release',
STOP_PRESS: 'media-stop-button-press',
STOP_RELEASE: 'media-stop-button-release',
NEXT_PRESS: 'media-next-track-button-press',
NEXT_RELEASE: 'media-next-track-button-release',
PREVIOUS_PRESS: 'media-previous-track-button-press',
PREVIOUS_RELEASE: 'media-previous-track-button-release',
FAST_FORWARD_PRESS: 'media-fast-forward-button-press',
FAST_FORWARD_RELEASE: 'media-fast-forward-button-release',
REWIND_PRESS: 'media-rewind-button-press',
REWIND_RELEASE: 'media-rewind-button-release'
};
// Commands for Inter-App Communication.
var IAC = {
PLAY_PRESS: 'play',
PLAY_PAUSE_PRESS: 'playpause',
PAUSE_PRESS: 'pause',
STOP_PRESS: 'stop',
NEXT_PRESS: 'nexttrack',
PREVIOUS_PRESS: 'prevtrack',
FAST_FORWARD_PRESS: 'fastforwardstart',
FAST_FORWARD_RELEASE: 'fastforwardend',
REWIND_PRESS: 'rewindstart',
REWIND_RELEASE: 'rewindend',
UPDATE_METADATA: 'updatemetadata',
UPDATE_PLAYSTATUS: 'updateplaystatus'
};
// Commands for the players to register, events will be fired to the listeners.
var REMOTE_CONTROLS = {
PLAY: 'play',
PLAY_PAUSE: 'playpause',
PAUSE: 'pause',
STOP: 'stop',
NEXT: 'next',
PREVIOUS: 'previous',
SEEK_PRESS: 'seekpress',
SEEK_RELEASE: 'seekrelease',
UPDATE_METADATA: 'updatemetadata',
UPDATE_PLAYSTATUS: 'updateplaystatus'
};
/**
* @constructor
*/
function MediaRemoteControls() {
this._bluetoothHelper = null;
this._commandListeners = {};
this._isSCOConnected = false;
// Create the empty object for all command listeners.
for (var command in REMOTE_CONTROLS) {
this._commandListeners[REMOTE_CONTROLS[command]] = [];
}
}
/**
* Add the command listener so that it will be executed when the remote
* commands fired.
*
* @param {String} command
* @param {Object} listener
*/
MediaRemoteControls.prototype.addCommandListener = function(command, listener) {
if (this._commandListeners[command]) {
this._commandListeners[command].push(listener);
}
};
/**
* Remove the command listener.
*
* @param {String} name
* @param {Object} listener
*/
MediaRemoteControls.prototype.removeCommandListener = function(name, listener) {
if (this._commandListeners[name]) {
var index = -1;
this._commandListeners[name].forEach(function(currListener, i) {
if (currListener === listener) {
index = i;
}
});
if (index !== -1) {
this._commandListeners[name].splice(index, 1);
}
}
};
/**
* Setup the bluetooth/system message, IAC and wired controls.
*/
MediaRemoteControls.prototype.start = function(callback) {
this._setupBluetooth(callback);
this._setupIAC();
};
/*
* Setup and configure the bluetooth/system message.
*/
MediaRemoteControls.prototype._setupBluetooth = function(callback) {
var self = this;
// AVRCP commands use system message.
navigator.mozSetMessageHandler(
'media-button', this._commandHandler.bind(this)
);
// bluetooth will be handled with BluetoothHelper
// to send metadata and play status when those information are changed.
this._bluetoothHelper = new BluetoothHelper();
this._bluetoothHelper.onrequestmediaplaystatus = playstatusHandler;
this._bluetoothHelper.ona2dpstatuschanged = a2dpConnectionHandler;
this._bluetoothHelper.onscostatuschanged = scoConnectionHandler;
if (callback) {
callback();
}
function playstatusHandler() {
if (self._commandListeners.updateplaystatus.length > 0) {
self._commandHandler(REMOTE_CONTROLS.UPDATE_PLAYSTATUS);
}
}
// A2DP is connected: update the status to the bluetooth device.
// A2DP is disconnected: pause the player like the headphone is unplugged.
function a2dpConnectionHandler(event) {
var isConnected = event.status;
if (isConnected && self._commandListeners.updatemetadata.length > 0) {
self._commandHandler(REMOTE_CONTROLS.UPDATE_METADATA);
}
}
// Also expose the SCO status with the custom event because the SCO connection
// could occupy the audio channel that the media apps using, besides sending
// the play/pause commands, these additional status can help the media apps
// to decide if they want more interaction with users while SCO is connected.
function scoConnectionHandler(event) {
self._isSCOConnected = event.status;
if (self._isSCOConnected) {
self._commandHandler(AVRCP.PAUSE_PRESS);
}
else {
self._commandHandler(AVRCP.PLAY_PRESS);
}
}
};
/*
* Setup and configure the IAC.
*/
MediaRemoteControls.prototype._setupIAC = function() {
var self = this;
this._queuedMessages = [];
// Set up Inter-App Communications
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
// If IAC doesn't exist, just bail out.
if (!app.connect) {
this._queuedMessages = null;
return;
}
app.connect('mediacomms').then(function(ports) {
self._ports = ports;
self._ports.forEach(function(port) {
port.onmessage = function(event) {
self._commandHandler(event.data.command);
};
self._queuedMessages.forEach(function(message) {
port.postMessage(message);
});
});
self._queuedMessages = null;
});
};
};
// Synchronous connection-oriented(SCO) link is the type of radio link used
// for voice data of bluetooth, it establishes when the user is in a call via
// bluetooth hands-free headset. And since only voice will be routed to the
// headset during a call, the media apps can use this api to know the SCO
// status, so that the apps can adjust the ui or notify the user to get better
// user experience, for more detail, please see:
// http://en.wikipedia.org/wiki/Bluetooth_protocols
MediaRemoteControls.prototype.getSCOStatus = function(callback) {
this._bluetoothHelper.isScoConnected(callback, function onerror() {
callback(false);
});
};
MediaRemoteControls.prototype._postMessage = function(name, value) {
var message = {type: name, data: value};
if (!this._ports) {
if (this._queuedMessages) {
this._queuedMessages.push(message);
}
} else {
this._ports.forEach(function(port) {
port.postMessage(message);
});
}
};
/**
* Convert the received messages to the REMOTE_CONTROLS commands, and create
* custom event then pass the new event to the command listeners executer.
*
* @param {String} message
*/
MediaRemoteControls.prototype._commandHandler = function(message) {
var type = 'remote';
var option = {};
switch (message) {
case AVRCP.PLAY_PRESS:
case IAC.PLAY_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PLAY,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.PLAY_PAUSE_PRESS:
case IAC.PLAY_PAUSE_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PLAY_PAUSE,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.PAUSE_PRESS:
case IAC.PAUSE_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PAUSE,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.STOP_PRESS:
case IAC.STOP_PRESS:
option.detail = { command: REMOTE_CONTROLS.STOP };
break;
case AVRCP.NEXT_PRESS:
case IAC.NEXT_PRESS:
option.detail = { command: REMOTE_CONTROLS.NEXT };
break;
case AVRCP.PREVIOUS_PRESS:
case IAC.PREVIOUS_PRESS:
option.detail = { command: REMOTE_CONTROLS.PREVIOUS };
break;
case AVRCP.FAST_FORWARD_PRESS:
case IAC.FAST_FORWARD_PRESS:
option.detail = { command: REMOTE_CONTROLS.SEEK_PRESS, direction: 1 };
break;
case AVRCP.REWIND_PRESS:
case IAC.REWIND_PRESS:
option.detail = { command: REMOTE_CONTROLS.SEEK_PRESS, direction: -1 };
break;
case AVRCP.FAST_FORWARD_RELEASE:
case IAC.FAST_FORWARD_RELEASE:
case AVRCP.REWIND_RELEASE:
case IAC.REWIND_RELEASE:
option.detail = { command: REMOTE_CONTROLS.SEEK_RELEASE };
break;
case IAC.UPDATE_METADATA:
case IAC.UPDATE_PLAYSTATUS:
case REMOTE_CONTROLS.UPDATE_METADATA:
case REMOTE_CONTROLS.UPDATE_PLAYSTATUS:
option.detail = { command: message };
break;
default:
return;
}
var event = new CustomEvent(type, option);
this._executeCommandListeners(event);
};
/**
* Fire the event to the command listeners.
*
* @param {Object} event
*/
MediaRemoteControls.prototype._executeCommandListeners = function(event) {
if (!event.detail) {
return;
}
this._commandListeners[event.detail.command].forEach(function(listener) {
listener(event);
});
};
MediaRemoteControls.prototype.notifyAppInfo = function(info) {
// Send the app info via IAC.
this._postMessage('appinfo', info);
};
/**
* Send the updated metadata to the remote requester/receiver.
*
* @param {Object} metadata
*/
MediaRemoteControls.prototype.notifyMetadataChanged = function(metadata) {
// Send the new metadata via bluetooth.
this._bluetoothHelper.sendMediaMetaData(metadata,
function onsuccess() {
// To do nothing here, expect onsuccess normally.
},
function onerror() {
console.log('Sending Metadata error');
});
// Now, send it via IAC.
this._postMessage('nowplaying', metadata);
};
/**
* Send the updated play status to the remote requester/receiver.
*
* @param {Object} status
*/
MediaRemoteControls.prototype.notifyStatusChanged = function(status) {
// Don't send the interrupted statuses to the remote client because
// they are not the AVRCP statuses.
if (status === 'mozinterruptbegin') {
return;
}
// Send the new status via bluetooth.
this._bluetoothHelper.sendMediaPlayStatus(status,
function onsuccess() {
// To do nothing here, expect onsuccess normally.
},
function onerror() {
console.log('Sending Playstatus error');
});
// Now, send it via IAC.
this._postMessage('status', status);
};