Skip to content
Snippets Groups Projects
config-policy.c 16.1 KiB
Newer Older
/* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author Julian Bouzas <julian.bouzas@collabora.com>
 *
 * SPDX-License-Identifier: MIT
 */

#include <spa/utils/keys.h>

#include <pipewire/pipewire.h>

#include <wp/wp.h>

#include "config-policy.h"
#include "parser-endpoint-link.h"
#include "parser-streams.h"

struct _WpConfigPolicy
{
  WpPolicy parent;

  WpConfiguration *config;

  gboolean pending_rescan;
  WpEndpoint *pending_endpoint;
  WpEndpoint *pending_target;
};

enum {
  PROP_0,
  PROP_CONFIG,
};

enum {
  SIGNAL_DONE,
  N_SIGNALS
};

static guint signals[N_SIGNALS];

G_DEFINE_TYPE (WpConfigPolicy, wp_config_policy, WP_TYPE_POLICY)

static void
on_endpoint_link_created (GObject *initable, GAsyncResult *res, gpointer p)
{
  WpConfigPolicy *self = p;
  g_autoptr (WpEndpointLink) link = NULL;
  g_autoptr (GError) error = NULL;
  g_autoptr (WpEndpoint) src_ep = NULL;
  g_autoptr (WpEndpoint) sink_ep = NULL;

  /* Get the link */
  link = wp_endpoint_link_new_finish(initable, res, &error);

  /* Log linking info */
  if (error) {
    g_warning ("Could not link endpoints: %s\n", error->message);
    return;
  }

  g_return_if_fail (link);
  src_ep = wp_endpoint_link_get_source_endpoint (link);
  sink_ep = wp_endpoint_link_get_sink_endpoint (link);
  g_info ("Sucessfully linked '%s' to '%s'\n", wp_endpoint_get_name (src_ep),
      wp_endpoint_get_name (sink_ep));

  /* Clear the pending target */
  g_clear_object (&self->pending_target);

  /* Emit the done signal */
  if (self->pending_endpoint) {
    gboolean is_capture =
      wp_endpoint_get_direction (self->pending_endpoint) == PW_DIRECTION_INPUT;
    if (self->pending_endpoint == (is_capture ? sink_ep : src_ep)) {
      g_signal_emit (self, signals[SIGNAL_DONE], 0, self->pending_endpoint, link);
      g_clear_object (&self->pending_endpoint);
    }
  }
}

static gboolean
wp_config_policy_can_link_stream (WpConfigPolicy *self, WpEndpoint *target,
    const struct WpParserEndpointLinkData *data, guint32 stream_id)
{
  g_autoptr (WpConfigParser) parser = NULL;
  const struct WpParserStreamsData *streams_data = NULL;

  /* If no streams data is specified, we can link */
  if (!data->te.streams)
    return TRUE;

  /* If the endpoint is not linked, we can link */
    return TRUE;

  /* Get the linked stream */
  gboolean is_capture = wp_endpoint_get_direction (target) == PW_DIRECTION_INPUT;
  GPtrArray *links = wp_endpoint_get_links (target);
  WpEndpointLink *l = g_ptr_array_index (links, 0);
  guint32 linked_stream = is_capture ?
      wp_endpoint_link_get_sink_stream (l) :
          wp_endpoint_link_get_source_stream (l);

  /* Check if linked stream is the same as target stream. Last one wins */
  if (linked_stream == stream_id)
    return TRUE;

  /* Get the linked stream name */
  g_autoptr (GVariant) s = wp_endpoint_get_stream (target, linked_stream);
  if (!s)
    return TRUE;
  const gchar *linked_stream_name;
  if (!g_variant_lookup (s, "name", "&s", &linked_stream_name))
    return TRUE;

  /* Get the target stream name */
  g_autoptr (GVariant) ts = wp_endpoint_get_stream (target, stream_id);
  if (!ts)
    return TRUE;
  const gchar *target_stream_name;
  if (!g_variant_lookup (ts, "name", "&s", &target_stream_name))
    return TRUE;

  /* Get the linked and ep streams data */
  parser = wp_configuration_get_parser (self->config,
      WP_PARSER_STREAMS_EXTENSION);
  streams_data = wp_config_parser_get_matched_data (parser, data->te.streams);
  if (!data)
    return TRUE;
  const struct WpParserStreamsStreamData *linked_stream_data =
      wp_parser_streams_find_stream (streams_data, linked_stream_name);
  const struct WpParserStreamsStreamData *ep_stream_data =
      wp_parser_streams_find_stream (streams_data, target_stream_name);

  g_debug ("Trying to link to '%s' (%d); target is linked on '%s' (%d)",
      target_stream_name, ep_stream_data ? ep_stream_data->priority : -1,
      linked_stream_name, linked_stream_data ? linked_stream_data->priority : -1);

  /* Return false if linked stream has higher priority than ep stream */
  if (linked_stream_data && ep_stream_data) {
    if (linked_stream_data->priority > ep_stream_data->priority)
      return FALSE;
    else
      return TRUE;
  }
  if (linked_stream_data && !ep_stream_data)
    return FALSE;
  if (!linked_stream_data && ep_stream_data)
    return TRUE;

  return TRUE;
}

