Skip to content
Snippets Groups Projects
endpoint.c 19 KiB
Newer Older
/* WirePlumber
 *
 * Copyright © 2019 Collabora Ltd.
 *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

/**
 * SECTION: Endpoint
 *
 * An endpoint is an abstraction layer that represents a physical place where
 * audio can be routed to/from.
 *
 * Examples of endpoints on a desktop-like system:
 * * Laptop speakers
 * * Laptop webcam
 * * USB microphone
 * * Docking station stereo jack port
 * * USB 5.1 Digital audio output
 *
 * Examples of endpoints on a car:
 * * Driver seat speakers
 * * Front right seat microphone array
 * * Rear left seat headphones
 * * Bluetooth phone gateway
 * * All speakers
 *
 * In ALSA terms, an endpoint may be representing an ALSA subdevice 1-to-1
 * (therefore a single alsa-source/alsa-sink node in pipewire),
 * but it may as well be representing a part of this subdevice (for instance,
 * only the front stereo channels, or only the rear stereo), or it may represent
 * a combination of devices (for instance, playing to all speakers of a system
 * while they are plugged on different sound cards).
 *
 * An endpoint is not necessarily tied to a device that is present on this
 * system using ALSA or V4L. It may also represent a hardware device that
 * can be accessed in some hardware-specific path and is not accessible to
 * applications through pipewire. In this case, the endpoint can only used
 * for controlling the hardware, or - if the appropriate EndpointLink object
 * is also implemented - it can be used to route media from some other
 * hardware endpoint.
 *
 * ## Streams
 *
 * An endpoint can contain multiple streams, which represent different,
 * controllable paths that can be used to reach this endpoint.
 * Streams can be used to implement grouping of applications based on their
 * role or other things.
 *
 * Examples of streams on an audio output endpoint: "multimedia", "radio",
 * "phone". In this example, an audio player would be routed through the
 * "multimedia" stream, for instance, while a voip app would be routed through
 * "phone". This would allow lowering the volume of the audio player while the
 * call is in progress by using the standard volume control of the "multimedia"
 * stream.
 *
 * Examples of streams on an audio capture endpoint: "standard",
 * "voice recognition". In this example, the "standard" capture gives a
 * real-time capture from the microphone, while "voice recognition" gives a
 * slightly delayed and DSP-optimized for speech input, which can be used
 * as input in a voice recognition engine.
 *
 * A stream is described as a dictionary GVariant (a{sv}) with the following
 * standard keys available:
 * "id": the id of the stream
 * "name": the name of the stream
 *
 * ## Controls
 *
 * An endpoint can have multiple controls, which can control anything in the
 * path of media. Typically, audio streams have volume and mute controls, while
 * video streams have hue, brightness, contrast, etc... Controls can be linked
 * to a specific stream, but may as well be global and apply to all streams
 * of the endpoint. This can be used to implement a master volume, for instance.
 *
 * A control is described as a dictionary GVariant (a{sv}) with the following
 * standard keys available:
 * "id": the id of the control
 * "stream-id": the id of the stream that this control applies to
 * "name": the name of the control
 * "type": a GVariant type string
 * "range": a tuple (min, max)
 * "default-value": the default value
#include "endpoint.h"
#include "error.h"
#include "factory.h"

/* private api in session-manager */
void wp_session_manager_register_endpoint (WpSessionManager * self,
    WpEndpoint * ep);
void wp_session_manager_remove_endpoint (WpSessionManager * self,
    WpEndpoint * ep);

typedef struct _WpEndpointPrivate WpEndpointPrivate;
struct _WpEndpointPrivate
{
  gchar *name;
  gchar media_class[40];
  GPtrArray *streams;
  GPtrArray *controls;
  GPtrArray *links;
};

enum {
  PROP_0,
  PROP_NAME,
  PROP_MEDIA_CLASS,
};

enum {
  SIGNAL_NOTIFY_CONTROL_VALUE,
  NUM_SIGNALS
};

static guint32 signals[NUM_SIGNALS];

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpEndpoint, wp_endpoint, G_TYPE_OBJECT)

