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