static gboolean
wp_config_policy_link_endpoint_with_target (WpConfigPolicy *policy,
    WpEndpoint *ep, guint32 ep_stream, WpEndpoint *target,
    guint32 target_stream, const struct WpParserEndpointLinkData *data)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
  g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
  gboolean is_capture = wp_endpoint_get_direction (ep) == PW_DIRECTION_INPUT;
  gboolean is_linked = wp_endpoint_is_linked (ep);
  gboolean target_linked = wp_endpoint_is_linked (target);

  g_debug ("Trying to link with '%s' to target '%s', ep_capture:%d, "
      "ep_linked:%d, target_linked:%d", wp_endpoint_get_name (ep),
      wp_endpoint_get_name (target), is_capture, is_linked, target_linked);

  /* Check if the endpoint is already linked with the proper target */
    GPtrArray *links = wp_endpoint_get_links (ep);
    WpEndpointLink *l = g_ptr_array_index (links, 0);
    g_autoptr (WpEndpoint) src_ep = wp_endpoint_link_get_source_endpoint (l);
    g_autoptr (WpEndpoint) sink_ep = wp_endpoint_link_get_sink_endpoint (l);
    WpEndpoint *existing_target = is_capture ? src_ep : sink_ep;

    if (existing_target == target) {
      /* linked to correct target so do nothing */
      g_debug ("Endpoint '%s' is already linked correctly",
          wp_endpoint_get_name (ep));
      return FALSE;
    } else {
      /* linked to the wrong target so unlink and continue */
      g_debug ("Unlinking endpoint '%s' from its previous target",
          wp_endpoint_get_name (ep));
      wp_endpoint_link_destroy (l);
    }
  }

  /* Make sure the target is not going to be linked with another endpoint */
  if (self->pending_target == target)
    return FALSE;
  g_clear_object (&self->pending_target);
  self->pending_target = g_object_ref (target);

  /* Unlink the target links that are not kept if endpoint is capture */
  if (!is_capture && target_linked) {
    GPtrArray *links = wp_endpoint_get_links (target);
    for (guint i = 0; i < links->len; i++) {
      WpEndpointLink *l = g_ptr_array_index (links, i);
      if (!wp_endpoint_link_is_kept (l))
        wp_endpoint_link_destroy (l);
    }
  }

  /* Link the client with the target */
  if (is_capture) {
    wp_endpoint_link_new (core, target, target_stream, ep, ep_stream,
        data->el.keep, on_endpoint_link_created, self);
  } else {
    wp_endpoint_link_new (core, ep, ep_stream, target, target_stream,
        data->el.keep, on_endpoint_link_created, self);
  }

  return TRUE;
}

