Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-systemvolume-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <json-glib/json-glib.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-systemvolume-device.h"
13 : :
14 : : #include "valent-systemvolume-plugin.h"
15 : :
16 : :
17 : : struct _ValentSystemvolumePlugin
18 : : {
19 : : ValentDevicePlugin parent_instance;
20 : :
21 : : ValentMixerAdapter *mixer;
22 : : unsigned int mixer_watch : 1;
23 : : GPtrArray *states;
24 : :
25 : : ValentMixerAdapter *adapter;
26 : : };
27 : :
28 : : static void valent_systemvolume_plugin_handle_request (ValentSystemvolumePlugin *self,
29 : : JsonNode *packet);
30 : : static void valent_systemvolume_plugin_handle_sink_change (ValentSystemvolumePlugin *self,
31 : : JsonNode *packet);
32 : : static void valent_systemvolume_plugin_send_sinklist (ValentSystemvolumePlugin *self);
33 : :
34 [ + + + - ]: 70 : G_DEFINE_FINAL_TYPE (ValentSystemvolumePlugin, valent_systemvolume_plugin, VALENT_TYPE_DEVICE_PLUGIN)
35 : :
36 : :
37 : : /*
38 : : * Local Mixer
39 : : */
40 : : typedef struct
41 : : {
42 : : ValentMixerStream *stream;
43 : : unsigned long notify_id;
44 : : char *name;
45 : : char *description;
46 : : unsigned int volume;
47 : : unsigned int muted : 1;
48 : : unsigned int enabled : 1;
49 : : } StreamState;
50 : :
51 : : static StreamState *
52 : 9 : stream_state_find (ValentSystemvolumePlugin *self,
53 : : const char *name)
54 : : {
55 [ + + ]: 11 : for (unsigned int i = 0; i < self->states->len; i++)
56 : : {
57 : 10 : StreamState *state = g_ptr_array_index (self->states, i);
58 : :
59 [ + + ]: 10 : if (g_strcmp0 (state->name, name) == 0)
60 : : return state;
61 : : }
62 : :
63 : : return NULL;
64 : : }
65 : :
66 : : static void
67 : 4 : on_stream_changed (ValentMixerStream *stream,
68 : : GParamSpec *pspec,
69 : : ValentSystemvolumePlugin *self)
70 : : {
71 : 4 : StreamState *state;
72 : 4 : const char *name;
73 : 4 : const char *description;
74 : 4 : gboolean enabled;
75 : 4 : gboolean muted;
76 : 4 : unsigned int volume;
77 : 4 : g_autoptr (JsonBuilder) builder = NULL;
78 [ - + - - ]: 4 : g_autoptr (JsonNode) packet = NULL;
79 : :
80 [ + - ]: 4 : g_assert (VALENT_IS_MIXER_STREAM (stream));
81 [ - + ]: 4 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
82 : :
83 : : /* If this is an unknown stream, send a new sink list */
84 : 4 : name = valent_mixer_stream_get_name (stream);
85 : :
86 [ - + ]: 4 : if ((state = stream_state_find (self, name)) == NULL)
87 : : {
88 : 0 : valent_systemvolume_plugin_send_sinklist (self);
89 : 0 : return;
90 : : }
91 : :
92 : : /* If the description changed it's probably because the port changed, so
93 : : * update the state and send the whole list */
94 : 4 : description = valent_mixer_stream_get_description (stream);
95 : :
96 [ - + ]: 4 : if (g_set_str (&state->description, description))
97 : : {
98 : 0 : valent_systemvolume_plugin_send_sinklist (self);
99 : 0 : return;
100 : : }
101 : :
102 : : /* If none of the other properties changed, there's nothing to update */
103 : 4 : enabled = valent_mixer_adapter_get_default_output (self->mixer) == stream;
104 : 4 : muted = valent_mixer_stream_get_muted (stream);
105 : 4 : volume = valent_mixer_stream_get_level (stream);
106 : :
107 [ + - ]: 4 : if (state->enabled == enabled &&
108 [ + + ]: 4 : state->muted == muted &&
109 [ + - ]: 2 : state->volume == volume)
110 : : return;
111 : :
112 : : /* Sink update */
113 : 4 : valent_packet_init (&builder, "kdeconnect.systemvolume");
114 : 4 : json_builder_set_member_name (builder, "name");
115 : 4 : json_builder_add_string_value (builder, state->name);
116 : :
117 [ + + ]: 4 : if (state->muted != muted)
118 : : {
119 : 2 : state->muted = muted;
120 : 2 : json_builder_set_member_name (builder, "muted");
121 : 2 : json_builder_add_boolean_value (builder, state->muted);
122 : : }
123 : :
124 [ + + ]: 4 : if (state->volume != volume)
125 : : {
126 : 2 : state->volume = volume;
127 : 2 : json_builder_set_member_name (builder, "volume");
128 : 2 : json_builder_add_int_value (builder, state->volume);
129 : : }
130 : :
131 [ - + ]: 4 : if (state->enabled != enabled)
132 : : {
133 : 0 : state->enabled = enabled;
134 : 0 : json_builder_set_member_name (builder, "enabled");
135 : 0 : json_builder_add_boolean_value (builder, state->enabled);
136 : : }
137 : :
138 : 4 : packet = valent_packet_end (&builder);
139 : :
140 [ + - ]: 4 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
141 : : }
142 : :
143 : : static StreamState *
144 : 8 : stream_state_new (ValentSystemvolumePlugin *self,
145 : : ValentMixerStream *stream)
146 : : {
147 : 8 : StreamState *state;
148 : :
149 [ + - ]: 8 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
150 [ - + ]: 8 : g_assert (VALENT_IS_MIXER_STREAM (stream));
151 : :
152 : 8 : state = g_new0 (StreamState, 1);
153 : 8 : state->stream = g_object_ref (stream);
154 : 8 : g_signal_connect_object (state->stream,
155 : : "notify",
156 : : G_CALLBACK (on_stream_changed),
157 : : self, 0);
158 : :
159 [ - + ]: 8 : state->name = g_strdup (valent_mixer_stream_get_name (stream));
160 [ - + ]: 8 : state->description = g_strdup (valent_mixer_stream_get_description (stream));
161 : 8 : state->volume = valent_mixer_stream_get_level (stream);
162 : 8 : state->muted = valent_mixer_stream_get_muted (stream);
163 : 8 : state->enabled = valent_mixer_adapter_get_default_output (self->mixer) == stream;
164 : :
165 : 8 : return state;
166 : : }
167 : :
168 : : static void
169 : 8 : stream_state_free (gpointer data)
170 : : {
171 : 8 : StreamState *state = data;
172 : :
173 [ - + ]: 8 : g_clear_signal_handler (&state->notify_id, state->stream);
174 [ + - ]: 8 : g_clear_object (&state->stream);
175 [ + - ]: 8 : g_clear_pointer (&state->name, g_free);
176 [ + - ]: 8 : g_clear_pointer (&state->description, g_free);
177 : 8 : g_clear_pointer (&state, g_free);
178 : 8 : }
179 : :
180 : : static void
181 : 0 : on_default_output_changed (ValentMixerAdapter *adapter,
182 : : GParamSpec *pspec,
183 : : ValentSystemvolumePlugin *self)
184 : : {
185 : 0 : ValentMixerStream *default_output = NULL;
186 : :
187 [ # # ]: 0 : g_assert (VALENT_IS_MIXER_ADAPTER (adapter));
188 [ # # ]: 0 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
189 : :
190 : 0 : default_output = valent_mixer_adapter_get_default_output (adapter);
191 : :
192 [ # # ]: 0 : for (unsigned int i = 0; i < self->states->len; i++)
193 : : {
194 : 0 : StreamState *state = g_ptr_array_index (self->states, i);
195 : :
196 : 0 : state->enabled = state->stream == default_output;
197 : : }
198 : :
199 : : /* It's unclear whether the `enabled` field with a value of `false` is
200 : : * relevant in the protocol, we resend the whole list */
201 : 0 : valent_systemvolume_plugin_send_sinklist (self);
202 : 0 : }
203 : :
204 : : static void
205 : 6 : on_items_changed (GListModel *list,
206 : : unsigned int position,
207 : : unsigned int removed,
208 : : unsigned int added,
209 : : ValentSystemvolumePlugin *self)
210 : : {
211 : 6 : unsigned int n_streams = 0;
212 : :
213 [ + - ]: 6 : g_assert (G_IS_LIST_MODEL (list));
214 [ - + ]: 6 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
215 : :
216 : 6 : g_ptr_array_remove_range (self->states, 0, self->states->len);
217 : 6 : n_streams = g_list_model_get_n_items (list);
218 : :
219 [ + + ]: 14 : for (unsigned int i = 0; i < n_streams; i++)
220 : : {
221 : 8 : g_autoptr (ValentMixerStream) stream = NULL;
222 : :
223 : 8 : stream = g_list_model_get_item (list, i);
224 : :
225 [ - + ]: 8 : if (valent_mixer_stream_get_direction (stream) != VALENT_MIXER_OUTPUT)
226 [ # # ]: 0 : continue;
227 : :
228 [ + - ]: 8 : g_ptr_array_add (self->states, stream_state_new (self, stream));
229 : : }
230 : :
231 : 6 : valent_systemvolume_plugin_send_sinklist (self);
232 : 6 : }
233 : :
234 : : static void
235 : 4 : on_primary_adapter_changed (ValentMixer *mixer,
236 : : GParamSpec *pspec,
237 : : ValentSystemvolumePlugin *self)
238 : : {
239 [ + + ]: 4 : if (self->mixer != NULL)
240 : : {
241 : 2 : g_signal_handlers_disconnect_by_data (self->mixer, self);
242 : 2 : g_ptr_array_remove_range (self->states, 0, self->states->len);
243 [ + - ]: 2 : g_clear_object (&self->mixer);
244 : : }
245 : :
246 : 4 : g_object_get (mixer, "primary-adapter", &self->mixer, NULL);
247 [ + - ]: 4 : if (self->mixer != NULL)
248 : : {
249 : 4 : g_signal_connect_object (self->mixer,
250 : : "notify::default-output",
251 : : G_CALLBACK (on_default_output_changed),
252 : : self,
253 : : G_CONNECT_DEFAULT);
254 : 4 : g_signal_connect_object (self->mixer,
255 : : "items-changed",
256 : : G_CALLBACK (on_items_changed),
257 : : self,
258 : : G_CONNECT_DEFAULT);
259 : 4 : on_items_changed (G_LIST_MODEL (self->mixer),
260 : : 0,
261 : : 0,
262 : 4 : g_list_model_get_n_items (G_LIST_MODEL (self->mixer)),
263 : : self);
264 : : }
265 : 4 : }
266 : :
267 : : static void
268 : 11 : valent_systemvolume_plugin_watch_mixer (ValentSystemvolumePlugin *self,
269 : : gboolean watch)
270 : : {
271 : 11 : ValentMixer *mixer = valent_mixer_get_default ();
272 : :
273 [ + - ]: 11 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
274 : :
275 [ + + ]: 11 : if (self->mixer_watch == watch)
276 : : return;
277 : :
278 [ + + ]: 4 : if (watch)
279 : : {
280 : 2 : g_signal_connect_object (mixer,
281 : : "notify::default-output",
282 : : G_CALLBACK (on_primary_adapter_changed),
283 : : self,
284 : : G_CONNECT_DEFAULT);
285 : 2 : on_primary_adapter_changed (mixer, NULL, self);
286 : :
287 [ + - ]: 2 : if (self->adapter == NULL)
288 : : {
289 : 2 : ValentDevice *device = NULL;
290 : :
291 : 2 : device = valent_resource_get_source (VALENT_RESOURCE (self));
292 : 2 : self->adapter = valent_systemvolume_device_new (device);
293 : 2 : valent_component_export_adapter (VALENT_COMPONENT (mixer),
294 : : VALENT_EXTENSION (self->adapter));
295 : : }
296 : : }
297 : : else
298 : : {
299 : 2 : g_signal_handlers_disconnect_by_func (mixer, self, on_primary_adapter_changed);
300 : :
301 [ + - ]: 2 : if (self->mixer != NULL)
302 : : {
303 : 2 : g_signal_handlers_disconnect_by_data (self->mixer, self);
304 : 2 : g_ptr_array_remove_range (self->states, 0, self->states->len);
305 [ + - ]: 2 : g_clear_object (&self->mixer);
306 : : }
307 : :
308 [ + - ]: 2 : if (self->adapter != NULL)
309 : : {
310 : 2 : valent_component_unexport_adapter (VALENT_COMPONENT (mixer),
311 : : VALENT_EXTENSION (self->adapter));
312 [ + - ]: 2 : g_clear_object (&self->adapter);
313 : : }
314 : : }
315 : :
316 : 4 : self->mixer_watch = watch;
317 : : }
318 : :
319 : : /*
320 : : * Packet Providers
321 : : */
322 : : static void
323 : 8 : valent_systemvolume_plugin_send_sinklist (ValentSystemvolumePlugin *self)
324 : : {
325 : 16 : g_autoptr (JsonBuilder) builder = NULL;
326 [ - + ]: 8 : g_autoptr (JsonNode) packet = NULL;
327 : 8 : unsigned int max_volume = 100;
328 : :
329 [ + - ]: 8 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
330 : :
331 : : /* Sink List */
332 : 8 : valent_packet_init (&builder, "kdeconnect.systemvolume");
333 : 8 : json_builder_set_member_name (builder, "sinkList");
334 : 8 : json_builder_begin_array (builder);
335 : :
336 [ + + ]: 18 : for (unsigned int i = 0; i < self->states->len; i++)
337 : : {
338 : 10 : StreamState *state;
339 : :
340 : 10 : state = g_ptr_array_index (self->states, i);
341 : :
342 : 10 : json_builder_begin_object (builder);
343 : 10 : json_builder_set_member_name (builder, "name");
344 : 10 : json_builder_add_string_value (builder, state->name);
345 : 10 : json_builder_set_member_name (builder, "description");
346 : 10 : json_builder_add_string_value (builder, state->description);
347 : 10 : json_builder_set_member_name (builder, "muted");
348 : 10 : json_builder_add_boolean_value (builder, state->muted);
349 : 10 : json_builder_set_member_name (builder, "volume");
350 : 10 : json_builder_add_int_value (builder, state->volume);
351 : 10 : json_builder_set_member_name (builder, "maxVolume");
352 : 10 : json_builder_add_int_value (builder, max_volume);
353 : 10 : json_builder_set_member_name (builder, "enabled");
354 : 10 : json_builder_add_boolean_value (builder, state->enabled);
355 : 10 : json_builder_end_object (builder);
356 : : }
357 : :
358 : 8 : json_builder_end_array (builder);
359 : 8 : packet = valent_packet_end (&builder);
360 : :
361 [ + - ]: 8 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
362 : 8 : }
363 : :
364 : : static void
365 : 5 : valent_systemvolume_plugin_handle_sink_change (ValentSystemvolumePlugin *self,
366 : : JsonNode *packet)
367 : : {
368 : 5 : StreamState *state;
369 : 5 : const char *name;
370 : 5 : int64_t volume;
371 : 5 : gboolean muted;
372 : 5 : gboolean enabled;
373 : :
374 [ + - ]: 5 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
375 [ - + ]: 5 : g_assert (VALENT_IS_PACKET (packet));
376 : :
377 [ - + ]: 5 : if (!valent_packet_get_string (packet, "name", &name))
378 : : {
379 : 0 : g_debug ("%s(): expected \"name\" field holding a string",
380 : : G_STRFUNC);
381 : 1 : return;
382 : : }
383 : :
384 [ + + ]: 5 : if ((state = stream_state_find (self, name)) == NULL)
385 : : {
386 : 1 : valent_systemvolume_plugin_send_sinklist (self);
387 : 1 : return;
388 : : }
389 : :
390 [ + + + - ]: 4 : if (valent_packet_get_int (packet, "volume", &volume) && volume >= 0)
391 : 1 : valent_mixer_stream_set_level (state->stream, volume);
392 : :
393 [ + + ]: 4 : if (valent_packet_get_boolean (packet, "muted", &muted))
394 : 1 : valent_mixer_stream_set_muted (state->stream, muted);
395 : :
396 [ + + + - ]: 4 : if (valent_packet_get_boolean (packet, "enabled", &enabled) && enabled)
397 : 2 : valent_mixer_adapter_set_default_output (self->mixer, state->stream);
398 : : }
399 : :
400 : : static void
401 : 6 : valent_systemvolume_plugin_handle_request (ValentSystemvolumePlugin *self,
402 : : JsonNode *packet)
403 : : {
404 [ + - ]: 6 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
405 : :
406 : : /* A request for a list of audio outputs */
407 [ + + ]: 6 : if (valent_packet_check_field (packet, "requestSinks"))
408 : 1 : valent_systemvolume_plugin_send_sinklist (self);
409 : :
410 : : /* A request to change an audio output */
411 [ + - ]: 5 : else if (valent_packet_check_field (packet, "name"))
412 : 5 : valent_systemvolume_plugin_handle_sink_change (self, packet);
413 : :
414 : : else
415 : 0 : g_warn_if_reached ();
416 : 6 : }
417 : :
418 : : /*
419 : : * ValentDevicePlugin
420 : : */
421 : : static void
422 : 7 : valent_systemvolume_plugin_update_state (ValentDevicePlugin *plugin,
423 : : ValentDeviceState state)
424 : : {
425 : 7 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (plugin);
426 : 7 : gboolean available;
427 : :
428 [ + - ]: 7 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
429 : :
430 : 7 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
431 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
432 : :
433 : : /* Watch stream changes */
434 [ + + ]: 7 : if (available)
435 : 2 : valent_systemvolume_plugin_watch_mixer (self, TRUE);
436 : : else
437 : 5 : valent_systemvolume_plugin_watch_mixer (self, FALSE);
438 : 7 : }
439 : :
440 : : static void
441 : 12 : valent_systemvolume_plugin_handle_packet (ValentDevicePlugin *plugin,
442 : : const char *type,
443 : : JsonNode *packet)
444 : : {
445 : 12 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (plugin);
446 : :
447 [ + - ]: 12 : g_assert (VALENT_IS_SYSTEMVOLUME_PLUGIN (self));
448 [ - + ]: 12 : g_assert (type != NULL);
449 [ - + ]: 12 : g_assert (VALENT_IS_PACKET (packet));
450 : :
451 [ + + ]: 12 : if (g_str_equal (type, "kdeconnect.systemvolume"))
452 : 6 : valent_systemvolume_device_handle_packet (VALENT_SYSTEMVOLUME_DEVICE (self->adapter),
453 : : packet);
454 [ + - ]: 6 : else if (g_str_equal (type, "kdeconnect.systemvolume.request"))
455 : 6 : valent_systemvolume_plugin_handle_request (self, packet);
456 : : else
457 : 0 : g_assert_not_reached ();
458 : 12 : }
459 : :
460 : : /*
461 : : * ValentObject
462 : : */
463 : : static void
464 : 4 : valent_systemvolume_plugin_destroy (ValentObject *object)
465 : : {
466 : 4 : ValentSystemvolumePlugin *self = VALENT_SYSTEMVOLUME_PLUGIN (object);
467 : :
468 : 4 : valent_systemvolume_plugin_watch_mixer (self, FALSE);
469 [ + + ]: 4 : g_clear_pointer (&self->states, g_ptr_array_unref);
470 [ - + ]: 4 : g_clear_object (&self->mixer);
471 : :
472 : 4 : VALENT_OBJECT_CLASS (valent_systemvolume_plugin_parent_class)->destroy (object);
473 : 4 : }
474 : :
475 : : /*
476 : : * GObject
477 : : */
478 : : static void
479 : 1 : valent_systemvolume_plugin_class_init (ValentSystemvolumePluginClass *klass)
480 : : {
481 : 1 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
482 : 1 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
483 : :
484 : 1 : vobject_class->destroy = valent_systemvolume_plugin_destroy;
485 : :
486 : 1 : plugin_class->handle_packet = valent_systemvolume_plugin_handle_packet;
487 : 1 : plugin_class->update_state = valent_systemvolume_plugin_update_state;
488 : : }
489 : :
490 : : static void
491 : 2 : valent_systemvolume_plugin_init (ValentSystemvolumePlugin *self)
492 : : {
493 : 2 : self->states = g_ptr_array_new_with_free_func (stream_state_free);
494 : 2 : }
495 : :
|