systemvolume.js 6.0 KB

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