mpris.js 26 KB

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