mpris.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const Gio = imports.gi.Gio;
  6. const GLib = imports.gi.GLib;
  7. const GObject = imports.gi.GObject;
  8. const Components = imports.service.components;
  9. const Config = imports.config;
  10. const DBus = imports.service.utils.dbus;
  11. const MPRIS = imports.service.components.mpris;
  12. const PluginBase = imports.service.plugin;
  13. var Metadata = {
  14. label: _('MPRIS'),
  15. description: _('Bidirectional remote media playback control'),
  16. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.MPRIS',
  17. incomingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
  18. outgoingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
  19. actions: {},
  20. };
  21. /**
  22. * MPRIS Plugin
  23. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mpriscontrol
  24. *
  25. * See also:
  26. * https://specifications.freedesktop.org/mpris-spec/latest/
  27. * https://github.com/GNOME/gnome-shell/blob/master/js/ui/mpris.js
  28. */
  29. var Plugin = GObject.registerClass({
  30. GTypeName: 'GSConnectMPRISPlugin',
  31. }, class Plugin extends PluginBase.Plugin {
  32. _init(device) {
  33. super._init(device, 'mpris');
  34. this._players = new Map();
  35. this._transferring = new WeakSet();
  36. this._updating = new WeakSet();
  37. this._mpris = Components.acquire('mpris');
  38. this._playerAddedId = this._mpris.connect(
  39. 'player-added',
  40. this._sendPlayerList.bind(this)
  41. );
  42. this._playerRemovedId = this._mpris.connect(
  43. 'player-removed',
  44. this._sendPlayerList.bind(this)
  45. );
  46. this._playerChangedId = this._mpris.connect(
  47. 'player-changed',
  48. this._onPlayerChanged.bind(this)
  49. );
  50. this._playerSeekedId = this._mpris.connect(
  51. 'player-seeked',
  52. this._onPlayerSeeked.bind(this)
  53. );
  54. }
  55. connected() {
  56. super.connected();
  57. this._requestPlayerList();
  58. this._sendPlayerList();
  59. }
  60. disconnected() {
  61. super.disconnected();
  62. for (const [identity, player] of this._players) {
  63. this._players.delete(identity);
  64. player.destroy();
  65. }
  66. }
  67. handlePacket(packet) {
  68. switch (packet.type) {
  69. case 'kdeconnect.mpris':
  70. this._handleUpdate(packet);
  71. break;
  72. case 'kdeconnect.mpris.request':
  73. this._handleRequest(packet);
  74. break;
  75. }
  76. }
  77. /**
  78. * Handle a remote player update.
  79. *
  80. * @param {Core.Packet} packet - A `kdeconnect.mpris`
  81. */
  82. _handleUpdate(packet) {
  83. try {
  84. if (packet.body.hasOwnProperty('playerList'))
  85. this._handlePlayerList(packet.body.playerList);
  86. else if (packet.body.hasOwnProperty('player'))
  87. this._handlePlayerUpdate(packet);
  88. } catch (e) {
  89. debug(e, this.device.name);
  90. }
  91. }
  92. /**
  93. * Handle an updated list of remote players.
  94. *
  95. * @param {string[]} playerList - A list of remote player names
  96. */
  97. _handlePlayerList(playerList) {
  98. // Destroy removed players before adding new ones
  99. for (const player of this._players.values()) {
  100. if (!playerList.includes(player.Identity)) {
  101. this._players.delete(player.Identity);
  102. player.destroy();
  103. }
  104. }
  105. for (const identity of playerList) {
  106. if (!this._players.has(identity)) {
  107. const player = new PlayerRemote(this.device, identity);
  108. this._players.set(identity, player);
  109. }
  110. // Always request player updates; packets are cheap
  111. this.device.sendPacket({
  112. type: 'kdeconnect.mpris.request',
  113. body: {
  114. player: identity,
  115. requestNowPlaying: true,
  116. requestVolume: true,
  117. },
  118. });
  119. }
  120. }
  121. /**
  122. * Handle an update for a remote player.
  123. *
  124. * @param {Object} packet - A `kdeconnect.mpris` packet
  125. */
  126. _handlePlayerUpdate(packet) {
  127. const player = this._players.get(packet.body.player);
  128. if (player === undefined)
  129. return;
  130. if (packet.body.hasOwnProperty('transferringAlbumArt'))
  131. player.handleAlbumArt(packet);
  132. else
  133. player.update(packet.body);
  134. }
  135. /**
  136. * Request a list of remote players.
  137. */
  138. _requestPlayerList() {
  139. this.device.sendPacket({
  140. type: 'kdeconnect.mpris.request',
  141. body: {
  142. requestPlayerList: true,
  143. },
  144. });
  145. }
  146. /**
  147. * Handle a request for player information or action.
  148. *
  149. * @param {Core.Packet} packet - a `kdeconnect.mpris.request`
  150. * @return {undefined} no return value
  151. */
  152. _handleRequest(packet) {
  153. // A request for the list of players
  154. if (packet.body.hasOwnProperty('requestPlayerList'))
  155. return this._sendPlayerList();
  156. // A request for an unknown player; send the list of players
  157. if (!this._mpris.hasPlayer(packet.body.player))
  158. return this._sendPlayerList();
  159. // An album art request
  160. if (packet.body.hasOwnProperty('albumArtUrl'))
  161. return this._sendAlbumArt(packet);
  162. // A player command
  163. this._handleCommand(packet);
  164. }
  165. /**
  166. * Handle an incoming player command or information request
  167. *
  168. * @param {Core.Packet} packet - A `kdeconnect.mpris.request`
  169. */
  170. async _handleCommand(packet) {
  171. if (!this.settings.get_boolean('share-players'))
  172. return;
  173. let player;
  174. try {
  175. player = this._mpris.getPlayer(packet.body.player);
  176. if (player === undefined || this._updating.has(player))
  177. return;
  178. this._updating.add(player);
  179. // Player Actions
  180. if (packet.body.hasOwnProperty('action')) {
  181. switch (packet.body.action) {
  182. case 'PlayPause':
  183. case 'Play':
  184. case 'Pause':
  185. case 'Next':
  186. case 'Previous':
  187. case 'Stop':
  188. player[packet.body.action]();
  189. break;
  190. default:
  191. debug(`unknown action: ${packet.body.action}`);
  192. }
  193. }
  194. // Player Properties
  195. if (packet.body.hasOwnProperty('setLoopStatus'))
  196. player.LoopStatus = packet.body.setLoopStatus;
  197. if (packet.body.hasOwnProperty('setShuffle'))
  198. player.Shuffle = packet.body.setShuffle;
  199. if (packet.body.hasOwnProperty('setVolume'))
  200. player.Volume = packet.body.setVolume / 100;
  201. if (packet.body.hasOwnProperty('Seek'))
  202. await player.Seek(packet.body.Seek * 1000);
  203. if (packet.body.hasOwnProperty('SetPosition')) {
  204. const offset = (packet.body.SetPosition * 1000) - player.Position;
  205. await player.Seek(offset);
  206. }
  207. // Information Request
  208. let hasResponse = false;
  209. const response = {
  210. type: 'kdeconnect.mpris',
  211. body: {
  212. player: packet.body.player,
  213. },
  214. };
  215. if (packet.body.hasOwnProperty('requestNowPlaying')) {
  216. hasResponse = true;
  217. Object.assign(response.body, {
  218. pos: Math.floor(player.Position / 1000),
  219. isPlaying: (player.PlaybackStatus === 'Playing'),
  220. canPause: player.CanPause,
  221. canPlay: player.CanPlay,
  222. canGoNext: player.CanGoNext,
  223. canGoPrevious: player.CanGoPrevious,
  224. canSeek: player.CanSeek,
  225. loopStatus: player.LoopStatus,
  226. shuffle: player.Shuffle,
  227. // default values for members that will be filled conditionally
  228. albumArtUrl: '',
  229. length: 0,
  230. artist: '',
  231. title: '',
  232. album: '',
  233. nowPlaying: '',
  234. volume: 0,
  235. });
  236. const metadata = player.Metadata;
  237. if (metadata.hasOwnProperty('mpris:artUrl')) {
  238. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  239. response.body.albumArtUrl = file.get_uri();
  240. }
  241. if (metadata.hasOwnProperty('mpris:length')) {
  242. const trackLen = Math.floor(metadata['mpris:length'] / 1000);
  243. response.body.length = trackLen;
  244. }
  245. if (metadata.hasOwnProperty('xesam:artist')) {
  246. const artists = metadata['xesam:artist'];
  247. response.body.artist = artists.join(', ');
  248. }
  249. if (metadata.hasOwnProperty('xesam:title'))
  250. response.body.title = metadata['xesam:title'];
  251. if (metadata.hasOwnProperty('xesam:album'))
  252. response.body.album = metadata['xesam:album'];
  253. // Now Playing
  254. if (response.body.artist && response.body.title) {
  255. response.body.nowPlaying = [
  256. response.body.artist,
  257. response.body.title,
  258. ].join(' - ');
  259. } else if (response.body.artist) {
  260. response.body.nowPlaying = response.body.artist;
  261. } else if (response.body.title) {
  262. response.body.nowPlaying = response.body.title;
  263. } else {
  264. response.body.nowPlaying = _('Unknown');
  265. }
  266. }
  267. if (packet.body.hasOwnProperty('requestVolume')) {
  268. hasResponse = true;
  269. response.body.volume = Math.floor(player.Volume * 100);
  270. }
  271. if (hasResponse)
  272. this.device.sendPacket(response);
  273. } catch (e) {
  274. debug(e, this.device.name);
  275. } finally {
  276. this._updating.delete(player);
  277. }
  278. }
  279. _onPlayerChanged(mpris, player) {
  280. if (!this.settings.get_boolean('share-players'))
  281. return;
  282. this._handleCommand({
  283. body: {
  284. player: player.Identity,
  285. requestNowPlaying: true,
  286. requestVolume: true,
  287. },
  288. });
  289. }
  290. _onPlayerSeeked(mpris, player, offset) {
  291. // TODO: although we can handle full seeked signals, kdeconnect-android
  292. // does not, and expects a position update instead
  293. this.device.sendPacket({
  294. type: 'kdeconnect.mpris',
  295. body: {
  296. player: player.Identity,
  297. pos: Math.floor(player.Position / 1000),
  298. // Seek: Math.floor(offset / 1000),
  299. },
  300. });
  301. }
  302. async _sendAlbumArt(packet) {
  303. let player;
  304. try {
  305. // Reject concurrent requests for album art
  306. player = this._mpris.getPlayer(packet.body.player);
  307. if (player === undefined || this._transferring.has(player))
  308. return;
  309. // Ensure the requested albumArtUrl matches the current mpris:artUrl
  310. const metadata = player.Metadata;
  311. if (!metadata.hasOwnProperty('mpris:artUrl'))
  312. return;
  313. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  314. const request = Gio.File.new_for_uri(packet.body.albumArtUrl);
  315. if (file.get_uri() !== request.get_uri())
  316. throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`);
  317. // Transfer the album art
  318. this._transferring.add(player);
  319. const transfer = this.device.createTransfer();
  320. transfer.addFile({
  321. type: 'kdeconnect.mpris',
  322. body: {
  323. transferringAlbumArt: true,
  324. player: packet.body.player,
  325. albumArtUrl: packet.body.albumArtUrl,
  326. },
  327. }, file);
  328. await transfer.start();
  329. } catch (e) {
  330. debug(e, this.device.name);
  331. } finally {
  332. this._transferring.delete(player);
  333. }
  334. }
  335. /**
  336. * Send the list of player identities and indicate whether we support
  337. * transferring album art
  338. */
  339. _sendPlayerList() {
  340. let playerList = [];
  341. if (this.settings.get_boolean('share-players'))
  342. playerList = this._mpris.getIdentities();
  343. this.device.sendPacket({
  344. type: 'kdeconnect.mpris',
  345. body: {
  346. playerList: playerList,
  347. supportAlbumArtPayload: true,
  348. },
  349. });
  350. }
  351. destroy() {
  352. if (this._mpris !== undefined) {
  353. this._mpris.disconnect(this._playerAddedId);
  354. this._mpris.disconnect(this._playerRemovedId);
  355. this._mpris.disconnect(this._playerChangedId);
  356. this._mpris.disconnect(this._playerSeekedId);
  357. this._mpris = Components.release('mpris');
  358. }
  359. for (const [identity, player] of this._players) {
  360. this._players.delete(identity);
  361. player.destroy();
  362. }
  363. super.destroy();
  364. }
  365. });
  366. /*
  367. * A class for mirroring a remote Media Player on DBus
  368. */
  369. const PlayerRemote = GObject.registerClass({
  370. GTypeName: 'GSConnectMPRISPlayerRemote',
  371. }, class PlayerRemote extends MPRIS.Player {
  372. _init(device, identity) {
  373. super._init();
  374. this._device = device;
  375. this._Identity = identity;
  376. this._isPlaying = false;
  377. this._artist = null;
  378. this._title = null;
  379. this._album = null;
  380. this._length = 0;
  381. this._artUrl = null;
  382. this._ownerId = 0;
  383. this._connection = null;
  384. this._applicationIface = null;
  385. this._playerIface = null;
  386. }
  387. _getFile(albumArtUrl) {
  388. const hash = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5,
  389. albumArtUrl, -1);
  390. const path = GLib.build_filenamev([Config.CACHEDIR, hash]);
  391. return Gio.File.new_for_uri(`file://${path}`);
  392. }
  393. _requestAlbumArt(state) {
  394. if (this._artUrl === state.albumArtUrl)
  395. return;
  396. const file = this._getFile(state.albumArtUrl);
  397. if (file.query_exists(null)) {
  398. this._artUrl = file.get_uri();
  399. this._Metadata = undefined;
  400. this.notify('Metadata');
  401. } else {
  402. this.device.sendPacket({
  403. type: 'kdeconnect.mpris.request',
  404. body: {
  405. player: this.Identity,
  406. albumArtUrl: state.albumArtUrl,
  407. },
  408. });
  409. }
  410. }
  411. _updateMetadata(state) {
  412. let metadataChanged = false;
  413. if (state.hasOwnProperty('artist')) {
  414. if (this._artist !== state.artist) {
  415. this._artist = state.artist;
  416. metadataChanged = true;
  417. }
  418. } else if (this._artist) {
  419. this._artist = null;
  420. metadataChanged = true;
  421. }
  422. if (state.hasOwnProperty('title')) {
  423. if (this._title !== state.title) {
  424. this._title = state.title;
  425. metadataChanged = true;
  426. }
  427. } else if (this._title) {
  428. this._title = null;
  429. metadataChanged = true;
  430. }
  431. if (state.hasOwnProperty('album')) {
  432. if (this._album !== state.album) {
  433. this._album = state.album;
  434. metadataChanged = true;
  435. }
  436. } else if (this._album) {
  437. this._album = null;
  438. metadataChanged = true;
  439. }
  440. if (state.hasOwnProperty('length')) {
  441. if (this._length !== state.length * 1000) {
  442. this._length = state.length * 1000;
  443. metadataChanged = true;
  444. }
  445. } else if (this._length) {
  446. this._length = 0;
  447. metadataChanged = true;
  448. }
  449. if (state.hasOwnProperty('albumArtUrl')) {
  450. this._requestAlbumArt(state);
  451. } else if (this._artUrl) {
  452. this._artUrl = null;
  453. metadataChanged = true;
  454. }
  455. if (metadataChanged) {
  456. this._Metadata = undefined;
  457. this.notify('Metadata');
  458. }
  459. }
  460. async export() {
  461. try {
  462. if (this._connection === null) {
  463. this._connection = await DBus.newConnection();
  464. const MPRISIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2');
  465. const MPRISPlayerIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2.Player');
  466. if (this._applicationIface === null) {
  467. this._applicationIface = new DBus.Interface({
  468. g_instance: this,
  469. g_connection: this._connection,
  470. g_object_path: '/org/mpris/MediaPlayer2',
  471. g_interface_info: MPRISIface,
  472. });
  473. }
  474. if (this._playerIface === null) {
  475. this._playerIface = new DBus.Interface({
  476. g_instance: this,
  477. g_connection: this._connection,
  478. g_object_path: '/org/mpris/MediaPlayer2',
  479. g_interface_info: MPRISPlayerIface,
  480. });
  481. }
  482. }
  483. if (this._ownerId !== 0)
  484. return;
  485. const name = [
  486. this.device.name,
  487. this.Identity,
  488. ].join('').replace(/[\W]*/g, '');
  489. this._ownerId = Gio.bus_own_name_on_connection(
  490. this._connection,
  491. `org.mpris.MediaPlayer2.GSConnect.${name}`,
  492. Gio.BusNameOwnerFlags.NONE,
  493. null,
  494. null
  495. );
  496. } catch (e) {
  497. debug(e, this.Identity);
  498. }
  499. }
  500. unexport() {
  501. if (this._ownerId === 0)
  502. return;
  503. Gio.bus_unown_name(this._ownerId);
  504. this._ownerId = 0;
  505. }
  506. /**
  507. * Download album art for the current track of the remote player.
  508. *
  509. * @param {Core.Packet} packet - A `kdeconnect.mpris` packet
  510. */
  511. async handleAlbumArt(packet) {
  512. let file;
  513. try {
  514. file = this._getFile(packet.body.albumArtUrl);
  515. // Transfer the album art
  516. const transfer = this.device.createTransfer();
  517. transfer.addFile(packet, file);
  518. await transfer.start();
  519. this._artUrl = file.get_uri();
  520. this._Metadata = undefined;
  521. this.notify('Metadata');
  522. } catch (e) {
  523. debug(e, this.device.name);
  524. if (file)
  525. file.delete_async(GLib.PRIORITY_DEFAULT, null, null);
  526. }
  527. }
  528. /**
  529. * Update the internal state of the media player.
  530. *
  531. * @param {Core.Packet} state - The body of a `kdeconnect.mpris` packet
  532. */
  533. update(state) {
  534. this.freeze_notify();
  535. // Metadata
  536. if (state.hasOwnProperty('nowPlaying') ||
  537. state.hasOwnProperty('artist') ||
  538. state.hasOwnProperty('title'))
  539. this._updateMetadata(state);
  540. // Playback Status
  541. if (state.hasOwnProperty('isPlaying')) {
  542. if (this._isPlaying !== state.isPlaying) {
  543. this._isPlaying = state.isPlaying;
  544. this.notify('PlaybackStatus');
  545. }
  546. }
  547. if (state.hasOwnProperty('canPlay')) {
  548. if (this.CanPlay !== state.canPlay) {
  549. this._CanPlay = state.canPlay;
  550. this.notify('CanPlay');
  551. }
  552. }
  553. if (state.hasOwnProperty('canPause')) {
  554. if (this.CanPause !== state.canPause) {
  555. this._CanPause = state.canPause;
  556. this.notify('CanPause');
  557. }
  558. }
  559. if (state.hasOwnProperty('canGoNext')) {
  560. if (this.CanGoNext !== state.canGoNext) {
  561. this._CanGoNext = state.canGoNext;
  562. this.notify('CanGoNext');
  563. }
  564. }
  565. if (state.hasOwnProperty('canGoPrevious')) {
  566. if (this.CanGoPrevious !== state.canGoPrevious) {
  567. this._CanGoPrevious = state.canGoPrevious;
  568. this.notify('CanGoPrevious');
  569. }
  570. }
  571. if (state.hasOwnProperty('pos'))
  572. this._Position = state.pos * 1000;
  573. if (state.hasOwnProperty('volume')) {
  574. if (this.Volume !== state.volume / 100) {
  575. this._Volume = state.volume / 100;
  576. this.notify('Volume');
  577. }
  578. }
  579. this.thaw_notify();
  580. if (!this._isPlaying && !this.CanControl)
  581. this.unexport();
  582. else
  583. this.export();
  584. }
  585. /*
  586. * Native properties
  587. */
  588. get device() {
  589. return this._device;
  590. }
  591. /*
  592. * The org.mpris.MediaPlayer2.Player Interface
  593. */
  594. get CanControl() {
  595. return (this.CanPlay || this.CanPause);
  596. }
  597. get Metadata() {
  598. if (this._Metadata === undefined) {
  599. this._Metadata = {};
  600. if (this._artist) {
  601. this._Metadata['xesam:artist'] = new GLib.Variant('as',
  602. [this._artist]);
  603. }
  604. if (this._title) {
  605. this._Metadata['xesam:title'] = new GLib.Variant('s',
  606. this._title);
  607. }
  608. if (this._album) {
  609. this._Metadata['xesam:album'] = new GLib.Variant('s',
  610. this._album);
  611. }
  612. if (this._artUrl) {
  613. this._Metadata['mpris:artUrl'] = new GLib.Variant('s',
  614. this._artUrl);
  615. }
  616. this._Metadata['mpris:length'] = new GLib.Variant('x',
  617. this._length);
  618. }
  619. return this._Metadata;
  620. }
  621. get PlaybackStatus() {
  622. if (this._isPlaying)
  623. return 'Playing';
  624. return 'Stopped';
  625. }
  626. get Volume() {
  627. if (this._Volume === undefined)
  628. this._Volume = 0.3;
  629. return this._Volume;
  630. }
  631. set Volume(level) {
  632. if (this._Volume === level)
  633. return;
  634. this._Volume = level;
  635. this.notify('Volume');
  636. this.device.sendPacket({
  637. type: 'kdeconnect.mpris.request',
  638. body: {
  639. player: this.Identity,
  640. setVolume: Math.floor(this._Volume * 100),
  641. },
  642. });
  643. }
  644. Next() {
  645. if (!this.CanGoNext)
  646. return;
  647. this.device.sendPacket({
  648. type: 'kdeconnect.mpris.request',
  649. body: {
  650. player: this.Identity,
  651. action: 'Next',
  652. },
  653. });
  654. }
  655. Pause() {
  656. if (!this.CanPause)
  657. return;
  658. this.device.sendPacket({
  659. type: 'kdeconnect.mpris.request',
  660. body: {
  661. player: this.Identity,
  662. action: 'Pause',
  663. },
  664. });
  665. }
  666. Play() {
  667. if (!this.CanPlay)
  668. return;
  669. this.device.sendPacket({
  670. type: 'kdeconnect.mpris.request',
  671. body: {
  672. player: this.Identity,
  673. action: 'Play',
  674. },
  675. });
  676. }
  677. PlayPause() {
  678. if (!this.CanPlay && !this.CanPause)
  679. return;
  680. this.device.sendPacket({
  681. type: 'kdeconnect.mpris.request',
  682. body: {
  683. player: this.Identity,
  684. action: 'PlayPause',
  685. },
  686. });
  687. }
  688. Previous() {
  689. if (!this.CanGoPrevious)
  690. return;
  691. this.device.sendPacket({
  692. type: 'kdeconnect.mpris.request',
  693. body: {
  694. player: this.Identity,
  695. action: 'Previous',
  696. },
  697. });
  698. }
  699. Seek(offset) {
  700. if (!this.CanSeek)
  701. return;
  702. this.device.sendPacket({
  703. type: 'kdeconnect.mpris.request',
  704. body: {
  705. player: this.Identity,
  706. Seek: offset,
  707. },
  708. });
  709. }
  710. SetPosition(trackId, position) {
  711. debug(`${this._Identity}: SetPosition(${trackId}, ${position})`);
  712. if (!this.CanControl || !this.CanSeek)
  713. return;
  714. this.device.sendPacket({
  715. type: 'kdeconnect.mpris.request',
  716. body: {
  717. player: this.Identity,
  718. SetPosition: position / 1000,
  719. },
  720. });
  721. }
  722. Stop() {
  723. if (!this.CanControl)
  724. return;
  725. this.device.sendPacket({
  726. type: 'kdeconnect.mpris.request',
  727. body: {
  728. player: this.Identity,
  729. action: 'Stop',
  730. },
  731. });
  732. }
  733. destroy() {
  734. this.unexport();
  735. if (this._connection) {
  736. this._connection.close(null, null);
  737. this._connection = null;
  738. if (this._applicationIface) {
  739. this._applicationIface.destroy();
  740. this._applicationIface = null;
  741. }
  742. if (this._playerIface) {
  743. this._playerIface.destroy();
  744. this._playerIface = null;
  745. }
  746. }
  747. }
  748. });