Skip to content
Snippets Groups Projects
module-default-metadata.c 10.1 KiB
Newer Older
/* WirePlumber
 *
 * Copyright © 2020 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

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

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

#define direction_to_dbg_string(dir) \
  ((dir == WP_DIRECTION_INPUT) ? "sink" : "source")

#define default_endpoint_key(dir) ((dir == WP_DIRECTION_INPUT) ? \
  "default.session.endpoint.sink" : "default.session.endpoint.source")

G_DECLARE_FINAL_TYPE (WpDefaultMetadata, wp_default_metadata, WP,
    DEFAULT_METADATA, WpPlugin)

struct _WpDefaultEndpoints
{
  WpDefaultMetadata *self;
  gchar *group;
  WpProperties *props;
};
typedef struct _WpDefaultEndpoints WpDefaultEndpoints;

struct _WpDefaultMetadata
{
  WpPlugin parent;
  WpState *state;
  WpDefaultEndpoints default_endpoints[2];
  WpObjectManager *metadatas_om;
  WpObjectManager *sessions_om;
  guint metadata_id;
  GSource *timeout_source;
};

G_DEFINE_TYPE (WpDefaultMetadata, wp_default_metadata, WP_TYPE_PLUGIN)

static gboolean
timeout_save_callback (gpointer p)
{
  WpDefaultEndpoints *d = p;
  WpDefaultMetadata *self = d->self;

  if (!wp_state_save (self->state, d->group, d->props))
    wp_warning_object (self, "could not save default endpoints in %s",
        STATE_NAME);

  return G_SOURCE_REMOVE;
}

static void
timeout_save_default_endpoints (WpDefaultMetadata *self, guint dir, guint ms)
{
  g_autoptr (WpCore) core = wp_plugin_get_core (WP_PLUGIN (self));
  g_return_if_fail (core);

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

  /* Add the timeout callback */
  wp_core_timeout_add (core, &self->timeout_source, ms, timeout_save_callback,
      self->default_endpoints + dir, NULL);
}

static void
on_default_metadata_changed (WpMetadata *m, guint32 subject,
    const gchar *key, const gchar *type, const gchar *value, gpointer *d)
{
  WpDefaultMetadata * self = WP_DEFAULT_METADATA (d);
  g_autoptr (WpSession) session = NULL;
  g_autoptr (WpEndpoint) ep = NULL;
  const gchar *session_name = NULL, *ep_name = NULL;
  guint dir = WP_DIRECTION_INPUT;

  /* Get the direction */
  if (!g_strcmp0 (key, default_endpoint_key (WP_DIRECTION_INPUT)))
    dir = WP_DIRECTION_INPUT;
  else if (!g_strcmp0 (key, default_endpoint_key (WP_DIRECTION_OUTPUT)))
    dir = WP_DIRECTION_OUTPUT;
  else
    return;

  /* Find the session */
  session = wp_object_manager_lookup (self->sessions_om, WP_TYPE_SESSION,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", subject, NULL);
  if (!session)
    return;

  /* Find the endpoint */
  ep = wp_session_lookup_endpoint (session, WP_CONSTRAINT_TYPE_G_PROPERTY,
      "bound-id", "=u", atoi (value), NULL);
  if (!ep)
    return;

  /* Get the session name and endpoint name */
  session_name = wp_session_get_name (session);
  g_return_if_fail (session_name);
  ep_name = wp_endpoint_get_name (ep);
  g_return_if_fail (ep_name);

  /* Set the property and save state */
  wp_properties_set (self->default_endpoints[dir].props, session_name, ep_name);
  timeout_save_default_endpoints (self, dir, SAVE_INTERVAL_MS);
}

static guint32
find_highest_prio (WpSession * session, WpDirection dir)
{
  g_autoptr (WpIterator) it = NULL;
  g_auto (GValue) val = G_VALUE_INIT;
  gint highest_prio = 0;
  guint32 id = 0;

  it = wp_session_iterate_endpoints_filtered (session,
      WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "#s",
      (dir == WP_DIRECTION_INPUT) ? "*/Sink" : "*/Source",
      NULL);

  for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
    WpProxy *ep = g_value_get_object (&val);
    const gchar *prio_str = wp_pipewire_object_get_property (
        WP_PIPEWIRE_OBJECT (ep), "endpoint.priority");
    gint prio = atoi (prio_str);

    if (prio > highest_prio || id == 0) {
      highest_prio = prio;
      id = wp_proxy_get_bound_id (ep);
    }
  }
  return id;
}

static void
reevaluate_default_endpoints (WpDefaultMetadata * self, WpMetadata *m,
    WpSession *session, guint dir)
{
  guint32 ep_id = 0;
  const gchar *session_name = NULL, *ep_name = NULL;

  g_return_if_fail (m);
  g_return_if_fail (self->default_endpoints[dir].props);

  /* Find the default endpoint */
  session_name = wp_session_get_name (session);
  ep_name = wp_properties_get (self->default_endpoints[dir].props, session_name);
  if (ep_name) {
    g_autoptr (WpEndpoint) ep = wp_session_lookup_endpoint (session,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "endpoint.name", "=s", ep_name,
        WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "#s",
        (dir == WP_DIRECTION_INPUT) ? "*/Sink" : "*/Source", NULL);
    if (ep)
      ep_id = wp_proxy_get_bound_id (WP_PROXY (ep));
  }

  /* If not found, use the highest priority one */
  if (ep_id == 0)
    ep_id = find_highest_prio (session, dir);

  if (ep_id != 0) {
    /* block the signal to avoid storing this; only selections done by the user
     * should be stored */
    g_autofree gchar *value = g_strdup_printf ("%d", ep_id);
    g_signal_handlers_block_by_func (m, on_default_metadata_changed, self);
    wp_metadata_set (m, wp_proxy_get_bound_id (WP_PROXY (session)),
        default_endpoint_key (dir), "Spa:Int", value);
    g_signal_handlers_unblock_by_func (m, on_default_metadata_changed, self);

    wp_info_object (self, "set default %s endpoint with id %d on session '%s'",
        direction_to_dbg_string (dir), ep_id, session_name);
  }

}

