systemvolume.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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 * as Core from '../core.js';
  8. import Plugin from '../plugin.js';
  9. export const 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. const SystemVolumePlugin = GObject.registerClass({
  23. GTypeName: 'GSConnectSystemVolumePlugin',
  24. }, class SystemVolumePlugin extends 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. /**
  62. * Handle a request to change an output
  63. *
  64. * @param {Core.Packet} packet - a `kdeconnect.systemvolume.request`
  65. */
  66. _changeSink(packet) {
  67. let stream;
  68. for (const sink of this._mixer.get_sinks()) {
  69. if (sink.name === packet.body.name) {
  70. stream = sink;
  71. break;
  72. }
  73. }
  74. // No sink with the given name
  75. if (stream === undefined) {
  76. this._sendSinkList();
  77. return;
  78. }
  79. // Get a cache and store volume and mute states if changed
  80. const cache = this._cache.get(stream) || {};
  81. if (packet.body.hasOwnProperty('muted')) {
  82. cache.muted = packet.body.muted;
  83. this._cache.set(stream, cache);
  84. stream.change_is_muted(packet.body.muted);
  85. }
  86. if (packet.body.hasOwnProperty('volume')) {
  87. cache.volume = packet.body.volume;
  88. this._cache.set(stream, cache);
  89. stream.volume = packet.body.volume;
  90. stream.push_volume();
  91. }
  92. }
  93. /**
  94. * Update the cache for @stream
  95. *
  96. * @param {"Gvc.MixerStream"} stream - The stream to cache
  97. * @returns {object} The updated cache object
  98. */
  99. _updateCache(stream) {
  100. const state = {
  101. name: stream.name,
  102. description: stream.display_name,
  103. muted: stream.is_muted,
  104. volume: stream.volume,
  105. maxVolume: this._mixer.get_vol_max_norm(),
  106. };
  107. this._cache.set(stream, state);
  108. return state;
  109. }
  110. /**
  111. * Send the state of a local sink
  112. *
  113. * @param {"Gvc.MixerControl"} mixer - The mixer that owns the stream
  114. * @param {number} id - The Id of the stream that changed
  115. */
  116. _sendSink(mixer, id) {
  117. // Avoid starving the packet channel when fading
  118. if (this._mixer.fading)
  119. return;
  120. // Check the cache
  121. const stream = this._mixer.lookup_stream_id(id);
  122. const cache = this._cache.get(stream) || {};
  123. // If the port has changed we have to send the whole list to update the
  124. // display name
  125. if (!cache.display_name || cache.display_name !== stream.display_name) {
  126. this._sendSinkList();
  127. return;
  128. }
  129. // If only volume and/or mute are set, send a single update
  130. if (cache.volume !== stream.volume || cache.muted !== stream.is_muted) {
  131. // Update the cache
  132. const state = this._updateCache(stream);
  133. // Send the stream update
  134. this.device.sendPacket({
  135. type: 'kdeconnect.systemvolume',
  136. body: state,
  137. });
  138. }
  139. }
  140. /**
  141. * Send a list of local sinks
  142. */
  143. _sendSinkList() {
  144. const sinkList = this._mixer.get_sinks().map(sink => {
  145. return this._updateCache(sink);
  146. });
  147. // Send the sinkList
  148. this.device.sendPacket({
  149. type: 'kdeconnect.systemvolume',
  150. body: {
  151. sinkList: sinkList,
  152. },
  153. });
  154. }
  155. destroy() {
  156. if (this._mixer !== undefined) {
  157. this._mixer.disconnect(this._streamChangedId);
  158. this._mixer.disconnect(this._outputAddedId);
  159. this._mixer.disconnect(this._outputRemovedId);
  160. this._mixer = Components.release('pulseaudio');
  161. }
  162. super.destroy();
  163. }
  164. });
  165. export default SystemVolumePlugin;