@jellybrick/mpris-service
Version:
Node.js implementation for the MPRIS D-Bus Interface Specification to create a mediaplayer service
443 lines (436 loc) • 18.8 kB
JavaScript
"use strict";
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
require('source-map-support').install();
const {
EventEmitter
} = require('events');
const dbus = require('@jellybrick/dbus-next');
const PlayerInterface = require('./interfaces/player');
const RootInterface = require('./interfaces/root');
const PlaylistsInterface = require('./interfaces/playlists');
const TracklistInterface = require('./interfaces/tracklist');
const types = require('./interfaces/types');
const constants = require('./constants');
const MPRIS_PATH = '/org/mpris/MediaPlayer2';
function lcfirst(str) {
return str[0].toLowerCase() + str.substring(1);
}
var _Player_brand = /*#__PURE__*/new WeakSet();
class Player extends EventEmitter {
/**
* Construct a new Player and export it on the DBus session bus.
*
* For more information about the properties of this class, see [the MPRIS DBus Interface Specification](https://specifications.freedesktop.org/mpris-spec/latest/).
*
* Method Call Events
* ------------------
*
* The Player is an `EventEmitter` that emits events when the corresponding
* methods are called on the DBus interface over the wire.
*
* The Player emits events whenever the corresponding methods on the DBus
* interface are called.
*
* * `raise` - Brings the media player's user interface to the front using any appropriate mechanism available.
* * `quit` - Causes the media player to stop running.
* * `next` - Skips to the next track in the tracklist.
* * `previous` - Skips to the previous track in the tracklist.
* * `pause` - Pauses playback.
* * `playPause` - Pauses playback. If playback is already paused, resumes playback. If playback is stopped, starts playback.
* * `stop` - Stops playback.
* * `play` - Starts or resumes playback.
* * `seek` - Seeks forward in the current track by the specified number of microseconds. With event data `offset`.
* * `position` - Sets the current track position in microseconds. With event data `{ trackId, position }`.
* * `open` - Opens the Uri given as an argument. With event data `{ uri }`.
* * `volume` - Sets the volume of the player. With event data `volume` (between 0.0 and 1.0).
* * `shuffle` - Sets whether shuffle is enabled on the player. With event data `shuffleStatus` (boolean).
* * `rate` - Sets the playback rate of the player. A value of 1.0 is the normal rate. With event data `rate`.
* * `loopStatus` - Sets the loop status of the player to either 'None', 'Track', or 'Playlist'. With event data `loopStatus`.
* * `activatePlaylist` - Starts playing the given playlist. With event data `playlistId`.
*
* The Player may also emit an `error` event with the underlying Node `Error`
* as the event data. After receiving this event, the Player may be
* disconnected.
*
* ```
* player.on('play', () => {
* realPlayer.play();
* });
*
* player.on('shuffle', (enableShuffle) => {
* realPlayer.setShuffle(enableShuffle);
* player.shuffle = enableShuffle;
* });
* ```
*
* Player Properties
* -----------------
*
* Player properties (documented below) should be kept up to date to reflect
* the state of your real player. These properties can be gotten by the client
* through the `org.freedesktop.DBus.Properties` interface which will return
* the value currently set on the player. Setting these properties on the
* player to a different value will emit the `PropertiesChanged` signal on the
* properties interface to notify clients that properties of the player have
* changed.
*
* ```
* realPlayer.on('shuffle:changed', (shuffleEnabled) => {
* player.shuffle = shuffleEnabled;
* });
*
* realPlayer.on('play', () => {
* player.playbackStatus = 'Playing';
* });
* ```
*
* Player Position
* ---------------
*
* Clients can get the position of your player by getting the `Position`
* property of the `org.mpris.MediaPlayer2.Player` interface. Since position
* updates continuously, {@link Player#getPosition} is implemented as a getter
* you can override on your Player. This getter will be called when a client
* requests the position and should return the position of your player for the
* client in microseconds.
*
* ```
* player.getPosition() {
* return realPlayer.getPositionInMicroseconds();
* }
* ```
*
* When your real player seeks to a new location, such as when someone clicks
* on the time bar, you can notify clients of the new position by calling the
* {@link Player#seeked} method. This will raise the `Seeked` signal on the
* `org.mpris.MediaPlayer2.Player` interface with the given current time of the
* player in microseconds.
*
* ```
* realPlayer.on('seeked', (positionInMicroseconds) => {
* player.seeked(positionInMicroseconds);
* });
* ```
*
* Clients can request to set position using the `Seek` and `SetPosition`
* methods of the `org.mpris.MediaPlayer2.Player` interface. These requests are
* implemented as events on the Player similar to the other requests.
*
* ```
* player.on('seek', (offset) => {
* // note that offset may be negative
* let currentPosition = realPlayer.getPositionInMicroseconds();
* let newPosition = currentPosition + offset;
* realPlayer.setPosition(newPosition);
* });
*
* player.on('position', (event) => {
* // check that event.trackId is the current track before continuing.
* realPlayer.setPosition(event.position);
* });
* ```
*
* @class Player
* @param {
* name: String,
* identity: String,
* supportedMimeTypes: string[],
* supportedInterfaces: string[]
* } options - Options for the player.
* @param {String} options.name - Name on the bus to export to as `org.mpris.MediaPlayer2.{name}`.
* @param {String} options.identity - Identity for the player to display on the root media player interface.
* @param {Array} options.supportedMimeTypes - Mime types this player can open with the `org.mpris.MediaPlayer2.Open` method.
* @param {Array} options.supportedInterfaces - The interfaces this player supports. Can include `'player'`, `'playlists'`, and `'trackList'`.
* @property {String} identity - A friendly name to identify the media player to users.
* @property {Boolean} fullscreen - Whether the media player is occupying the fullscreen.
* @property {Array} supportedUriSchemes - The URI schemes supported by the media player.
* @property {Array} supportedMimeTypes - The mime-types supported by the media player.
* @property {Boolean} canQuit - Whether the player can quit.
* @property {Boolean} canRaise - Whether the player can raise.
* @property {Boolean} canSetFullscreen - Whether the player can be set to fullscreen.
* @property {Boolean} hasTrackList - Indicates whether the /org/mpris/MediaPlayer2 object implements the org.mpris.MediaPlayer2.TrackList interface.
* @property {String} desktopEntry - The basename of an installed .desktop file which complies with the Desktop entry specification, with the ".desktop" extension stripped.
* @property {String} playbackStatus - The current playback status. May be "Playing", "Paused" or "Stopped".
* @property {String} loopStatus - The current loop/repeat status. May be "None", "Track", or "Playlist".
* @property {Boolean} shuffle - Whether the player is shuffling.
* @property {Object} metadata - The metadata of the current element. If there is a current track, this must have a "mpris:trackid" entry (of D-Bus type "o") at the very least, which contains a D-Bus path that uniquely identifies this track.
* @property {Number} volume - The volume level. (Double)
* @property {Boolean} canControl - Whether the media player may be controlled over this interface.
* @property {Boolean} canPause - Whether playback can be paused using Pause or PlayPause.
* @property {Boolean} canPlay - Whether playback can be started using Play or PlayPause.
* @property {Boolean} canSeek - Whether the client can control the playback position using Seek and SetPosition.
* @property {Boolean} canGoNext - Whether the client can call the Next method on this interface and expect the current track to change.
* @property {Boolean} canGoPrevious - Whether the client can call the Previous method on this interface and expect the current track to change.
* @property {Number} rate - The current playback rate. (Double)
* @property {Number} minimumRate - The minimum value which the Rate property can take. (Double)
* @property {Number} maximumRate - The maximum value which the Rate property can take. (Double)
* @property {Array} playlists - The current playlists set by {@link Player#setPlaylists}. (Not a DBus property).
* @property {String} activePlaylist - The id of the currently-active playlist.
*/
constructor(options) {
super();
_classPrivateMethodInitSpec(this, _Player_brand);
this.name = options.name;
this.supportedInterfaces = options.supportedInterfaces || ['player'];
this._tracks = [];
this.init(options);
}
init(opts) {
this.serviceName = `org.mpris.MediaPlayer2.${this.name}`;
dbus.validators.assertBusNameValid(this.serviceName);
this._bus = dbus.sessionBus();
this._bus.on('error', err => {
this.emit('error', err);
});
this.interfaces = {};
_assertClassBrand(_Player_brand, this, _addRootInterface).call(this, this._bus, opts);
if (this.supportedInterfaces.indexOf('player') >= 0) {
_assertClassBrand(_Player_brand, this, _addPlayerInterface).call(this, this._bus);
}
if (this.supportedInterfaces.indexOf('trackList') >= 0) {
_assertClassBrand(_Player_brand, this, _addTracklistInterface).call(this, this._bus);
}
if (this.supportedInterfaces.indexOf('playlists') >= 0) {
_assertClassBrand(_Player_brand, this, _addPlaylistsInterface).call(this, this._bus);
}
for (let k of Object.keys(this.interfaces)) {
let iface = this.interfaces[k];
this._bus.export(MPRIS_PATH, iface);
}
this._bus.requestName(this.serviceName, dbus.NameFlag.DO_NOT_QUEUE).then(reply => {
if (reply === dbus.RequestNameReply.EXISTS) {
this.serviceName = `${this.serviceName}.instance${process.pid}`;
return this._bus.requestName(this.serviceName);
}
}).catch(err => {
this.emit('error', err);
});
}
/**
* Get a valid object path with the `subpath` as the basename which is suitable
* for use as an id.
*
* @name Player#objectPath
* @function
* @param {String} subpath - The basename of this path
* @returns {String} - A valid object path that can be used as an id.
*/
objectPath(subpath) {
let path = `/org/node/mediaplayer/${this.name}`;
if (subpath) {
path += `/${subpath}`;
}
return path;
}
/**
* Gets the position of this player. This method is intended to be overridden
* by the user to return the position of the player in microseconds.
*
* @name Player#getPosition
* @function
* @returns {Number} - The current position of the player in microseconds. (Integer)
*/
getPosition() {
return 0;
}
/**
* Emits the `Seeked` DBus signal to listening clients with the given position.
*
* @name Player#seeked
* @function
* @param {Number} position - The position in microseconds. (Integer)
*/
seeked(position) {
let seekTo = Math.floor(position || 0);
if (isNaN(seekTo)) {
throw new Error(`seeked expected a number (got ${position})`);
}
this.interfaces.player.Seeked(seekTo);
}
getTrackIndex(trackId) {
for (let i = 0; i < this.tracks.length; i++) {
let track = this.tracks[i];
if (track['mpris:trackid'] === trackId) {
return i;
}
}
return -1;
}
getTrack(trackId) {
return this.tracks[this.getTrackIndex(trackId)];
}
addTrack(track) {
this.tracks.push(track);
this.interfaces.tracklist.setTracks(this.tracks);
let afterTrack = '/org/mpris/MediaPlayer2/TrackList/NoTrack';
if (this.tracks.length > 2) {
afterTrack = this.tracks[this.tracks.length - 2]['mpris:trackid'];
}
this.interfaces.tracklist.TrackAdded(afterTrack);
}
removeTrack(trackId) {
let i = this.getTrackIndex(trackId);
this.tracks.splice(i, 1);
this.interfaces.tracklist.setTracks(this.tracks);
this.interfaces.tracklist.TrackRemoved(trackId);
}
/**
* Get the index of a playlist entry in the `playlists` list property of the
* player from the given id.
*
* @name Player#getPlaylistIndex
* @function
* @param {String} playlistId - The id for the playlist entry.
*/
getPlaylistIndex(playlistId) {
for (let i = 0; i < this.playlists.length; i++) {
let playlist = this.playlists[i];
if (playlist.Id === playlistId) {
return i;
}
}
return -1;
}
/**
* Set the list of playlists advertised to listeners on the bus. Each playlist
* must have string members `Id`, `Name`, and `Icon`.
*
* @name Player#setPlaylists
* @function
* @param {Array} playlists - A list of playlists.
*/
setPlaylists(playlists) {
this.playlists = playlists;
this.playlistCount = playlists.length;
this.playlists.forEach(playlist => {
if (playlist) {
this.interfaces.playlists.PlaylistChanged(playlist);
}
});
}
/**
* Set the playlist identified by `playlistId` to be the currently active
* playlist.
*
* @name Player#setActivePlaylist
* @function
* @param {String} playlistId - The id of the playlist to activate.
*/
setActivePlaylist(playlistId) {
this.interfaces.playlists.setActivePlaylistId(playlistId);
}
/**
* Enumerated value for the `playbackStatus` property of the player to indicate
* a track is currently playing.
*
* @name Player#PLAYBACK_STATUS_PLAYING
* @static
* @constant
*/
}
function _addRootInterface(bus, opts) {
this.interfaces.root = new RootInterface(this, opts);
_assertClassBrand(_Player_brand, this, _addEventedPropertiesList).call(this, this.interfaces.root, ['Identity', 'Fullscreen', 'SupportedUriSchemes', 'SupportedMimeTypes', 'CanQuit', 'CanRaise', 'CanSetFullscreen', 'HasTrackList', 'DesktopEntry']);
}
function _addPlayerInterface(bus) {
this.interfaces.player = new PlayerInterface(this);
let eventedProps = ['PlaybackStatus', 'LoopStatus', 'Rate', 'Shuffle', 'Metadata', 'Volume', 'CanControl', 'CanPause', 'CanPlay', 'CanSeek', 'CanGoNext', 'CanGoPrevious', 'MinimumRate', 'MaximumRate'];
_assertClassBrand(_Player_brand, this, _addEventedPropertiesList).call(this, this.interfaces.player, eventedProps);
}
function _addTracklistInterface(bus) {
this.interfaces.tracklist = new TracklistInterface(this);
_assertClassBrand(_Player_brand, this, _addEventedPropertiesList).call(this, this.interfaces.tracklist, ['CanEditTracks']);
Object.defineProperty(this, 'tracks', {
get: function () {
return this._tracks;
},
set: function (value) {
this._tracks = value;
this.interfaces.tracklist.TrackListReplaced(value);
},
enumerable: true,
configurable: true
});
}
function _addPlaylistsInterface(bus) {
this.interfaces.playlists = new PlaylistsInterface(this);
_assertClassBrand(_Player_brand, this, _addEventedPropertiesList).call(this, this.interfaces.playlists, ['PlaylistCount', 'ActivePlaylist']);
}
function _addEventedProperty(iface, name) {
let localName = lcfirst(name);
Object.defineProperty(this, localName, {
get: function () {
let value = iface[name];
if (name === 'ActivePlaylist') {
return types.playlistToPlain(value);
} else if (name === 'Metadata') {
return types.metadataToPlain(value);
}
return value;
},
set: function (value) {
iface.setProperty(name, value);
},
enumerable: true,
configurable: true
});
}
function _addEventedPropertiesList(iface, props) {
for (let i = 0; i < props.length; i++) {
_assertClassBrand(_Player_brand, this, _addEventedProperty).call(this, iface, props[i]);
}
}
_defineProperty(Player, "PLAYBACK_STATUS_PLAYING", constants.PLAYBACK_STATUS_PLAYING);
/**
* Enumerated value for the `playbackStatus` property of the player to indicate
* a track is currently paused.
*
* @name Player#PLAYBACK_STATUS_PAUSED
* @static
* @constant
*/
_defineProperty(Player, "PLAYBACK_STATUS_PAUSED", constants.PLAYBACK_STATUS_PAUSED);
/**
* Enumerated value for the `playbackStatus` property of the player to indicate
* there is no track currently playing.
*
* @name Player#PLAYBACK_STATUS_STOPPED
* @static
* @constant
*/
_defineProperty(Player, "PLAYBACK_STATUS_STOPPED", constants.PLAYBACK_STATUS_STOPPED);
/**
* Enumerated value for the `loopStatus` property of the player to indicate
* playback will stop when there are no more tracks to play.
*
* @name Player#LOOP_STATUS_NONE
* @static
* @constant
*/
_defineProperty(Player, "LOOP_STATUS_NONE", constants.LOOP_STATUS_NONE);
/**
* Enumerated value for the `loopStatus` property of the player to indicate the
* current track will start again from the beginning once it has finished
* playing.
*
* @name Player#LOOP_STATUS_TRACK
* @static
* @constant
*/
_defineProperty(Player, "LOOP_STATUS_TRACK", constants.LOOP_STATUS_TRACK);
/**
* Enumerated value for the `loopStatus` property of the player to indicate the
* playback loops through a list of tracks.
*
* @name Player#LOOP_STATUS_PLAYLIST
* @static
* @constant
*/
_defineProperty(Player, "LOOP_STATUS_PLAYLIST", constants.LOOP_STATUS_PLAYLIST);
module.exports = Player;
//# sourceMappingURL=index.js.map