mpris.js 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  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. export const Player = GObject.registerClass({
  8. GTypeName: 'GSConnectMediaPlayerInterface',
  9. Properties: {
  10. // Application Properties
  11. 'CanQuit': GObject.ParamSpec.boolean(
  12. 'CanQuit',
  13. 'Can Quit',
  14. 'Whether the client can call the Quit method.',
  15. GObject.ParamFlags.READABLE,
  16. false
  17. ),
  18. 'Fullscreen': GObject.ParamSpec.boolean(
  19. 'Fullscreen',
  20. 'Fullscreen',
  21. 'Whether the player is in fullscreen mode.',
  22. GObject.ParamFlags.READWRITE,
  23. false
  24. ),
  25. 'CanSetFullscreen': GObject.ParamSpec.boolean(
  26. 'CanSetFullscreen',
  27. 'Can Set Fullscreen',
  28. 'Whether the client can set the Fullscreen property.',
  29. GObject.ParamFlags.READABLE,
  30. false
  31. ),
  32. 'CanRaise': GObject.ParamSpec.boolean(
  33. 'CanRaise',
  34. 'Can Raise',
  35. 'Whether the client can call the Raise method.',
  36. GObject.ParamFlags.READABLE,
  37. false
  38. ),
  39. 'HasTrackList': GObject.ParamSpec.boolean(
  40. 'HasTrackList',
  41. 'Has Track List',
  42. 'Whether the player has a track list.',
  43. GObject.ParamFlags.READABLE,
  44. false
  45. ),
  46. 'Identity': GObject.ParamSpec.string(
  47. 'Identity',
  48. 'Identity',
  49. 'The application name.',
  50. GObject.ParamFlags.READABLE,
  51. null
  52. ),
  53. 'DesktopEntry': GObject.ParamSpec.string(
  54. 'DesktopEntry',
  55. 'DesktopEntry',
  56. 'The basename of an installed .desktop file.',
  57. GObject.ParamFlags.READABLE,
  58. null
  59. ),
  60. 'SupportedUriSchemes': GObject.param_spec_variant(
  61. 'SupportedUriSchemes',
  62. 'Supported URI Schemes',
  63. 'The URI schemes supported by the media player.',
  64. new GLib.VariantType('as'),
  65. null,
  66. GObject.ParamFlags.READABLE
  67. ),
  68. 'SupportedMimeTypes': GObject.param_spec_variant(
  69. 'SupportedMimeTypes',
  70. 'Supported MIME Types',
  71. 'The mime-types supported by the media player.',
  72. new GLib.VariantType('as'),
  73. null,
  74. GObject.ParamFlags.READABLE
  75. ),
  76. // Player Properties
  77. 'PlaybackStatus': GObject.ParamSpec.string(
  78. 'PlaybackStatus',
  79. 'Playback Status',
  80. 'The current playback status.',
  81. GObject.ParamFlags.READABLE,
  82. null
  83. ),
  84. 'LoopStatus': GObject.ParamSpec.string(
  85. 'LoopStatus',
  86. 'Loop Status',
  87. 'The current loop status.',
  88. GObject.ParamFlags.READWRITE,
  89. null
  90. ),
  91. 'Rate': GObject.ParamSpec.double(
  92. 'Rate',
  93. 'Rate',
  94. 'The current playback rate.',
  95. GObject.ParamFlags.READWRITE,
  96. 0.0, 1.0,
  97. 1.0
  98. ),
  99. 'MinimumRate': GObject.ParamSpec.double(
  100. 'MinimumRate',
  101. 'Minimum Rate',
  102. 'The minimum playback rate.',
  103. GObject.ParamFlags.READWRITE,
  104. 0.0, 1.0,
  105. 1.0
  106. ),
  107. 'MaximimRate': GObject.ParamSpec.double(
  108. 'MaximumRate',
  109. 'Maximum Rate',
  110. 'The maximum playback rate.',
  111. GObject.ParamFlags.READWRITE,
  112. 0.0, 1.0,
  113. 1.0
  114. ),
  115. 'Shuffle': GObject.ParamSpec.boolean(
  116. 'Shuffle',
  117. 'Shuffle',
  118. 'Whether track changes are linear.',
  119. GObject.ParamFlags.READWRITE,
  120. null
  121. ),
  122. 'Metadata': GObject.param_spec_variant(
  123. 'Metadata',
  124. 'Metadata',
  125. 'The metadata of the current element.',
  126. new GLib.VariantType('a{sv}'),
  127. null,
  128. GObject.ParamFlags.READABLE
  129. ),
  130. 'Volume': GObject.ParamSpec.double(
  131. 'Volume',
  132. 'Volume',
  133. 'The volume level.',
  134. GObject.ParamFlags.READWRITE,
  135. 0.0, 1.0,
  136. 1.0
  137. ),
  138. 'Position': GObject.ParamSpec.int64(
  139. 'Position',
  140. 'Position',
  141. 'The current track position in microseconds.',
  142. GObject.ParamFlags.READABLE,
  143. 0, Number.MAX_SAFE_INTEGER,
  144. 0
  145. ),
  146. 'CanGoNext': GObject.ParamSpec.boolean(
  147. 'CanGoNext',
  148. 'Can Go Next',
  149. 'Whether the client can call the Next method.',
  150. GObject.ParamFlags.READABLE,
  151. false
  152. ),
  153. 'CanGoPrevious': GObject.ParamSpec.boolean(
  154. 'CanGoPrevious',
  155. 'Can Go Previous',
  156. 'Whether the client can call the Previous method.',
  157. GObject.ParamFlags.READABLE,
  158. false
  159. ),
  160. 'CanPlay': GObject.ParamSpec.boolean(
  161. 'CanPlay',
  162. 'Can Play',
  163. 'Whether playback can be started using Play or PlayPause.',
  164. GObject.ParamFlags.READABLE,
  165. false
  166. ),
  167. 'CanPause': GObject.ParamSpec.boolean(
  168. 'CanPause',
  169. 'Can Pause',
  170. 'Whether playback can be paused using Play or PlayPause.',
  171. GObject.ParamFlags.READABLE,
  172. false
  173. ),
  174. 'CanSeek': GObject.ParamSpec.boolean(
  175. 'CanSeek',
  176. 'Can Seek',
  177. 'Whether the client can control the playback position using Seek and SetPosition.',
  178. GObject.ParamFlags.READABLE,
  179. false
  180. ),
  181. 'CanControl': GObject.ParamSpec.boolean(
  182. 'CanControl',
  183. 'Can Control',
  184. 'Whether the media player may be controlled over this interface.',
  185. GObject.ParamFlags.READABLE,
  186. false
  187. ),
  188. },
  189. Signals: {
  190. 'Seeked': {
  191. flags: GObject.SignalFlags.RUN_FIRST,
  192. param_types: [GObject.TYPE_INT64],
  193. },
  194. },
  195. }, class Player extends GObject.Object {
  196. /*
  197. * The org.mpris.MediaPlayer2 Interface
  198. */
  199. get CanQuit() {
  200. if (this._CanQuit === undefined)
  201. this._CanQuit = false;
  202. return this._CanQuit;
  203. }
  204. get CanRaise() {
  205. if (this._CanRaise === undefined)
  206. this._CanRaise = false;
  207. return this._CanRaise;
  208. }
  209. get CanSetFullscreen() {
  210. if (this._CanFullscreen === undefined)
  211. this._CanFullscreen = false;
  212. return this._CanFullscreen;
  213. }
  214. get DesktopEntry() {
  215. if (this._DesktopEntry === undefined)
  216. return 'org.gnome.Shell.Extensions.GSConnect';
  217. return this._DesktopEntry;
  218. }
  219. get Fullscreen() {
  220. if (this._Fullscreen === undefined)
  221. this._Fullscreen = false;
  222. return this._Fullscreen;
  223. }
  224. set Fullscreen(mode) {
  225. if (this.Fullscreen === mode)
  226. return;
  227. this._Fullscreen = mode;
  228. this.notify('Fullscreen');
  229. }
  230. get HasTrackList() {
  231. if (this._HasTrackList === undefined)
  232. this._HasTrackList = false;
  233. return this._HasTrackList;
  234. }
  235. get Identity() {
  236. if (this._Identity === undefined)
  237. this._Identity = '';
  238. return this._Identity;
  239. }
  240. get SupportedMimeTypes() {
  241. if (this._SupportedMimeTypes === undefined)
  242. this._SupportedMimeTypes = [];
  243. return this._SupportedMimeTypes;
  244. }
  245. get SupportedUriSchemes() {
  246. if (this._SupportedUriSchemes === undefined)
  247. this._SupportedUriSchemes = [];
  248. return this._SupportedUriSchemes;
  249. }
  250. Quit() {
  251. throw new GObject.NotImplementedError();
  252. }
  253. Raise() {
  254. throw new GObject.NotImplementedError();
  255. }
  256. /*
  257. * The org.mpris.MediaPlayer2.Player Interface
  258. */
  259. get CanControl() {
  260. if (this._CanControl === undefined)
  261. this._CanControl = false;
  262. return this._CanControl;
  263. }
  264. get CanGoNext() {
  265. if (this._CanGoNext === undefined)
  266. this._CanGoNext = false;
  267. return this._CanGoNext;
  268. }
  269. get CanGoPrevious() {
  270. if (this._CanGoPrevious === undefined)
  271. this._CanGoPrevious = false;
  272. return this._CanGoPrevious;
  273. }
  274. get CanPause() {
  275. if (this._CanPause === undefined)
  276. this._CanPause = false;
  277. return this._CanPause;
  278. }
  279. get CanPlay() {
  280. if (this._CanPlay === undefined)
  281. this._CanPlay = false;
  282. return this._CanPlay;
  283. }
  284. get CanSeek() {
  285. if (this._CanSeek === undefined)
  286. this._CanSeek = false;
  287. return this._CanSeek;
  288. }
  289. get LoopStatus() {
  290. if (this._LoopStatus === undefined)
  291. this._LoopStatus = 'None';
  292. return this._LoopStatus;
  293. }
  294. set LoopStatus(status) {
  295. if (this.LoopStatus === status)
  296. return;
  297. this._LoopStatus = status;
  298. this.notify('LoopStatus');
  299. }
  300. get MaximumRate() {
  301. if (this._MaximumRate === undefined)
  302. this._MaximumRate = 1.0;
  303. return this._MaximumRate;
  304. }
  305. get Metadata() {
  306. if (this._Metadata === undefined) {
  307. this._Metadata = {
  308. 'xesam:artist': [_('Unknown')],
  309. 'xesam:album': _('Unknown'),
  310. 'xesam:title': _('Unknown'),
  311. 'mpris:length': 0,
  312. };
  313. }
  314. return this._Metadata;
  315. }
  316. get MinimumRate() {
  317. if (this._MinimumRate === undefined)
  318. this._MinimumRate = 1.0;
  319. return this._MinimumRate;
  320. }
  321. get PlaybackStatus() {
  322. if (this._PlaybackStatus === undefined)
  323. this._PlaybackStatus = 'Stopped';
  324. return this._PlaybackStatus;
  325. }
  326. get Position() {
  327. if (this._Position === undefined)
  328. this._Position = 0;
  329. return this._Position;
  330. }
  331. get Rate() {
  332. if (this._Rate === undefined)
  333. this._Rate = 1.0;
  334. return this._Rate;
  335. }
  336. set Rate(rate) {
  337. if (this.Rate === rate)
  338. return;
  339. this._Rate = rate;
  340. this.notify('Rate');
  341. }
  342. get Shuffle() {
  343. if (this._Shuffle === undefined)
  344. this._Shuffle = false;
  345. return this._Shuffle;
  346. }
  347. set Shuffle(mode) {
  348. if (this.Shuffle === mode)
  349. return;
  350. this._Shuffle = mode;
  351. this.notify('Shuffle');
  352. }
  353. get Volume() {
  354. if (this._Volume === undefined)
  355. this._Volume = 1.0;
  356. return this._Volume;
  357. }
  358. set Volume(level) {
  359. if (this.Volume === level)
  360. return;
  361. this._Volume = level;
  362. this.notify('Volume');
  363. }
  364. Next() {
  365. throw new GObject.NotImplementedError();
  366. }
  367. OpenUri(uri) {
  368. throw new GObject.NotImplementedError();
  369. }
  370. Previous() {
  371. throw new GObject.NotImplementedError();
  372. }
  373. Pause() {
  374. throw new GObject.NotImplementedError();
  375. }
  376. Play() {
  377. throw new GObject.NotImplementedError();
  378. }
  379. PlayPause() {
  380. throw new GObject.NotImplementedError();
  381. }
  382. Seek(offset) {
  383. throw new GObject.NotImplementedError();
  384. }
  385. SetPosition(trackId, position) {
  386. throw new GObject.NotImplementedError();
  387. }
  388. Stop() {
  389. throw new GObject.NotImplementedError();
  390. }
  391. });
  392. /**
  393. * An aggregate of the org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player
  394. * interfaces.
  395. */
  396. const PlayerProxy = GObject.registerClass({
  397. GTypeName: 'GSConnectMPRISPlayer',
  398. }, class PlayerProxy extends Player {
  399. /**
  400. * @constructs GSConnectMPRISPlayer
  401. * @param {string} name - The name of the player
  402. */
  403. _init(name) {
  404. super._init();
  405. this._application = new Gio.DBusProxy({
  406. g_bus_type: Gio.BusType.SESSION,
  407. g_name: name,
  408. g_object_path: '/org/mpris/MediaPlayer2',
  409. g_interface_name: 'org.mpris.MediaPlayer2',
  410. });
  411. this._applicationChangedId = this._application.connect(
  412. 'g-properties-changed',
  413. this._onPropertiesChanged.bind(this)
  414. );
  415. this._player = new Gio.DBusProxy({
  416. g_bus_type: Gio.BusType.SESSION,
  417. g_name: name,
  418. g_object_path: '/org/mpris/MediaPlayer2',
  419. g_interface_name: 'org.mpris.MediaPlayer2.Player',
  420. });
  421. this._playerChangedId = this._player.connect(
  422. 'g-properties-changed',
  423. this._onPropertiesChanged.bind(this)
  424. );
  425. this._playerSignalId = this._player.connect(
  426. 'g-signal',
  427. this._onSignal.bind(this)
  428. );
  429. this._cancellable = new Gio.Cancellable();
  430. }
  431. _onSignal(proxy, sender_name, signal_name, parameters) {
  432. try {
  433. if (signal_name !== 'Seeked')
  434. return;
  435. this.emit('Seeked', parameters.deepUnpack()[0]);
  436. } catch (e) {
  437. debug(e, proxy.g_name);
  438. }
  439. }
  440. _call(proxy, name, parameters = null) {
  441. proxy.call(
  442. name,
  443. parameters,
  444. Gio.DBusCallFlags.NO_AUTO_START,
  445. -1,
  446. this._cancellable,
  447. (proxy, result) => {
  448. try {
  449. proxy.call_finish(result);
  450. } catch (e) {
  451. Gio.DBusError.strip_remote_error(e);
  452. debug(e, proxy.g_name);
  453. }
  454. }
  455. );
  456. }
  457. _get(proxy, name, fallback = null) {
  458. try {
  459. return proxy.get_cached_property(name).recursiveUnpack();
  460. } catch {
  461. return fallback;
  462. }
  463. }
  464. _set(proxy, name, value) {
  465. try {
  466. proxy.set_cached_property(name, value);
  467. proxy.call(
  468. 'org.freedesktop.DBus.Properties.Set',
  469. new GLib.Variant('(ssv)', [proxy.g_interface_name, name, value]),
  470. Gio.DBusCallFlags.NO_AUTO_START,
  471. -1,
  472. this._cancellable,
  473. (proxy, result) => {
  474. try {
  475. proxy.call_finish(result);
  476. } catch (e) {
  477. Gio.DBusError.strip_remote_error(e);
  478. debug(e, proxy.g_name);
  479. }
  480. }
  481. );
  482. } catch (e) {
  483. debug(e, proxy.g_name);
  484. }
  485. }
  486. _onPropertiesChanged(proxy, changed, invalidated) {
  487. try {
  488. this.freeze_notify();
  489. for (const name in changed.deepUnpack())
  490. this.notify(name);
  491. this.thaw_notify();
  492. } catch (e) {
  493. debug(e, proxy.g_name);
  494. }
  495. }
  496. /*
  497. * The org.mpris.MediaPlayer2 Interface
  498. */
  499. get CanQuit() {
  500. return this._get(this._application, 'CanQuit', false);
  501. }
  502. get CanRaise() {
  503. return this._get(this._application, 'CanRaise', false);
  504. }
  505. get CanSetFullscreen() {
  506. return this._get(this._application, 'CanSetFullscreen', false);
  507. }
  508. get DesktopEntry() {
  509. return this._get(this._application, 'DesktopEntry', null);
  510. }
  511. get Fullscreen() {
  512. return this._get(this._application, 'Fullscreen', false);
  513. }
  514. set Fullscreen(mode) {
  515. this._set(this._application, 'Fullscreen', new GLib.Variant('b', mode));
  516. }
  517. get HasTrackList() {
  518. return this._get(this._application, 'HasTrackList', false);
  519. }
  520. get Identity() {
  521. return this._get(this._application, 'Identity', _('Unknown'));
  522. }
  523. get SupportedMimeTypes() {
  524. return this._get(this._application, 'SupportedMimeTypes', []);
  525. }
  526. get SupportedUriSchemes() {
  527. return this._get(this._application, 'SupportedUriSchemes', []);
  528. }
  529. Quit() {
  530. this._call(this._application, 'Quit');
  531. }
  532. Raise() {
  533. this._call(this._application, 'Raise');
  534. }
  535. /*
  536. * The org.mpris.MediaPlayer2.Player Interface
  537. */
  538. get CanControl() {
  539. return this._get(this._player, 'CanControl', false);
  540. }
  541. get CanGoNext() {
  542. return this._get(this._player, 'CanGoNext', false);
  543. }
  544. get CanGoPrevious() {
  545. return this._get(this._player, 'CanGoPrevious', false);
  546. }
  547. get CanPause() {
  548. return this._get(this._player, 'CanPause', false);
  549. }
  550. get CanPlay() {
  551. return this._get(this._player, 'CanPlay', false);
  552. }
  553. get CanSeek() {
  554. return this._get(this._player, 'CanSeek', false);
  555. }
  556. get LoopStatus() {
  557. return this._get(this._player, 'LoopStatus', 'None');
  558. }
  559. set LoopStatus(status) {
  560. this._set(this._player, 'LoopStatus', new GLib.Variant('s', status));
  561. }
  562. get MaximumRate() {
  563. return this._get(this._player, 'MaximumRate', 1.0);
  564. }
  565. get Metadata() {
  566. if (this._metadata === undefined) {
  567. this._metadata = {
  568. 'xesam:artist': [_('Unknown')],
  569. 'xesam:album': _('Unknown'),
  570. 'xesam:title': _('Unknown'),
  571. 'mpris:length': 0,
  572. };
  573. }
  574. return this._get(this._player, 'Metadata', this._metadata);
  575. }
  576. get MinimumRate() {
  577. return this._get(this._player, 'MinimumRate', 1.0);
  578. }
  579. get PlaybackStatus() {
  580. return this._get(this._player, 'PlaybackStatus', 'Stopped');
  581. }
  582. // g-properties-changed is not emitted for this property
  583. get Position() {
  584. try {
  585. const reply = this._player.call_sync(
  586. 'org.freedesktop.DBus.Properties.Get',
  587. new GLib.Variant('(ss)', [
  588. 'org.mpris.MediaPlayer2.Player',
  589. 'Position',
  590. ]),
  591. Gio.DBusCallFlags.NONE,
  592. -1,
  593. null
  594. );
  595. return reply.recursiveUnpack()[0];
  596. } catch {
  597. return 0;
  598. }
  599. }
  600. get Rate() {
  601. return this._get(this._player, 'Rate', 1.0);
  602. }
  603. set Rate(rate) {
  604. this._set(this._player, 'Rate', new GLib.Variant('d', rate));
  605. }
  606. get Shuffle() {
  607. return this._get(this._player, 'Shuffle', false);
  608. }
  609. set Shuffle(mode) {
  610. this._set(this._player, 'Shuffle', new GLib.Variant('b', mode));
  611. }
  612. get Volume() {
  613. return this._get(this._player, 'Volume', 1.0);
  614. }
  615. set Volume(level) {
  616. this._set(this._player, 'Volume', new GLib.Variant('d', level));
  617. }
  618. Next() {
  619. this._call(this._player, 'Next');
  620. }
  621. OpenUri(uri) {
  622. this._call(this._player, 'OpenUri', new GLib.Variant('(s)', [uri]));
  623. }
  624. Previous() {
  625. this._call(this._player, 'Previous');
  626. }
  627. Pause() {
  628. this._call(this._player, 'Pause');
  629. }
  630. Play() {
  631. this._call(this._player, 'Play');
  632. }
  633. PlayPause() {
  634. this._call(this._player, 'PlayPause');
  635. }
  636. Seek(offset) {
  637. this._call(this._player, 'Seek', new GLib.Variant('(x)', [offset]));
  638. }
  639. SetPosition(trackId, position) {
  640. this._call(this._player, 'SetPosition',
  641. new GLib.Variant('(ox)', [trackId, position]));
  642. }
  643. Stop() {
  644. this._call(this._player, 'Stop');
  645. }
  646. destroy() {
  647. if (this._cancellable.is_cancelled())
  648. return;
  649. this._cancellable.cancel();
  650. this._application.disconnect(this._applicationChangedId);
  651. this._player.disconnect(this._playerChangedId);
  652. this._player.disconnect(this._playerSignalId);
  653. }
  654. });
  655. /**
  656. * A manager for media players
  657. */
  658. const Manager = GObject.registerClass({
  659. GTypeName: 'GSConnectMPRISManager',
  660. Signals: {
  661. 'player-added': {
  662. param_types: [GObject.TYPE_OBJECT],
  663. },
  664. 'player-removed': {
  665. param_types: [GObject.TYPE_OBJECT],
  666. },
  667. 'player-changed': {
  668. param_types: [GObject.TYPE_OBJECT],
  669. },
  670. 'player-seeked': {
  671. param_types: [GObject.TYPE_OBJECT, GObject.TYPE_INT64],
  672. },
  673. },
  674. }, class Manager extends GObject.Object {
  675. _init() {
  676. super._init();
  677. // Asynchronous setup
  678. this._cancellable = new Gio.Cancellable();
  679. this._connection = Gio.DBus.session;
  680. this._players = new Map();
  681. this._paused = new Map();
  682. this._nameOwnerChangedId = Gio.DBus.session.signal_subscribe(
  683. 'org.freedesktop.DBus',
  684. 'org.freedesktop.DBus',
  685. 'NameOwnerChanged',
  686. '/org/freedesktop/DBus',
  687. 'org.mpris.MediaPlayer2',
  688. Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE,
  689. this._onNameOwnerChanged.bind(this)
  690. );
  691. this._loadPlayers();
  692. }
  693. async _loadPlayers() {
  694. try {
  695. const reply = await this._connection.call(
  696. 'org.freedesktop.DBus',
  697. '/org/freedesktop/DBus',
  698. 'org.freedesktop.DBus',
  699. 'ListNames',
  700. null,
  701. null,
  702. Gio.DBusCallFlags.NONE,
  703. -1,
  704. this._cancellable);
  705. const names = reply.deepUnpack()[0];
  706. for (let i = 0, len = names.length; i < len; i++) {
  707. const name = names[i];
  708. if (!name.startsWith('org.mpris.MediaPlayer2'))
  709. continue;
  710. if (!name.includes('GSConnect'))
  711. this._addPlayer(name);
  712. }
  713. } catch (e) {
  714. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  715. logError(e);
  716. }
  717. }
  718. _onNameOwnerChanged(connection, sender, object, iface, signal, parameters) {
  719. const [name, oldOwner, newOwner] = parameters.deepUnpack();
  720. if (name.includes('GSConnect'))
  721. return;
  722. if (newOwner.length)
  723. this._addPlayer(name);
  724. else if (oldOwner.length)
  725. this._removePlayer(name);
  726. }
  727. async _addPlayer(name) {
  728. try {
  729. if (!this._players.has(name)) {
  730. const player = new PlayerProxy(name);
  731. await Promise.all([
  732. player._application.init_async(GLib.PRIORITY_DEFAULT,
  733. this._cancellable),
  734. player._player.init_async(GLib.PRIORITY_DEFAULT,
  735. this._cancellable),
  736. ]);
  737. player.connect('notify',
  738. (player) => this.emit('player-changed', player));
  739. player.connect('Seeked', this.emit.bind(this, 'player-seeked'));
  740. this._players.set(name, player);
  741. this.emit('player-added', player);
  742. }
  743. } catch (e) {
  744. debug(e, name);
  745. }
  746. }
  747. _removePlayer(name) {
  748. try {
  749. const player = this._players.get(name);
  750. if (player !== undefined) {
  751. this._paused.delete(name);
  752. this._players.delete(name);
  753. this.emit('player-removed', player);
  754. player.destroy();
  755. }
  756. } catch (e) {
  757. debug(e, name);
  758. }
  759. }
  760. /**
  761. * Check for a player by its Identity.
  762. *
  763. * @param {string} identity - A player name
  764. * @returns {boolean} %true if the player was found
  765. */
  766. hasPlayer(identity) {
  767. for (const player of this._players.values()) {
  768. if (player.Identity === identity)
  769. return true;
  770. }
  771. return false;
  772. }
  773. /**
  774. * Get a player by its Identity.
  775. *
  776. * @param {string} identity - A player name
  777. * @returns {GSConnectMPRISPlayer|null} A player or %null
  778. */
  779. getPlayer(identity) {
  780. for (const player of this._players.values()) {
  781. if (player.Identity === identity)
  782. return player;
  783. }
  784. return null;
  785. }
  786. /**
  787. * Get a list of player identities.
  788. *
  789. * @returns {string[]} A list of player identities
  790. */
  791. getIdentities() {
  792. const identities = [];
  793. for (const player of this._players.values()) {
  794. const identity = player.Identity;
  795. if (identity)
  796. identities.push(identity);
  797. }
  798. return identities;
  799. }
  800. /**
  801. * A convenience function for pausing all players currently playing.
  802. */
  803. pauseAll() {
  804. for (const [name, player] of this._players) {
  805. if (player.PlaybackStatus === 'Playing' && player.CanPause) {
  806. player.Pause();
  807. this._paused.set(name, player);
  808. }
  809. }
  810. }
  811. /**
  812. * A convenience function for restarting all players paused with pauseAll().
  813. */
  814. unpauseAll() {
  815. for (const player of this._paused.values()) {
  816. if (player.PlaybackStatus === 'Paused' && player.CanPlay)
  817. player.Play();
  818. }
  819. this._paused.clear();
  820. }
  821. destroy() {
  822. if (this._cancellable.is_cancelled())
  823. return;
  824. this._cancellable.cancel();
  825. this._connection.signal_unsubscribe(this._nameOwnerChangedId);
  826. this._paused.clear();
  827. this._players.forEach(player => player.destroy());
  828. this._players.clear();
  829. }
  830. });
  831. /**
  832. * The service class for this component
  833. */
  834. export default Manager;