static void
wp_endpoint_init (WpEndpoint * self)
{
  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);

  priv->streams =
      g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
  priv->controls =
      g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
  priv->links = g_ptr_array_new_with_free_func (g_object_unref);
}

static void
wp_endpoint_dispose (GObject * object)
{
  WpEndpointPrivate *priv =
      wp_endpoint_get_instance_private (WP_ENDPOINT (object));
  gint i;

  /* wp_endpoint_link_destroy removes elements from the array,
   * so traversing in reverse order is faster and less complicated */
  for (i = priv->links->len - 1; i >= 0; i--) {
    wp_endpoint_link_destroy (g_ptr_array_index (priv->links, i));
  }

  G_OBJECT_CLASS (wp_endpoint_parent_class)->dispose (object);
}

static void
wp_endpoint_finalize (GObject * object)
{
  WpEndpointPrivate *priv =
      wp_endpoint_get_instance_private (WP_ENDPOINT (object));

  g_ptr_array_unref (priv->streams);
  g_ptr_array_unref (priv->controls);
  g_ptr_array_unref (priv->links);

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

static void
wp_endpoint_set_property (GObject * object, guint property_id,
    const GValue * value, GParamSpec * pspec)
{
  WpEndpointPrivate *priv =
      wp_endpoint_get_instance_private (WP_ENDPOINT (object));

  switch (property_id) {
  case PROP_NAME:
    priv->name = g_value_dup_string (value);
    break;
  case PROP_MEDIA_CLASS:
    strncpy (priv->media_class, g_value_get_string (value),
        sizeof (priv->media_class) - 1);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_endpoint_get_property (GObject * object, guint property_id, GValue * value,
    GParamSpec * pspec)
{
  WpEndpointPrivate *priv =
      wp_endpoint_get_instance_private (WP_ENDPOINT (object));

  switch (property_id) {
  case PROP_NAME:
    g_value_set_string (value, priv->name);
    break;
  case PROP_MEDIA_CLASS:
    g_value_set_string (value, priv->media_class);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    break;
  }
}

static void
wp_endpoint_class_init (WpEndpointClass * klass)
{
  GObjectClass * object_class = (GObjectClass *) klass;

  object_class->dispose = wp_endpoint_dispose;
  object_class->finalize = wp_endpoint_finalize;
  object_class->get_property = wp_endpoint_get_property;
  object_class->set_property = wp_endpoint_set_property;

  /**
   * WpEndpoint::name:
   * The name of the endpoint.
   */
  g_object_class_install_property (object_class, PROP_NAME,
      g_param_spec_string ("name", "name", "The name of the endpoint", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  /**
   * WpEndpoint::media-class:
   * The media class describes the type of media that this endpoint handles.
   * This should be the same as PipeWire media class strings.
   * For instance:
   * * Audio/Sink
   * * Audio/Source
   * * Video/Source
   * * Stream/Audio/Source
   */
  g_object_class_install_property (object_class, PROP_MEDIA_CLASS,
      g_param_spec_string ("media-class", "media-class",
          "The media class of the endpoint", NULL,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));

  signals[SIGNAL_NOTIFY_CONTROL_VALUE] = g_signal_new ("notify-control-value",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
      G_TYPE_NONE, 1, G_TYPE_UINT);
/**
 * wp_endpoint_register:
 * @self: the endpoint
 * @sm: the session manager
 *
 * Registers the endpoint on the @sm.
 */
void
wp_endpoint_register (WpEndpoint * self, WpSessionManager * sm)
{
  WpEndpointPrivate *priv;

  g_return_if_fail (WP_IS_ENDPOINT (self));
  g_return_if_fail (WP_IS_SESSION_MANAGER (sm));

  priv = wp_endpoint_get_instance_private (self);
  g_weak_ref_set (&priv->sm, sm);
  wp_session_manager_register_endpoint (sm, self);
}

/**
 * wp_endpoint_unregister:
 * @self: the endpoint
 *
 * Unregisters the endpoint from the session manager, if it was registered
 * and the session manager object still exists
 */
void
wp_endpoint_unregister (WpEndpoint * self)
{
  WpEndpointPrivate *priv;
  g_autoptr (WpSessionManager) sm = NULL;

  g_return_if_fail (WP_IS_ENDPOINT (self));

  priv = wp_endpoint_get_instance_private (self);
  sm = g_weak_ref_get (&priv->sm);
  if (sm) {
    g_weak_ref_clear (&priv->sm);
    wp_session_manager_remove_endpoint (sm, self);
  }
}

const gchar *
wp_endpoint_get_name (WpEndpoint * self)
{
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);

  priv = wp_endpoint_get_instance_private (self);
  return priv->name;
}

const gchar *
wp_endpoint_get_media_class (WpEndpoint * self)
{
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);

  priv = wp_endpoint_get_instance_private (self);
  return priv->media_class;
}

/**
 * wp_endpoint_register_stream:
 * @self: the endpoint
 * @stream: (transfer floating): a dictionary GVariant with the stream info
 */
void
wp_endpoint_register_stream (WpEndpoint * self, GVariant * stream)
  WpEndpointPrivate *priv;
  g_return_if_fail (WP_IS_ENDPOINT (self));
  g_return_if_fail (g_variant_is_of_type (stream, G_VARIANT_TYPE_VARDICT));

  priv = wp_endpoint_get_instance_private (self);
  g_ptr_array_add (priv->streams, g_variant_ref_sink (stream));
/**
 * wp_endpoint_list_streams:
 * @self: the endpoint
 *
 * Returns: (transfer floating): a floating GVariant that contains an array of
 *    dictionaries (aa{sv}) where each dictionary contains information about
 *    a single stream
 */
GVariant *
wp_endpoint_list_streams (WpEndpoint * self)
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);

  priv = wp_endpoint_get_instance_private (self);
  return g_variant_new_array (G_VARIANT_TYPE_VARDICT,
      (GVariant * const *) priv->streams->pdata, priv->streams->len);
/**
 * wp_endpoint_register_control:
 * @self: the endpoint
 * @control: (transfer floating): a dictionary GVariant with the control info
 */
void
wp_endpoint_register_control (WpEndpoint * self, GVariant * control)
  WpEndpointPrivate *priv;

  g_return_if_fail (WP_IS_ENDPOINT (self));
  g_return_if_fail (g_variant_is_of_type (control, G_VARIANT_TYPE_VARDICT));
  priv = wp_endpoint_get_instance_private (self);
  g_ptr_array_add (priv->controls, g_variant_ref_sink (control));
/**
 * wp_endpoint_list_controls:
 * @self: the endpoint
 *
 * Returns: (transfer floating): a floating GVariant that contains an array of
 *    dictionaries (aa{sv}) where each dictionary contains information about
 *    a single control
 */
GVariant *
wp_endpoint_list_controls (WpEndpoint * self)
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);

  priv = wp_endpoint_get_instance_private (self);
  return g_variant_new_array (G_VARIANT_TYPE_VARDICT,
      (GVariant * const *) priv->controls->pdata, priv->controls->len);
/**
 * wp_endpoint_get_control_value: (virtual get_control_value)
 * @self: the endpoint
 * @control_id: the id of the control to set
 *
 * Returns a GVariant that holds the value of the control. The type
 * should be the same type specified in the control variant's "type" field.
 *
 * On error, NULL will be returned.
 *
 * Returns: (transfer floating) (nullable): a dictionary GVariant containing
 *    the control value
 */
GVariant *
wp_endpoint_get_control_value (WpEndpoint * self, guint32 control_id)
  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
  if (WP_ENDPOINT_GET_CLASS (self)->get_control_value)
    return WP_ENDPOINT_GET_CLASS (self)->get_control_value (self, control_id);
  else
    return NULL;
/**
 * wp_endpoint_set_control_value: (virtual set_control_value)
 * @self: the endpoint
 * @control_id: the id of the control to set
 * @value: (transfer none): the value to set on the control
 *
 * Sets the @value on the specified control. The implementation should
 * call wp_endpoint_notify_control_value() if the value has been changed
 * in order to signal the change.
 *
 * Returns: TRUE on success, FALSE on failure
 */
gboolean
wp_endpoint_set_control_value (WpEndpoint * self, guint32 control_id,
    GVariant * value)
{
  g_return_val_if_fail (WP_IS_ENDPOINT (self), FALSE);

  if (WP_ENDPOINT_GET_CLASS (self)->set_control_value)
    return WP_ENDPOINT_GET_CLASS (self)->set_control_value (self, control_id,
        value);
  else
    return FALSE;
}

/**
 * wp_endpoint_notify_control_value:
 * @self: the endpoint
 * @control_id: the id of the control
 *
 * Emits the "notify-control-value" signal so that others can be informed
 * about a value change in some of the controls. This is meant to be used
 * by subclasses only.
 */
void
wp_endpoint_notify_control_value (WpEndpoint * self, guint32 control_id)
{
  g_return_if_fail (WP_IS_ENDPOINT (self));
  g_signal_emit (self, signals[SIGNAL_NOTIFY_CONTROL_VALUE], 0, control_id);
/**
 * wp_endpoint_is_linked:
 * @self: the endpoint
 *
 * Returns: TRUE if there is at least one link associated with this endpoint
 */
gboolean
wp_endpoint_is_linked (WpEndpoint * self)
{
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), FALSE);

  priv = wp_endpoint_get_instance_private (self);
  return (priv->links->len > 0);
}

/**
 * wp_endpoint_get_links:
 * @self: the endpoint
 *
 * Returns: (transfer none) (element-type WpEndpointLink): an array of
 *    #WpEndpointLink objects that are currently associated with this endpoint
 */
GPtrArray *
wp_endpoint_get_links (WpEndpoint * self)
{
  WpEndpointPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);

  priv = wp_endpoint_get_instance_private (self);
  return priv->links;
}