static gboolean
wp_config_policy_handle_endpoint (WpPolicy *policy, WpEndpoint *ep)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
  g_autoptr (WpCore) core = wp_policy_get_core (policy);
  g_autoptr (WpConfigParser) parser = NULL;
  const struct WpParserEndpointLinkData *data;
  GVariantBuilder b;
  GVariant *target_data = NULL;
  g_autoptr (WpEndpoint) target = NULL;
  guint32 stream_id;
  const char *role = NULL;

  /* Get the parser for the endpoint-link extension */
  parser = wp_configuration_get_parser (self->config,
      WP_PARSER_ENDPOINT_LINK_EXTENSION);

  /* Get the matched endpoint data from the parser */
  data = wp_config_parser_get_matched_data (parser, G_OBJECT (ep));
  if (!data)
    return FALSE;

  /* Create the target gvariant */
  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&b, "{sv}",
      "data", g_variant_new_uint64 ((guint64) data));
  role = wp_endpoint_get_role (ep);
  if (role)
    g_variant_builder_add (&b, "{sv}", "role", g_variant_new_string (role));
  target_data = g_variant_builder_end (&b);

  /* Find the target endpoint */
  target = wp_policy_find_endpoint (core, target_data, &stream_id);
  if (!target) {
    g_info ("Target not found for endpoint '%s'", wp_endpoint_get_name (ep));
    return FALSE;
  }

  /* Don't link if the target is linked with a higher priority stream */
  can_link = wp_config_policy_can_link_stream (self, target, data, stream_id);
  g_debug ("Trying to handle endpoint: %s, role:%s, can_link:%d",
      wp_endpoint_get_name (ep), role, can_link);

  /* Link the endpoint with its target */
  return can_link && wp_config_policy_link_endpoint_with_target (self, ep,
          WP_STREAM_ID_NONE, target, stream_id, data);
static const char *
wp_config_policy_get_prioritized_stream (WpPolicy *policy,
    const char *ep_stream, const char *te_stream, const char *te_streams)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (policy);

  /* The target stream has higher priority than the endpoint stream */
  const char *res = te_stream ? te_stream : ep_stream;
  if (res)
    return res;

  /* If both streams are null, and no streams file is defined, return NULL */
  if (!te_streams)
    return NULL;

  /* Otherwise get the lowest stream from streams */
  g_autoptr (WpConfigParser) parser = NULL;
  const struct WpParserStreamsData *streams = NULL;
  const struct WpParserStreamsStreamData *lowest = NULL;
  parser = wp_configuration_get_parser (self->config,
      WP_PARSER_STREAMS_EXTENSION);
  streams = wp_config_parser_get_matched_data (parser, (gpointer)te_streams);
  if (!streams)
    return NULL;
  lowest = wp_parser_streams_get_lowest_stream (streams);
  return lowest ? lowest->name : NULL;
}

static WpEndpoint *
wp_config_policy_find_endpoint (WpPolicy *policy, GVariant *props,
    guint32 *stream_id)
{
  g_autoptr (WpCore) core = NULL;
  g_autoptr (WpPolicyManager) pmgr = NULL;
  const struct WpParserEndpointLinkData *data = NULL;
  g_autoptr (GPtrArray) endpoints = NULL;
  guint i;
  WpEndpoint *target = NULL;
  g_autoptr (WpProxy) proxy = NULL;
  g_autoptr (WpProperties) p = NULL;
  const char *role = NULL;

  /* Get the data from props */
  g_variant_lookup (props, "data", "t", &data);
  if (!data)
    return NULL;

  /* Get all the endpoints matching the media class */
  core = wp_policy_get_core (policy);
  pmgr = wp_policy_manager_get_instance (core);
  endpoints = wp_policy_manager_list_endpoints (pmgr,
      data->te.endpoint_data.media_class);
  if (!endpoints)
    return NULL;

  /* Get the first endpoint that matches target data */
  for (i = 0; i < endpoints->len; i++) {
    target = g_ptr_array_index (endpoints, i);
    if (wp_parser_endpoint_link_matches_endpoint_data (target,
        &data->te.endpoint_data))
      break;
  }

  /* If target did not match any data, return NULL */
  if (i >= endpoints->len)
    return NULL;

  /* Set the stream id */
  if (stream_id) {
    if (target) {
      g_variant_lookup (props, "role", "&s", &role);
      const char *prioritized = wp_config_policy_get_prioritized_stream (policy,
          role, data->te.stream, data->te.streams);
      *stream_id = prioritized ? wp_endpoint_find_stream (target, prioritized) :
          WP_STREAM_ID_NONE;
    } else {
      *stream_id = WP_STREAM_ID_NONE;
    }
  }

  return g_object_ref (target);
}

static void
wp_config_policy_sync_rescan (WpCore *core, GAsyncResult *res, gpointer data)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (data);
  g_autoptr (WpPolicyManager) pmgr = wp_policy_manager_get_instance (core);
  g_autoptr (GPtrArray) endpoints = NULL;
  WpEndpoint *ep;
  gboolean handled = FALSE;

  /* Handle all endpoints when rescanning */
  endpoints = wp_policy_manager_list_endpoints (pmgr, NULL);
  if (endpoints) {
    for (guint i = 0; i < endpoints->len; i++) {
      ep = g_ptr_array_index (endpoints, i);
      if (wp_config_policy_handle_endpoint (WP_POLICY (self), ep))
        handled = ep == self->pending_endpoint;
    }
  }

  /* If endpoint was not handled, we are done */
  if (!handled) {
      g_signal_emit (self, signals[SIGNAL_DONE], 0, self->pending_endpoint,
          NULL);
      g_clear_object (&self->pending_endpoint);
  }

  self->pending_rescan = FALSE;
}

