module-default-profile.c 9.45 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <wp/wp.h>
#include <pipewire/pipewire.h>

#define STATE_NAME "default-profile"
#define SAVE_INTERVAL_MS 1000

G_DEFINE_QUARK (wp-module-default-profile-profiles, profiles);

/* Signals */
enum
{
  SIGNAL_GET_PROFILE,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

G_DECLARE_DERIVABLE_TYPE (WpDefaultProfile, wp_default_profile, WP,
    DEFAULT_PROFILE, WpPlugin)

struct _WpDefaultProfileClass
{
  WpPluginClass parent_class;

33
  void (*get_profile) (WpDefaultProfile *self, WpPipewireObject *device,
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
      const char **curr_profile);
};

typedef struct _WpDefaultProfilePrivate WpDefaultProfilePrivate;
struct _WpDefaultProfilePrivate
{
  WpState *state;
  WpProperties *profiles;
  GSource *timeout_source;

  WpObjectManager *devices_om;
};

G_DEFINE_TYPE_WITH_PRIVATE (WpDefaultProfile, wp_default_profile,
    WP_TYPE_PLUGIN)

static gint
51
find_device_profile (WpPipewireObject *device, const gchar *lookup_name)
52
53
54
55
56
57
58
59
60
61
62
63
64
65
{
  WpIterator *profiles = NULL;
  g_auto (GValue) item = G_VALUE_INIT;

  profiles = g_object_get_qdata (G_OBJECT (device), profiles_quark ());
  g_return_val_if_fail (profiles, -1);

  wp_iterator_reset (profiles);
  for (; wp_iterator_next (profiles, &item); g_value_unset (&item)) {
    WpSpaPod *pod = g_value_get_boxed (&item);
    gint index = 0;
    const gchar *name = NULL;

    /* Parse */
George Kiagiadakis's avatar
George Kiagiadakis committed
66
    if (!wp_spa_pod_get_object (pod, NULL,
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
        "index", "i", &index,
        "name", "s", &name,
        NULL)) {
      continue;
    }

    if (g_strcmp0 (name, lookup_name) == 0)
      return index;
  }

  return -1;
}

static gboolean
timeout_save_callback (WpDefaultProfile *self)
{
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);

86
  if (!wp_state_save (priv->state, "group", priv->profiles))
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
    wp_warning_object (self, "could not save profiles");

  return G_SOURCE_REMOVE;
}

static void
timeout_save_profiles (WpDefaultProfile *self, guint ms)
{
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);
  g_autoptr (WpCore) core = wp_plugin_get_core (WP_PLUGIN (self));

  g_return_if_fail (core);
  g_return_if_fail (priv->profiles);

  /* Clear the current timeout callback */
  if (priv->timeout_source)
      g_source_destroy (priv->timeout_source);
  g_clear_pointer (&priv->timeout_source, g_source_unref);

  /* Add the timeout callback */
  wp_core_timeout_add_closure (core, &priv->timeout_source, ms,
      g_cclosure_new_object (G_CALLBACK (timeout_save_callback),
      G_OBJECT (self)));
}

static void
114
115
wp_default_profile_get_profile (WpDefaultProfile *self,
    WpPipewireObject *device, const gchar **curr_profile)
116
117
118
119
120
121
122
123
124
125
{
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);
  const gchar *dev_name = NULL;

  g_return_if_fail (device);
  g_return_if_fail (curr_profile);
  g_return_if_fail (priv->profiles);

  /* Get the device name */
126
  dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME);
127
128
129
130
131
132
133
  g_return_if_fail (dev_name);

  /* Get the profile */
  *curr_profile = wp_properties_get (priv->profiles, dev_name);
}

static void
134
update_profile (WpDefaultProfile *self, WpPipewireObject *device,
135
136
137
138
139
140
141
142
143
144
145
    const gchar *new_profile)
{
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);
  const gchar *dev_name, *curr_profile = NULL;
  gint index;

  g_return_if_fail (new_profile);
  g_return_if_fail (priv->profiles);

  /* Get the device name */
146
  dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME);
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
  g_return_if_fail (dev_name);

  /* Check if the new profile is the same as the current one */
  curr_profile = wp_properties_get (priv->profiles, dev_name);
  if (curr_profile && g_strcmp0 (curr_profile, new_profile) == 0)
    return;

  /* Make sure the profile is valid */
  index = find_device_profile (device, new_profile);
  if (index < 0) {
    wp_info_object (self, "profile '%s' (%d) is not valid on device '%s'",
        new_profile, index, dev_name);
    return;
  }

  /* Otherwise update the profile and add timeout save callback */
  wp_properties_set (priv->profiles, dev_name, new_profile);
  timeout_save_profiles (self, SAVE_INTERVAL_MS);

  wp_info_object (self, "updated profile '%s' (%d) on device '%s'", new_profile,
      index, dev_name);
}

