Commit dca3ef6f authored by Julian Bouzas's avatar Julian Bouzas
Browse files

modules: add module to store device routes each time they change

parent f979210b
Pipeline #250030 passed with stages
in 1 minute
......@@ -91,6 +91,7 @@ static struct spa_type_table_data s_tables [WP_SPA_TYPE_TABLE_LAST] = {
[WP_SPA_TYPE_TABLE_FORMAT] = {spa_type_format, SPA_N_ELEMENTS (spa_type_format), NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_PARAM_PORT_CONFIG] = {spa_type_param_port_config, SPA_N_ELEMENTS (spa_type_param_port_config), NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_PARAM_PROFILE] = {spa_type_param_profile, SPA_N_ELEMENTS (spa_type_param_profile), NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_PARAM_ROUTE] = {spa_type_param_route, SPA_N_ELEMENTS (spa_type_param_route), NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_CONTROL] = {spa_type_control, SPA_CONTROL_LAST, NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_CHOICE] = {spa_type_choice, SPA_N_ELEMENTS (spa_type_choice), NULL, NULL, NULL, },
[WP_SPA_TYPE_TABLE_AUDIO_CHANNEL] = {spa_type_audio_channel, SPA_N_ELEMENTS (spa_type_audio_channel), NULL, NULL, NULL, },
......
......@@ -39,6 +39,7 @@ typedef enum {
WP_SPA_TYPE_TABLE_FORMAT,
WP_SPA_TYPE_TABLE_PARAM_PORT_CONFIG,
WP_SPA_TYPE_TABLE_PARAM_PROFILE,
WP_SPA_TYPE_TABLE_PARAM_ROUTE,
WP_SPA_TYPE_TABLE_AUDIO_CHANNEL,
WP_SPA_TYPE_TABLE_LAST,
} WpSpaTypeTable;
......
......@@ -67,6 +67,17 @@ shared_library(
dependencies : [wp_dep, pipewire_dep],
)
shared_library(
'wireplumber-module-default-routes',
[
'module-default-routes.c',
],
c_args : [common_c_args, '-DG_LOG_DOMAIN="m-default-routes"'],
install : true,
install_dir : wireplumber_module_dir,
dependencies : [wp_dep, pipewire_dep],
)
shared_library(
'wireplumber-module-device-activation',
[
......
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/param/audio/raw.h>
#include <spa/param/audio/type-info.h>
#include <spa/debug/types.h>
#define STATE_NAME "default-routes"
#define SAVE_INTERVAL_MS 1000
/* Signals */
enum
{
SIGNAL_GET_ROUTE,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
G_DECLARE_DERIVABLE_TYPE (WpDefaultRoutes, wp_default_routes, WP,
DEFAULT_ROUTES, WpPlugin)
struct _WpDefaultRoutesClass
{
WpPluginClass parent_class;
void (*get_route) (WpDefaultRoutes *self, const gchar *device_port,
const gchar **curr_route);
};
typedef struct _WpDefaultRoutesPrivate WpDefaultRoutesPrivate;
struct _WpDefaultRoutesPrivate
{
WpState *state;
WpProperties *routes;
GSource *timeout_source;
WpObjectManager *devices_om;
};
G_DEFINE_TYPE_WITH_PRIVATE (WpDefaultRoutes, wp_default_routes, WP_TYPE_PLUGIN)
static gchar *
parse_route (WpProxy *dev, WpSpaPod * route, gint *device_id, WpSpaPod **props)
{
g_autoptr (WpSpaPodParser) p = NULL;
gint index = 0;
WpDirection direction = WP_DIRECTION_INPUT;
const gchar *dev_name = NULL, *name = NULL;
g_return_val_if_fail (dev, NULL);
g_return_val_if_fail (route, NULL);
dev_name = wp_proxy_get_property (dev, PW_KEY_DEVICE_NAME);
g_return_val_if_fail (dev_name, NULL);
/* Parse */
p = wp_spa_pod_parser_new_object (route, "Route", NULL);
wp_spa_pod_parser_get (p, "index", "i", &index, NULL);
wp_spa_pod_parser_get (p, "device", "i", device_id, NULL);
wp_spa_pod_parser_get (p, "direction", "I", &direction, NULL);
wp_spa_pod_parser_get (p, "name", "s", &name, NULL);
wp_spa_pod_parser_get (p, "props", "P", &props, NULL);
return name ? g_strdup_printf ("%s:%s:%s", dev_name,
direction == WP_DIRECTION_INPUT ? "input" : "output", name) : NULL;
}
static const gchar *
channel_to_name (uint32_t channel)
{
gint i;
for (i = 0; spa_type_audio_channel[i].name; i++) {
if (spa_type_audio_channel[i].type == channel)
return spa_debug_type_short_name (spa_type_audio_channel[i].name);
}
return "UNK";
}
static char *
serialize_props (WpSpaPod *props)
{
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
gboolean comma = FALSE;
gchar *ptr;
size_t size;
FILE *f;
it = wp_spa_pod_iterate (props);
g_return_val_if_fail (it, NULL);
f = open_memstream(&ptr, &size);
fprintf(f, "{ ");
for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
WpSpaPod *p = g_value_get_boxed (&item);
const char *p_key = NULL;
g_autoptr (WpSpaPod) p_val = NULL;
wp_spa_pod_get_property (p, &p_key, &p_val);
/* volume */
if (g_strcmp0 (p_key, "volume") == 0) {
float vol = 0.0f;
wp_spa_pod_get_float (p_val, &vol);
fprintf(f, "%s\"volume\": %f ", (comma ? ", " : ""), vol);
}
/* mute */
else if (g_strcmp0 (p_key, "mute") == 0) {
gboolean b = FALSE;
wp_spa_pod_get_boolean (p_val, &b);
fprintf(f, "%s\"mute\": %s ", (comma ? ", " : ""), b ? "true" : "false");
}
/* channelVolumes */
else if (g_strcmp0 (p_key, "channelVolumes") == 0) {
g_autoptr (WpIterator) it2 = wp_spa_pod_iterate (p_val);
g_auto (GValue) item2 = G_VALUE_INIT;
guint i = 0, n_vols = 0;
float vols[SPA_AUDIO_MAX_CHANNELS];
for (; wp_iterator_next (it2, &item2); g_value_unset (&item2)) {
float *vol = (float *)g_value_get_pointer (&item2);
vols[n_vols] = *vol;
n_vols++;
}
if (n_vols == 0)
continue;
fprintf(f, "%s\"volumes\": [", (comma ? ", " : ""));
for (i = 0; i < n_vols; i++)
fprintf(f, "%s%f", (i == 0 ? " " : ", "), vols[i]);
fprintf(f, " ]");
}
/* channelMap */
else if (g_strcmp0 (p_key, "channelMap") == 0) {
g_autoptr (WpIterator) it2 = wp_spa_pod_iterate (p_val);
g_auto (GValue) item2 = G_VALUE_INIT;
guint i = 0, n_vals = 0;
guint vals[SPA_AUDIO_MAX_CHANNELS];
for (; wp_iterator_next (it2, &item2); g_value_unset (&item2)) {
guint *val = (guint *)g_value_get_pointer (&item2);
vals[n_vals] = *val;
n_vals++;
}
if (n_vals == 0)
continue;
fprintf(f, "%s\"channels\": [", (comma ? ", " : ""));
for (i = 0; i < n_vals; i++)
fprintf(f, "%s\"%s\"", (i == 0 ? " " : ", "),
channel_to_name (vals[i]));
fprintf(f, " ]");
}
/* default */
else {
continue;
}
comma = TRUE;
}
fprintf(f, " }");
fclose(f);
return ptr;
}
static gboolean
timeout_save_callback (WpDefaultRoutes *self)
{
WpDefaultRoutesPrivate *priv =
wp_default_routes_get_instance_private (self);
if (!wp_state_save (priv->state, priv->routes))
wp_warning_object (self, "could not save routes");
return G_SOURCE_REMOVE;
}
static void
timeout_save_routes (WpDefaultRoutes *self, guint ms)
{
WpDefaultRoutesPrivate *priv =
wp_default_routes_get_instance_private (self);
g_autoptr (WpCore) core = wp_plugin_get_core (WP_PLUGIN (self));
g_return_if_fail (core);
g_return_if_fail (priv->routes);
/* Clear the current timeout callback */
if (priv->timeout_source)
g_source_destroy (priv->timeout_source);
g_clear_pointer (&priv->timeout_source, g_source_unref);
/* Add the timeout callback */
wp_core_timeout_add_closure (core, &priv->timeout_source, ms,
g_cclosure_new_object (G_CALLBACK (timeout_save_callback),
G_OBJECT (self)));
}
static void
wp_default_routes_get_route (WpDefaultRoutes *self, const gchar *device_port,
const gchar **curr_route)
{
WpDefaultRoutesPrivate *priv =
wp_default_routes_get_instance_private (self);
g_return_if_fail (device_port);
g_return_if_fail (curr_route);
g_return_if_fail (priv->routes);
/* Get the route */
*curr_route = wp_properties_get (priv->routes, device_port);
}
static void
on_device_route_notified (WpProxy *device, GAsyncResult *res,
WpDefaultRoutes *self)
{
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
g_autoptr (WpIterator) routes = NULL;
g_autoptr (GError) error = NULL;
g_auto (GValue) item = G_VALUE_INIT;
g_autofree gchar *key = NULL;
g_autofree gchar *val = NULL;
/* Finish */
routes = wp_proxy_enum_params_finish (device, res, &error);
if (error) {
wp_warning_object (self, "failed to get current route on device");
return;
}
/* Ignore empty route notifications */
if (!wp_iterator_next (routes, &item))
return;
/* Parse route key and value */
{
WpSpaPod *pod = g_value_get_boxed (&item);
WpSpaPod *props = NULL;
gint device_id = 0;
key = parse_route (device, pod, &device_id, &props);
if (props)
val = serialize_props (props);
}
g_value_unset (&item);
/* Ignore routes without properties */
if (val)
return;
/* Update and save route */
wp_properties_set (priv->routes, key, val);
timeout_save_routes (self, SAVE_INTERVAL_MS);
wp_info_object (self, "updated property '%s' on route '%s'", val, key);
}
static void
on_device_param_info_notified (WpProxy * device, GParamSpec * param,
WpDefaultRoutes *self)
{
/* Check the route every time the params have changed */
wp_proxy_enum_params (device, "Route", NULL, NULL,
(GAsyncReadyCallback) on_device_route_notified, self);
}
static void
on_device_enum_route_done (WpProxy *device, GAsyncResult *res,
WpDefaultRoutes *self)
{
g_autoptr (WpIterator) routes = NULL;
g_autoptr (GError) error = NULL;
/* Finish */
routes = wp_proxy_enum_params_finish (device, res, &error);
if (error) {
wp_warning_object (self, "failed to enum routes in device "
WP_OBJECT_FORMAT, WP_OBJECT_ARGS (device));
return;
}
/* Watch for param info changes */
g_signal_connect_object (device, "notify::param-info",
G_CALLBACK (on_device_param_info_notified), self, 0);
}
static void
on_device_added (WpObjectManager *om, WpProxy *proxy, gpointer d)
{
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (d);
wp_debug_object (self, "device " WP_OBJECT_FORMAT " added",
WP_OBJECT_ARGS (proxy));
/* Enum available routes */
wp_proxy_enum_params (WP_PROXY (proxy), "EnumRoute", NULL, NULL,
(GAsyncReadyCallback) on_device_enum_route_done, self);
}
static void
wp_default_routes_activate (WpPlugin * plugin)
{
g_autoptr (WpCore) core = wp_plugin_get_core (plugin);
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (plugin);
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
/* Create the devices object manager */
priv->devices_om = wp_object_manager_new ();
wp_object_manager_add_interest (priv->devices_om, WP_TYPE_DEVICE, NULL);
wp_object_manager_request_proxy_features (priv->devices_om,
WP_TYPE_DEVICE, WP_PROXY_FEATURES_STANDARD);
g_signal_connect_object (priv->devices_om, "object-added",
G_CALLBACK (on_device_added), self, 0);
wp_core_install_object_manager (core, priv->devices_om);
}
static void
wp_default_routes_deactivate (WpPlugin * plugin)
{
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (plugin);
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
g_clear_object (&priv->devices_om);
}
static void
wp_default_routes_finalize (GObject * object)
{
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (object);
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
/* Clear the current timeout callback */
if (priv->timeout_source)
g_source_destroy (priv->timeout_source);
g_clear_pointer (&priv->timeout_source, g_source_unref);
g_clear_pointer (&priv->routes, wp_properties_unref);
g_clear_object (&priv->state);
}
static void
wp_default_routes_init (WpDefaultRoutes * self)
{
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
priv->state = wp_state_new (STATE_NAME);
/* Load the saved routes */
priv->routes = wp_state_load (priv->state);
if (!priv->routes) {
wp_warning_object (self, "could not load routes");
return;
}
}
static void
wp_default_routes_class_init (WpDefaultRoutesClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpPluginClass *plugin_class = (WpPluginClass *) klass;
object_class->finalize = wp_default_routes_finalize;
plugin_class->activate = wp_default_routes_activate;
plugin_class->deactivate = wp_default_routes_deactivate;
klass->get_route = wp_default_routes_get_route;
/* Signals */
signals[SIGNAL_GET_ROUTE] = g_signal_new ("get-route",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (WpDefaultRoutesClass, get_route), NULL, NULL,
NULL, G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_POINTER);
}
WP_PLUGIN_EXPORT void
wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
{
wp_plugin_register (g_object_new (wp_default_routes_get_type (),
"name", STATE_NAME,
"module", module,
NULL));
}
......@@ -55,6 +55,9 @@ load-module C libwireplumber-module-dbus-reservation
# Grants functionality to store and restaure default device profiles
load-module C libwireplumber-module-default-profile
# Grants functionality to store and restaure default device routes
load-module C libwireplumber-module-default-routes
# Grants access to security confined clients
load-module C libwireplumber-module-client-permissions
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment