Commit a499e02a authored by Arnaud Ferraris's avatar Arnaud Ferraris
Browse files

modules: add module-default-routes for storing/restoring routes

This commit adds a WirePlumber module which stores the current route for
a given device, and implements the necessary interfaces to restore those
on startup.

The routes are stored as an array of objects in JSON format.

Please note that actually restoring routes on startup is not done by this
module but will be implemented separately in a future commit.
parent 79ba66e4
......@@ -25,6 +25,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 Arnaud Ferraris <arnaud.ferraris@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>
#include <spa/utils/json.h>
#define STATE_NAME "default-routes"
#define SAVE_INTERVAL_MS 1000
G_DEFINE_QUARK (wp-module-default-routes-routes, routes);
/* Signals */
enum
{
SIGNAL_GET_ROUTES,
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_routes) (WpDefaultRoutes *self, WpPipewireObject *device,
GHashTable **routes);
};
typedef struct _WpDefaultRoutesPrivate WpDefaultRoutesPrivate;
struct _WpDefaultRoutesPrivate
{
WpState *state;
WpProperties *routes;
GSource *routes_timeout;
GHashTable *current_routes;
GHashTable *default_routes;
WpObjectManager *devices_om;
};
G_DEFINE_TYPE_WITH_PRIVATE (WpDefaultRoutes, wp_default_routes,
WP_TYPE_PLUGIN)
#define MAX_JSON_STRING_LEN 256
static gint
find_device_route (WpPipewireObject *device, const gchar *lookup_name,
gint lookup_device)
{
WpIterator *routes = NULL;
g_auto (GValue) item = G_VALUE_INIT;
routes = g_object_get_qdata (G_OBJECT (device), routes_quark ());
g_return_val_if_fail (routes, -1);
wp_iterator_reset (routes);
for (; wp_iterator_next (routes, &item); g_value_unset (&item)) {
WpSpaPod *pod = g_value_get_boxed (&item);
gint index = 0;
const gchar *name = NULL;
guint size, type, num, i;
gint *devlist = NULL;
/* Parse */
if (!wp_spa_pod_get_object (pod, NULL,
"index", "i", &index,
"name", "s", &name,
"devices", "a", &size, &type, &num, &devlist,
NULL)) {
continue;
}
for (i = 0; i < num; i++) {
if (devlist[i] == lookup_device && g_strcmp0 (name, lookup_name) == 0)
return index;
}
}
return -1;
}
static gchar *
serialize_routes (GHashTable *routes)
{
GHashTableIter iter;
const gchar *key;
gpointer value;
gchar *ptr;
FILE *f;
size_t size;
gboolean first = TRUE;
g_return_val_if_fail (routes, NULL);
g_return_val_if_fail (g_hash_table_size (routes), NULL);
f = open_memstream (&ptr, &size);
g_return_val_if_fail (f, NULL);
/* Routes are stored in a JSON array */
fprintf (f, "[ ");
g_hash_table_iter_init (&iter, routes);
while (g_hash_table_iter_next (&iter, (gpointer *) &key, &value)) {
if (!first)
fprintf (f, ", ");
/* Each route is stored as a JSON object with "name" and "device" attributes */
fprintf (f, "{ \"name\": \"%s\", \"device\": %d }", key, GPOINTER_TO_INT (value));
first = FALSE;
}
fprintf (f, " ]");
fclose (f);
return ptr;
}
static GHashTable *
parse_routes (const gchar *routes_str)
{
struct spa_json array;
struct spa_json item;
struct spa_json object;
GHashTable *routes;
spa_json_init (&array, routes_str, strlen (routes_str));
if (spa_json_enter_array (&array, &item) <= 0)
return NULL;
routes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
while (spa_json_enter_object (&item, &object) > 0) {
const char *prop;
char name[MAX_JSON_STRING_LEN];
int dev_id, res;
dev_id = -1;
name[0] = 0;
while ((res = spa_json_next (&object, &prop)) > 0) {
if (strncmp (prop, "\"name\"", res) == 0)
res = spa_json_get_string (&object, name, MAX_JSON_STRING_LEN);
else if (strncmp (prop, "\"device\"", res) == 0)
res = spa_json_get_int (&object, &dev_id);
if (res <= 0) {
g_critical ("unable to parse route");
g_hash_table_destroy (routes);
return NULL;
}
}
if (dev_id >= 0 && strlen(name) > 0)
g_hash_table_insert (routes, g_strdup(name), GINT_TO_POINTER (dev_id));
}
return routes;
}
static void
hash_table_copy_elements (gpointer key, gpointer value, gpointer data)
{
GHashTable *ht = data;
gchar *route = key;
g_hash_table_insert (ht, g_strdup(route), value);
}
static gboolean
timeout_save_routes_cb (WpDefaultRoutes *self)
{
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
WpPipewireObject *device;
GHashTableIter iter;
GHashTable *ht;
/*
* `default_routes` is the reference list, used when other modules require
* the default routes for a given device.
* `current_routes` is a working copy, which is copied into `default_routes`
* when saving to disk.
*/
g_hash_table_remove_all (priv->default_routes);
g_hash_table_iter_init (&iter, priv->current_routes);
while (g_hash_table_iter_next (&iter, (gpointer*) &device, (gpointer*) &ht)) {
const gchar *dev_name;
gchar *routes;
GHashTable *copy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
NULL);
dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME);
if (!dev_name)
continue;
routes = serialize_routes (ht);
if (routes) {
wp_properties_set (priv->routes, dev_name, routes);
g_free (routes);
}
g_hash_table_foreach (ht, hash_table_copy_elements, copy);
g_hash_table_insert (priv->default_routes, device, copy);
}
if (!wp_state_save (priv->state, "routes", 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_object_get_core (WP_OBJECT (self));
g_return_if_fail (core);
g_return_if_fail (priv->routes);
/* Clear the current timeout callback */
if (priv->routes_timeout)
g_source_destroy (priv->routes_timeout);
g_clear_pointer (&priv->routes_timeout, g_source_unref);
/* Add the timeout callback */
wp_core_timeout_add_closure (core, &priv->routes_timeout, ms,
g_cclosure_new_object (G_CALLBACK (timeout_save_routes_cb),
G_OBJECT (self)));
}
static void
wp_default_routes_get_routes (WpDefaultRoutes *self, WpPipewireObject *device,
GHashTable **routes)
{
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
g_return_if_fail (device);
g_return_if_fail (routes);
g_return_if_fail (priv->default_routes);
*routes = g_hash_table_lookup (priv->default_routes, device);
}
static void
update_routes (WpDefaultRoutes *self, WpPipewireObject *device,
GHashTable *new_routes)
{
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
GHashTable *curr_routes;
GHashTableIter iter;
g_return_if_fail (new_routes);
g_return_if_fail (priv->routes);
curr_routes = g_hash_table_lookup (priv->current_routes, device);
/* Check if the new routes are the same as the current ones */
if (curr_routes) {
if (g_hash_table_size (curr_routes) == g_hash_table_size (new_routes)) {
gboolean identical = TRUE;
const gchar *key;
gpointer value;
gint index;
g_hash_table_iter_init (&iter, new_routes);
while (g_hash_table_iter_next (&iter, (gpointer*) &key, &value)) {
/* Make sure the route is valid */
index = find_device_route (device, key, GPOINTER_TO_INT (value));
if (index < 0) {
wp_info_object (self, "route '%s' (%d) is not valid", key, index);
return;
}
if (!g_hash_table_contains (curr_routes, key)) {
identical = FALSE;
break;
}
}
if (identical)
return;
}
}
/* Otherwise update the route and add timeout save callback */
g_hash_table_insert (priv->current_routes, device, new_routes);
timeout_save_routes (self, SAVE_INTERVAL_MS);
}
static void
on_device_routes_notified (WpPipewireObject *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;
const gchar *name = NULL;
gint device_id, direction = 0;
GHashTable *new_routes;
/* Finish */
routes = wp_pipewire_object_enum_params_finish (device, res, &error);
if (error) {
wp_warning_object (self, "failed to get current route on device");
return;
}
new_routes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
if (!new_routes) {
wp_warning_object (self, "failed to allocate new hash table");
return;
}
for (; wp_iterator_next (routes, &item); g_value_unset (&item)) {
/* Parse the route */
WpSpaPod *pod = g_value_get_boxed (&item);
if (!wp_spa_pod_get_object (pod, NULL,
"direction", "I", &direction,
"device", "i", &device_id,
"name", "s", &name,
NULL)) {
wp_warning_object (self, "failed to parse current route");
continue;
}
g_hash_table_insert (new_routes, g_strdup (name),
GINT_TO_POINTER (device_id));
}
/* Update the routes list */
update_routes (self, device, new_routes);
}
static void
on_device_param_info_notified (WpPipewireObject * device, GParamSpec * param,
WpDefaultRoutes *self)
{
/* Check the route every time the params have changed */
wp_pipewire_object_enum_params (device, "Route", NULL, NULL,
(GAsyncReadyCallback) on_device_routes_notified, self);
}
static void
on_device_enum_routes_done (WpPipewireObject *device, GAsyncResult *res,
WpDefaultRoutes *self)
{
g_autoptr (WpIterator) routes = NULL;
g_autoptr (GError) error = NULL;
/* Finish */
routes = wp_pipewire_object_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;
}
/* Keep a reference of the routes in the device object */
g_object_set_qdata_full (G_OBJECT (device), routes_quark (),
g_steal_pointer (&routes), (GDestroyNotify) wp_iterator_unref);
/* 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, WpPipewireObject *device, gpointer d)
{
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (d);
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
const gchar *dev_name = NULL;
const gchar *routes = NULL;
GHashTable *ht;
wp_warning_object (self, "device " WP_OBJECT_FORMAT " added",
WP_OBJECT_ARGS (device));
/* Enum available routes */
wp_pipewire_object_enum_params (device, "EnumRoute", NULL, NULL,
(GAsyncReadyCallback) on_device_enum_routes_done, self);
/* Load default routes for device */
dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME);
g_return_if_fail (dev_name);
routes = wp_properties_get (priv->routes, dev_name);
g_return_if_fail (routes);
ht = parse_routes (routes);
g_return_if_fail (ht);
g_hash_table_insert (priv->default_routes, device, ht);
}
static void
on_device_removed (WpObjectManager *om, WpPipewireObject *device, gpointer d)
{
WpDefaultRoutes *self = WP_DEFAULT_ROUTES (d);
WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self);
wp_warning_object (self, "device " WP_OBJECT_FORMAT " removed",
WP_OBJECT_ARGS (device));
g_hash_table_remove (priv->current_routes, device);
g_hash_table_remove (priv->default_routes, device);
}
static void
wp_default_routes_enable (WpPlugin * plugin, WpTransition * transition)
{
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (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_object_features (priv->devices_om,
WP_TYPE_DEVICE, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL);
g_signal_connect_object (priv->devices_om, "object-added",
G_CALLBACK (on_device_added), self, 0);
g_signal_connect_object (priv->devices_om, "object-removed",
G_CALLBACK (on_device_removed), self, 0);
wp_core_install_object_manager (core, priv->devices_om);
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}
static void
wp_default_routes_disable (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->routes_timeout)
g_source_destroy (priv->routes_timeout);
g_clear_pointer (&priv->routes_timeout, g_source_unref);
g_hash_table_destroy (priv->current_routes);
g_hash_table_destroy (priv->default_routes);
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);
wp_warning_object (self, "module default route loaded");
priv->current_routes = g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, (GDestroyNotify) g_hash_table_destroy);
priv->default_routes = g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, (GDestroyNotify) g_hash_table_destroy);
/* Load the saved routes */
priv->routes = wp_state_load (priv->state, "routes");
if (!priv->routes) {
wp_warning_object (self, "could not load routes");
}
}
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->enable = wp_default_routes_enable;
plugin_class->disable = wp_default_routes_disable;
klass->get_routes = wp_default_routes_get_routes;
/* Signals */
signals[SIGNAL_GET_ROUTES] = g_signal_new ("get-routes",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (WpDefaultRoutesClass, get_routes), NULL, NULL,
NULL, G_TYPE_NONE, 2, WP_TYPE_DEVICE, G_TYPE_POINTER);
}
WP_PLUGIN_EXPORT gboolean
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
{
wp_plugin_register (g_object_new (wp_default_routes_get_type (),
"name", STATE_NAME,
"core", core,
NULL));
return TRUE;
}
......@@ -51,3 +51,6 @@ load_script("suspend-node.lua")
-- Automatically sets device profiles to 'On'
load_module("device-activation")
-- Automatically save and restore default routes
load_module("default-routes")
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