mpris.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GLib from 'gi://GLib';
  6. import GObject from 'gi://GObject';
  7. import * as Components from '../components/index.js';
  8. import Config from '../../config.js';
  9. import * as Core from '../core.js';
  10. import * as DBus from '../utils/dbus.js';
  11. import {Player} from '../components/mpris.js';
  12. import Plugin from '../plugin.js';
  13. export const 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. const MPRISPlugin = GObject.registerClass({
  30. GTypeName: 'GSConnectMPRISPlugin',
  31. }, class MPRISPlugin extends 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. * @returns {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);
  203. if (packet.body.hasOwnProperty('SetPosition')) {
  204. // We want to avoid implementing this as a seek operation,
  205. // because some players seek a fixed amount for every
  206. // seek request, only respecting the sign of the parameter.
  207. // (Chrome, for example, will only seek ±5 seconds, regardless
  208. // what value is passed to Seek().)
  209. const position = packet.body.SetPosition;
  210. const metadata = player.Metadata;
  211. if (metadata.hasOwnProperty('mpris:trackid')) {
  212. const trackId = metadata['mpris:trackid'];
  213. await player.SetPosition(trackId, position * 1000);
  214. } else {
  215. await player.Seek(position * 1000 - player.Position);
  216. }
  217. }
  218. // Information Request
  219. let hasResponse = false;
  220. const response = {
  221. type: 'kdeconnect.mpris',
  222. body: {
  223. player: packet.body.player,
  224. },
  225. };
  226. if (packet.body.hasOwnProperty('requestNowPlaying')) {
  227. hasResponse = true;
  228. Object.assign(response.body, {
  229. pos: Math.floor(player.Position / 1000),
  230. isPlaying: (player.PlaybackStatus === 'Playing'),
  231. canPause: player.CanPause,
  232. canPlay: player.CanPlay,
  233. canGoNext: player.CanGoNext,
  234. canGoPrevious: player.CanGoPrevious,
  235. canSeek: player.CanSeek,
  236. loopStatus: player.LoopStatus,
  237. shuffle: player.Shuffle,
  238. // default values for members that will be filled conditionally
  239. albumArtUrl: '',
  240. length: 0,
  241. artist: '',
  242. title: '',
  243. album: '',
  244. nowPlaying: '',
  245. volume: 0,
  246. });
  247. const metadata = player.Metadata;
  248. if (metadata.hasOwnProperty('mpris:artUrl')) {
  249. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  250. response.body.albumArtUrl = file.get_uri();
  251. }
  252. if (metadata.hasOwnProperty('mpris:length')) {
  253. const trackLen = Math.floor(metadata['mpris:length'] / 1000);
  254. response.body.length = trackLen;
  255. }
  256. if (metadata.hasOwnProperty('xesam:artist')) {
  257. const artists = metadata['xesam:artist'];
  258. response.body.artist = artists.join(', ');
  259. }
  260. if (metadata.hasOwnProperty('xesam:title'))
  261. response.body.title = metadata['xesam:title'];
  262. if (metadata.hasOwnProperty('xesam:album'))
  263. response.body.album = metadata['xesam:album'];
  264. // Now Playing
  265. if (response.body.artist && response.body.title) {
  266. response.body.nowPlaying = [
  267. response.body.artist,
  268. response.body.title,
  269. ].join(' - ');
  270. } else if (response.body.artist) {
  271. response.body.nowPlaying = response.body.artist;
  272. } else if (response.body.title) {
  273. response.body.nowPlaying = response.body.title;
  274. } else {
  275. response.body.nowPlaying = _('Unknown');
  276. }
  277. }
  278. if (packet.body.hasOwnProperty('requestVolume')) {
  279. hasResponse = true;
  280. response.body.volume = Math.floor(player.Volume * 100);
  281. }
  282. if (hasResponse)
  283. this.device.sendPacket(response);
  284. } catch (e) {
  285. debug(e, this.device.name);
  286. } finally {
  287. this._updating.delete(player);
  288. }
  289. }
  290. _onPlayerChanged(mpris, player) {
  291. if (!this.settings.get_boolean('share-players'))
  292. return;
  293. this._handleCommand({
  294. body: {
  295. player: player.Identity,
  296. requestNowPlaying: true,
  297. requestVolume: true,
  298. },
  299. });
  300. }
  301. _onPlayerSeeked(mpris, player, offset) {
  302. // TODO: although we can handle full seeked signals, kdeconnect-android
  303. // does not, and expects a position update instead
  304. this.device.sendPacket({
  305. type: 'kdeconnect.mpris',
  306. body: {
  307. player: player.Identity,
  308. pos: Math.floor(player.Position / 1000),
  309. // Seek: Math.floor(offset / 1000),
  310. },
  311. });
  312. }
  313. async _sendAlbumArt(packet) {
  314. let player;
  315. try {
  316. // Reject concurrent requests for album art
  317. player = this._mpris.getPlayer(packet.body.player);
  318. if (player === undefined || this._transferring.has(player))
  319. return;
  320. // Ensure the requested albumArtUrl matches the current mpris:artUrl
  321. const metadata = player.Metadata;
  322. if (!metadata.hasOwnProperty('mpris:artUrl'))
  323. return;
  324. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  325. const request = Gio.File.new_for_uri(packet.body.albumArtUrl);
  326. if (file.get_uri() !== request.get_uri())
  327. throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`);
  328. // Transfer the album art
  329. this._transferring.add(player);
  330. const transfer = this.device.createTransfer();
  331. transfer.addFile({
  332. type: 'kdeconnect.mpris',
  333. body: {
  334. transferringAlbumArt: true,
  335. player: packet.body.player,
  336. albumArtUrl: packet.body.albumArtUrl,
  337. },
  338. }, file);
  339. await transfer.start();
  340. } catch (e) {
  341. debug(e, this.device.name);
  342. } finally {
  343. this._transferring.delete(player);
  344. }
  345. }
  346. /**
  347. * Send the list of player identities and indicate whether we support
  348. * transferring album art
  349. */
  350. _sendPlayerList() {
  351. let playerList = [];
  352. if (this.settings.get_boolean('share-players'))
  353. playerList = this._mpris.getIdentities();
  354. this.device.sendPacket({
  355. type: 'kdeconnect.mpris',
  356. body: {
  357. playerList: playerList,
  358. supportAlbumArtPayload: true,
  359. },
  360. });
  361. }
  362. destroy() {
  363. if (this._mpris !== undefined) {
  364. this._mpris.disconnect(this._playerAddedId);
  365. this._mpris.disconnect(this._playerRemovedId);
  366. this._mpris.disconnect(this._playerChangedId);
  367. this._mpris.disconnect(this._playerSeekedId);
  368. this._mpris = Components.release('mpris');
  369. }
  370. for (const [identity, player] of this._players) {
  371. this._players.delete(identity);
  372. player.destroy();
  373. }
  374. super.destroy();
  375. }
  376. });
  377. /*
  378. * A class for mirroring a remote Media Player on DBus
  379. */
  380. const PlayerRemote = GObject.registerClass({
  381. GTypeName: 'GSConnectMPRISPlayerRemote',
  382. }, class PlayerRemote extends Player {
  383. _init(device, identity) {
  384. super._init();
  385. this._device = device;
  386. this._Identity = identity;
  387. this._isPlaying = false;
  388. this._artist = null;
  389. this._title = null;
  390. this._album = null;
  391. this._length = 0;
  392. this._artUrl = null;
  393. this._ownerId = 0;
  394. this._connection = null;
  395. this._applicationIface = null;
  396. this._playerIface = null;
  397. }
  398. _getFile(albumArtUrl) {
  399. const hash = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5,
  400. albumArtUrl, -1);
  401. const path = GLib.build_filenamev([Config.CACHEDIR, hash]);
  402. return Gio.File.new_for_uri(`file://${path}`);
  403. }
  404. _requestAlbumArt(state) {
  405. if (this._artUrl === state.albumArtUrl)
  406. return;
  407. const file = this._getFile(state.albumArtUrl);
  408. if (file.query_exists(null)) {
  409. this._artUrl = file.get_uri();
  410. this._Metadata = undefined;
  411. this.notify('Metadata');
  412. } else {
  413. this.device.sendPacket({
  414. type: 'kdeconnect.mpris.request',
  415. body: {
  416. player: this.Identity,
  417. albumArtUrl: state.albumArtUrl,
  418. },
  419. });
  420. }
  421. }
  422. _updateMetadata(state) {
  423. let metadataChanged = false;
  424. if (state.hasOwnProperty('artist')) {
  425. if (this._artist !== state.artist) {
  426. this._artist = state.artist;
  427. metadataChanged = true;
  428. }
  429. } else if (this._artist) {
  430. this._artist = null;
  431. metadataChanged = true;
  432. }
  433. if (state.hasOwnProperty('title')) {
  434. if (this._title !== state.title) {
  435. this._title = state.title;
  436. metadataChanged = true;
  437. }
  438. } else if (this._title) {
  439. this._title = null;
  440. metadataChanged = true;
  441. }
  442. if (state.hasOwnProperty('album')) {
  443. if (this._album !== state.album) {
  444. this._album = state.album;
  445. metadataChanged = true;
  446. }
  447. } else if (this._album) {
  448. this._album = null;
  449. metadataChanged = true;
  450. }
  451. if (state.hasOwnProperty('length')) {
  452. if (this._length !== state.length * 1000) {
  453. this._length = state.length * 1000;
  454. metadataChanged = true;
  455. }
  456. } else if (this._length) {
  457. this._length = 0;
  458. metadataChanged = true;
  459. }
  460. if (state.hasOwnProperty('albumArtUrl')) {
  461. this._requestAlbumArt(state);
  462. } else if (this._artUrl) {
  463. this._artUrl = null;
  464. metadataChanged = true;
  465. }
  466. if (metadataChanged) {
  467. this._Metadata = undefined;
  468. this.notify('Metadata');
  469. }
  470. }
  471. async export() {
  472. try {
  473. if (this._connection === null) {
  474. this._connection = await DBus.newConnection();
  475. const MPRISIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2');
  476. const MPRISPlayerIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2.Player');
  477. if (this._applicationIface === null) {
  478. this._applicationIface = new DBus.Interface({
  479. g_instance: this,
  480. g_connection: this._connection,
  481. g_object_path: '/org/mpris/MediaPlayer2',
  482. g_interface_info: MPRISIface,
  483. });
  484. }
  485. if (this._playerIface === null) {
  486. this._playerIface = new DBus.Interface({
  487. g_instance: this,
  488. g_connection: this._connection,
  489. g_object_path: '/org/mpris/MediaPlayer2',
  490. g_interface_info: MPRISPlayerIface,
  491. });
  492. }
  493. }
  494. if (this._ownerId !== 0)
  495. return;
  496. const name = [
  497. this.device.name,
  498. this.Identity,
  499. ].join('').replace(/[\W]*/g, '');
  500. this._ownerId = Gio.bus_own_name_on_connection(
  501. this._connection,
  502. `org.mpris.MediaPlayer2.GSConnect.${name}`,
  503. Gio.BusNameOwnerFlags.NONE,
  504. null,
  505. null
  506. );
  507. } catch (e) {
  508. debug(e, this.Identity);
  509. }
  510. }
  511. unexport() {
  512. if (this._ownerId === 0)
  513. return;
  514. Gio.bus_unown_name(this._ownerId);
  515. this._ownerId = 0;
  516. }
  517. /**
  518. * Download album art for the current track of the remote player.
  519. *
  520. * @param {Core.Packet} packet - A `kdeconnect.mpris` packet
  521. */
  522. async handleAlbumArt(packet) {
  523. let file;
  524. try {
  525. file = this._getFile(packet.body.albumArtUrl);
  526. // Transfer the album art
  527. const transfer = this.device.createTransfer();
  528. transfer.addFile(packet, file);
  529. await transfer.start();
  530. this._artUrl = file.get_uri();
  531. this._Metadata = undefined;
  532. this.notify('Metadata');
  533. } catch (e) {
  534. debug(e, this.device.name);
  535. if (file)
  536. file.delete_async(GLib.PRIORITY_DEFAULT, null, null);
  537. }
  538. }
  539. /**
  540. * Update the internal state of the media player.
  541. *
  542. * @param {Core.Packet} state - The body of a `kdeconnect.mpris` packet
  543. */
  544. update(state) {
  545. this.freeze_notify();
  546. // Metadata
  547. if (state.hasOwnProperty('nowPlaying') ||
  548. state.hasOwnProperty('artist') ||
  549. state.hasOwnProperty('title'))
  550. this._updateMetadata(state);
  551. // Playback Status
  552. if (state.hasOwnProperty('isPlaying')) {
  553. if (this._isPlaying !== state.isPlaying) {
  554. this._isPlaying = state.isPlaying;
  555. this.notify('PlaybackStatus');
  556. }
  557. }
  558. if (state.hasOwnProperty('canPlay')) {
  559. if (this.CanPlay !== state.canPlay) {
  560. this._CanPlay = state.canPlay;
  561. this.notify('CanPlay');
  562. }
  563. }
  564. if (state.hasOwnProperty('canPause')) {
  565. if (this.CanPause !== state.canPause) {
  566. this._CanPause = state.canPause;
  567. this.notify('CanPause');
  568. }
  569. }
  570. if (state.hasOwnProperty('canGoNext')) {
  571. if (this.CanGoNext !== state.canGoNext) {
  572. this._CanGoNext = state.canGoNext;
  573. this.notify('CanGoNext');
  574. }
  575. }
  576. if (state.hasOwnProperty('canGoPrevious')) {
  577. if (this.CanGoPrevious !== state.canGoPrevious) {
  578. this._CanGoPrevious = state.canGoPrevious;
  579. this.notify('CanGoPrevious');
  580. }
  581. }
  582. if (state.hasOwnProperty('pos'))
  583. this._Position = state.pos * 1000;
  584. if (state.hasOwnProperty('volume')) {
  585. if (this.Volume !== state.volume / 100) {
  586. this._Volume = state.volume / 100;
  587. this.notify('Volume');
  588. }
  589. }
  590. this.thaw_notify();
  591. if (!this._isPlaying && !this.CanControl)
  592. this.unexport();
  593. else
  594. this.export();
  595. }
  596. /*
  597. * Native properties
  598. */
  599. get device() {
  600. return this._device;
  601. }
  602. /*
  603. * The org.mpris.MediaPlayer2.Player Interface
  604. */
  605. get CanControl() {
  606. return (this.CanPlay || this.CanPause);
  607. }
  608. get Metadata() {
  609. if (this._Metadata === undefined) {
  610. this._Metadata = {};
  611. if (this._artist) {
  612. this._Metadata['xesam:artist'] = new GLib.Variant('as',
  613. [this._artist]);
  614. }
  615. if (this._title) {
  616. this._Metadata['xesam:title'] = new GLib.Variant('s',
  617. this._title);
  618. }
  619. if (this._album) {
  620. this._Metadata['xesam:album'] = new GLib.Variant('s',
  621. this._album);
  622. }
  623. if (this._artUrl) {
  624. this._Metadata['mpris:artUrl'] = new GLib.Variant('s',
  625. this._artUrl);
  626. }
  627. this._Metadata['mpris:length'] = new GLib.Variant('x',
  628. this._length);
  629. }
  630. return this._Metadata;
  631. }
  632. get PlaybackStatus() {
  633. if (this._isPlaying)
  634. return 'Playing';
  635. return 'Stopped';
  636. }
  637. get Volume() {
  638. if (this._Volume === undefined)
  639. this._Volume = 0.3;
  640. return this._Volume;
  641. }
  642. set Volume(level) {
  643. if (this._Volume === level)
  644. return;
  645. this._Volume = level;
  646. this.notify('Volume');
  647. this.device.sendPacket({
  648. type: 'kdeconnect.mpris.request',
  649. body: {
  650. player: this.Identity,
  651. setVolume: Math.floor(this._Volume * 100),
  652. },
  653. });
  654. }
  655. Next() {
  656. if (!this.CanGoNext)
  657. return;
  658. this.device.sendPacket({
  659. type: 'kdeconnect.mpris.request',
  660. body: {
  661. player: this.Identity,
  662. action: 'Next',
  663. },
  664. });
  665. }
  666. Pause() {
  667. if (!this.CanPause)
  668. return;
  669. this.device.sendPacket({
  670. type: 'kdeconnect.mpris.request',
  671. body: {
  672. player: this.Identity,
  673. action: 'Pause',
  674. },
  675. });
  676. }
  677. Play() {
  678. if (!this.CanPlay)
  679. return;
  680. this.device.sendPacket({
  681. type: 'kdeconnect.mpris.request',
  682. body: {
  683. player: this.Identity,
  684. action: 'Play',
  685. },
  686. });
  687. }
  688. PlayPause() {
  689. if (!this.CanPlay && !this.CanPause)
  690. return;
  691. this.device.sendPacket({
  692. type: 'kdeconnect.mpris.request',
  693. body: {
  694. player: this.Identity,
  695. action: 'PlayPause',
  696. },
  697. });
  698. }
  699. Previous() {
  700. if (!this.CanGoPrevious)
  701. return;
  702. this.device.sendPacket({
  703. type: 'kdeconnect.mpris.request',
  704. body: {
  705. player: this.Identity,
  706. action: 'Previous',
  707. },
  708. });
  709. }
  710. Seek(offset) {
  711. if (!this.CanSeek)
  712. return;
  713. this.device.sendPacket({
  714. type: 'kdeconnect.mpris.request',
  715. body: {
  716. player: this.Identity,
  717. Seek: offset,
  718. },
  719. });
  720. }
  721. SetPosition(trackId, position) {
  722. debug(`${this._Identity}: SetPosition(${trackId}, ${position})`);
  723. if (!this.CanControl || !this.CanSeek)
  724. return;
  725. this.device.sendPacket({
  726. type: 'kdeconnect.mpris.request',
  727. body: {
  728. player: this.Identity,
  729. SetPosition: position / 1000,
  730. },
  731. });
  732. }
  733. Stop() {
  734. if (!this.CanControl)
  735. return;
  736. this.device.sendPacket({
  737. type: 'kdeconnect.mpris.request',
  738. body: {
  739. player: this.Identity,
  740. action: 'Stop',
  741. },
  742. });
  743. }
  744. destroy() {
  745. this.unexport();
  746. if (this._connection) {
  747. this._connection.close(null, null);
  748. this._connection = null;
  749. if (this._applicationIface) {
  750. this._applicationIface.destroy();
  751. this._applicationIface = null;
  752. }
  753. if (this._playerIface) {
  754. this._playerIface.destroy();
  755. this._playerIface = null;
  756. }
  757. }
  758. }
  759. });
  760. export default MPRISPlugin;