systemvolume.js 5.7 KB

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