policy: implement a basic policy based on role priorities
* Every client has a priority based on its role * For playback, we allow only a single client to play at a time * For capture, we allow all clients to capture simultaneously * Every time the "selected" device changes (either because devices are discovered/removed or because the user changed the selection), the clients are re-linked to the new "selected" device. * When a playback client quits and there are others waiting unlinked, the highest priority one is linked automatically. * This also properly fixes re-linking the correct client(s) to the correct device(s) when wireplumber exits and restarts.
This commit is contained in:
@@ -339,6 +339,12 @@ wp_endpoint_unregister (WpEndpoint * self)
|
|||||||
g_return_if_fail (WP_IS_ENDPOINT (self));
|
g_return_if_fail (WP_IS_ENDPOINT (self));
|
||||||
|
|
||||||
priv = wp_endpoint_get_instance_private (self);
|
priv = wp_endpoint_get_instance_private (self);
|
||||||
|
|
||||||
|
/* unlink before unregistering so that policy modules
|
||||||
|
* can find dangling unlinked endpoints */
|
||||||
|
for (gint i = priv->links->len - 1; i >= 0; i--)
|
||||||
|
wp_endpoint_link_destroy (g_ptr_array_index (priv->links, i));
|
||||||
|
|
||||||
core = g_weak_ref_get (&priv->core);
|
core = g_weak_ref_get (&priv->core);
|
||||||
if (core) {
|
if (core) {
|
||||||
g_info ("WpEndpoint:%p unregistering '%s' (%s)", self, priv->name,
|
g_info ("WpEndpoint:%p unregistering '%s' (%s)", self, priv->name,
|
||||||
|
@@ -20,16 +20,18 @@ struct _WpSimplePolicy
|
|||||||
guint32 selected_ctl_id[2];
|
guint32 selected_ctl_id[2];
|
||||||
gchar *default_playback;
|
gchar *default_playback;
|
||||||
gchar *default_capture;
|
gchar *default_capture;
|
||||||
GQueue *unhandled_endpoints;
|
GVariant *role_priorities;
|
||||||
|
guint pending_rescan;
|
||||||
};
|
};
|
||||||
|
|
||||||
G_DECLARE_FINAL_TYPE (WpSimplePolicy, simple_policy, WP, SIMPLE_POLICY, WpPolicy)
|
G_DECLARE_FINAL_TYPE (WpSimplePolicy, simple_policy, WP, SIMPLE_POLICY, WpPolicy)
|
||||||
G_DEFINE_TYPE (WpSimplePolicy, simple_policy, WP_TYPE_POLICY)
|
G_DEFINE_TYPE (WpSimplePolicy, simple_policy, WP_TYPE_POLICY)
|
||||||
|
|
||||||
|
static void simple_policy_rescan (WpSimplePolicy *self);
|
||||||
|
|
||||||
static void
|
static void
|
||||||
simple_policy_init (WpSimplePolicy *self)
|
simple_policy_init (WpSimplePolicy *self)
|
||||||
{
|
{
|
||||||
self->unhandled_endpoints = g_queue_new ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -39,7 +41,7 @@ simple_policy_finalize (GObject *object)
|
|||||||
|
|
||||||
g_free (self->default_playback);
|
g_free (self->default_playback);
|
||||||
g_free (self->default_capture);
|
g_free (self->default_capture);
|
||||||
g_queue_free_full (self->unhandled_endpoints, (GDestroyNotify)g_object_unref);
|
g_clear_pointer (&self->role_priorities, g_variant_unref);
|
||||||
|
|
||||||
G_OBJECT_CLASS (simple_policy_parent_class)->finalize (object);
|
G_OBJECT_CLASS (simple_policy_parent_class)->finalize (object);
|
||||||
}
|
}
|
||||||
@@ -92,6 +94,9 @@ endpoint_notify_control_value (WpEndpoint * ep, guint control_id,
|
|||||||
|
|
||||||
/* notify policy watchers that things have changed */
|
/* notify policy watchers that things have changed */
|
||||||
wp_policy_notify_changed (WP_POLICY (self));
|
wp_policy_notify_changed (WP_POLICY (self));
|
||||||
|
|
||||||
|
/* rescan for clients that need to be linked */
|
||||||
|
simple_policy_rescan (self);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -111,6 +116,9 @@ select_endpoint (WpSimplePolicy *self, gint direction, WpEndpoint *ep,
|
|||||||
|
|
||||||
/* notify policy watchers that things have changed */
|
/* notify policy watchers that things have changed */
|
||||||
wp_policy_notify_changed (WP_POLICY (self));
|
wp_policy_notify_changed (WP_POLICY (self));
|
||||||
|
|
||||||
|
/* rescan for clients that need to be linked */
|
||||||
|
simple_policy_rescan (self);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
@@ -205,6 +213,8 @@ simple_policy_endpoint_removed (WpPolicy *policy, WpEndpoint *ep)
|
|||||||
WpSimplePolicy *self = WP_SIMPLE_POLICY (policy);
|
WpSimplePolicy *self = WP_SIMPLE_POLICY (policy);
|
||||||
gint direction;
|
gint direction;
|
||||||
|
|
||||||
|
simple_policy_rescan (self);
|
||||||
|
|
||||||
/* if the "selected" endpoint was removed, select another one */
|
/* if the "selected" endpoint was removed, select another one */
|
||||||
|
|
||||||
if (ep == self->selected[DIRECTION_SINK])
|
if (ep == self->selected[DIRECTION_SINK])
|
||||||
@@ -251,7 +261,7 @@ on_endpoint_link_created(GObject *initable, GAsyncResult *res, gpointer d)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static void
|
||||||
handle_client (WpPolicy *policy, WpEndpoint *ep)
|
handle_client (WpPolicy *policy, WpEndpoint *ep)
|
||||||
{
|
{
|
||||||
const char *media_class = wp_endpoint_get_media_class(ep);
|
const char *media_class = wp_endpoint_get_media_class(ep);
|
||||||
@@ -259,17 +269,17 @@ handle_client (WpPolicy *policy, WpEndpoint *ep)
|
|||||||
g_autoptr (WpCore) core = NULL;
|
g_autoptr (WpCore) core = NULL;
|
||||||
g_autoptr (WpEndpoint) target = NULL;
|
g_autoptr (WpEndpoint) target = NULL;
|
||||||
guint32 stream_id;
|
guint32 stream_id;
|
||||||
gboolean is_sink = FALSE;
|
gboolean is_capture = FALSE;
|
||||||
g_autofree gchar *role = NULL;
|
g_autofree gchar *role = NULL;
|
||||||
|
|
||||||
/* Detect if the client is a sink or a source */
|
/* Detect if the client is doing capture or playback */
|
||||||
is_sink = g_str_has_prefix (media_class, "Stream/Input");
|
is_capture = g_str_has_prefix (media_class, "Stream/Input");
|
||||||
|
|
||||||
/* Locate the target endpoint */
|
/* Locate the target endpoint */
|
||||||
g_variant_dict_init (&d, NULL);
|
g_variant_dict_init (&d, NULL);
|
||||||
g_variant_dict_insert (&d, "action", "s", "link");
|
g_variant_dict_insert (&d, "action", "s", "link");
|
||||||
g_variant_dict_insert (&d, "media.class", "s",
|
g_variant_dict_insert (&d, "media.class", "s",
|
||||||
is_sink ? "Audio/Source" : "Audio/Sink");
|
is_capture ? "Audio/Source" : "Audio/Sink");
|
||||||
|
|
||||||
g_object_get (ep, "role", &role, NULL);
|
g_object_get (ep, "role", &role, NULL);
|
||||||
if (role)
|
if (role)
|
||||||
@@ -280,46 +290,124 @@ handle_client (WpPolicy *policy, WpEndpoint *ep)
|
|||||||
core = wp_policy_get_core (policy);
|
core = wp_policy_get_core (policy);
|
||||||
target = wp_policy_find_endpoint (core, g_variant_dict_end (&d), &stream_id);
|
target = wp_policy_find_endpoint (core, g_variant_dict_end (&d), &stream_id);
|
||||||
if (!target)
|
if (!target)
|
||||||
return FALSE;
|
return;
|
||||||
|
|
||||||
|
/* if the client is already linked... */
|
||||||
|
if (wp_endpoint_is_linked (ep)) {
|
||||||
|
g_autoptr (WpEndpoint) existing_target = NULL;
|
||||||
|
GPtrArray *links = wp_endpoint_get_links (ep);
|
||||||
|
WpEndpointLink *l = g_ptr_array_index (links, 0);
|
||||||
|
|
||||||
|
existing_target = is_capture ?
|
||||||
|
wp_endpoint_link_get_source_endpoint (l) :
|
||||||
|
wp_endpoint_link_get_sink_endpoint (l);
|
||||||
|
|
||||||
|
if (existing_target == target) {
|
||||||
|
/* ... do nothing if it's already linked to the correct target */
|
||||||
|
g_debug ("Client '%s' already linked correctly",
|
||||||
|
wp_endpoint_get_name (ep));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
/* ... or else unlink it and continue */
|
||||||
|
g_debug ("Unlink client '%s' from its previous target",
|
||||||
|
wp_endpoint_get_name (ep));
|
||||||
|
wp_endpoint_link_destroy (l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Unlink the target if it is already linked */
|
/* Unlink the target if it is already linked */
|
||||||
if (wp_endpoint_is_linked (target))
|
/* At this point we are certain that if the target is linked, it is linked
|
||||||
|
* with another client. If it was linked with @ep, we would have catched it
|
||||||
|
* above, where we check if the client is linked.
|
||||||
|
* In the capture case, we don't care, we just allow all clients to capture
|
||||||
|
* from the same device.
|
||||||
|
* In the playback case, we are certain that @ep has higher priority because
|
||||||
|
* this function is being called after sorting all the client endpoints
|
||||||
|
* and therefore we can safely unlink the previous client
|
||||||
|
*/
|
||||||
|
if (wp_endpoint_is_linked (target) && !is_capture) {
|
||||||
|
g_debug ("Unlink target '%s' from other clients",
|
||||||
|
wp_endpoint_get_name (target));
|
||||||
wp_endpoint_unlink (target);
|
wp_endpoint_unlink (target);
|
||||||
|
}
|
||||||
|
|
||||||
/* Link the client with the target */
|
/* Link the client with the target */
|
||||||
if (is_sink) {
|
if (is_capture) {
|
||||||
wp_endpoint_link_new (core, target, 0, ep, stream_id,
|
wp_endpoint_link_new (core, target, stream_id, ep, 0,
|
||||||
on_endpoint_link_created, NULL);
|
on_endpoint_link_created, NULL);
|
||||||
} else {
|
} else {
|
||||||
wp_endpoint_link_new (core, ep, 0, target, stream_id,
|
wp_endpoint_link_new (core, ep, 0, target, stream_id,
|
||||||
on_endpoint_link_created, NULL);
|
on_endpoint_link_created, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TRUE;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gint
|
||||||
|
compare_client_roles (gconstpointer a, gconstpointer b, gpointer user_data)
|
||||||
|
{
|
||||||
|
GVariant *v = user_data;
|
||||||
|
WpEndpoint *ae = *(const gpointer *) a;
|
||||||
|
WpEndpoint *be = *(const gpointer *) b;
|
||||||
|
g_autofree gchar *a_role = NULL;
|
||||||
|
g_autofree gchar *b_role = NULL;
|
||||||
|
gint a_priority = 0, b_priority = 0;
|
||||||
|
|
||||||
|
/* if no role priorities specified, everything is equal */
|
||||||
|
if (!v) return 0;
|
||||||
|
|
||||||
|
g_object_get (ae, "role", &a_role, NULL);
|
||||||
|
g_object_get (be, "role", &b_role, NULL);
|
||||||
|
|
||||||
|
if (a_role)
|
||||||
|
g_variant_lookup (v, a_role, "i", &a_priority);
|
||||||
|
if (b_role)
|
||||||
|
g_variant_lookup (v, b_role, "i", &b_priority);
|
||||||
|
|
||||||
|
/* return b - a in order to sort descending */
|
||||||
|
return b_priority - a_priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
simple_policy_rescan_in_idle (WpSimplePolicy *self)
|
||||||
|
{
|
||||||
|
g_autoptr (WpCore) core = wp_policy_get_core (WP_POLICY (self));
|
||||||
|
GPtrArray *endpoints;
|
||||||
|
WpEndpoint *ep;
|
||||||
|
gint i;
|
||||||
|
|
||||||
|
g_debug ("rescanning for clients that need linking");
|
||||||
|
|
||||||
|
endpoints = wp_endpoint_find (core, "Stream/Input/Audio");
|
||||||
|
if (endpoints) {
|
||||||
|
/* link all capture clients */
|
||||||
|
for (i = 0; i < endpoints->len; i++) {
|
||||||
|
ep = g_ptr_array_index (endpoints, i);
|
||||||
|
handle_client (WP_POLICY (self), ep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints = wp_endpoint_find (core, "Stream/Output/Audio");
|
||||||
|
if (endpoints && endpoints->len > 0) {
|
||||||
|
/* sort based on role priorities */
|
||||||
|
g_ptr_array_sort_with_data (endpoints, compare_client_roles,
|
||||||
|
self->role_priorities);
|
||||||
|
|
||||||
|
/* link the highest priority client */
|
||||||
|
ep = g_ptr_array_index (endpoints, 0);
|
||||||
|
handle_client (WP_POLICY (self), ep);
|
||||||
|
}
|
||||||
|
|
||||||
|
self->pending_rescan = 0;
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
try_unhandled_clients (WpPolicy *policy)
|
simple_policy_rescan (WpSimplePolicy *self)
|
||||||
{
|
{
|
||||||
WpSimplePolicy *self = WP_SIMPLE_POLICY (policy);
|
if (!self->pending_rescan)
|
||||||
WpEndpoint *ep = NULL;
|
self->pending_rescan = g_idle_add (
|
||||||
GQueue *tmp = g_queue_new ();
|
(GSourceFunc) simple_policy_rescan_in_idle, self);
|
||||||
|
|
||||||
/* Try to handle all the unhandled endpoints, and add them into a tmp queue
|
|
||||||
* if they were not handled */
|
|
||||||
while ((ep = g_queue_pop_head (self->unhandled_endpoints))) {
|
|
||||||
if (handle_client (policy, ep))
|
|
||||||
g_object_unref (ep);
|
|
||||||
else
|
|
||||||
g_queue_push_tail (tmp, ep);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add back the unhandled endpoints to the unhandled queue */
|
|
||||||
while ((ep = g_queue_pop_head (tmp)))
|
|
||||||
g_queue_push_tail (self->unhandled_endpoints, ep);
|
|
||||||
|
|
||||||
/* Clean up */
|
|
||||||
g_queue_free_full (tmp, (GDestroyNotify)g_object_unref);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
@@ -332,18 +420,12 @@ simple_policy_handle_endpoint (WpPolicy *policy, WpEndpoint *ep)
|
|||||||
media_class = wp_endpoint_get_media_class(ep);
|
media_class = wp_endpoint_get_media_class(ep);
|
||||||
if (!g_str_has_prefix (media_class, "Stream") ||
|
if (!g_str_has_prefix (media_class, "Stream") ||
|
||||||
!g_str_has_suffix (media_class, "Audio")) {
|
!g_str_has_suffix (media_class, "Audio")) {
|
||||||
/* Try handling unhandled endpoints if a non client one has been added */
|
|
||||||
try_unhandled_clients (policy);
|
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle the endpoint */
|
/* Schedule a rescan that will handle the endpoint */
|
||||||
if (handle_client (policy, ep))
|
simple_policy_rescan (self);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|
||||||
/* Otherwise add it to the unhandled queue */
|
|
||||||
g_queue_push_tail (self->unhandled_endpoints, g_object_ref (ep));
|
|
||||||
return FALSE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static WpEndpoint *
|
static WpEndpoint *
|
||||||
@@ -431,5 +513,7 @@ wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
|
|||||||
NULL);
|
NULL);
|
||||||
g_variant_lookup (args, "default-playback-device", "s", &p->default_playback);
|
g_variant_lookup (args, "default-playback-device", "s", &p->default_playback);
|
||||||
g_variant_lookup (args, "default-capture-device", "s", &p->default_capture);
|
g_variant_lookup (args, "default-capture-device", "s", &p->default_capture);
|
||||||
|
p->role_priorities = g_variant_lookup_value (args, "role-priorities",
|
||||||
|
G_VARIANT_TYPE ("a{si}"));
|
||||||
wp_policy_register (WP_POLICY (p), core);
|
wp_policy_register (WP_POLICY (p), core);
|
||||||
}
|
}
|
||||||
|
@@ -30,10 +30,18 @@ load-module C libwireplumber-module-pw-audio-client
|
|||||||
|
|
||||||
# Implements linking clients to devices and maintains
|
# Implements linking clients to devices and maintains
|
||||||
# information about the devices to be used.
|
# information about the devices to be used.
|
||||||
# If you want to override the default audio devices,
|
# Notes:
|
||||||
# comment the first line and uncomment the lines below
|
# - Devices must be specified in hw:X,Y format, where X and Y are integers.
|
||||||
load-module C libwireplumber-module-simple-policy
|
# Things like hw:Intel,0 or paths are not understood.
|
||||||
#load-module C libwireplumber-module-simple-policy {
|
# - Roles and priorities can be arbitrary strings and arbitrary numbers
|
||||||
# "default-playback-device": <"hw:0,0">,
|
# - Roles are matched against the stream names specified in the modules above.
|
||||||
# "default-capture-device": <"hw:0,0">
|
load-module C libwireplumber-module-simple-policy {
|
||||||
#}
|
"default-playback-device": <"hw:0,0">,
|
||||||
|
"default-capture-device": <"hw:0,0">,
|
||||||
|
"role-priorities": <{
|
||||||
|
"Multimedia": 1,
|
||||||
|
"Communication": 5,
|
||||||
|
"Navigation": 8,
|
||||||
|
"Emergency": 10
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user