static void
wp_config_policy_rescan (WpConfigPolicy *self, WpEndpoint *ep)
{
  if (self->pending_rescan)
    return;

  /* Check if there is a pending link while a new endpoint is added/removed */
  if (self->pending_endpoint) {
    g_warning ("Not handling endpoint '%s' beacause of pending link",
        wp_endpoint_get_name (ep));
    return;
  }

  g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
  if (!core)
    return;

  self->pending_endpoint = g_object_ref (ep);
  wp_core_sync (core, NULL, (GAsyncReadyCallback)wp_config_policy_sync_rescan,
      self);
  self->pending_rescan = TRUE;
}

static void
wp_config_policy_endpoint_added (WpPolicy *policy, WpEndpoint *ep)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
  wp_config_policy_rescan (self, ep);
}

static void
wp_config_policy_endpoint_removed (WpPolicy *policy, WpEndpoint *ep)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
  wp_config_policy_rescan (self, ep);
}

static void
wp_config_policy_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (object);

  switch (property_id) {
  case PROP_CONFIG:
    self->config = g_value_dup_object (value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_config_policy_get_property (GObject * object, guint property_id,
    GValue * value, GParamSpec * pspec)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (object);

  switch (property_id) {
  case PROP_CONFIG:
    g_value_take_object (value, g_object_ref (self->config));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_config_policy_constructed (GObject * object)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (object);

  /* Add the parsers */
  wp_configuration_add_extension (self->config,
      WP_PARSER_ENDPOINT_LINK_EXTENSION, WP_TYPE_PARSER_ENDPOINT_LINK);
  wp_configuration_add_extension (self->config,
      WP_PARSER_STREAMS_EXTENSION, WP_TYPE_PARSER_STREAMS);

  /* Parse the file */
  wp_configuration_reload (self->config, WP_PARSER_ENDPOINT_LINK_EXTENSION);
  wp_configuration_reload (self->config, WP_PARSER_STREAMS_EXTENSION);

  G_OBJECT_CLASS (wp_config_policy_parent_class)->constructed (object);
}

static void
wp_config_policy_finalize (GObject *object)
{
  WpConfigPolicy *self = WP_CONFIG_POLICY (object);

  /* Remove the extensions from the configuration */
  wp_configuration_remove_extension (self->config,
      WP_PARSER_ENDPOINT_LINK_EXTENSION);
  wp_configuration_remove_extension (self->config,
      WP_PARSER_STREAMS_EXTENSION);

  /* Clear the configuration */
  g_clear_object (&self->config);

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

static void
wp_config_policy_init (WpConfigPolicy *self)
{
  self->pending_rescan = FALSE;
}

static void
wp_config_policy_class_init (WpConfigPolicyClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;
  WpPolicyClass *policy_class = (WpPolicyClass *) klass;

  object_class->constructed = wp_config_policy_constructed;
  object_class->finalize = wp_config_policy_finalize;
  object_class->set_property = wp_config_policy_set_property;
  object_class->get_property = wp_config_policy_get_property;

  policy_class->endpoint_added = wp_config_policy_endpoint_added;
  policy_class->endpoint_removed = wp_config_policy_endpoint_removed;
  policy_class->find_endpoint = wp_config_policy_find_endpoint;

  /* Properties */
  g_object_class_install_property (object_class, PROP_CONFIG,
      g_param_spec_object ("configuration", "configuration",
      "The configuration this policy is based on", WP_TYPE_CONFIGURATION,
      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  /* Signals */
  signals[SIGNAL_DONE] = g_signal_new ("done",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 2, WP_TYPE_ENDPOINT, WP_TYPE_ENDPOINT_LINK);
}

WpConfigPolicy *
wp_config_policy_new (WpConfiguration *config)
{
  return g_object_new (wp_config_policy_get_type (),
      "rank", WP_POLICY_RANK_UPSTREAM,
      "configuration", config,
      NULL);
}