mpris.js 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  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. _init(name) {
  400. super._init();
  401. this._application = new Gio.DBusProxy({
  402. g_bus_type: Gio.BusType.SESSION,
  403. g_name: name,
  404. g_object_path: '/org/mpris/MediaPlayer2',
  405. g_interface_name: 'org.mpris.MediaPlayer2',
  406. });
  407. this._applicationChangedId = this._application.connect(
  408. 'g-properties-changed',
  409. this._onPropertiesChanged.bind(this)
  410. );
  411. this._player = new Gio.DBusProxy({
  412. g_bus_type: Gio.BusType.SESSION,
  413. g_name: name,
  414. g_object_path: '/org/mpris/MediaPlayer2',
  415. g_interface_name: 'org.mpris.MediaPlayer2.Player',
  416. });
  417. this._playerChangedId = this._player.connect(
  418. 'g-properties-changed',
  419. this._onPropertiesChanged.bind(this)
  420. );
  421. this._playerSignalId = this._player.connect(
  422. 'g-signal',
  423. this._onSignal.bind(this)
  424. );
  425. this._cancellable = new Gio.Cancellable();
  426. }
  427. _onSignal(proxy, sender_name, signal_name, parameters) {
  428. try {
  429. if (signal_name !== 'Seeked')
  430. return;
  431. this.emit('Seeked', parameters.deepUnpack()[0]);
  432. } catch (e) {
  433. debug(e, proxy.g_name);
  434. }
  435. }
  436. _call(proxy, name, parameters = null) {
  437. proxy.call(
  438. name,
  439. parameters,
  440. Gio.DBusCallFlags.NO_AUTO_START,
  441. -1,
  442. this._cancellable,
  443. (proxy, result) => {
  444. try {
  445. proxy.call_finish(result);
  446. } catch (e) {
  447. Gio.DBusError.strip_remote_error(e);
  448. debug(e, proxy.g_name);
  449. }
  450. }
  451. );
  452. }
  453. _get(proxy, name, fallback = null) {
  454. try {
  455. return proxy.get_cached_property(name).recursiveUnpack();
  456. } catch (e) {
  457. return fallback;
  458. }
  459. }
  460. _set(proxy, name, value) {
  461. try {
  462. proxy.set_cached_property(name, value);
  463. proxy.call(
  464. 'org.freedesktop.DBus.Properties.Set',
  465. new GLib.Variant('(ssv)', [proxy.g_interface_name, name, value]),
  466. Gio.DBusCallFlags.NO_AUTO_START,
  467. -1,
  468. this._cancellable,
  469. (proxy, result) => {
  470. try {
  471. proxy.call_finish(result);
  472. } catch (e) {
  473. Gio.DBusError.strip_remote_error(e);
  474. debug(e, proxy.g_name);
  475. }
  476. }
  477. );
  478. } catch (e) {
  479. debug(e, proxy.g_name);
  480. }
  481. }
  482. _onPropertiesChanged(proxy, changed, invalidated) {
  483. try {
  484. this.freeze_notify();
  485. for (const name in changed.deepUnpack())
  486. this.notify(name);
  487. this.thaw_notify();
  488. } catch (e) {
  489. debug(e, proxy.g_name);
  490. }
  491. }
  492. /*
  493. * The org.mpris.MediaPlayer2 Interface
  494. */
  495. get CanQuit() {
  496. return this._get(this._application, 'CanQuit', false);
  497. }
  498. get CanRaise() {
  499. return this._get(this._application, 'CanRaise', false);
  500. }
  501. get CanSetFullscreen() {
  502. return this._get(this._application, 'CanSetFullscreen', false);
  503. }
  504. get DesktopEntry() {
  505. return this._get(this._application, 'DesktopEntry', null);
  506. }
  507. get Fullscreen() {
  508. return this._get(this._application, 'Fullscreen', false);
  509. }
  510. set Fullscreen(mode) {
  511. this._set(this._application, 'Fullscreen', new GLib.Variant('b', mode));
  512. }
  513. get HasTrackList() {
  514. return this._get(this._application, 'HasTrackList', false);
  515. }
  516. get Identity() {
  517. return this._get(this._application, 'Identity', _('Unknown'));
  518. }
  519. get SupportedMimeTypes() {
  520. return this._get(this._application, 'SupportedMimeTypes', []);
  521. }
  522. get SupportedUriSchemes() {
  523. return this._get(this._application, 'SupportedUriSchemes', []);
  524. }
  525. Quit() {
  526. this._call(this._application, 'Quit');
  527. }
  528. Raise() {
  529. this._call(this._application, 'Raise');
  530. }
  531. /*
  532. * The org.mpris.MediaPlayer2.Player Interface
  533. */
  534. get CanControl() {
  535. return this._get(this._player, 'CanControl', false);
  536. }
  537. get CanGoNext() {
  538. return this._get(this._player, 'CanGoNext', false);
  539. }
  540. get CanGoPrevious() {
  541. return this._get(this._player, 'CanGoPrevious', false);
  542. }
  543. get CanPause() {
  544. return this._get(this._player, 'CanPause', false);
  545. }
  546. get CanPlay() {
  547. return this._get(this._player, 'CanPlay', false);
  548. }
  549. get CanSeek() {
  550. return this._get(this._player, 'CanSeek', false);
  551. }
  552. get LoopStatus() {
  553. return this._get(this._player, 'LoopStatus', 'None');
  554. }
  555. set LoopStatus(status) {
  556. this._set(this._player, 'LoopStatus', new GLib.Variant('s', status));
  557. }
  558. get MaximumRate() {
  559. return this._get(this._player, 'MaximumRate', 1.0);
  560. }
  561. get Metadata() {
  562. if (this._metadata === undefined) {
  563. this._metadata = {
  564. 'xesam:artist': [_('Unknown')],
  565. 'xesam:album': _('Unknown'),
  566. 'xesam:title': _('Unknown'),
  567. 'mpris:length': 0,
  568. };
  569. }
  570. return this._get(this._player, 'Metadata', this._metadata);
  571. }
  572. get MinimumRate() {
  573. return this._get(this._player, 'MinimumRate', 1.0);
  574. }
  575. get PlaybackStatus() {
  576. return this._get(this._player, 'PlaybackStatus', 'Stopped');
  577. }
  578. // g-properties-changed is not emitted for this property
  579. get Position() {
  580. try {
  581. const reply = this._player.call_sync(
  582. 'org.freedesktop.DBus.Properties.Get',
  583. new GLib.Variant('(ss)', [
  584. 'org.mpris.MediaPlayer2.Player',
  585. 'Position',
  586. ]),
  587. Gio.DBusCallFlags.NONE,
  588. -1,
  589. null
  590. );
  591. return reply.recursiveUnpack()[0];
  592. } catch (e) {
  593. return 0;
  594. }
  595. }
  596. get Rate() {
  597. return this._get(this._player, 'Rate', 1.0);
  598. }
  599. set Rate(rate) {
  600. this._set(this._player, 'Rate', new GLib.Variant('d', rate));
  601. }
  602. get Shuffle() {
  603. return this._get(this._player, 'Shuffle', false);
  604. }
  605. set Shuffle(mode) {
  606. this._set(this._player, 'Shuffle', new GLib.Variant('b', mode));
  607. }
  608. get Volume() {
  609. return this._get(this._player, 'Volume', 1.0);
  610. }
  611. set Volume(level) {
  612. this._set(this._player, 'Volume', new GLib.Variant('d', level));
  613. }
  614. Next() {
  615. this._call(this._player, 'Next');
  616. }
  617. OpenUri(uri) {
  618. this._call(this._player, 'OpenUri', new GLib.Variant('(s)', [uri]));
  619. }
  620. Previous() {
  621. this._call(this._player, 'Previous');
  622. }
  623. Pause() {
  624. this._call(this._player, 'Pause');
  625. }
  626. Play() {
  627. this._call(this._player, 'Play');
  628. }
  629. PlayPause() {
  630. this._call(this._player, 'PlayPause');
  631. }
  632. Seek(offset) {
  633. this._call(this._player, 'Seek', new GLib.Variant('(x)', [offset]));
  634. }
  635. SetPosition(trackId, position) {
  636. this._call(this._player, 'SetPosition',
  637. new GLib.Variant('(ox)', [trackId, position]));
  638. }
  639. Stop() {
  640. this._call(this._player, 'Stop');
  641. }
  642. destroy() {
  643. if (this._cancellable.is_cancelled())
  644. return;
  645. this._cancellable.cancel();
  646. this._application.disconnect(this._applicationChangedId);
  647. this._player.disconnect(this._playerChangedId);
  648. this._player.disconnect(this._playerSignalId);
  649. }
  650. });
  651. /**
  652. * A manager for media players
  653. */
  654. const Manager = GObject.registerClass({
  655. GTypeName: 'GSConnectMPRISManager',
  656. Signals: {
  657. 'player-added': {
  658. param_types: [GObject.TYPE_OBJECT],
  659. },
  660. 'player-removed': {
  661. param_types: [GObject.TYPE_OBJECT],
  662. },
  663. 'player-changed': {
  664. param_types: [GObject.TYPE_OBJECT],
  665. },
  666. 'player-seeked': {
  667. param_types: [GObject.TYPE_OBJECT, GObject.TYPE_INT64],
  668. },
  669. },
  670. }, class Manager extends GObject.Object {
  671. _init() {
  672. super._init();
  673. // Asynchronous setup
  674. this._cancellable = new Gio.Cancellable();
  675. this._connection = Gio.DBus.session;
  676. this._players = new Map();
  677. this._paused = new Map();
  678. this._nameOwnerChangedId = Gio.DBus.session.signal_subscribe(
  679. 'org.freedesktop.DBus',
  680. 'org.freedesktop.DBus',
  681. 'NameOwnerChanged',
  682. '/org/freedesktop/DBus',
  683. 'org.mpris.MediaPlayer2',
  684. Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE,
  685. this._onNameOwnerChanged.bind(this)
  686. );
  687. this._loadPlayers();
  688. }
  689. async _loadPlayers() {
  690. try {
  691. const reply = await this._connection.call(
  692. 'org.freedesktop.DBus',
  693. '/org/freedesktop/DBus',
  694. 'org.freedesktop.DBus',
  695. 'ListNames',
  696. null,
  697. null,
  698. Gio.DBusCallFlags.NONE,
  699. -1,
  700. this._cancellable);
  701. const names = reply.deepUnpack()[0];
  702. for (let i = 0, len = names.length; i < len; i++) {
  703. const name = names[i];
  704. if (!name.startsWith('org.mpris.MediaPlayer2'))
  705. continue;
  706. if (!name.includes('GSConnect'))
  707. this._addPlayer(name);
  708. }
  709. } catch (e) {
  710. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  711. logError(e);
  712. }
  713. }
  714. _onNameOwnerChanged(connection, sender, object, iface, signal, parameters) {
  715. const [name, oldOwner, newOwner] = parameters.deepUnpack();
  716. if (name.includes('GSConnect'))
  717. return;
  718. if (newOwner.length)
  719. this._addPlayer(name);
  720. else if (oldOwner.length)
  721. this._removePlayer(name);
  722. }
  723. async _addPlayer(name) {
  724. try {
  725. if (!this._players.has(name)) {
  726. const player = new PlayerProxy(name);
  727. await Promise.all([
  728. player._application.init_async(GLib.PRIORITY_DEFAULT,
  729. this._cancellable),
  730. player._player.init_async(GLib.PRIORITY_DEFAULT,
  731. this._cancellable),
  732. ]);
  733. player.connect('notify',
  734. (player) => this.emit('player-changed', player));
  735. player.connect('Seeked', this.emit.bind(this, 'player-seeked'));
  736. this._players.set(name, player);
  737. this.emit('player-added', player);
  738. }
  739. } catch (e) {
  740. debug(e, name);
  741. }
  742. }
  743. _removePlayer(name) {
  744. try {
  745. const player = this._players.get(name);
  746. if (player !== undefined) {
  747. this._paused.delete(name);
  748. this._players.delete(name);
  749. this.emit('player-removed', player);
  750. player.destroy();
  751. }
  752. } catch (e) {
  753. debug(e, name);
  754. }
  755. }
  756. /**
  757. * Check for a player by its Identity.
  758. *
  759. * @param {string} identity - A player name
  760. * @return {boolean} %true if the player was found
  761. */
  762. hasPlayer(identity) {
  763. for (const player of this._players.values()) {
  764. if (player.Identity === identity)
  765. return true;
  766. }
  767. return false;
  768. }
  769. /**
  770. * Get a player by its Identity.
  771. *
  772. * @param {string} identity - A player name
  773. * @return {GSConnectMPRISPlayer|null} A player or %null
  774. */
  775. getPlayer(identity) {
  776. for (const player of this._players.values()) {
  777. if (player.Identity === identity)
  778. return player;
  779. }
  780. return null;
  781. }
  782. /**
  783. * Get a list of player identities.
  784. *
  785. * @return {string[]} A list of player identities
  786. */
  787. getIdentities() {
  788. const identities = [];
  789. for (const player of this._players.values()) {
  790. const identity = player.Identity;
  791. if (identity)
  792. identities.push(identity);
  793. }
  794. return identities;
  795. }
  796. /**
  797. * A convenience function for pausing all players currently playing.
  798. */
  799. pauseAll() {
  800. for (const [name, player] of this._players) {
  801. if (player.PlaybackStatus === 'Playing' && player.CanPause) {
  802. player.Pause();
  803. this._paused.set(name, player);
  804. }
  805. }
  806. }
  807. /**
  808. * A convenience function for restarting all players paused with pauseAll().
  809. */
  810. unpauseAll() {
  811. for (const player of this._paused.values()) {
  812. if (player.PlaybackStatus === 'Paused' && player.CanPlay)
  813. player.Play();
  814. }
  815. this._paused.clear();
  816. }
  817. destroy() {
  818. if (this._cancellable.is_cancelled())
  819. return;
  820. this._cancellable.cancel();
  821. this._connection.signal_unsubscribe(this._nameOwnerChangedId);
  822. this._paused.clear();
  823. this._players.forEach(player => player.destroy());
  824. this._players.clear();
  825. }
  826. });
  827. /**
  828. * The service class for this component
  829. */
  830. export default Manager;