static void
on_endpoints_changed (WpSession * session, WpDefaultMetadata * self)
{
  g_autoptr (WpMetadata) metadata = NULL;

  /* Get the metadata */
  metadata = wp_object_manager_lookup (self->metadatas_om, WP_TYPE_METADATA,
      WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", self->metadata_id, NULL);
  if (!metadata)
    return;

  wp_trace_object (session, "endpoints changed, re-evaluating defaults");
  reevaluate_default_endpoints (self, metadata, session, WP_DIRECTION_INPUT);
  reevaluate_default_endpoints (self, metadata, session, WP_DIRECTION_OUTPUT);
}

static void
on_session_added (WpObjectManager * om, WpSession * session,
    WpDefaultMetadata * self)
{
  g_signal_connect_object (session, "endpoints-changed",
      G_CALLBACK (on_endpoints_changed), self, 0);
}

static void
on_metadata_added (WpObjectManager *om, WpMetadata *metadata, gpointer d)
{
  WpDefaultMetadata * self = WP_DEFAULT_METADATA (d);
  g_autoptr (WpCore) core = wp_plugin_get_core (WP_PLUGIN (self));

  g_return_if_fail (core);

  /* Only handle the first available metadata and skip the rest */
  if (self->metadata_id > 0)
    return;
  self->metadata_id = wp_proxy_get_bound_id (WP_PROXY (metadata));

  /* Handle the changed signal */
  g_signal_connect_object (metadata, "changed",
      G_CALLBACK (on_default_metadata_changed), self, 0);

  /* Create the sessions object manager */
  self->sessions_om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->sessions_om, WP_TYPE_SESSION, NULL);
  wp_object_manager_request_object_features (self->sessions_om, WP_TYPE_SESSION,
      WP_OBJECT_FEATURES_ALL);
  g_signal_connect_object (self->sessions_om, "object-added",
      G_CALLBACK (on_session_added), self, 0);
  wp_core_install_object_manager (core, self->sessions_om);
}

static void
wp_default_metadata_activate (WpPlugin * plugin)
{
  WpDefaultMetadata * self = WP_DEFAULT_METADATA (plugin);
  g_autoptr (WpCore) core = wp_plugin_get_core (plugin);

  g_return_if_fail (core);

  /* Create the metadatas object manager */
  self->metadata_id = 0;
  self->metadatas_om = wp_object_manager_new ();
  wp_object_manager_add_interest (self->metadatas_om, WP_TYPE_METADATA, NULL);
  wp_object_manager_request_object_features (self->metadatas_om,
      WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
  g_signal_connect_object (self->metadatas_om, "object-added",
      G_CALLBACK (on_metadata_added), self, 0);
  wp_core_install_object_manager (core, self->metadatas_om);
}

static void
wp_default_metadata_deactivate (WpPlugin * plugin)
{
  WpDefaultMetadata * self = WP_DEFAULT_METADATA (plugin);

  g_clear_object (&self->metadatas_om);
  g_clear_object (&self->sessions_om);
}

static void
unload_default_endpoints (WpDefaultMetadata * self, WpDefaultEndpoints * d)
{
  g_clear_pointer (&d->props, wp_properties_unref);
  g_clear_pointer (&d->group, g_free);
  d->self = NULL;
}

static void
load_default_endpoints (WpDefaultMetadata * self, WpDefaultEndpoints * d,
    const gchar *group)
{
  d->self = self;
  d->group = g_strdup (group);
  d->props = wp_state_load (self->state, d->group);
  if (!d->props)
    wp_warning_object ("could not load default endpoints from %s",
        STATE_NAME);
}

static void
wp_default_metadata_finalize (GObject * object)
{
  WpDefaultMetadata * self = WP_DEFAULT_METADATA (object);

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

  unload_default_endpoints (self, self->default_endpoints + WP_DIRECTION_INPUT);
  unload_default_endpoints (self, self->default_endpoints + WP_DIRECTION_OUTPUT);
  g_clear_object (&self->state);

  G_OBJECT_CLASS (wp_default_metadata_parent_class)->finalize (object);
}

static void
wp_default_metadata_init (WpDefaultMetadata * self)
{
   self->state = wp_state_new (STATE_NAME);
   load_default_endpoints (self, self->default_endpoints + WP_DIRECTION_INPUT,
       default_endpoint_key (WP_DIRECTION_INPUT));
   load_default_endpoints (self, self->default_endpoints + WP_DIRECTION_OUTPUT,
       default_endpoint_key (WP_DIRECTION_OUTPUT));
}

static void
wp_default_metadata_class_init (WpDefaultMetadataClass * klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPluginClass *plugin_class = (WpPluginClass *) klass;

  object_class->finalize = wp_default_metadata_finalize;

  plugin_class->activate = wp_default_metadata_activate;
  plugin_class->deactivate = wp_default_metadata_deactivate;
}

WP_PLUGIN_EXPORT void
wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
{
  wp_plugin_register (g_object_new (wp_default_metadata_get_type (),
          "name", "default-metadata",
          "module", module,
          NULL));
}