763 lines
25 KiB
C
763 lines
25 KiB
C
/* WirePlumber
|
|
*
|
|
* Copyright © 2020 Collabora Ltd.
|
|
* @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include <wp/wp.h>
|
|
#include <errno.h>
|
|
#include <pipewire/pipewire.h>
|
|
#include <pipewire/keys.h>
|
|
|
|
#include "module-default-nodes/common.h"
|
|
|
|
#define NAME "default-nodes"
|
|
#define DEFAULT_SAVE_INTERVAL_MS 1000
|
|
#define DEFAULT_USE_PERSISTENT_STORAGE TRUE
|
|
#define DEFAULT_AUTO_ECHO_CANCEL TRUE
|
|
#define DEFAULT_ECHO_CANCEL_SINK_NAME "echo-cancel-sink"
|
|
#define DEFAULT_ECHO_CANCEL_SOURCE_NAME "echo-cancel-source"
|
|
#define N_PREV_CONFIGS 16
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_SAVE_INTERVAL_MS,
|
|
PROP_USE_PERSISTENT_STORAGE,
|
|
PROP_AUTO_ECHO_CANCEL,
|
|
PROP_ECHO_CANCEL_SINK_NAME,
|
|
PROP_ECHO_CANCEL_SOURCE_NAME,
|
|
};
|
|
|
|
/*
|
|
* Module maintains the default devices to be used for a given media class. The
|
|
* module looks for changes in user preference and the changes in devices(when
|
|
* new devices like headsets, BT devices, HDMI etc are plugged in). User
|
|
* preference can be expressed via pavuctrl, gnome settings or metadata etc.
|
|
* These apps typically update the
|
|
* default.configured.*(default-configured-nodes) keys.
|
|
*/
|
|
|
|
typedef struct _WpDefaultNode WpDefaultNode;
|
|
struct _WpDefaultNode
|
|
{
|
|
gchar *value;
|
|
gchar *config_value;
|
|
gchar *prev_config_value[N_PREV_CONFIGS];
|
|
};
|
|
|
|
struct _WpDefaultNodes
|
|
{
|
|
WpPlugin parent;
|
|
|
|
WpState *state;
|
|
WpDefaultNode defaults[N_DEFAULT_NODES];
|
|
WpObjectManager *metadata_om;
|
|
WpObjectManager *rescan_om;
|
|
GSource *timeout_source;
|
|
|
|
/* properties */
|
|
guint save_interval_ms;
|
|
gboolean use_persistent_storage;
|
|
gboolean auto_echo_cancel;
|
|
gchar *echo_cancel_names[2];
|
|
};
|
|
|
|
G_DECLARE_FINAL_TYPE (WpDefaultNodes, wp_default_nodes,
|
|
WP, DEFAULT_NODES, WpPlugin)
|
|
G_DEFINE_TYPE (WpDefaultNodes, wp_default_nodes, WP_TYPE_PLUGIN)
|
|
|
|
static void
|
|
wp_default_nodes_init (WpDefaultNodes * self)
|
|
{
|
|
}
|
|
|
|
static void
|
|
update_prev_config_values (WpDefaultNode *def)
|
|
{
|
|
gint pos = N_PREV_CONFIGS - 1;
|
|
|
|
if (!def->config_value)
|
|
return;
|
|
|
|
/* Find if the current configured value is already in the stack */
|
|
for (gint i = 0; i < N_PREV_CONFIGS; ++i) {
|
|
if (!g_strcmp0(def->config_value, def->prev_config_value[i])) {
|
|
pos = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pos == 0)
|
|
return;
|
|
|
|
/* Insert on top position */
|
|
g_clear_pointer (&def->prev_config_value[pos], g_free);
|
|
|
|
for (gint i = pos; i > 0; --i)
|
|
def->prev_config_value[i] = def->prev_config_value[i-1];
|
|
|
|
def->prev_config_value[0] = g_strdup(def->config_value);
|
|
}
|
|
|
|
static void
|
|
load_state (WpDefaultNodes * self)
|
|
{
|
|
g_autoptr (WpProperties) props = wp_state_load (self->state);
|
|
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
|
const gchar *value = wp_properties_get (props, DEFAULT_CONFIG_KEY[i]);
|
|
|
|
self->defaults[i].config_value = g_strdup (value);
|
|
|
|
for (gint j = 0; j < N_PREV_CONFIGS; ++j) {
|
|
g_autofree gchar *key = g_strdup_printf("%s.%d", DEFAULT_CONFIG_KEY[i], j);
|
|
|
|
value = wp_properties_get (props, key);
|
|
self->defaults[i].prev_config_value[j] = g_strdup(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
timeout_save_state_callback (WpDefaultNodes *self)
|
|
{
|
|
g_autoptr (WpProperties) props = wp_properties_new_empty ();
|
|
g_autoptr (GError) error = NULL;
|
|
|
|
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
|
if (self->defaults[i].config_value)
|
|
wp_properties_set (props, DEFAULT_CONFIG_KEY[i],
|
|
self->defaults[i].config_value);
|
|
|
|
for (gint j = 0; j < N_PREV_CONFIGS; ++j) {
|
|
g_autofree gchar *key = g_strdup_printf("%s.%d", DEFAULT_CONFIG_KEY[i], j);
|
|
|
|
wp_properties_set (props, key, self->defaults[i].prev_config_value[j]);
|
|
}
|
|
}
|
|
|
|
if (!wp_state_save (self->state, props, &error))
|
|
wp_warning_object (self, "%s", error->message);
|
|
|
|
g_clear_pointer (&self->timeout_source, g_source_unref);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
timer_start (WpEvent *event, gpointer d)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
|
if (!self->timeout_source && self->use_persistent_storage) {
|
|
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
g_return_if_fail (core);
|
|
|
|
/* Add the timeout callback */
|
|
wp_core_timeout_add_closure (core, &self->timeout_source,
|
|
self->save_interval_ms, g_cclosure_new_object (
|
|
G_CALLBACK (timeout_save_state_callback), G_OBJECT (self)));
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
node_has_available_routes (WpDefaultNodes * self, WpNode *node)
|
|
{
|
|
const gchar *dev_id_str = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), PW_KEY_DEVICE_ID);
|
|
const gchar *cpd_str = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), "card.profile.device");
|
|
gint dev_id = dev_id_str ? atoi (dev_id_str) : -1;
|
|
gint cpd = cpd_str ? atoi (cpd_str) : -1;
|
|
g_autoptr (WpDevice) device = NULL;
|
|
gint found = 0;
|
|
|
|
if (dev_id == -1 || cpd == -1)
|
|
return TRUE;
|
|
|
|
/* Get the device */
|
|
device = wp_object_manager_lookup (self->rescan_om, WP_TYPE_DEVICE,
|
|
WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=i", dev_id, NULL);
|
|
if (!device)
|
|
return TRUE;
|
|
|
|
/* Check if the current device route supports the node card device profile */
|
|
{
|
|
g_autoptr (WpIterator) routes = NULL;
|
|
g_auto (GValue) val = G_VALUE_INIT;
|
|
routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device),
|
|
"Route", NULL);
|
|
for (; wp_iterator_next (routes, &val); g_value_unset (&val)) {
|
|
WpSpaPod *route = g_value_get_boxed (&val);
|
|
gint route_device = -1;
|
|
guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown;
|
|
|
|
if (!wp_spa_pod_get_object (route, NULL,
|
|
"device", "i", &route_device,
|
|
"available", "?I", &route_avail,
|
|
NULL))
|
|
continue;
|
|
|
|
if (route_device != cpd)
|
|
continue;
|
|
|
|
if (route_avail == SPA_PARAM_AVAILABILITY_no)
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/* Check if available routes support the node card device profile */
|
|
{
|
|
g_autoptr (WpIterator) routes = NULL;
|
|
g_auto (GValue) val = G_VALUE_INIT;
|
|
routes = wp_pipewire_object_enum_params_sync (WP_PIPEWIRE_OBJECT (device),
|
|
"EnumRoute", NULL);
|
|
for (; wp_iterator_next (routes, &val); g_value_unset (&val)) {
|
|
WpSpaPod *route = g_value_get_boxed (&val);
|
|
guint32 route_avail = SPA_PARAM_AVAILABILITY_unknown;
|
|
g_autoptr (WpSpaPod) route_devices = NULL;
|
|
|
|
if (!wp_spa_pod_get_object (route, NULL,
|
|
"available", "?I", &route_avail,
|
|
"devices", "?P", &route_devices,
|
|
NULL))
|
|
continue;
|
|
|
|
{
|
|
g_autoptr (WpIterator) it = wp_spa_pod_new_iterator (route_devices);
|
|
g_auto (GValue) v = G_VALUE_INIT;
|
|
for (; wp_iterator_next (it, &v); g_value_unset (&v)) {
|
|
gint32 *d = (gint32 *)g_value_get_pointer (&v);
|
|
if (d && *d == cpd) {
|
|
found++;
|
|
if (route_avail != SPA_PARAM_AVAILABILITY_no)
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* The node is part of a profile without routes so we assume it
|
|
* is available. This can happen for Pro Audio profiles */
|
|
if (found == 0)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
is_echo_cancel_node (WpDefaultNodes * self, WpNode *node, WpDirection direction)
|
|
{
|
|
const gchar *name = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME);
|
|
const gchar *virtual_str = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_VIRTUAL);
|
|
gboolean virtual = virtual_str && pw_properties_parse_bool (virtual_str);
|
|
|
|
if (!name || !virtual)
|
|
return FALSE;
|
|
|
|
return g_strcmp0 (name, self->echo_cancel_names[direction]) == 0;
|
|
}
|
|
|
|
static WpNode *
|
|
find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class,
|
|
const WpDefaultNode *def, WpDirection direction, gint *priority)
|
|
{
|
|
g_autoptr (WpIterator) it = NULL;
|
|
g_auto (GValue) val = G_VALUE_INIT;
|
|
gint highest_prio = 0;
|
|
WpNode *res = NULL;
|
|
|
|
g_return_val_if_fail (media_class, NULL);
|
|
|
|
it = wp_object_manager_new_filtered_iterator (self->rescan_om, WP_TYPE_NODE,
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "=s", media_class,
|
|
NULL);
|
|
|
|
for (; wp_iterator_next (it, &val); g_value_unset (&val)) {
|
|
WpNode *node = g_value_get_object (&val);
|
|
g_autoptr (WpPort) port = wp_object_manager_lookup (self->rescan_om,
|
|
WP_TYPE_PORT, WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_ID,
|
|
"=u", wp_proxy_get_bound_id (WP_PROXY (node)),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_PORT_DIRECTION,
|
|
"=s", direction == WP_DIRECTION_INPUT ? "in" : "out",
|
|
NULL);
|
|
if (port) {
|
|
const gchar *name = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), PW_KEY_NODE_NAME);
|
|
const gchar *prio_str = wp_pipewire_object_get_property (
|
|
WP_PIPEWIRE_OBJECT (node), PW_KEY_PRIORITY_SESSION);
|
|
gint prio = prio_str ? atoi (prio_str) : -1;
|
|
|
|
if (!node_has_available_routes (self, node))
|
|
continue;
|
|
|
|
if (self->auto_echo_cancel && is_echo_cancel_node (self, node, direction))
|
|
prio += 10000;
|
|
|
|
if (name && def->config_value && g_strcmp0 (name, def->config_value) == 0) {
|
|
prio += 20000 * (N_PREV_CONFIGS + 1);
|
|
} else if (name) {
|
|
for (gint i = 0; i < N_PREV_CONFIGS; ++i) {
|
|
if (!def->prev_config_value[i])
|
|
continue;
|
|
|
|
/* Match by name */
|
|
if (g_strcmp0 (name, def->prev_config_value[i]) == 0) {
|
|
prio += (N_PREV_CONFIGS - i) * 20000;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prio > highest_prio || res == NULL) {
|
|
highest_prio = prio;
|
|
res = node;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (priority)
|
|
*priority = highest_prio;
|
|
return res;
|
|
}
|
|
|
|
static WpNode *
|
|
find_best_media_classes_node (WpDefaultNodes * self,
|
|
const gchar **media_classes, const WpDefaultNode *def, WpDirection direction)
|
|
{
|
|
gint highest_prio = -1;
|
|
WpNode *res = NULL;
|
|
for (guint i = 0; media_classes[i]; i++) {
|
|
gint prio = -1;
|
|
WpNode *node = find_best_media_class_node (self, media_classes[i],
|
|
def, direction, &prio);
|
|
if (node && (!res || prio > highest_prio)) {
|
|
highest_prio = prio;
|
|
res = node;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static WpNode *
|
|
find_best_node (WpDefaultNodes * self, gint node_t)
|
|
{
|
|
const WpDefaultNode *def = &self->defaults[node_t];
|
|
|
|
switch (node_t) {
|
|
case AUDIO_SINK: {
|
|
const gchar *media_classes[] = {
|
|
"Audio/Sink",
|
|
"Audio/Duplex",
|
|
NULL};
|
|
return find_best_media_classes_node (self, media_classes, def,
|
|
WP_DIRECTION_INPUT);
|
|
}
|
|
case AUDIO_SOURCE: {
|
|
const gchar *media_classes[] = {
|
|
"Audio/Source",
|
|
"Audio/Source/Virtual",
|
|
"Audio/Duplex",
|
|
"Audio/Sink",
|
|
NULL};
|
|
return find_best_media_classes_node (self, media_classes, def,
|
|
WP_DIRECTION_OUTPUT);
|
|
}
|
|
case VIDEO_SOURCE: {
|
|
const gchar *media_classes[] = {
|
|
"Video/Source",
|
|
"Video/Source/Virtual",
|
|
NULL};
|
|
return find_best_media_classes_node (self, media_classes, def,
|
|
WP_DIRECTION_OUTPUT);
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
reevaluate_default_node (WpDefaultNodes * self, WpMetadata *m, gint node_t)
|
|
{
|
|
WpNode *node = NULL;
|
|
const gchar *node_name = NULL;
|
|
|
|
node = find_best_node (self, node_t);
|
|
if (node)
|
|
node_name = wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node),
|
|
PW_KEY_NODE_NAME);
|
|
|
|
/* store it in the metadata if it was changed */
|
|
if (node && node_name &&
|
|
g_strcmp0 (node_name, self->defaults[node_t].value) != 0)
|
|
{
|
|
g_autoptr (WpSpaJson) json = NULL;
|
|
|
|
g_free (self->defaults[node_t].value);
|
|
self->defaults[node_t].value = g_strdup (node_name);
|
|
|
|
wp_info_object (self, "set default node for %s: %s",
|
|
NODE_TYPE_STR[node_t], node_name);
|
|
|
|
json = wp_spa_json_new_object ("name", "s", node_name, NULL);
|
|
wp_metadata_set (m, 0, DEFAULT_KEY[node_t], "Spa:String:JSON",
|
|
wp_spa_json_get_data (json));
|
|
} else if (!node && self->defaults[node_t].value) {
|
|
g_clear_pointer (&self->defaults[node_t].value, g_free);
|
|
wp_info_object (self, "unset default node for %s", NODE_TYPE_STR[node_t]);
|
|
wp_metadata_set (m, 0, DEFAULT_KEY[node_t], NULL, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
sync_rescan (WpCore * core, GAsyncResult * res, WpDefaultNodes * self)
|
|
{
|
|
g_autoptr (WpMetadata) metadata = NULL;
|
|
g_autoptr (GError) error = NULL;
|
|
|
|
if (!wp_core_sync_finish (core, res, &error)) {
|
|
wp_warning_object (self, "core sync error: %s", error->message);
|
|
return;
|
|
}
|
|
|
|
/* Get the metadata */
|
|
metadata = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA,
|
|
NULL);
|
|
if (!metadata)
|
|
return;
|
|
|
|
wp_trace_object (self, "re-evaluating defaults");
|
|
reevaluate_default_node (self, metadata, AUDIO_SINK);
|
|
reevaluate_default_node (self, metadata, AUDIO_SOURCE);
|
|
reevaluate_default_node (self, metadata, VIDEO_SOURCE);
|
|
}
|
|
|
|
static void
|
|
schedule_rescan (WpEvent *event, gpointer d)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
|
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
g_return_if_fail (core);
|
|
|
|
wp_debug_object (self, "scheduling default nodes rescan");
|
|
// Event-Stack TBD: do we need to retain this behavior? or push this as a
|
|
// event & hook pair on to event stack
|
|
wp_core_sync_closure (core, NULL, g_cclosure_new_object (
|
|
G_CALLBACK (sync_rescan), G_OBJECT (self)));
|
|
}
|
|
|
|
static void
|
|
on_metadata_changed (WpEvent *event, gpointer d)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
|
gint node_t = -1;
|
|
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
|
WpMetadata *m = WP_METADATA (subject);
|
|
|
|
g_autoptr (WpProperties) p = wp_event_get_properties (event);
|
|
guint32 subject_id = atoi (wp_properties_get (p, "event.subject.id"));
|
|
const gchar *key = wp_properties_get (p, "event.subject.key");
|
|
const gchar *type = wp_properties_get (p, "event.subject.spa_type");
|
|
const gchar *value = wp_properties_get (p, "event.subject.value");
|
|
|
|
if (subject_id == 0) {
|
|
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
|
if (!g_strcmp0 (key, DEFAULT_CONFIG_KEY[i])) {
|
|
node_t = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node_t != -1) {
|
|
g_clear_pointer (&self->defaults[node_t].config_value, g_free);
|
|
|
|
if (value && !g_strcmp0 (type, "Spa:String:JSON")) {
|
|
g_autoptr (WpSpaJson) json = wp_spa_json_new_from_string (value);
|
|
g_autofree gchar *name = NULL;
|
|
if (wp_spa_json_object_get (json, "name", "s", &name, NULL))
|
|
self->defaults[node_t].config_value = g_strdup (name);
|
|
}
|
|
|
|
update_prev_config_values (&self->defaults[node_t]);
|
|
|
|
wp_debug_object (m, "changed '%s' -> '%s'", key,
|
|
self->defaults[node_t].config_value);
|
|
|
|
/* schedule rescan */
|
|
schedule_rescan (self);
|
|
|
|
/* Save state after specific interval */
|
|
timer_start (self);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_metadata_added (WpEvent *event, gpointer d)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (d);
|
|
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
g_return_if_fail (core);
|
|
g_autoptr (GObject) subject = wp_event_get_subject (event);
|
|
WpMetadata *metadata = WP_METADATA (subject);
|
|
|
|
for (gint i = 0; i < N_DEFAULT_NODES; i++) {
|
|
if (self->defaults[i].config_value) {
|
|
g_autoptr (WpSpaJson) json = wp_spa_json_new_object (
|
|
"name", "s", self->defaults[i].config_value, NULL);
|
|
wp_metadata_set (metadata, 0, DEFAULT_CONFIG_KEY[i], "Spa:String:JSON",
|
|
wp_spa_json_get_data (json));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_default_nodes_enable (WpPlugin * plugin, WpTransition * transition)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (plugin);
|
|
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin));
|
|
g_return_if_fail (core);
|
|
g_autoptr (WpEventDispatcher) dispatcher =
|
|
wp_event_dispatcher_get_instance (core);
|
|
g_autoptr (WpEventHook) hook = NULL;
|
|
g_return_if_fail (dispatcher);
|
|
|
|
/* default metadata added */
|
|
hook = wp_simple_event_hook_new (10, WP_EVENT_HOOK_EXEC_TYPE_ON_EVENT,
|
|
g_cclosure_new ((GCallback) on_metadata_added, self, NULL));
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-added",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "metadata",
|
|
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
|
NULL);
|
|
wp_event_dispatcher_register_hook (dispatcher, hook);
|
|
g_clear_object(&hook);
|
|
|
|
/* default metadata changed */
|
|
hook = wp_simple_event_hook_new (10, WP_EVENT_HOOK_EXEC_TYPE_ON_EVENT,
|
|
g_cclosure_new ((GCallback) on_metadata_changed, self, NULL));
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "metadata",
|
|
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
|
NULL);
|
|
wp_event_dispatcher_register_hook (dispatcher, hook);
|
|
g_clear_object(&hook);
|
|
|
|
/* register rescan hook as an after event */
|
|
/* priority: before the policy rescan & state save */
|
|
hook = wp_simple_event_hook_new (90, WP_EVENT_HOOK_EXEC_TYPE_AFTER_EVENTS,
|
|
g_cclosure_new ((GCallback) schedule_rescan, self, NULL));
|
|
/* default metadata changed */
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "metadata",
|
|
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
|
NULL);
|
|
/* device changed */
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "device",
|
|
NULL);
|
|
/* node changed */
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "node",
|
|
NULL);
|
|
/* device parms changed */
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "params-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "device",
|
|
NULL);
|
|
/* node parms changed */
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "params-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "node",
|
|
NULL);
|
|
wp_event_dispatcher_register_hook (dispatcher, hook);
|
|
g_clear_object (&hook);
|
|
|
|
/* register state save hook as an after event */
|
|
/* priority: after the default-nodes rescan */
|
|
hook = wp_simple_event_hook_new (80, WP_EVENT_HOOK_EXEC_TYPE_AFTER_EVENTS,
|
|
g_cclosure_new ((GCallback) timer_start, self, NULL));
|
|
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "object-changed",
|
|
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "metadata",
|
|
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
|
NULL);
|
|
wp_event_dispatcher_register_hook (dispatcher, hook);
|
|
g_clear_object (&hook);
|
|
|
|
/* Create the rescan object manager */
|
|
self->rescan_om = wp_object_manager_new ();
|
|
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_DEVICE, NULL);
|
|
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_NODE, NULL);
|
|
wp_object_manager_add_interest (self->rescan_om, WP_TYPE_PORT, NULL);
|
|
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_DEVICE,
|
|
WP_OBJECT_FEATURES_ALL);
|
|
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_NODE,
|
|
WP_OBJECT_FEATURES_ALL);
|
|
wp_object_manager_request_object_features (self->rescan_om, WP_TYPE_PORT,
|
|
WP_OBJECT_FEATURES_ALL);
|
|
wp_core_install_object_manager (core, self->rescan_om);
|
|
|
|
if (self->use_persistent_storage) {
|
|
self->state = wp_state_new (NAME);
|
|
load_state (self);
|
|
}
|
|
|
|
/* Create the metadata object manager */
|
|
self->metadata_om = wp_object_manager_new ();
|
|
wp_object_manager_add_interest (self->metadata_om, WP_TYPE_METADATA,
|
|
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "metadata.name", "=s", "default",
|
|
NULL);
|
|
wp_object_manager_request_object_features (self->metadata_om,
|
|
WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
|
|
wp_core_install_object_manager (core, self->metadata_om);
|
|
|
|
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
|
}
|
|
|
|
static void
|
|
wp_default_nodes_disable (WpPlugin * plugin)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (plugin);
|
|
|
|
/* Clear the current timeout callback */
|
|
if (self->timeout_source)
|
|
g_source_destroy (self->timeout_source);
|
|
g_clear_pointer (&self->timeout_source, g_source_unref);
|
|
|
|
for (guint i = 0; i < N_DEFAULT_NODES; i++) {
|
|
g_clear_pointer (&self->defaults[i].value, g_free);
|
|
g_clear_pointer (&self->defaults[i].config_value, g_free);
|
|
|
|
for (guint j = 0; j < N_PREV_CONFIGS; j++)
|
|
g_clear_pointer (&self->defaults[i].prev_config_value[j], g_free);
|
|
}
|
|
|
|
g_clear_object (&self->metadata_om);
|
|
g_clear_object (&self->rescan_om);
|
|
g_clear_object (&self->state);
|
|
}
|
|
|
|
static void
|
|
wp_default_nodes_set_property (GObject * object, guint property_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_SAVE_INTERVAL_MS:
|
|
self->save_interval_ms = g_value_get_uint (value);
|
|
break;
|
|
case PROP_USE_PERSISTENT_STORAGE:
|
|
self->use_persistent_storage = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_AUTO_ECHO_CANCEL:
|
|
self->auto_echo_cancel = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_ECHO_CANCEL_SINK_NAME:
|
|
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_INPUT], g_free);
|
|
self->echo_cancel_names[WP_DIRECTION_INPUT] = g_value_dup_string (value);
|
|
break;
|
|
case PROP_ECHO_CANCEL_SOURCE_NAME:
|
|
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_OUTPUT], g_free);
|
|
self->echo_cancel_names[WP_DIRECTION_OUTPUT] = g_value_dup_string (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_default_nodes_finalize (GObject * object)
|
|
{
|
|
WpDefaultNodes * self = WP_DEFAULT_NODES (object);
|
|
|
|
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_INPUT], g_free);
|
|
g_clear_pointer (&self->echo_cancel_names[WP_DIRECTION_OUTPUT], g_free);
|
|
|
|
G_OBJECT_CLASS (wp_default_nodes_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
wp_default_nodes_class_init (WpDefaultNodesClass * klass)
|
|
{
|
|
GObjectClass *object_class = (GObjectClass *) klass;
|
|
WpPluginClass *plugin_class = (WpPluginClass *) klass;
|
|
|
|
object_class->finalize = wp_default_nodes_finalize;
|
|
object_class->set_property = wp_default_nodes_set_property;
|
|
|
|
plugin_class->enable = wp_default_nodes_enable;
|
|
plugin_class->disable = wp_default_nodes_disable;
|
|
|
|
g_object_class_install_property (object_class, PROP_SAVE_INTERVAL_MS,
|
|
g_param_spec_uint ("save-interval-ms", "save-interval-ms",
|
|
"save-interval-ms", 1, G_MAXUINT32, DEFAULT_SAVE_INTERVAL_MS,
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_USE_PERSISTENT_STORAGE,
|
|
g_param_spec_boolean ("use-persistent-storage", "use-persistent-storage",
|
|
"use-persistent-storage", DEFAULT_USE_PERSISTENT_STORAGE,
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_AUTO_ECHO_CANCEL,
|
|
g_param_spec_boolean ("auto-echo-cancel", "auto-echo-cancel",
|
|
"auto-echo-cancel", DEFAULT_AUTO_ECHO_CANCEL,
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_ECHO_CANCEL_SINK_NAME,
|
|
g_param_spec_string ("echo-cancel-sink-name", "echo-cancel-sink-name",
|
|
"echo-cancel-sink-name", DEFAULT_ECHO_CANCEL_SINK_NAME,
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_ECHO_CANCEL_SOURCE_NAME,
|
|
g_param_spec_string ("echo-cancel-source-name", "echo-cancel-source-name",
|
|
"echo-cancel-source-name", DEFAULT_ECHO_CANCEL_SOURCE_NAME,
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
}
|
|
|
|
WP_PLUGIN_EXPORT gboolean
|
|
wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
|
{
|
|
gint64 save_interval_ms = DEFAULT_SAVE_INTERVAL_MS;
|
|
gboolean use_persistent_storage = DEFAULT_USE_PERSISTENT_STORAGE;
|
|
gboolean auto_echo_cancel = DEFAULT_AUTO_ECHO_CANCEL;
|
|
const gchar *echo_cancel_sink_name = DEFAULT_ECHO_CANCEL_SINK_NAME;
|
|
const gchar *echo_cancel_source_name = DEFAULT_ECHO_CANCEL_SOURCE_NAME;
|
|
|
|
g_autoptr (WpSettings) settings = wp_settings_get_instance(core, NULL);
|
|
wp_settings_get_int (settings, "device.save-interval-ms",
|
|
&save_interval_ms);
|
|
wp_settings_get_boolean (settings, "device.use-persistent-storage",
|
|
&use_persistent_storage);
|
|
wp_settings_get_boolean (settings, "device.auto-echo-cancel",
|
|
&auto_echo_cancel);
|
|
wp_settings_get_string (settings, "device.echo-cancel-sink-name",
|
|
&echo_cancel_sink_name);
|
|
wp_settings_get_string (settings, "device.echo-cancel-source-name",
|
|
&echo_cancel_source_name);
|
|
|
|
wp_plugin_register (g_object_new (wp_default_nodes_get_type (),
|
|
"name", NAME,
|
|
"core", core,
|
|
"save-interval-ms", save_interval_ms,
|
|
"use-persistent-storage", use_persistent_storage,
|
|
"auto-echo-cancel", auto_echo_cancel,
|
|
"echo-cancel-sink-name", echo_cancel_sink_name,
|
|
"echo-cancel-source-name", echo_cancel_source_name,
|
|
NULL));
|
|
return TRUE;
|
|
}
|
|
|
|
|