wireplumber/modules/module-config-policy/config-policy.c
2020-03-17 12:06:21 -04:00

628 lines
18 KiB
C

/* 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"
#if !defined(HAVE_AUDIOFADE)
# define SPA_CONTROL_AUDIO_FADE_TYPE_LOGARITHMIC 2
#else
# include <spa/param/control/audio.h>
#endif
struct link_info {
WpBaseEndpoint *ep;
guint32 stream_id;
gboolean keep;
};
static void
link_info_destroy (gpointer p)
{
struct link_info *li = p;
g_return_if_fail (li);
g_clear_object (&li->ep);
g_slice_free (struct link_info, li);
}
struct _WpConfigPolicy
{
WpPolicy parent;
WpConfiguration *config;
gboolean pending_rescan;
gboolean pending_links;
gboolean pending_done;
};
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
wp_config_policy_rescan_done (WpConfigPolicy *self)
{
/* Reset pending flags */
self->pending_rescan = FALSE;
self->pending_links = FALSE;
self->pending_done = FALSE;
/* Emit the done signal */
g_signal_emit (self, signals[SIGNAL_DONE], 0);
}
static void
wp_config_policy_sync_done (WpCore *core, GAsyncResult *res, gpointer data)
{
WpConfigPolicy *self = WP_CONFIG_POLICY (data);
wp_config_policy_rescan_done (self);
}
static void
on_endpoint_link_created (GObject *initable, GAsyncResult *res, gpointer data)
{
WpConfigPolicy *self = WP_CONFIG_POLICY (data);
g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
g_autoptr (WpBaseEndpointLink) link = NULL;
g_autoptr (GError) error = NULL;
g_autoptr (WpBaseEndpoint) src_ep = NULL;
g_autoptr (WpBaseEndpoint) sink_ep = NULL;
/* Get the link */
link = wp_base_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_base_endpoint_link_get_source_endpoint (link);
sink_ep = wp_base_endpoint_link_get_sink_endpoint (link);
g_info ("Sucessfully linked '%s' to '%s'\n", wp_base_endpoint_get_name (src_ep),
wp_base_endpoint_get_name (sink_ep));
/* Finish rescanning if no pending done */
g_return_if_fail (core);
if (!self->pending_done) {
self->pending_done = TRUE;
wp_core_sync (core, NULL, (GAsyncReadyCallback)wp_config_policy_sync_done,
self);
}
}
static void
on_faded_destroy_link (GObject *stream, GAsyncResult *res, gpointer data)
{
WpBaseEndpointLink *l = WP_BASE_ENDPOINT_LINK (data);
g_return_if_fail (l);
wp_base_endpoint_link_destroy (l);
}
static void
on_faded_done (GObject *stream, GAsyncResult *res, gpointer data)
{
g_info ("done fading\n");
}
static void
fade_out (WpBaseEndpoint *ep, guint stream_id, GAsyncReadyCallback callback,
gpointer data)
{
wp_base_endpoint_begin_fade (ep, stream_id, 200, 0.003, PW_DIRECTION_INPUT,
SPA_CONTROL_AUDIO_FADE_TYPE_LOGARITHMIC, NULL, callback, data);
}
static void
fade_in (WpBaseEndpoint *ep, guint stream_id, GAsyncReadyCallback callback,
gpointer data)
{
wp_base_endpoint_begin_fade (ep, stream_id, 200, 0.003, PW_DIRECTION_OUTPUT,
SPA_CONTROL_AUDIO_FADE_TYPE_LOGARITHMIC, NULL, callback, data);
}
static gboolean
wp_config_policy_handle_pending_link (WpConfigPolicy *self,
struct link_info *li, WpBaseEndpoint *target)
{
g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
gboolean is_capture = wp_base_endpoint_get_direction (li->ep) == PW_DIRECTION_INPUT;
gboolean is_linked = wp_base_endpoint_is_linked (li->ep);
gboolean target_linked = wp_base_endpoint_is_linked (target);
g_debug ("Trying to link with '%s' to target '%s', ep_capture:%d, "
"ep_linked:%d, target_linked:%d", wp_base_endpoint_get_name (li->ep),
wp_base_endpoint_get_name (target), is_capture, is_linked, target_linked);
/* Check if the endpoint is already linked with the proper target */
if (is_linked) {
GPtrArray *links = wp_base_endpoint_get_links (li->ep);
WpBaseEndpointLink *l = g_ptr_array_index (links, 0);
g_autoptr (WpBaseEndpoint) src_ep = wp_base_endpoint_link_get_source_endpoint (l);
g_autoptr (WpBaseEndpoint) sink_ep = wp_base_endpoint_link_get_sink_endpoint (l);
WpBaseEndpoint *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_base_endpoint_get_name (li->ep));
return FALSE;
} else {
/* linked to the wrong target so unlink and continue */
g_debug ("Unlinking endpoint '%s' from its previous target",
wp_base_endpoint_get_name (li->ep));
g_autoptr (WpBaseEndpoint) ep = wp_base_endpoint_link_get_sink_endpoint (l);
guint32 stream_id = wp_base_endpoint_link_get_sink_stream (l);
fade_out (ep, stream_id, on_faded_destroy_link, l);
}
}
/* Unlink the target links that are not kept if endpoint is playback */
if (!is_capture && target_linked && !li->keep) {
GPtrArray *links = wp_base_endpoint_get_links (target);
for (guint i = 0; i < links->len; i++) {
WpBaseEndpointLink *l = g_ptr_array_index (links, i);
if (!wp_base_endpoint_link_is_kept (l)) {
g_autoptr (WpBaseEndpoint) ep = wp_base_endpoint_link_get_sink_endpoint (l);
guint32 stream_id = wp_base_endpoint_link_get_sink_stream (l);
fade_out (ep, stream_id, on_faded_destroy_link, l);
}
}
}
/* Link the client with the target */
if (is_capture) {
fade_in (li->ep, WP_STREAM_ID_NONE, on_faded_done, NULL);
wp_base_endpoint_link_new (core, target, li->stream_id, li->ep,
WP_STREAM_ID_NONE, li->keep, on_endpoint_link_created, self);
} else {
fade_in (target, li->stream_id, on_faded_done, NULL);
wp_base_endpoint_link_new (core, li->ep, WP_STREAM_ID_NONE, target,
li->stream_id, li->keep, on_endpoint_link_created, self);
}
return TRUE;
}
static WpBaseEndpoint*
wp_config_policy_get_data_target (WpConfigPolicy *self, const char *ep_role,
const struct WpParserEndpointLinkData *data, guint32 *stream_id)
{
g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
GVariantBuilder b;
GVariant *target_data = NULL;
g_return_val_if_fail (data, NULL);
/* 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));
if (ep_role)
g_variant_builder_add (&b, "{sv}", "role", g_variant_new_string (ep_role));
target_data = g_variant_builder_end (&b);
/* Find the target endpoint */
return wp_policy_find_endpoint (core, target_data, stream_id);
}
static guint
wp_config_policy_get_endpoint_lowest_priority_stream_id (WpBaseEndpoint *ep)
{
g_autoptr (GVariant) streams = NULL;
g_autoptr (GVariant) child = NULL;
GVariantIter *iter;
guint lowest_priority = G_MAXUINT;
guint res = WP_STREAM_ID_NONE;
guint priority;
guint id;
g_return_val_if_fail (ep, WP_STREAM_ID_NONE);
streams = wp_base_endpoint_list_streams (ep);
g_return_val_if_fail (streams, WP_STREAM_ID_NONE);
g_variant_get (streams, "aa{sv}", &iter);
while ((child = g_variant_iter_next_value (iter))) {
g_variant_lookup (child, "id", "u", &id);
g_variant_lookup (child, "priority", "u", &priority);
if (priority <= lowest_priority) {
lowest_priority = priority;
res = id;
}
}
g_variant_iter_free (iter);
return res;
}
static WpBaseEndpoint *
wp_config_policy_find_endpoint (WpPolicy *policy, GVariant *props,
guint32 *stream_id)
{
g_autoptr (WpCore) core = wp_policy_get_core (policy);
g_autoptr (WpPolicyManager) pmgr = wp_policy_manager_get_instance (core);
g_autoptr (WpSession) session = wp_policy_manager_get_session (pmgr);
const struct WpParserEndpointLinkData *data = NULL;
g_autoptr (GPtrArray) endpoints = NULL;
guint i;
WpBaseEndpoint *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;
/* If target-endpoint data was defined in the configuration file, find the
* matching endpoint based on target-endpoint data */
if (data->has_te) {
/* Get all the endpoints matching the media class */
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;
}
}
/* Otherwise, use the default session endpoint if the session is valid */
else if (session) {
/* Get the default type */
WpDefaultEndpointType type;
switch (data->me.endpoint_data.direction) {
case PW_DIRECTION_INPUT:
type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SOURCE;
break;
case PW_DIRECTION_OUTPUT:
type = WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK;
break;
default:
g_warn_if_reached ();
return NULL;
}
/* Get all the endpoints */
endpoints = wp_policy_manager_list_endpoints (pmgr, NULL);
if (!endpoints)
return NULL;
/* Find the default session endpoint */
for (i = 0; i < endpoints->len; i++) {
target = g_ptr_array_index (endpoints, i);
guint def_id = wp_session_get_default_endpoint (session, type);
if (def_id == wp_base_endpoint_get_global_id (target))
break;
}
}
/* If no target data has been defined and session is not valid, return null */
else
return NULL;
/* 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);
/* The target stream has higher priority than the endpoint stream */
const char *prioritized = data->te.stream ? data->te.stream : role;
*stream_id = prioritized ?
wp_base_endpoint_find_stream (target, prioritized) :
wp_config_policy_get_endpoint_lowest_priority_stream_id (target);
} else {
*stream_id = WP_STREAM_ID_NONE;
}
}
return g_object_ref (target);
}
static gint
link_info_compare_func (gconstpointer a, gconstpointer b, gpointer data)
{
WpBaseEndpoint *target = data;
struct link_info *li_a = *(struct link_info *const *)a;
struct link_info *li_b = *(struct link_info *const *)b;
g_autoptr (GVariant) stream_a = NULL;
g_autoptr (GVariant) stream_b = NULL;
guint priority_a = 0;
guint priority_b = 0;
gint res = 0;
res = (gint) li_a->keep - (gint) li_b->keep;
if (res != 0)
return res;
/* Get the role priority of a */
stream_a = wp_base_endpoint_get_stream (target, li_a->stream_id);
if (stream_a)
g_variant_lookup (stream_a, "priority", "u", &priority_a);
/* Get the role priority of b */
stream_b = wp_base_endpoint_get_stream (target, li_b->stream_id);
if (stream_b)
g_variant_lookup (stream_b, "priority", "u", &priority_b);
/* We want to sort from high priority to low priority */
res = priority_b - priority_a;
/* If both priorities are the same, sort by creation time */
if (res == 0)
res = wp_base_endpoint_get_creation_time (li_b->ep) -
wp_base_endpoint_get_creation_time (li_a->ep);
return res;
}
static void
wp_config_policy_add_link_info (WpConfigPolicy *self, GHashTable *table,
WpBaseEndpoint *ep)
{
g_autoptr (WpConfigParser) parser = NULL;
const struct WpParserEndpointLinkData *data = NULL;
const char *ep_role = wp_base_endpoint_get_role (ep);
struct link_info *res = NULL;
g_autoptr (WpBaseEndpoint) target = NULL;
guint32 stream_id = WP_STREAM_ID_NONE;
/* 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;
/* Get the target */
target = wp_config_policy_get_data_target (self, ep_role, data, &stream_id);
if (!target)
return;
/* Create the link info */
res = g_slice_new0 (struct link_info);
res->ep = g_object_ref (ep);
res->stream_id = stream_id;
res->keep = data->el.keep;
/* Get the link infos for the target, or create a new one if not found */
GPtrArray *link_infos = g_hash_table_lookup (table, target);
if (!link_infos) {
link_infos = g_ptr_array_new_with_free_func (link_info_destroy);
g_ptr_array_add (link_infos, res);
g_hash_table_insert (table, g_steal_pointer (&target), link_infos);
} else {
g_ptr_array_add (link_infos, res);
}
}
static void
links_table_handle_foreach (gpointer key, gpointer value, gpointer data)
{
WpConfigPolicy *self = data;
WpBaseEndpoint *target = key;
GPtrArray *link_infos = value;
/* Sort the link infos by role and creation time */
g_ptr_array_sort_with_data (link_infos, link_info_compare_func, target);
g_debug ("handling endpoints:");
/* Handle the first endpoint and also the ones with keep=true */
for (guint i = 0; i < link_infos->len; i++) {
struct link_info *li = g_ptr_array_index (link_infos, i);
g_debug (" %2u: %s:%d, keep:%d", i, wp_base_endpoint_get_name (li->ep),
li->stream_id, li->keep);
if (i == 0 || li->keep)
if (wp_config_policy_handle_pending_link (self, li, target))
self->pending_links = TRUE;
}
}
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;
g_debug ("rescanning");
/* Set pending-links to false */
self->pending_links = FALSE;
/* Handle all endpoints when rescanning */
endpoints = wp_policy_manager_list_endpoints (pmgr, NULL);
if (endpoints) {
/* Create the links table <target, array-of-link-info> */
GHashTable *links_table = g_hash_table_new_full (g_direct_hash,
g_direct_equal, g_object_unref, (GDestroyNotify) g_ptr_array_unref);
/* Fill the links table */
for (guint i = 0; i < endpoints->len; i++) {
WpBaseEndpoint *ep = g_ptr_array_index (endpoints, i);
wp_config_policy_add_link_info (self, links_table, ep);
}
/* Handle the links */
g_hash_table_foreach (links_table, links_table_handle_foreach, self);
/* Clean up */
g_clear_pointer (&links_table, g_hash_table_unref);
}
/* Finish rescanning if no pending links */
if (!self->pending_links)
wp_config_policy_rescan_done (self);
}
static void
wp_config_policy_rescan (WpConfigPolicy *self)
{
g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
if (!core)
return;
/* Scan if no pending scan */
if (!self->pending_rescan) {
self->pending_rescan = TRUE;
wp_core_sync (core, NULL, (GAsyncReadyCallback)wp_config_policy_sync_rescan,
self);
}
}
static void
wp_config_policy_endpoint_added (WpPolicy *policy, WpBaseEndpoint *ep)
{
WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
wp_config_policy_rescan (self);
}
static void
wp_config_policy_endpoint_removed (WpPolicy *policy, WpBaseEndpoint *ep)
{
WpConfigPolicy *self = WP_CONFIG_POLICY (policy);
wp_config_policy_rescan (self);
}
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);
/* Parse the file */
wp_configuration_reload (self->config, WP_PARSER_ENDPOINT_LINK_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);
/* 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)
{
}
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, 0);
}
WpConfigPolicy *
wp_config_policy_new (WpConfiguration *config)
{
return g_object_new (wp_config_policy_get_type (),
"rank", WP_POLICY_RANK_UPSTREAM,
"configuration", config,
NULL);
}