systemvolume.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const GObject = imports.gi.GObject;
  6. const Components = imports.service.components;
  7. const Config = imports.config;
  8. const PluginBase = imports.service.plugin;
  9. var Metadata = {
  10. label: _('System Volume'),
  11. description: _('Enable the paired device to control the system volume'),
  12. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SystemVolume',
  13. incomingCapabilities: ['kdeconnect.systemvolume.request'],
  14. outgoingCapabilities: ['kdeconnect.systemvolume'],
  15. actions: {},
  16. };
  17. /**
  18. * SystemVolume Plugin
  19. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/systemvolume
  20. * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/
  21. */
  22. var Plugin = GObject.registerClass({
  23. GTypeName: 'GSConnectSystemVolumePlugin',
  24. }, class Plugin extends PluginBase.Plugin {
  25. _init(device) {
  26. super._init(device, 'systemvolume');
  27. // Cache stream properties
  28. this._cache = new WeakMap();
  29. // Connect to the mixer
  30. try {
  31. this._mixer = Components.acquire('pulseaudio');
  32. this._streamChangedId = this._mixer.connect(
  33. 'stream-changed',
  34. this._sendSink.bind(this)
  35. );
  36. this._outputAddedId = this._mixer.connect(
  37. 'output-added',
  38. this._sendSinkList.bind(this)
  39. );
  40. this._outputRemovedId = this._mixer.connect(
  41. 'output-removed',
  42. this._sendSinkList.bind(this)
  43. );
  44. // Modify the error to redirect to the wiki
  45. } catch (e) {
  46. e.name = _('PulseAudio not found');
  47. e.url = `${Config.PACKAGE_URL}/wiki/Error#pulseaudio-not-found`;
  48. throw e;
  49. }
  50. }
  51. handlePacket(packet) {
  52. switch (true) {
  53. case packet.body.hasOwnProperty('requestSinks'):
  54. this._sendSinkList();
  55. break;
  56. case packet.body.hasOwnProperty('name'):
  57. this._changeSink(packet);
  58. break;
  59. }
  60. }
  61. connected() {
  62. super.connected();
  63. this._sendSinkList();
  64. }
  65. /**
  66. * Handle a request to change an output
  67. *
  68. * @param {Core.Packet} packet - a `kdeconnect.systemvolume.request`
  69. */
  70. _changeSink(packet) {
  71. let stream;
  72. for (const sink of this._mixer.get_sinks()) {
  73. if (sink.name === packet.body.name) {
  74. stream = sink;
  75. break;
  76. }
  77. }
  78. // No sink with the given name
  79. if (stream === undefined) {
  80. this._sendSinkList();
  81. return;
  82. }
  83. // Get a cache and store volume and mute states if changed
  84. const cache = this._cache.get(stream) || {};
  85. if (packet.body.hasOwnProperty('muted')) {
  86. cache.muted = packet.body.muted;
  87. this._cache.set(stream, cache);
  88. stream.change_is_muted(packet.body.muted);
  89. }
  90. if (packet.body.hasOwnProperty('volume')) {
  91. cache.volume = packet.body.volume;
  92. this._cache.set(stream, cache);
  93. stream.volume = packet.body.volume;
  94. stream.push_volume();
  95. }
  96. }
  97. /**
  98. * Update the cache for @stream
  99. *
  100. * @param {Gvc.MixerStream} stream - The stream to cache
  101. * @return {Object} The updated cache object
  102. */
  103. _updateCache(stream) {
  104. const state = {
  105. name: stream.name,
  106. description: stream.display_name,
  107. muted: stream.is_muted,
  108. volume: stream.volume,
  109. maxVolume: this._mixer.get_vol_max_norm(),
  110. };
  111. this._cache.set(stream, state);
  112. return state;
  113. }
  114. /**
  115. * Send the state of a local sink
  116. *
  117. * @param {Gvc.MixerControl} mixer - The mixer that owns the stream
  118. * @param {number} id - The Id of the stream that changed
  119. */
  120. _sendSink(mixer, id) {
  121. // Avoid starving the packet channel when fading
  122. if (this._mixer.fading)
  123. return;
  124. // Check the cache
  125. const stream = this._mixer.lookup_stream_id(id);
  126. const cache = this._cache.get(stream) || {};
  127. // If the port has changed we have to send the whole list to update the
  128. // display name
  129. if (!cache.display_name || cache.display_name !== stream.display_name) {
  130. this._sendSinkList();
  131. return;
  132. }
  133. // If only volume and/or mute are set, send a single update
  134. if (cache.volume !== stream.volume || cache.muted !== stream.is_muted) {
  135. // Update the cache
  136. const state = this._updateCache(stream);
  137. // Send the stream update
  138. this.device.sendPacket({
  139. type: 'kdeconnect.systemvolume',
  140. body: state,
  141. });
  142. }
  143. }
  144. /**
  145. * Send a list of local sinks
  146. */
  147. _sendSinkList() {
  148. const sinkList = this._mixer.get_sinks().map(sink => {
  149. return this._updateCache(sink);
  150. });
  151. // Send the sinkList
  152. this.device.sendPacket({
  153. type: 'kdeconnect.systemvolume',
  154. body: {
  155. sinkList: sinkList,
  156. },
  157. });
  158. }
  159. destroy() {
  160. if (this._mixer !== undefined) {
  161. this._mixer.disconnect(this._streamChangedId);
  162. this._mixer.disconnect(this._outputAddedId);
  163. this._mixer.disconnect(this._outputRemovedId);
  164. this._mixer = Components.release('pulseaudio');
  165. }
  166. super.destroy();
  167. }
  168. });