static void
171
on_device_profile_notified (WpPipewireObject *device, GAsyncResult *res,
172
173
174
175
176
177
178
179
180
    WpDefaultProfile *self)
{
  g_autoptr (WpIterator) profiles = NULL;
  g_autoptr (GError) error = NULL;
  g_auto (GValue) item = G_VALUE_INIT;
  const gchar *name = NULL;
  gint index = 0;

  /* Finish */
181
  profiles = wp_pipewire_object_enum_params_finish (device, res, &error);
182
183
184
185
186
187
188
189
190
191
192
  if (error) {
    wp_warning_object (self, "failed to get current profile on device");
    return;
  }

  /* Ignore empty profile notifications */
  if (!wp_iterator_next (profiles, &item))
    return;

  /* Parse the profile */
  WpSpaPod *pod = g_value_get_boxed (&item);
George Kiagiadakis's avatar
George Kiagiadakis committed
193
  if (!wp_spa_pod_get_object (pod, NULL,
194
195
196
197
198
199
200
201
202
203
      "index", "i", &index,
      "name", "s", &name,
      NULL)) {
    wp_warning_object (self, "failed to parse current profile");
    return;
  }

  g_value_unset (&item);

  /* Update the profile */
204
  update_profile (self, device, name);
205
206
207
}

static void
208
on_device_param_info_notified (WpPipewireObject * device, GParamSpec * param,
209
210
211
    WpDefaultProfile *self)
{
  /* Check the profile every time the params have changed */
212
  wp_pipewire_object_enum_params (device, "Profile", NULL, NULL,
213
214
215
216
      (GAsyncReadyCallback) on_device_profile_notified, self);
}

static void
217
on_device_enum_profile_done (WpPipewireObject *device, GAsyncResult *res,
218
219
220
221
222
223
    WpDefaultProfile *self)
{
  g_autoptr (WpIterator) profiles = NULL;
  g_autoptr (GError) error = NULL;

  /* Finish */
224
  profiles = wp_pipewire_object_enum_params_finish (device, res, &error);
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
  if (error) {
    wp_warning_object (self, "failed to enum profiles in device "
        WP_OBJECT_FORMAT, WP_OBJECT_ARGS (device));
    return;
  }

  /* Keep a reference of the profiles in the device object */
  g_object_set_qdata_full (G_OBJECT (device), profiles_quark (),
        g_steal_pointer (&profiles), (GDestroyNotify) wp_iterator_unref);

  /* Watch for param info changes */
  g_signal_connect_object (device, "notify::param-info",
      G_CALLBACK (on_device_param_info_notified), self, 0);
}

static void
241
on_device_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
242
243
244
245
246
247
248
{
  WpDefaultProfile *self = WP_DEFAULT_PROFILE (d);

  wp_debug_object (self, "device " WP_OBJECT_FORMAT " added",
      WP_OBJECT_ARGS (proxy));

  /* Enum available profiles */
249
  wp_pipewire_object_enum_params (proxy, "EnumProfile", NULL, NULL,
250
251
252
253
254
255
256
257
258
259
260
261
262
263
      (GAsyncReadyCallback) on_device_enum_profile_done, self);
}

static void
wp_default_profile_activate (WpPlugin * plugin)
{
  g_autoptr (WpCore) core = wp_plugin_get_core (plugin);
  WpDefaultProfile *self = WP_DEFAULT_PROFILE (plugin);
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);

  /* Create the devices object manager */
  priv->devices_om = wp_object_manager_new ();
  wp_object_manager_add_interest (priv->devices_om, WP_TYPE_DEVICE, NULL);
264
265
  wp_object_manager_request_object_features (priv->devices_om,
      WP_TYPE_DEVICE, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
  g_signal_connect_object (priv->devices_om, "object-added",
      G_CALLBACK (on_device_added), self, 0);
  wp_core_install_object_manager (core, priv->devices_om);
}

static void
wp_default_profile_deactivate (WpPlugin * plugin)
{
  WpDefaultProfile *self = WP_DEFAULT_PROFILE (plugin);
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);

  g_clear_object (&priv->devices_om);
}

static void
wp_default_profile_finalize (GObject * object)
{
  WpDefaultProfile *self = WP_DEFAULT_PROFILE (object);
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);

  /* Clear the current timeout callback */
  if (priv->timeout_source)
      g_source_destroy (priv->timeout_source);
  g_clear_pointer (&priv->timeout_source, g_source_unref);

  g_clear_pointer (&priv->profiles, wp_properties_unref);
  g_clear_object (&priv->state);
}

static void
wp_default_profile_init (WpDefaultProfile * self)
{
  WpDefaultProfilePrivate *priv =
      wp_default_profile_get_instance_private (self);

  priv->state = wp_state_new (STATE_NAME);

  /* Load the saved profiles */
306
  priv->profiles = wp_state_load (priv->state, "group");
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
  if (!priv->profiles) {
    wp_warning_object (self, "could not load profiles");
    return;
  }
}

static void
wp_default_profile_class_init (WpDefaultProfileClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->finalize = wp_default_profile_finalize;
  plugin_class->activate = wp_default_profile_activate;
  plugin_class->deactivate = wp_default_profile_deactivate;

  klass->get_profile = wp_default_profile_get_profile;

  /* Signals */
  signals[SIGNAL_GET_PROFILE] = g_signal_new ("get-profile",
      G_TYPE_FROM_CLASS (klass),
      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
      G_STRUCT_OFFSET (WpDefaultProfileClass, get_profile), NULL, NULL,
      NULL, G_TYPE_NONE, 2, WP_TYPE_DEVICE, G_TYPE_POINTER);
}

WP_PLUGIN_EXPORT void
wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
{
  wp_plugin_register (g_object_new (wp_default_profile_get_type (),
      "name", STATE_NAME,
      "module", module,
      NULL));
}