typedef struct _WpEndpointLinkPrivate WpEndpointLinkPrivate;
struct _WpEndpointLinkPrivate
{
  WpEndpoint *src;
  guint32 src_stream;
  WpEndpoint *sink;
  guint32 sink_stream;
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpEndpointLink, wp_endpoint_link, G_TYPE_OBJECT)

static void
wp_endpoint_link_init (WpEndpointLink * self)
{
}

static void
wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
{
}

void
wp_endpoint_link_set_endpoints (WpEndpointLink * self, WpEndpoint * src,
    guint32 src_stream, WpEndpoint * sink, guint32 sink_stream)
{
  WpEndpointLinkPrivate *priv;

  g_return_if_fail (WP_IS_ENDPOINT_LINK (self));
  g_return_if_fail (WP_IS_ENDPOINT (src));
  g_return_if_fail (WP_IS_ENDPOINT (sink));

  priv = wp_endpoint_link_get_instance_private (self);
  priv->src = src;
  priv->src_stream = src_stream;
  priv->sink = sink;
  priv->sink_stream = sink_stream;
}

WpEndpoint *
wp_endpoint_link_get_source_endpoint (WpEndpointLink * self)
{
  WpEndpointLinkPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), NULL);

  priv = wp_endpoint_link_get_instance_private (self);
  return priv->src;
}

