782 lines
21 KiB
JavaScript
782 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
const Gio = imports.gi.Gio;
|
|
const GLib = imports.gi.GLib;
|
|
const GObject = imports.gi.GObject;
|
|
|
|
|
|
var Player = GObject.registerClass({
|
|
GTypeName: 'GSConnectMPRISPlayer',
|
|
Implements: [Gio.DBusInterface],
|
|
Properties: {
|
|
// Application Properties
|
|
'CanQuit': GObject.ParamSpec.boolean(
|
|
'CanQuit',
|
|
'Can Quit',
|
|
'Whether the client can call the Quit method.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'Fullscreen': GObject.ParamSpec.boolean(
|
|
'Fullscreen',
|
|
'Fullscreen',
|
|
'Whether the player is in fullscreen mode.',
|
|
GObject.ParamFlags.READWRITE,
|
|
false
|
|
),
|
|
'CanSetFullscreen': GObject.ParamSpec.boolean(
|
|
'CanSetFullscreen',
|
|
'Can Set Fullscreen',
|
|
'Whether the client can set the Fullscreen property.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanRaise': GObject.ParamSpec.boolean(
|
|
'CanRaise',
|
|
'Can Raise',
|
|
'Whether the client can call the Raise method.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'HasTrackList': GObject.ParamSpec.boolean(
|
|
'HasTrackList',
|
|
'Has Track List',
|
|
'Whether the player has a track list.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'Identity': GObject.ParamSpec.string(
|
|
'Identity',
|
|
'Identity',
|
|
'The application name.',
|
|
GObject.ParamFlags.READABLE,
|
|
null
|
|
),
|
|
'DesktopEntry': GObject.ParamSpec.string(
|
|
'DesktopEntry',
|
|
'DesktopEntry',
|
|
'The basename of an installed .desktop file.',
|
|
GObject.ParamFlags.READABLE,
|
|
null
|
|
),
|
|
'SupportedUriSchemes': GObject.param_spec_variant(
|
|
'SupportedUriSchemes',
|
|
'Supported URI Schemes',
|
|
'The URI schemes supported by the media player.',
|
|
new GLib.VariantType('as'),
|
|
null,
|
|
GObject.ParamFlags.READABLE
|
|
),
|
|
'SupportedMimeTypes': GObject.param_spec_variant(
|
|
'SupportedMimeTypes',
|
|
'Supported MIME Types',
|
|
'The mime-types supported by the media player.',
|
|
new GLib.VariantType('as'),
|
|
null,
|
|
GObject.ParamFlags.READABLE
|
|
),
|
|
|
|
// Player Properties
|
|
'PlaybackStatus': GObject.ParamSpec.string(
|
|
'PlaybackStatus',
|
|
'Playback Status',
|
|
'The current playback status.',
|
|
GObject.ParamFlags.READABLE,
|
|
null
|
|
),
|
|
'LoopStatus': GObject.ParamSpec.string(
|
|
'LoopStatus',
|
|
'Loop Status',
|
|
'The current loop status.',
|
|
GObject.ParamFlags.READWRITE,
|
|
null
|
|
),
|
|
'Rate': GObject.ParamSpec.double(
|
|
'Rate',
|
|
'Rate',
|
|
'The current playback rate.',
|
|
GObject.ParamFlags.READWRITE,
|
|
0.0, 1.0,
|
|
1.0
|
|
),
|
|
'MinimumRate': GObject.ParamSpec.double(
|
|
'MinimumRate',
|
|
'Minimum Rate',
|
|
'The minimum playback rate.',
|
|
GObject.ParamFlags.READWRITE,
|
|
0.0, 1.0,
|
|
1.0
|
|
),
|
|
'MaximimRate': GObject.ParamSpec.double(
|
|
'MaximumRate',
|
|
'Maximum Rate',
|
|
'The maximum playback rate.',
|
|
GObject.ParamFlags.READWRITE,
|
|
0.0, 1.0,
|
|
1.0
|
|
),
|
|
'Shuffle': GObject.ParamSpec.boolean(
|
|
'Shuffle',
|
|
'Shuffle',
|
|
'Whether track changes are linear.',
|
|
GObject.ParamFlags.READWRITE,
|
|
null
|
|
),
|
|
'Metadata': GObject.param_spec_variant(
|
|
'Metadata',
|
|
'Metadata',
|
|
'The metadata of the current element.',
|
|
new GLib.VariantType('a{sv}'),
|
|
null,
|
|
GObject.ParamFlags.READABLE
|
|
),
|
|
'Volume': GObject.ParamSpec.double(
|
|
'Volume',
|
|
'Volume',
|
|
'The volume level.',
|
|
GObject.ParamFlags.READWRITE,
|
|
0.0, 1.0,
|
|
1.0
|
|
),
|
|
'Position': GObject.ParamSpec.int64(
|
|
'Position',
|
|
'Position',
|
|
'The current track position in microseconds.',
|
|
GObject.ParamFlags.READABLE,
|
|
0, Number.MAX_SAFE_INTEGER,
|
|
0
|
|
),
|
|
'CanGoNext': GObject.ParamSpec.boolean(
|
|
'CanGoNext',
|
|
'Can Go Next',
|
|
'Whether the client can call the Next method.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanGoPrevious': GObject.ParamSpec.boolean(
|
|
'CanGoPrevious',
|
|
'Can Go Previous',
|
|
'Whether the client can call the Previous method.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanPlay': GObject.ParamSpec.boolean(
|
|
'CanPlay',
|
|
'Can Play',
|
|
'Whether playback can be started using Play or PlayPause.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanPause': GObject.ParamSpec.boolean(
|
|
'CanPause',
|
|
'Can Pause',
|
|
'Whether playback can be paused using Play or PlayPause.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanSeek': GObject.ParamSpec.boolean(
|
|
'CanSeek',
|
|
'Can Seek',
|
|
'Whether the client can control the playback position using Seek and SetPosition.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'CanControl': GObject.ParamSpec.boolean(
|
|
'CanControl',
|
|
'Can Control',
|
|
'Whether the media player may be controlled over this interface.',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
)
|
|
},
|
|
Signals: {
|
|
'Seeked': {
|
|
flags: GObject.SignalFlags.RUN_FIRST,
|
|
param_types: [GObject.TYPE_INT64]
|
|
}
|
|
}
|
|
}, class Player extends Gio.DBusProxy {
|
|
|
|
_init(name) {
|
|
super._init({
|
|
g_bus_type: Gio.BusType.SESSION,
|
|
g_name: name,
|
|
g_object_path: '/org/mpris/MediaPlayer2',
|
|
g_interface_name: 'org.mpris.MediaPlayer2.Player'
|
|
});
|
|
|
|
this._application = new Gio.DBusProxy({
|
|
g_bus_type: Gio.BusType.SESSION,
|
|
g_name: name,
|
|
g_object_path: '/org/mpris/MediaPlayer2',
|
|
g_interface_name: 'org.mpris.MediaPlayer2'
|
|
});
|
|
|
|
this._propertiesChangedId = this._application.connect(
|
|
'g-properties-changed',
|
|
this._onPropertiesChanged.bind(this)
|
|
);
|
|
|
|
this._cancellable = new Gio.Cancellable();
|
|
}
|
|
|
|
vfunc_g_properties_changed(changed, invalidated) {
|
|
try {
|
|
if (this.__disposed !== undefined)
|
|
return;
|
|
|
|
for (let name in changed.deepUnpack()) {
|
|
this.notify(name);
|
|
}
|
|
} catch (e) {
|
|
debug(e, this.g_name);
|
|
}
|
|
}
|
|
|
|
vfunc_g_signal(sender_name, signal_name, parameters) {
|
|
try {
|
|
if (signal_name === 'Seeked') {
|
|
this.emit('Seeked', parameters.deepUnpack()[0]);
|
|
}
|
|
} catch (e) {
|
|
debug(e, this.g_name);
|
|
}
|
|
}
|
|
|
|
_call(name, parameters = null) {
|
|
this.call(
|
|
name,
|
|
parameters,
|
|
Gio.DBusCallFlags.NO_AUTO_START,
|
|
-1,
|
|
this._cancellable,
|
|
(proxy, result) => {
|
|
try {
|
|
proxy.call_finish(result);
|
|
} catch (e) {
|
|
Gio.DBusError.strip_remote_error(e);
|
|
logError(e, this.g_name);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
_get(name, fallback = null) {
|
|
try {
|
|
return this.get_cached_property(name).recursiveUnpack();
|
|
} catch (e) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
_set(name, value) {
|
|
try {
|
|
this.set_cached_property(name, value);
|
|
|
|
this.call(
|
|
'org.freedesktop.DBus.Properties.Set',
|
|
new GLib.Variant('(ssv)', [this.g_interface_name, name, value]),
|
|
Gio.DBusCallFlags.NO_AUTO_START,
|
|
-1,
|
|
this._cancellable,
|
|
(proxy, result) => {
|
|
try {
|
|
proxy.call_finish(result);
|
|
} catch (e) {
|
|
logError(e);
|
|
}
|
|
}
|
|
);
|
|
} catch (e) {
|
|
logError(e, this.g_name);
|
|
}
|
|
}
|
|
|
|
_onPropertiesChanged(proxy, changed, invalidated) {
|
|
try {
|
|
if (this.__disposed !== undefined)
|
|
return;
|
|
|
|
for (let name in changed.deepUnpack()) {
|
|
this.notify(name);
|
|
}
|
|
} catch (e) {
|
|
logError(e, this.g_name);
|
|
}
|
|
}
|
|
|
|
initPromise() {
|
|
let player = new Promise((resolve, reject) => {
|
|
this.init_async(0, this._cancellable, (proxy, res) => {
|
|
try {
|
|
resolve(proxy.init_finish(res));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
|
|
let application = new Promise((resolve, reject) => {
|
|
this._application.init_async(0, this._cancellable, (proxy, res) => {
|
|
try {
|
|
resolve(proxy.init_finish(res));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
|
|
return Promise.all([player, application]);
|
|
}
|
|
|
|
/*
|
|
* The org.mpris.MediaPlayer2 Interface
|
|
*/
|
|
get CanQuit() {
|
|
return this._get.call(this._application, 'CanQuit', false);
|
|
}
|
|
|
|
get Fullscreen() {
|
|
return this._get.call(this._application, 'Fullscreen', false);
|
|
}
|
|
|
|
set Fullscreen(mode) {
|
|
this._set.call(this._application, 'Fullscreen', new GLib.Variant('b', mode));
|
|
}
|
|
|
|
get CanSetFullscreen() {
|
|
return this._get.call(this._application, 'CanSetFullscreen', false);
|
|
}
|
|
|
|
get CanRaise() {
|
|
return this._get.call(this._application, 'CanRaise', false);
|
|
}
|
|
|
|
get HasTrackList() {
|
|
return this._get.call(this._application, 'HasTrackList', false);
|
|
}
|
|
|
|
get Identity() {
|
|
return this._get.call(this._application, 'Identity', _('Unknown'));
|
|
}
|
|
|
|
get DesktopEntry() {
|
|
return this._get.call(this._application, 'DesktopEntry', null);
|
|
}
|
|
|
|
get SupportedUriSchemes() {
|
|
return this._get.call(this._application, 'SupportedUriSchemes', []);
|
|
}
|
|
|
|
get SupportedMimeTypes() {
|
|
return this._get.call(this._application, 'SupportedMimeTypes', []);
|
|
}
|
|
|
|
Raise() {
|
|
this._call.call(this._application, 'Raise');
|
|
}
|
|
|
|
Quit() {
|
|
this._call.call(this._application, 'Quit');
|
|
}
|
|
|
|
/*
|
|
* The org.mpris.MediaPlayer2.Player Interface
|
|
*/
|
|
get PlaybackStatus() {
|
|
return this._get('PlaybackStatus', 'Stopped');
|
|
}
|
|
|
|
// 'None', 'Track', 'Playlist'
|
|
get LoopStatus() {
|
|
return this._get('LoopStatus', 'None');
|
|
}
|
|
|
|
set LoopStatus(status) {
|
|
this._set('LoopStatus', new GLib.Variant('s', status));
|
|
}
|
|
|
|
get Rate() {
|
|
return this._get('Rate', 1.0);
|
|
}
|
|
|
|
set Rate(rate) {
|
|
this._set('Rate', new GLib.Variant('d', rate));
|
|
}
|
|
|
|
get Shuffle() {
|
|
return this._get('Shuffle', false);
|
|
}
|
|
|
|
set Shuffle(mode) {
|
|
this._set('Shuffle', new GLib.Variant('b', mode));
|
|
}
|
|
|
|
get Metadata() {
|
|
if (this._metadata == undefined) {
|
|
this._metadata = {
|
|
'xesam:artist': [_('Unknown')],
|
|
'xesam:album': _('Unknown'),
|
|
'xesam:title': _('Unknown'),
|
|
'mpris:length': 0
|
|
};
|
|
}
|
|
|
|
return this._get('Metadata', this._metadata);
|
|
}
|
|
|
|
get Volume() {
|
|
return this._get('Volume', 1.0);
|
|
}
|
|
|
|
set Volume(level) {
|
|
this._set('Volume', new GLib.Variant('d', level));
|
|
}
|
|
|
|
// g-properties-changed is not emitted for this property
|
|
get Position() {
|
|
try {
|
|
let reply = this.call_sync(
|
|
'org.freedesktop.DBus.Properties.Get',
|
|
new GLib.Variant('(ss)', [this.g_interface_name, 'Position']),
|
|
Gio.DBusCallFlags.NONE,
|
|
-1,
|
|
null
|
|
);
|
|
|
|
return reply.recursiveUnpack()[0];
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
get MinimumRate() {
|
|
return this._get('MinimumRate', 1.0);
|
|
}
|
|
|
|
get MaximumRate() {
|
|
return this._get('MaximumRate', 1.0);
|
|
}
|
|
|
|
get CanGoNext() {
|
|
return this._get('CanGoNext', false);
|
|
}
|
|
|
|
get CanGoPrevious() {
|
|
return this._get('CanGoPrevious', false);
|
|
}
|
|
|
|
get CanPlay() {
|
|
return this._get('CanPlay', false);
|
|
}
|
|
|
|
get CanPause() {
|
|
return this._get('CanPause', false);
|
|
}
|
|
|
|
get CanSeek() {
|
|
return this._get('CanSeek', false);
|
|
}
|
|
|
|
get CanControl() {
|
|
return this._get('CanControl', false);
|
|
}
|
|
|
|
Next() {
|
|
this._call('Next');
|
|
}
|
|
|
|
Previous() {
|
|
this._call('Previous');
|
|
}
|
|
|
|
Pause() {
|
|
this._call('Pause');
|
|
}
|
|
|
|
PlayPause() {
|
|
this._call('PlayPause');
|
|
}
|
|
|
|
Stop() {
|
|
this._call('Stop');
|
|
}
|
|
|
|
Play() {
|
|
this._call('Play');
|
|
}
|
|
|
|
Seek(offset) {
|
|
this._call('Seek', new GLib.Variant('(x)', [offset]));
|
|
}
|
|
|
|
SetPosition(trackId, position) {
|
|
this._call('SetPosition', new GLib.Variant('(ox)', [trackId, position]));
|
|
}
|
|
|
|
OpenUri(uri) {
|
|
this._call('OpenUri', new GLib.Variant('(s)', [uri]));
|
|
}
|
|
|
|
destroy() {
|
|
if (this.__disposed === undefined) {
|
|
this.__disposed = true;
|
|
|
|
this._cancellable.cancel();
|
|
|
|
this._application.disconnect(this._propertiesChangedId);
|
|
this._application.run_dispose();
|
|
|
|
this.run_dispose();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
var Manager = GObject.registerClass({
|
|
GTypeName: 'GSConnectMPRISManager',
|
|
Implements: [Gio.DBusInterface],
|
|
Properties: {
|
|
'identities': GObject.param_spec_variant(
|
|
'identities',
|
|
'IdentityList',
|
|
'A list of MediaPlayer2.Identity for each player',
|
|
new GLib.VariantType('as'),
|
|
null,
|
|
GObject.ParamFlags.READABLE
|
|
),
|
|
// Actually returns an Object of MediaPlayer2Proxy objects,
|
|
// Player.Identity as key
|
|
'players': GObject.param_spec_variant(
|
|
'players',
|
|
'PlayerList',
|
|
'A list of known devices',
|
|
new GLib.VariantType('a{sv}'),
|
|
null,
|
|
GObject.ParamFlags.READABLE
|
|
)
|
|
},
|
|
Signals: {
|
|
'player-changed': {
|
|
flags: GObject.SignalFlags.RUN_FIRST,
|
|
param_types: [GObject.TYPE_OBJECT]
|
|
},
|
|
'player-seeked': {
|
|
flags: GObject.SignalFlags.RUN_FIRST,
|
|
param_types: [GObject.TYPE_OBJECT]
|
|
}
|
|
}
|
|
}, class Manager extends Gio.DBusProxy {
|
|
|
|
_init() {
|
|
super._init({
|
|
g_bus_type: Gio.BusType.SESSION,
|
|
g_name: 'org.freedesktop.DBus',
|
|
g_object_path: '/org/freedesktop/DBus'
|
|
});
|
|
|
|
// Asynchronous setup
|
|
this._cancellable = new Gio.Cancellable();
|
|
this._init_async();
|
|
}
|
|
|
|
async _init_async() {
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
this.init_async(0, this._cancellable, (proxy, res) => {
|
|
try {
|
|
resolve(proxy.init_finish(res));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
|
|
let names = await new Promise((resolve, reject) => {
|
|
this.call(
|
|
'org.freedesktop.DBus.ListNames',
|
|
null,
|
|
Gio.DBusCallFlags.NO_AUTO_START,
|
|
-1,
|
|
this._cancellable,
|
|
(proxy, res) => {
|
|
try {
|
|
resolve(proxy.call_finish(res).deepUnpack()[0]);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
for (let i = 0, len = names.length; i < len; i++) {
|
|
let name = names[i];
|
|
|
|
if (name.startsWith('org.mpris.MediaPlayer2') &&
|
|
!name.includes('GSConnect')) {
|
|
this._addPlayer(name);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// FIXME: if something goes wrong the component will appear active
|
|
logError(e);
|
|
this.destroy();
|
|
}
|
|
}
|
|
|
|
get identities() {
|
|
let identities = [];
|
|
|
|
for (let player of this.players.values()) {
|
|
let identity = player.Identity;
|
|
|
|
if (identity)
|
|
identities.push(identity);
|
|
}
|
|
|
|
return identities;
|
|
}
|
|
|
|
get players() {
|
|
if (this._players === undefined) {
|
|
this._players = new Map();
|
|
}
|
|
|
|
return this._players;
|
|
}
|
|
|
|
get paused() {
|
|
if (this._paused === undefined) {
|
|
this._paused = new Map();
|
|
}
|
|
|
|
return this._paused;
|
|
}
|
|
|
|
vfunc_g_signal(sender_name, signal_name, parameters) {
|
|
try {
|
|
if (signal_name === 'NameOwnerChanged') {
|
|
let [name, old_owner, new_owner] = parameters.deepUnpack();
|
|
|
|
if (name.startsWith('org.mpris.MediaPlayer2') &&
|
|
!name.includes('GSConnect')) {
|
|
if (new_owner.length) {
|
|
this._addPlayer(name);
|
|
} else if (old_owner.length) {
|
|
this._removePlayer(name);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debug(e);
|
|
}
|
|
}
|
|
|
|
async _addPlayer(name) {
|
|
try {
|
|
if (!this.players.has(name)) {
|
|
let player = new Player(name);
|
|
await player.initPromise();
|
|
|
|
player.__propertiesId = player.connect(
|
|
'g-properties-changed',
|
|
(player) => this.emit('player-changed', player)
|
|
);
|
|
|
|
player.__seekedId = player.connect(
|
|
'Seeked',
|
|
(player) => this.emit('player-seeked', player)
|
|
);
|
|
|
|
this.players.set(name, player);
|
|
this.notify('players');
|
|
}
|
|
} catch (e) {
|
|
debug(e);
|
|
}
|
|
}
|
|
|
|
async _removePlayer(name) {
|
|
try {
|
|
let player = this.players.get(name);
|
|
|
|
if (player !== undefined) {
|
|
debug(`Removing MPRIS Player ${name}`);
|
|
|
|
player.disconnect(player.__propertiesId);
|
|
player.disconnect(player.__seekedId);
|
|
player.destroy();
|
|
|
|
this.paused.delete(name);
|
|
this.players.delete(name);
|
|
this.notify('players');
|
|
}
|
|
} catch (e) {
|
|
debug(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a player by its Identity.
|
|
*/
|
|
getPlayer(identity) {
|
|
for (let player of this.players.values()) {
|
|
if (player.Identity === identity) {
|
|
return player;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A convenience function for pausing all players currently playing.
|
|
*/
|
|
pauseAll() {
|
|
for (let [name, player] of this.players.entries()) {
|
|
if (player.PlaybackStatus === 'Playing' && player.CanPause) {
|
|
player.Pause();
|
|
this.paused.set(name, player);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A convenience function for restarting all players paused with pauseAll().
|
|
*/
|
|
unpauseAll() {
|
|
for (let player of this.paused.values()) {
|
|
if (player.PlaybackStatus === 'Paused' && player.CanPlay) {
|
|
player.Play();
|
|
}
|
|
}
|
|
|
|
this.paused.clear();
|
|
}
|
|
|
|
destroy() {
|
|
if (this.__disposed == undefined) {
|
|
this.__disposed = true;
|
|
|
|
this._cancellable.cancel();
|
|
|
|
for (let player of this.players.values()) {
|
|
player.disconnect(player.__propertiesId);
|
|
player.disconnect(player.__seekedId);
|
|
player.destroy();
|
|
}
|
|
|
|
this.players.clear();
|
|
|
|
this.run_dispose();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* The service class for this component
|
|
*/
|
|
var Component = Manager;
|
|
|