mpris.js 25 KB

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