guint32
wp_endpoint_link_get_source_stream (WpEndpointLink * self)
{
  WpEndpointLinkPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), 0);

  priv = wp_endpoint_link_get_instance_private (self);
  return priv->src_stream;
}

WpEndpoint *
wp_endpoint_link_get_sink_endpoint (WpEndpointLink * self)
{
  WpEndpointLinkPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), NULL);

  priv = wp_endpoint_link_get_instance_private (self);
  return priv->sink;
}

guint32
wp_endpoint_link_get_sink_stream (WpEndpointLink * self)
{
  WpEndpointLinkPrivate *priv;

  g_return_val_if_fail (WP_IS_ENDPOINT_LINK (self), 0);

  priv = wp_endpoint_link_get_instance_private (self);
  return priv->sink_stream;
}

WpEndpointLink * wp_endpoint_link_new (WpCore * core, WpEndpoint * src,
    guint32 src_stream, WpEndpoint * sink, guint32 sink_stream, GError ** error)
{
  g_autoptr (WpEndpointLink) link = NULL;
  g_autoptr (GVariant) src_props = NULL;
  g_autoptr (GVariant) sink_props = NULL;
  const gchar *src_factory = NULL, *sink_factory = NULL;
  WpEndpointPrivate *endpoint_priv;

  g_return_val_if_fail (WP_IS_ENDPOINT (src), NULL);
  g_return_val_if_fail (WP_IS_ENDPOINT (sink), NULL);
  g_return_val_if_fail (WP_ENDPOINT_GET_CLASS (src)->prepare_link, NULL);
  g_return_val_if_fail (WP_ENDPOINT_GET_CLASS (sink)->prepare_link, NULL);

  /* find the factory */

  if (WP_ENDPOINT_GET_CLASS (src)->get_endpoint_link_factory)
    src_factory = WP_ENDPOINT_GET_CLASS (src)->get_endpoint_link_factory (src);
  if (WP_ENDPOINT_GET_CLASS (sink)->get_endpoint_link_factory)
    sink_factory = WP_ENDPOINT_GET_CLASS (sink)->get_endpoint_link_factory (sink);

  if (src_factory || sink_factory) {
    if (src_factory && sink_factory && strcmp (src_factory, sink_factory) != 0) {
      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
          "It is not possible to link endpoints that both specify different "
          "custom link factories");
      return NULL;
    } else if (sink_factory)
      src_factory = sink_factory;
  } else {
    src_factory = "pipewire-simple-endpoint-link";
  }

  /* create link object */

  link = wp_factory_make (core, src_factory, WP_TYPE_ENDPOINT_LINK, NULL);
  if (!link) {
    g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
        "Failed to create link object from factory %s", src_factory);
    return NULL;
  }
  g_return_val_if_fail (WP_ENDPOINT_LINK_GET_CLASS (link)->create, NULL);

  /* prepare the link */

  wp_endpoint_link_set_endpoints (link, src, src_stream, sink, sink_stream);

  if (!WP_ENDPOINT_GET_CLASS (src)->prepare_link (src, src_stream, link,
          &src_props, error))
    return NULL;
  if (!WP_ENDPOINT_GET_CLASS (src)->prepare_link (sink, sink_stream, link,
          &sink_props, error))
    return NULL;

  /* create the link */

  if (!WP_ENDPOINT_LINK_GET_CLASS (link)->create (link, src_props, sink_props,
          error))
    return NULL;

  /* register the link on the endpoints */

  endpoint_priv = wp_endpoint_get_instance_private (src);
  g_ptr_array_add (endpoint_priv->links, g_object_ref (link));

  endpoint_priv = wp_endpoint_get_instance_private (sink);
  g_ptr_array_add (endpoint_priv->links, g_object_ref (link));

  return link;
}

void
wp_endpoint_link_destroy (WpEndpointLink * self)
{
  WpEndpointLinkPrivate *priv;
  WpEndpointPrivate *endpoint_priv;

  g_return_if_fail (WP_IS_ENDPOINT_LINK (self));
  g_return_if_fail (WP_ENDPOINT_LINK_GET_CLASS (self)->destroy);

  priv = wp_endpoint_link_get_instance_private (self);

  WP_ENDPOINT_LINK_GET_CLASS (self)->destroy (self);
  if (WP_ENDPOINT_GET_CLASS (priv->src)->release_link)
    WP_ENDPOINT_GET_CLASS (priv->src)->release_link (priv->src, self);
  if (WP_ENDPOINT_GET_CLASS (priv->sink)->release_link)
    WP_ENDPOINT_GET_CLASS (priv->sink)->release_link (priv->sink, self);

  endpoint_priv = wp_endpoint_get_instance_private (priv->src);
  g_ptr_array_remove_fast (endpoint_priv->links, self);

  endpoint_priv = wp_endpoint_get_instance_private (priv->sink);
  g_ptr_array_remove_fast (endpoint_priv->links, self);
}