mirror of
https://github.com/ublue-os/bazzite.git
synced 2025-01-01 03:21:41 +00:00
1604 lines
53 KiB
Diff
1604 lines
53 KiB
Diff
diff --git a/modules/meson.build b/modules/meson.build
|
|
index 4930bfae..4a337017 100644
|
|
--- a/modules/meson.build
|
|
+++ b/modules/meson.build
|
|
@@ -159,6 +159,17 @@ shared_library(
|
|
dependencies : [wp_dep, pipewire_dep],
|
|
)
|
|
|
|
+shared_library(
|
|
+ 'wireplumber-module-filters-api',
|
|
+ [
|
|
+ 'module-filters-api.c',
|
|
+ ],
|
|
+ c_args : [common_c_args, '-DG_LOG_DOMAIN="m-filters-api"'],
|
|
+ install : true,
|
|
+ install_dir : wireplumber_module_dir,
|
|
+ dependencies : [wp_dep, pipewire_dep],
|
|
+)
|
|
+
|
|
if libsystemd_dep.found() or libelogind_dep.found()
|
|
shared_library(
|
|
'wireplumber-module-logind',
|
|
diff --git a/modules/module-default-nodes.c b/modules/module-default-nodes.c
|
|
index aaf29389..3bb93f00 100644
|
|
--- a/modules/module-default-nodes.c
|
|
+++ b/modules/module-default-nodes.c
|
|
@@ -16,18 +16,12 @@
|
|
#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,
|
|
};
|
|
|
|
typedef struct _WpDefaultNode WpDefaultNode;
|
|
@@ -51,8 +45,6 @@ struct _WpDefaultNodes
|
|
/* 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,
|
|
@@ -243,21 +235,6 @@ node_has_available_routes (WpDefaultNodes * self, WpNode *node)
|
|
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)
|
|
@@ -291,9 +268,6 @@ find_best_media_class_node (WpDefaultNodes * self, const gchar *media_class,
|
|
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) {
|
|
@@ -597,41 +571,18 @@ wp_default_nodes_set_property (GObject * object, guint property_id,
|
|
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;
|
|
@@ -646,21 +597,6 @@ wp_default_nodes_class_init (WpDefaultNodesClass * klass)
|
|
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
|
|
@@ -668,19 +604,11 @@ wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
|
{
|
|
guint 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;
|
|
|
|
if (args) {
|
|
g_variant_lookup (args, "save-interval-ms", "u", &save_interval_ms);
|
|
g_variant_lookup (args, "use-persistent-storage", "b",
|
|
&use_persistent_storage);
|
|
- g_variant_lookup (args, "auto-echo-cancel", "&s", &auto_echo_cancel);
|
|
- g_variant_lookup (args, "echo-cancel-sink-name", "&s",
|
|
- &echo_cancel_sink_name);
|
|
- g_variant_lookup (args, "echo-cancel-source-name", "&s",
|
|
- &echo_cancel_source_name);
|
|
}
|
|
|
|
wp_plugin_register (g_object_new (wp_default_nodes_get_type (),
|
|
@@ -688,9 +616,6 @@ wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
|
"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;
|
|
}
|
|
diff --git a/modules/module-filters-api.c b/modules/module-filters-api.c
|
|
new file mode 100644
|
|
index 00000000..8852a6c5
|
|
--- /dev/null
|
|
+++ b/modules/module-filters-api.c
|
|
@@ -0,0 +1,920 @@
|
|
+/* WirePlumber
|
|
+ *
|
|
+ * Copyright © 2023 Collabora Ltd.
|
|
+ * @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
+ *
|
|
+ * SPDX-License-Identifier: MIT
|
|
+ */
|
|
+
|
|
+#include <pipewire/keys.h>
|
|
+
|
|
+#include <wp/wp.h>
|
|
+
|
|
+struct _WpFiltersApi
|
|
+{
|
|
+ WpPlugin parent;
|
|
+
|
|
+ WpObjectManager *metadata_om;
|
|
+ WpObjectManager *stream_nodes_om;
|
|
+ WpObjectManager *nodes_om;
|
|
+ WpObjectManager *filter_nodes_om;
|
|
+ guint n_playback_stream_nodes;
|
|
+ guint n_capture_stream_nodes;
|
|
+ GList *filters[2];
|
|
+ GHashTable *targets;
|
|
+};
|
|
+
|
|
+enum {
|
|
+ ACTION_IS_FILTER_ENABLED,
|
|
+ ACTION_GET_FILTER_TARGET,
|
|
+ ACTION_GET_FILTER_FROM_TARGET,
|
|
+ ACTION_GET_DEFAULT_FILTER,
|
|
+ SIGNAL_CHANGED,
|
|
+ N_SIGNALS
|
|
+};
|
|
+
|
|
+static guint signals[N_SIGNALS] = {0};
|
|
+
|
|
+G_DECLARE_FINAL_TYPE (WpFiltersApi, wp_filters_api, WP, FILTERS_API, WpPlugin)
|
|
+G_DEFINE_TYPE (WpFiltersApi, wp_filters_api, WP_TYPE_PLUGIN)
|
|
+
|
|
+struct _Filter {
|
|
+ gchar *link_group;
|
|
+ WpDirection direction;
|
|
+ WpNode *node;
|
|
+ WpNode *stream;
|
|
+ gchar *target;
|
|
+ gboolean enabled;
|
|
+ gint priority;
|
|
+};
|
|
+typedef struct _Filter Filter;
|
|
+
|
|
+struct _Target {
|
|
+ gboolean exclusive;
|
|
+ WpNode *node;
|
|
+};
|
|
+typedef struct _Target Target;
|
|
+
|
|
+static guint
|
|
+get_filter_priority (const gchar *link_group)
|
|
+{
|
|
+ if (strstr (link_group, "loopback"))
|
|
+ return 300;
|
|
+ if (strstr (link_group, "filter-chain"))
|
|
+ return 200;
|
|
+ /* By default echo-cancel is the lowest priority to properly cancel audio */
|
|
+ if (strstr (link_group, "echo-cancel"))
|
|
+ return 0;
|
|
+ return 100;
|
|
+}
|
|
+
|
|
+static Filter *
|
|
+filter_new (const gchar *link_group, WpDirection dir, gboolean is_stream,
|
|
+ WpNode *node)
|
|
+{
|
|
+ Filter *f = g_malloc0 (sizeof (Filter));
|
|
+ f->link_group = g_strdup (link_group);
|
|
+ f->direction = dir;
|
|
+ f->node = is_stream ? NULL : g_object_ref (node);
|
|
+ f->stream = is_stream ? g_object_ref (node) : NULL;
|
|
+ f->target = NULL;
|
|
+ f->enabled = TRUE;
|
|
+ f->priority = get_filter_priority (link_group);
|
|
+ return f;
|
|
+}
|
|
+
|
|
+static void
|
|
+filter_free (Filter *f)
|
|
+{
|
|
+ g_clear_pointer (&f->link_group, g_free);
|
|
+ g_clear_pointer (&f->target, g_free);
|
|
+ g_clear_object (&f->node);
|
|
+ g_clear_object (&f->stream);
|
|
+ g_free (f);
|
|
+}
|
|
+
|
|
+static Target *
|
|
+target_new (gboolean exclusive, WpNode *node)
|
|
+{
|
|
+ Target *t = g_malloc0 (sizeof (Target));
|
|
+ t->exclusive = exclusive;
|
|
+ t->node = node ? g_object_ref (node) : NULL;
|
|
+ return t;
|
|
+}
|
|
+
|
|
+static void
|
|
+target_free (Target *t)
|
|
+{
|
|
+ g_clear_object (&t->node);
|
|
+ g_free (t);
|
|
+}
|
|
+
|
|
+static gint
|
|
+filter_equal_func (const Filter *f, const gchar *link_group)
|
|
+{
|
|
+ return g_str_equal (f->link_group, link_group) ? 0 : 1;
|
|
+}
|
|
+
|
|
+static gint
|
|
+filter_compare_func (const Filter *a, const Filter *b)
|
|
+{
|
|
+ gint diff = a->priority - b->priority;
|
|
+ if (diff != 0)
|
|
+ return diff;
|
|
+ return g_strcmp0 (a->link_group, b->link_group);
|
|
+}
|
|
+
|
|
+static void
|
|
+wp_filters_api_init (WpFiltersApi * self)
|
|
+{
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+wp_filters_api_is_filter_enabled (WpFiltersApi * self, const gchar *direction,
|
|
+ const gchar *link_group)
|
|
+{
|
|
+ WpDirection dir = WP_DIRECTION_INPUT;
|
|
+ GList *filters;
|
|
+ Filter *found = NULL;
|
|
+
|
|
+ g_return_val_if_fail (direction, FALSE);
|
|
+ g_return_val_if_fail (link_group, FALSE);
|
|
+
|
|
+ /* Get the filters for the given direction */
|
|
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ filters = self->filters[dir];
|
|
+
|
|
+ /* Find the filter in the filters list */
|
|
+ filters = g_list_find_custom (filters, link_group,
|
|
+ (GCompareFunc) filter_equal_func);
|
|
+ if (!filters)
|
|
+ return FALSE;
|
|
+
|
|
+ found = filters->data;
|
|
+ return found->enabled;
|
|
+}
|
|
+
|
|
+static WpSpaJson *
|
|
+wp_filters_api_get_filter_target (WpFiltersApi * self, const gchar *direction,
|
|
+ const gchar *link_group)
|
|
+{
|
|
+ WpDirection dir = WP_DIRECTION_INPUT;
|
|
+ GList *filters;
|
|
+ Filter *found;
|
|
+ g_autoptr (WpSpaJson) res = wp_spa_json_new_object (
|
|
+ "exclusive", "b", FALSE, "bound_id", "i", -1, NULL);
|
|
+
|
|
+ g_return_val_if_fail (direction, g_steal_pointer (&res));
|
|
+ g_return_val_if_fail (link_group, g_steal_pointer (&res));
|
|
+
|
|
+ /* Get the filters for the given direction */
|
|
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ filters = self->filters[dir];
|
|
+
|
|
+ /* Find the filter in the filters list */
|
|
+ filters = g_list_find_custom (filters, link_group,
|
|
+ (GCompareFunc) filter_equal_func);
|
|
+ if (!filters)
|
|
+ return g_steal_pointer (&res);
|
|
+ found = filters->data;
|
|
+ if (!found->enabled)
|
|
+ return g_steal_pointer (&res);
|
|
+
|
|
+ /* Return the previous filter with matching target that is enabled */
|
|
+ while ((filters = g_list_previous (filters))) {
|
|
+ Filter *prev = (Filter *) filters->data;
|
|
+ if ((prev->target == found->target ||
|
|
+ (prev->target && found->target &&
|
|
+ g_str_equal (prev->target, found->target))) &&
|
|
+ prev->enabled) {
|
|
+ return wp_spa_json_new_object (
|
|
+ "exclusive", "b", FALSE,
|
|
+ "bound_id", "i", wp_proxy_get_bound_id (WP_PROXY (prev->node)),
|
|
+ NULL);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Find the target */
|
|
+ if (found->target) {
|
|
+ Target *t = g_hash_table_lookup (self->targets, found->target);
|
|
+ if (t) {
|
|
+ return wp_spa_json_new_object (
|
|
+ "exclusive", "b", t->exclusive,
|
|
+ "bound_id", "i",
|
|
+ t->node ? (gint)wp_proxy_get_bound_id (WP_PROXY (t->node)) : -1,
|
|
+ NULL);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return g_steal_pointer (&res);
|
|
+}
|
|
+
|
|
+static gint
|
|
+wp_filters_api_get_filter_from_target (WpFiltersApi * self,
|
|
+ const gchar *direction, gint target_id)
|
|
+{
|
|
+ WpDirection dir = WP_DIRECTION_INPUT;
|
|
+ GList *filters;
|
|
+ gboolean found = FALSE;
|
|
+ const gchar *target = NULL;
|
|
+ gint res = target_id;
|
|
+
|
|
+ g_return_val_if_fail (direction, res);
|
|
+
|
|
+ /* Get the filters for the given direction */
|
|
+ if (g_str_equal (direction, "output") || g_str_equal (direction, "Output"))
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ filters = self->filters[dir];
|
|
+
|
|
+ /* Find the first target matching target_id */
|
|
+ while (filters) {
|
|
+ Filter *f = (Filter *) filters->data;
|
|
+ if (f->enabled) {
|
|
+ gint f_target_id;
|
|
+ g_autoptr (WpSpaJson) f_target = wp_filters_api_get_filter_target (self,
|
|
+ direction, f->link_group);
|
|
+ if (f_target && wp_spa_json_is_object (f_target) &&
|
|
+ wp_spa_json_object_get (f_target, "bound_id", "i", &f_target_id, NULL)
|
|
+ && f_target_id == target_id) {
|
|
+ target = f->target;
|
|
+ found = TRUE;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Advance */
|
|
+ filters = g_list_next (filters);
|
|
+ }
|
|
+
|
|
+ /* Just return if target was not found */
|
|
+ if (!found)
|
|
+ return res;
|
|
+
|
|
+ /* Get the last filter node ID of the target found */
|
|
+ filters = self->filters[dir];
|
|
+ while (filters) {
|
|
+ Filter *f = (Filter *) filters->data;
|
|
+ if ((f->target == target ||
|
|
+ (f->target && target && g_str_equal (f->target, target))) &&
|
|
+ f->enabled)
|
|
+ res = wp_proxy_get_bound_id (WP_PROXY (f->node));
|
|
+
|
|
+ /* Advance */
|
|
+ filters = g_list_next (filters);
|
|
+ }
|
|
+
|
|
+ return res;
|
|
+}
|
|
+
|
|
+static void
|
|
+sync_changed (WpCore * core, GAsyncResult * res, WpFiltersApi * self)
|
|
+{
|
|
+ g_autoptr (GError) error = NULL;
|
|
+
|
|
+ if (!wp_core_sync_finish (core, res, &error)) {
|
|
+ wp_warning_object (self, "core sync error: %s", error->message);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ g_signal_emit (self, signals[SIGNAL_CHANGED], 0);
|
|
+}
|
|
+
|
|
+static WpNode *
|
|
+find_target_node (WpFiltersApi *self, WpSpaJson *props_json)
|
|
+{
|
|
+ g_auto (GValue) item = G_VALUE_INIT;
|
|
+ g_autoptr (WpIterator) it = NULL;
|
|
+ g_autoptr (WpObjectInterest) interest = NULL;
|
|
+
|
|
+ /* Make sure the properties are a JSON object */
|
|
+ if (!props_json || !wp_spa_json_is_object (props_json)) {
|
|
+ wp_warning_object (self, "Target properties must be a JSON object");
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ /* Create the object intereset with the target properties */
|
|
+ interest = wp_object_interest_new (WP_TYPE_NODE, NULL);
|
|
+ it = wp_spa_json_new_iterator (props_json);
|
|
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
+ WpSpaJson *j = g_value_get_boxed (&item);
|
|
+ g_autofree gchar *key = NULL;
|
|
+ WpSpaJson *value_json;
|
|
+ g_autofree gchar *value = NULL;
|
|
+
|
|
+ key = wp_spa_json_parse_string (j);
|
|
+ g_value_unset (&item);
|
|
+ if (!wp_iterator_next (it, &item)) {
|
|
+ wp_warning_object (self,
|
|
+ "Could not get valid key-value pairs from target properties");
|
|
+ break;
|
|
+ }
|
|
+ value_json = g_value_get_boxed (&item);
|
|
+ value = wp_spa_json_parse_string (value_json);
|
|
+ if (!value) {
|
|
+ wp_warning_object (self,
|
|
+ "Could not get '%s' value from target properties", key);
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ wp_object_interest_add_constraint (interest, WP_CONSTRAINT_TYPE_PW_PROPERTY,
|
|
+ key, WP_CONSTRAINT_VERB_MATCHES, g_variant_new_string (value));
|
|
+ }
|
|
+
|
|
+ return wp_object_manager_lookup_full (self->nodes_om,
|
|
+ wp_object_interest_ref (interest));
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+reevaluate_targets (WpFiltersApi *self)
|
|
+{
|
|
+ g_autoptr (WpMetadata) m = NULL;
|
|
+ const gchar *json_str;
|
|
+ g_autoptr (WpSpaJson) json = NULL;
|
|
+ g_auto (GValue) item = G_VALUE_INIT;
|
|
+ g_autoptr (WpIterator) it = NULL;
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ g_hash_table_remove_all (self->targets);
|
|
+
|
|
+ /* Make sure the metadata exists */
|
|
+ m = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA, NULL);
|
|
+ if (!m)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Don't update anything if the metadata value is not set */
|
|
+ json_str = wp_metadata_find (m, 0, "filters.configured.targets", NULL);
|
|
+ if (!json_str)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Make sure the metadata value is an object */
|
|
+ json = wp_spa_json_new_from_string (json_str);
|
|
+ if (!json || !wp_spa_json_is_object (json)) {
|
|
+ wp_warning_object (self,
|
|
+ "ignoring metadata value as it is not a JSON object: %s", json_str);
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ /* Find the target node for each target, and add it to the hash table */
|
|
+ it = wp_spa_json_new_iterator (json);
|
|
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
+ WpSpaJson *j = g_value_get_boxed (&item);
|
|
+ g_autofree gchar *key = NULL;
|
|
+ WpSpaJson *value;
|
|
+ gboolean exclusive = FALSE;
|
|
+ g_autoptr (WpSpaJson) props = NULL;
|
|
+ g_autoptr (WpNode) target_node = NULL;
|
|
+ Target *curr_target;
|
|
+
|
|
+ key = wp_spa_json_parse_string (j);
|
|
+ g_value_unset (&item);
|
|
+ if (!wp_iterator_next (it, &item)) {
|
|
+ wp_warning_object (self,
|
|
+ "Could not get valid key-value pairs from target object");
|
|
+ break;
|
|
+ }
|
|
+ value = g_value_get_boxed (&item);
|
|
+ if (!value || !wp_spa_json_is_object (value)) {
|
|
+ wp_warning_object (self, "Target value must be a JSON object");
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ /* Get exclusive */
|
|
+ wp_spa_json_object_get (value, "exclusive", "b", &exclusive, NULL);
|
|
+
|
|
+ /* Get target node */
|
|
+ wp_spa_json_object_get (value, "props", "J", &props, NULL);
|
|
+ if (props)
|
|
+ target_node = find_target_node (self, props);
|
|
+
|
|
+ /* Update values if target exists in the table, otherwise add new target */
|
|
+ curr_target = g_hash_table_lookup (self->targets, key);
|
|
+ if (curr_target) {
|
|
+ if (curr_target->exclusive != exclusive) {
|
|
+ curr_target->exclusive = exclusive;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ if (curr_target->node != target_node) {
|
|
+ g_clear_object (&curr_target->node);
|
|
+ curr_target->node = g_steal_pointer (&target_node);
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else {
|
|
+ g_hash_table_insert (self->targets, g_strdup (key),
|
|
+ target_new (exclusive, target_node));
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return changed;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+update_values_from_metadata (WpFiltersApi * self, Filter *f)
|
|
+{
|
|
+ g_autoptr (WpMetadata) m = NULL;
|
|
+ const gchar *f_stream_name;
|
|
+ const gchar *f_node_name;
|
|
+ const gchar *json_str;
|
|
+ g_autoptr (WpSpaJson) json = NULL;
|
|
+ g_auto (GValue) item = G_VALUE_INIT;
|
|
+ g_autoptr (WpIterator) it = NULL;
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ /* Make sure the metadata exists */
|
|
+ m = wp_object_manager_lookup (self->metadata_om, WP_TYPE_METADATA, NULL);
|
|
+ if (!m)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Make sure both the stream and node are available */
|
|
+ if (!f->stream || !f->node)
|
|
+ return FALSE;
|
|
+ f_stream_name = wp_pipewire_object_get_property (
|
|
+ WP_PIPEWIRE_OBJECT (f->stream), PW_KEY_NODE_NAME);
|
|
+ f_node_name = wp_pipewire_object_get_property (
|
|
+ WP_PIPEWIRE_OBJECT (f->node), PW_KEY_NODE_NAME);
|
|
+
|
|
+ /* Don't update anything if the metadata value is not set */
|
|
+ json_str = wp_metadata_find (m, 0, "filters.configured.filters", NULL);
|
|
+ if (!json_str)
|
|
+ return FALSE;
|
|
+
|
|
+ /* Make sure the metadata value is an array */
|
|
+ json = wp_spa_json_new_from_string (json_str);
|
|
+ if (!json || !wp_spa_json_is_array (json)) {
|
|
+ wp_warning_object (self,
|
|
+ "ignoring metadata value as it is not a JSON array: %s", json_str);
|
|
+ return FALSE;
|
|
+ }
|
|
+
|
|
+ /* Find the filter values in the metadata */
|
|
+ it = wp_spa_json_new_iterator (json);
|
|
+ for (; wp_iterator_next (it, &item); g_value_unset (&item)) {
|
|
+ WpSpaJson *j = g_value_get_boxed (&item);
|
|
+ g_autofree gchar *stream_name = NULL;
|
|
+ g_autofree gchar *node_name = NULL;
|
|
+ g_autofree gchar *direction = NULL;
|
|
+ g_autofree gchar *target = NULL;
|
|
+ g_autofree gchar *mode = NULL;
|
|
+ WpDirection dir = WP_DIRECTION_INPUT;
|
|
+ gint priority;
|
|
+
|
|
+ if (!j || !wp_spa_json_is_object (j))
|
|
+ continue;
|
|
+
|
|
+ /* Parse mandatory fields */
|
|
+ if (!wp_spa_json_object_get (j, "stream-name", "s", &stream_name,
|
|
+ "node-name", "s", &node_name, "direction", "s", &direction, NULL)) {
|
|
+ g_autofree gchar *str = wp_spa_json_to_string (j);
|
|
+ wp_warning_object (self,
|
|
+ "failed to parse stream-name, node-name and direction in filter: %s",
|
|
+ str);
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* Make sure direction is valid */
|
|
+ if (g_str_equal (direction, "input")) {
|
|
+ dir = WP_DIRECTION_INPUT;
|
|
+ } else if (g_str_equal (direction, "output")) {
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ } else {
|
|
+ g_autofree gchar *str = wp_spa_json_to_string (j);
|
|
+ wp_warning_object (self,
|
|
+ "direction %s is not valid for filter: %s", direction, str);
|
|
+ }
|
|
+
|
|
+ /* Find first filter matching stream-name, node-name and direction */
|
|
+ if (g_str_equal (f_stream_name, stream_name) &&
|
|
+ g_str_equal (f_node_name, node_name) &&
|
|
+ f->direction == dir) {
|
|
+
|
|
+ /* Update target */
|
|
+ if (wp_spa_json_object_get (j, "target", "s", &target, NULL)) {
|
|
+ if (!f->target || !g_str_equal (f->target, target)) {
|
|
+ g_clear_pointer (&f->target, g_free);
|
|
+ f->target = g_strdup (target);
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else {
|
|
+ if (f->target) {
|
|
+ g_clear_pointer (&f->target, g_free);
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Update mode */
|
|
+ if (wp_spa_json_object_get (j, "mode", "s", &mode, NULL)) {
|
|
+ if (g_str_equal (mode, "always")) {
|
|
+ if (!f->enabled) {
|
|
+ f->enabled = TRUE;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else if (g_str_equal (mode, "never")) {
|
|
+ if (f->enabled) {
|
|
+ f->enabled = FALSE;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else if (g_str_equal (mode, "playback-only")) {
|
|
+ if (f->enabled != (self->n_playback_stream_nodes > 0)) {
|
|
+ f->enabled = self->n_playback_stream_nodes > 0;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else if (g_str_equal (mode, "capture-only")) {
|
|
+ if (f->enabled != (self->n_capture_stream_nodes > 0)) {
|
|
+ f->enabled = self->n_capture_stream_nodes > 0;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ } else {
|
|
+ wp_warning_object (self,
|
|
+ "The '%s' value is not a valid for the 'mode' filter field",
|
|
+ mode);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Update priority */
|
|
+ if (wp_spa_json_object_get (j, "priority", "i", &priority, NULL)) {
|
|
+ if (f->priority != priority) {
|
|
+ f->priority = priority;
|
|
+ changed = TRUE;
|
|
+ }
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return changed;
|
|
+}
|
|
+
|
|
+static gboolean
|
|
+reevaluate_filters (WpFiltersApi *self, WpDirection direction)
|
|
+{
|
|
+ GList *filters;
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ /* Update filter values */
|
|
+ filters = self->filters[direction];
|
|
+ while (filters) {
|
|
+ Filter *f = (Filter *) filters->data;
|
|
+ if (update_values_from_metadata (self, f))
|
|
+ changed = TRUE;
|
|
+ filters = g_list_next (filters);
|
|
+ }
|
|
+
|
|
+ /* Sort filters if changed */
|
|
+ if (changed)
|
|
+ self->filters[direction] = g_list_sort (self->filters[direction],
|
|
+ (GCompareFunc) filter_compare_func);
|
|
+
|
|
+ return changed;
|
|
+}
|
|
+
|
|
+static void
|
|
+schedule_changed (WpFiltersApi * self)
|
|
+{
|
|
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
+ g_return_if_fail (core);
|
|
+
|
|
+ wp_core_sync_closure (core, NULL, g_cclosure_new_object (
|
|
+ G_CALLBACK (sync_changed), G_OBJECT (self)));
|
|
+}
|
|
+
|
|
+static void
|
|
+on_stream_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ const gchar* media_class = wp_pipewire_object_get_property (proxy,
|
|
+ PW_KEY_MEDIA_CLASS);
|
|
+
|
|
+ if (g_str_equal (media_class, "Stream/Output/Audio"))
|
|
+ self->n_playback_stream_nodes++;
|
|
+ else if (g_str_equal (media_class, "Stream/Input/Audio"))
|
|
+ self->n_capture_stream_nodes++;
|
|
+}
|
|
+
|
|
+static void
|
|
+on_stream_node_removed (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ const gchar* media_class = wp_pipewire_object_get_property (proxy,
|
|
+ PW_KEY_MEDIA_CLASS);
|
|
+
|
|
+ if (g_str_equal (media_class, "Stream/Output/Audio") &&
|
|
+ self->n_playback_stream_nodes > 0)
|
|
+ self->n_playback_stream_nodes--;
|
|
+ else if (g_str_equal (media_class, "Stream/Input/Audio") &&
|
|
+ self->n_capture_stream_nodes > 0)
|
|
+ self->n_capture_stream_nodes--;
|
|
+}
|
|
+
|
|
+static void
|
|
+on_stream_nodes_changed (WpObjectManager *om, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ /* Reevaluate everything */
|
|
+ for (guint i = 0; i < 2; i++)
|
|
+ if (reevaluate_filters (self, i))
|
|
+ changed = TRUE;
|
|
+
|
|
+ if (changed)
|
|
+ schedule_changed (self);
|
|
+}
|
|
+
|
|
+static void
|
|
+on_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ reevaluate_targets (self);
|
|
+}
|
|
+
|
|
+static void
|
|
+on_node_removed (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ reevaluate_targets (self);
|
|
+}
|
|
+
|
|
+static void
|
|
+on_filter_node_added (WpObjectManager *om, WpPipewireObject *proxy, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ const gchar *key;
|
|
+ WpDirection dir;
|
|
+ gboolean is_stream;
|
|
+ GList *found;
|
|
+
|
|
+ /* Get direction */
|
|
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_MEDIA_CLASS);
|
|
+ if (!key)
|
|
+ return;
|
|
+
|
|
+ if (g_str_equal (key, "Audio/Sink") ||
|
|
+ g_str_equal (key, "Stream/Output/Audio")) {
|
|
+ dir = WP_DIRECTION_INPUT;
|
|
+ } else if (g_str_equal (key, "Audio/Source") ||
|
|
+ g_str_equal (key, "Stream/Input/Audio")) {
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ } else {
|
|
+ wp_debug_object (self, "ignoring node with media class: %s", key);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* Check whether the proxy is a stream or not */
|
|
+ is_stream = FALSE;
|
|
+ if (g_str_equal (key, "Stream/Output/Audio") ||
|
|
+ g_str_equal (key, "Stream/Input/Audio"))
|
|
+ is_stream = TRUE;
|
|
+
|
|
+ /* We use the link group as filter name */
|
|
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_NODE_LINK_GROUP);
|
|
+ if (!key) {
|
|
+ wp_debug_object (self, "ignoring node without link group");
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* Check if the filter already exists, and add it if it does not exist */
|
|
+ found = g_list_find_custom (self->filters[dir], key,
|
|
+ (GCompareFunc) filter_equal_func);
|
|
+ if (!found) {
|
|
+ Filter *f = filter_new (key, dir, is_stream, WP_NODE (proxy));
|
|
+ update_values_from_metadata (self, f);
|
|
+ self->filters[dir] = g_list_insert_sorted (self->filters[dir],
|
|
+ f, (GCompareFunc) filter_compare_func);
|
|
+ } else {
|
|
+ Filter *f = found->data;
|
|
+ if (is_stream) {
|
|
+ g_clear_object (&f->stream);
|
|
+ f->stream = g_object_ref (WP_NODE (proxy));
|
|
+ } else {
|
|
+ g_clear_object (&f->node);
|
|
+ f->node = g_object_ref (WP_NODE (proxy));
|
|
+ }
|
|
+ update_values_from_metadata (self, f);
|
|
+ self->filters[dir] = g_list_sort (self->filters[dir],
|
|
+ (GCompareFunc) filter_compare_func);
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+on_filter_node_removed (WpObjectManager *om, WpPipewireObject *proxy,
|
|
+ gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+
|
|
+ const gchar *key;
|
|
+ WpDirection dir;
|
|
+ GList *found;
|
|
+
|
|
+ /* Get direction */
|
|
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_MEDIA_CLASS);
|
|
+ if (!key)
|
|
+ return;
|
|
+
|
|
+ if (g_str_equal (key, "Audio/Sink") ||
|
|
+ g_str_equal (key, "Stream/Output/Audio")) {
|
|
+ dir = WP_DIRECTION_INPUT;
|
|
+ } else if (g_str_equal (key, "Audio/Source") ||
|
|
+ g_str_equal (key, "Stream/Input/Audio")) {
|
|
+ dir = WP_DIRECTION_OUTPUT;
|
|
+ } else {
|
|
+ wp_debug_object (self, "ignoring node with media class: %s", key);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* We use the link group as filter name */
|
|
+ key = wp_pipewire_object_get_property (proxy, PW_KEY_NODE_LINK_GROUP);
|
|
+ if (!key) {
|
|
+ wp_debug_object (self, "ignoring node without link group");
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ /* Find and remove the filter */
|
|
+ found = g_list_find_custom (self->filters[dir], key,
|
|
+ (GCompareFunc) filter_equal_func);
|
|
+ if (found) {
|
|
+ self->filters[dir] = g_list_remove (self->filters[dir], found->data);
|
|
+ }
|
|
+}
|
|
+
|
|
+static void
|
|
+on_metadata_changed (WpMetadata *m, guint32 subject,
|
|
+ const gchar *key, const gchar *type, const gchar *value, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ /* Reevaluate everything */
|
|
+ if (reevaluate_targets (self))
|
|
+ changed = TRUE;
|
|
+ for (guint i = 0; i < 2; i++)
|
|
+ if (reevaluate_filters (self, i))
|
|
+ changed = TRUE;
|
|
+
|
|
+ if (changed)
|
|
+ schedule_changed (self);
|
|
+}
|
|
+
|
|
+static void
|
|
+on_metadata_added (WpObjectManager *om, WpMetadata *metadata, gpointer d)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (d);
|
|
+ gboolean changed = FALSE;
|
|
+
|
|
+ /* Handle the changed signal */
|
|
+ g_signal_connect_object (metadata, "changed",
|
|
+ G_CALLBACK (on_metadata_changed), self, 0);
|
|
+
|
|
+ /* Reevaluate everything */
|
|
+ if (reevaluate_targets (self))
|
|
+ changed = TRUE;
|
|
+ for (guint i = 0; i < 2; i++)
|
|
+ if (reevaluate_filters (self, i))
|
|
+ changed = TRUE;
|
|
+
|
|
+ if (changed)
|
|
+ schedule_changed (self);
|
|
+}
|
|
+
|
|
+static void
|
|
+on_metadata_installed (WpObjectManager * om, WpFiltersApi * self)
|
|
+{
|
|
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
+
|
|
+ /* Create the stream nodes object manager */
|
|
+ self->stream_nodes_om = wp_object_manager_new ();
|
|
+ wp_object_manager_add_interest (self->stream_nodes_om, WP_TYPE_NODE,
|
|
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Stream/*/Audio",
|
|
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
|
|
+ NULL);
|
|
+ wp_object_manager_request_object_features (self->stream_nodes_om,
|
|
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
|
|
+ g_signal_connect_object (self->stream_nodes_om, "object-added",
|
|
+ G_CALLBACK (on_stream_node_added), self, 0);
|
|
+ g_signal_connect_object (self->stream_nodes_om, "object-removed",
|
|
+ G_CALLBACK (on_stream_node_removed), self, 0);
|
|
+ g_signal_connect_object (self->stream_nodes_om, "objects-changed",
|
|
+ G_CALLBACK (on_stream_nodes_changed), self, 0);
|
|
+ wp_core_install_object_manager (core, self->stream_nodes_om);
|
|
+
|
|
+ /* Create the nodes object manager */
|
|
+ self->nodes_om = wp_object_manager_new ();
|
|
+ wp_object_manager_add_interest (self->nodes_om, WP_TYPE_NODE,
|
|
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Audio/*",
|
|
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "-",
|
|
+ NULL);
|
|
+ wp_object_manager_request_object_features (self->nodes_om,
|
|
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
|
|
+ g_signal_connect_object (self->nodes_om, "object-added",
|
|
+ G_CALLBACK (on_node_added), self, 0);
|
|
+ g_signal_connect_object (self->nodes_om, "object-removed",
|
|
+ G_CALLBACK (on_node_removed), self, 0);
|
|
+ g_signal_connect_object (self->nodes_om, "objects-changed",
|
|
+ G_CALLBACK (schedule_changed), self, G_CONNECT_SWAPPED);
|
|
+ wp_core_install_object_manager (core, self->nodes_om);
|
|
+
|
|
+ /* Create the filter nodes object manager */
|
|
+ self->filter_nodes_om = wp_object_manager_new ();
|
|
+ wp_object_manager_add_interest (self->filter_nodes_om, WP_TYPE_NODE,
|
|
+ WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_NODE_LINK_GROUP, "+",
|
|
+ NULL);
|
|
+ wp_object_manager_request_object_features (self->filter_nodes_om,
|
|
+ WP_TYPE_NODE, WP_OBJECT_FEATURES_ALL);
|
|
+ g_signal_connect_object (self->filter_nodes_om, "object-added",
|
|
+ G_CALLBACK (on_filter_node_added), self, 0);
|
|
+ g_signal_connect_object (self->filter_nodes_om, "object-removed",
|
|
+ G_CALLBACK (on_filter_node_removed), self, 0);
|
|
+ g_signal_connect_object (self->filter_nodes_om, "objects-changed",
|
|
+ G_CALLBACK (schedule_changed), self, G_CONNECT_SWAPPED);
|
|
+ wp_core_install_object_manager (core, self->filter_nodes_om);
|
|
+
|
|
+ wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
|
|
+}
|
|
+
|
|
+static void
|
|
+wp_filters_api_enable (WpPlugin * plugin, WpTransition * transition)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (plugin);
|
|
+ g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
|
|
+
|
|
+ self->targets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
|
|
+ (GDestroyNotify) target_free);
|
|
+
|
|
+ /* 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", "filters",
|
|
+ NULL);
|
|
+ wp_object_manager_request_object_features (self->metadata_om,
|
|
+ WP_TYPE_METADATA, WP_OBJECT_FEATURES_ALL);
|
|
+ g_signal_connect_object (self->metadata_om, "object-added",
|
|
+ G_CALLBACK (on_metadata_added), self, 0);
|
|
+ g_signal_connect_object (self->metadata_om, "installed",
|
|
+ G_CALLBACK (on_metadata_installed), self, 0);
|
|
+ wp_core_install_object_manager (core, self->metadata_om);
|
|
+}
|
|
+
|
|
+static void
|
|
+wp_filters_api_disable (WpPlugin * plugin)
|
|
+{
|
|
+ WpFiltersApi * self = WP_FILTERS_API (plugin);
|
|
+
|
|
+ for (guint i = 0; i < 2; i++) {
|
|
+ if (self->filters[i]) {
|
|
+ g_list_free_full (self->filters[i], (GDestroyNotify) filter_free);
|
|
+ self->filters[i] = NULL;
|
|
+ }
|
|
+ }
|
|
+ g_clear_pointer (&self->targets, g_hash_table_unref);
|
|
+
|
|
+ g_clear_object (&self->metadata_om);
|
|
+ g_clear_object (&self->stream_nodes_om);
|
|
+ g_clear_object (&self->nodes_om);
|
|
+ g_clear_object (&self->filter_nodes_om);
|
|
+}
|
|
+
|
|
+static void
|
|
+wp_filters_api_class_init (WpFiltersApiClass * klass)
|
|
+{
|
|
+ WpPluginClass *plugin_class = (WpPluginClass *) klass;
|
|
+
|
|
+ plugin_class->enable = wp_filters_api_enable;
|
|
+ plugin_class->disable = wp_filters_api_disable;
|
|
+
|
|
+ signals[ACTION_IS_FILTER_ENABLED] = g_signal_new_class_handler (
|
|
+ "is-filter-enabled", G_TYPE_FROM_CLASS (klass),
|
|
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
+ (GCallback) wp_filters_api_is_filter_enabled,
|
|
+ NULL, NULL, NULL,
|
|
+ G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_STRING);
|
|
+
|
|
+ signals[ACTION_GET_FILTER_TARGET] = g_signal_new_class_handler (
|
|
+ "get-filter-target", G_TYPE_FROM_CLASS (klass),
|
|
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
+ (GCallback) wp_filters_api_get_filter_target,
|
|
+ NULL, NULL, NULL,
|
|
+ WP_TYPE_SPA_JSON, 2, G_TYPE_STRING, G_TYPE_STRING);
|
|
+
|
|
+ signals[ACTION_GET_FILTER_FROM_TARGET] = g_signal_new_class_handler (
|
|
+ "get-filter-from-target", G_TYPE_FROM_CLASS (klass),
|
|
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
+ (GCallback) wp_filters_api_get_filter_from_target,
|
|
+ NULL, NULL, NULL,
|
|
+ G_TYPE_INT, 2, G_TYPE_STRING, G_TYPE_INT);
|
|
+
|
|
+ signals[SIGNAL_CHANGED] = g_signal_new (
|
|
+ "changed", G_TYPE_FROM_CLASS (klass),
|
|
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
|
+ G_TYPE_NONE, 0);
|
|
+}
|
|
+
|
|
+WP_PLUGIN_EXPORT gboolean
|
|
+wireplumber__module_init (WpCore * core, GVariant * args, GError ** error)
|
|
+{
|
|
+ wp_plugin_register (g_object_new (wp_filters_api_get_type (),
|
|
+ "name", "filters-api",
|
|
+ "core", core,
|
|
+ NULL));
|
|
+ return TRUE;
|
|
+}
|
|
diff --git a/modules/module-lua-scripting/api/json.c b/modules/module-lua-scripting/api/json.c
|
|
index 2883a021..3d486be8 100644
|
|
--- a/modules/module-lua-scripting/api/json.c
|
|
+++ b/modules/module-lua-scripting/api/json.c
|
|
@@ -264,7 +264,7 @@ spa_json_array_new (lua_State *L)
|
|
break;
|
|
}
|
|
default:
|
|
- luaL_error (L, "Json does not support lua type ",
|
|
+ luaL_error (L, "Json does not support lua type %s",
|
|
lua_typename(L, lua_type(L, -1)));
|
|
break;
|
|
}
|
|
diff --git a/src/config/main.lua.d/40-device-defaults.lua b/src/config/main.lua.d/40-device-defaults.lua
|
|
index 19202914..b87eec51 100644
|
|
--- a/src/config/main.lua.d/40-device-defaults.lua
|
|
+++ b/src/config/main.lua.d/40-device-defaults.lua
|
|
@@ -10,15 +10,6 @@ device_defaults.properties = {
|
|
-- the default volumes to apply to ACP device nodes, in the linear scale
|
|
--["default-volume"] = 0.064,
|
|
--["default-input-volume"] = 1.0,
|
|
-
|
|
- -- Whether to auto-switch to echo cancel sink and source nodes or not
|
|
- ["auto-echo-cancel"] = true,
|
|
-
|
|
- -- Sets the default echo-cancel-sink node name to automatically switch to
|
|
- ["echo-cancel-sink-name"] = "echo-cancel-sink",
|
|
-
|
|
- -- Sets the default echo-cancel-source node name to automatically switch to
|
|
- ["echo-cancel-source-name"] = "echo-cancel-source",
|
|
}
|
|
|
|
-- Sets persistent device profiles that should never change when wireplumber is
|
|
@@ -48,13 +39,13 @@ device_defaults.profile_priorities = {
|
|
},
|
|
-- lower the index higher the priority
|
|
priorities = {
|
|
- -- "a2dp-sink-sbc",
|
|
- -- "a2dp-sink-aptx_ll",
|
|
- -- "a2dp-sink-aptx",
|
|
- -- "a2dp-sink-aptx_hd",
|
|
- -- "a2dp-sink-ldac",
|
|
- -- "a2dp-sink-aac",
|
|
- -- "a2dp-sink-sbc_xq",
|
|
+ "a2dp-sink-aptx_ll",
|
|
+ "a2dp-sink-aptx",
|
|
+ "a2dp-sink-aptx_hd",
|
|
+ "a2dp-sink-ldac",
|
|
+ "a2dp-sink-aac",
|
|
+ "a2dp-sink-sbc",
|
|
+ "a2dp-sink-sbc_xq",
|
|
}
|
|
},
|
|
}
|
|
diff --git a/src/config/main.lua.d/40-stream-defaults.lua b/src/config/main.lua.d/40-stream-defaults.lua
|
|
index b869099b..d25aab0d 100644
|
|
--- a/src/config/main.lua.d/40-stream-defaults.lua
|
|
+++ b/src/config/main.lua.d/40-stream-defaults.lua
|
|
@@ -6,7 +6,7 @@ stream_defaults.properties = {
|
|
["restore-props"] = true,
|
|
|
|
-- whether to restore the last stream target or not
|
|
- ["restore-target"] = true,
|
|
+ ["restore-target"] = false,
|
|
|
|
-- the default channel volume for new streams whose props were never saved
|
|
-- previously. This is only used if "restore-props" is set to true.
|
|
diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua
|
|
index 83d0a3b2..412d47a8 100644
|
|
--- a/src/config/policy.lua.d/10-default-policy.lua
|
|
+++ b/src/config/policy.lua.d/10-default-policy.lua
|
|
@@ -12,6 +12,9 @@ default_policy.policy = {
|
|
-- surround audio if echo-cancel is enabled.
|
|
["filter.forward-format"] = false,
|
|
|
|
+ -- Whether to enable smart filter policy or not (experimental feature)
|
|
+ ["filter.smart"] = true,
|
|
+
|
|
-- Set to 'true' to disable channel splitting & merging on nodes and enable
|
|
-- passthrough of audio in the same format as the format of the device.
|
|
-- Note that this breaks JACK support; it is generally not recommended
|
|
@@ -71,6 +74,12 @@ function default_policy.enable()
|
|
-- API to access default nodes from scripts
|
|
load_module("default-nodes-api")
|
|
|
|
+ -- Load smart filter policy if requested
|
|
+ if default_policy.policy["filter.smart"] then
|
|
+ load_script("filters-metadata.lua", default_policy.filters_metadata)
|
|
+ load_module("filters-api")
|
|
+ end
|
|
+
|
|
-- API to access mixer controls, needed for volume ducking
|
|
load_module("mixer-api")
|
|
|
|
diff --git a/src/config/policy.lua.d/30-filters-config.lua b/src/config/policy.lua.d/30-filters-config.lua
|
|
new file mode 100644
|
|
index 00000000..8e8725fc
|
|
--- /dev/null
|
|
+++ b/src/config/policy.lua.d/30-filters-config.lua
|
|
@@ -0,0 +1,80 @@
|
|
+-- The smart filter policy configuration.
|
|
+-- You need to enable "filter.smart" in 10-default-policy.lua
|
|
+--
|
|
+
|
|
+-- The default filter metadata configuration when wireplumber starts. They also
|
|
+-- can be changed at runtime.
|
|
+default_policy.filters_metadata = {
|
|
+ ["filters"] = {
|
|
+ -- Input filters (meant to be linked with Audio/Sink device nodes)
|
|
+ {
|
|
+ ["stream-name"] = "output.virtual-sink", -- loopback playback
|
|
+ ["node-name"] = "input.virtual-sink", -- loopback sink
|
|
+ ["direction"] = "input", -- can only be 'input' or 'output'
|
|
+ ["target"] = nil, -- if nil, the default node will be used as target
|
|
+ ["mode"] = "always", -- can be 'always', 'never', 'capture-only' or 'playback-only'
|
|
+ ["priority"] = 30,
|
|
+ },
|
|
+ {
|
|
+ ["stream-name"] = "filter-chain-playback", -- filter-chain playback
|
|
+ ["node-name"] = "filter-chain-sink", -- filter-chain sink
|
|
+ ["direction"] = "input", -- can only be 'input' or 'output'
|
|
+ ["target"] = "speakers", -- if nil, the default node will be used as target
|
|
+ ["mode"] = "always", -- can be 'always', 'never', 'capture-only' or 'playback-only'
|
|
+ ["priority"] = 20,
|
|
+ },
|
|
+ {
|
|
+ ["stream-name"] = "echo-cancel-playback", -- echo-cancel playback
|
|
+ ["node-name"] = "echo-cancel-sink", -- echo-cancel sink
|
|
+ ["direction"] = "input", -- can only be 'input' or 'output'
|
|
+ ["target"] = "speakers", -- if nil, the default node will be used as target
|
|
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
|
|
+ ["priority"] = 10,
|
|
+ },
|
|
+
|
|
+ -- Output filters (meant to be linked with Audio/Source device nodes)
|
|
+ {
|
|
+ ["stream-name"] = "input.virtual-source", -- loopback capture
|
|
+ ["node-name"] = "output.virtual-source", -- loopback source
|
|
+ ["direction"] = "output", -- can only be 'input' or 'output'
|
|
+ ["target"] = nil, -- if nil, the default node will be used as target
|
|
+ ["mode"] = "always", -- can be 'always', 'never', 'playback-only' or 'capture-only'
|
|
+ ["priority"] = 30,
|
|
+ },
|
|
+ {
|
|
+ ["stream-name"] = "filter-chain-capture", -- filter-chain capture
|
|
+ ["node-name"] = "filter-chain-source", -- filter-chain source
|
|
+ ["direction"] = "output", -- can only be 'input' or 'output'
|
|
+ ["target"] = "microphone", -- if nil, the default node will be used as target
|
|
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
|
|
+ ["priority"] = 20,
|
|
+ },
|
|
+ {
|
|
+ ["stream-name"] = "echo-cancel-capture", -- echo-cancel capture
|
|
+ ["node-name"] = "echo-cancel-source", -- echo-cancel source
|
|
+ ["direction"] = "output", -- can only be 'input' or 'output'
|
|
+ ["target"] = "microphone", -- if nil, the default node will be used as target
|
|
+ ["mode"] = "capture-only", -- can be 'always', 'never', 'playback-only' or 'capture-only'
|
|
+ ["priority"] = 10,
|
|
+ }
|
|
+ },
|
|
+
|
|
+ -- The filter targets
|
|
+ ["targets"] = {
|
|
+ ["speakers"] = {
|
|
+ ["exclusive"] = false,
|
|
+ ["props"] = {
|
|
+ ["media.class"] = "Audio/Sink",
|
|
+ ["alsa.card_name"] = "sof-nau8821-max",
|
|
+ ["device.profile.description"] = "Speaker",
|
|
+ }
|
|
+ },
|
|
+ ["microphone"] = {
|
|
+ ["exclusive"] = false,
|
|
+ ["props"] = {
|
|
+ ["media.class"] = "Audio/Source",
|
|
+ ["alsa.card_name"] = "sof-nau8821-max",
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/scripts/filters-metadata.lua b/src/scripts/filters-metadata.lua
|
|
new file mode 100644
|
|
index 00000000..49b4d0e8
|
|
--- /dev/null
|
|
+++ b/src/scripts/filters-metadata.lua
|
|
@@ -0,0 +1,42 @@
|
|
+-- WirePlumber
|
|
+--
|
|
+-- Copyright © 2023 Collabora Ltd.
|
|
+-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
+--
|
|
+-- SPDX-License-Identifier: MIT
|
|
+
|
|
+-- Receive script arguments
|
|
+local config = ... or {}
|
|
+config["filters"] = config["filters"] or {}
|
|
+config["targets"] = config["targets"] or {}
|
|
+
|
|
+f_metadata = ImplMetadata("filters")
|
|
+f_metadata:activate(Features.ALL, function (m, e)
|
|
+ if e then
|
|
+ Log.warning("failed to activate filters metadata: " .. tostring(e))
|
|
+ return
|
|
+ end
|
|
+
|
|
+ Log.info("activated filters metadata")
|
|
+
|
|
+ -- Set filters metadata
|
|
+ local filters = {}
|
|
+ for _, f in ipairs(config["filters"]) do
|
|
+ table.insert (filters, Json.Object (f))
|
|
+ end
|
|
+ local filters_json = Json.Array (filters)
|
|
+ m:set (0, "filters.configured.filters", "Spa:String:JSON",
|
|
+ filters_json:to_string())
|
|
+
|
|
+ -- Set targets metadata
|
|
+ local targets = {}
|
|
+ for name, value in pairs(config["targets"]) do
|
|
+ targets[name] = Json.Object {
|
|
+ exclusive = value.exclusive and true or false,
|
|
+ props = Json.Object (value.props)
|
|
+ }
|
|
+ end
|
|
+ local targets_json = Json.Object (targets)
|
|
+ m:set (0, "filters.configured.targets", "Spa:String:JSON",
|
|
+ targets_json:to_string())
|
|
+end)
|
|
diff --git a/src/scripts/policy-device-profile.lua b/src/scripts/policy-device-profile.lua
|
|
index af10f9b7..8d3a6d7e 100644
|
|
--- a/src/scripts/policy-device-profile.lua
|
|
+++ b/src/scripts/policy-device-profile.lua
|
|
@@ -34,12 +34,12 @@ createIntrestObjects(self.config.persistent)
|
|
createIntrestObjects(self.config.priorities)
|
|
|
|
-- Checks whether a device profile is persistent or not
|
|
-function isProfilePersistent(device_props, profile_name)
|
|
- for _, p in ipairs(self.config.persistent or {}) do
|
|
+function isProfilePersistent (device_props, profile_name)
|
|
+ for _, p in ipairs (self.config.persistent or {}) do
|
|
if p.profile_names then
|
|
- for _, interest in ipairs(p.interests) do
|
|
- if interest:matches(device_props) then
|
|
- for _, pn in ipairs(p.profile_names) do
|
|
+ for _, interest in ipairs (p.interests) do
|
|
+ if interest:matches (device_props) then
|
|
+ for _, pn in ipairs (p.profile_names) do
|
|
if pn == profile_name then
|
|
return true
|
|
end
|
|
@@ -72,7 +72,7 @@ function setDeviceProfile (device, dev_id, dev_name, profile)
|
|
index = profile.index,
|
|
}
|
|
Log.info ("Setting profile " .. profile.name .. " on " .. dev_name)
|
|
- device:set_param("Profile", param)
|
|
+ device:set_param ("Profile", param)
|
|
end
|
|
|
|
function findDefaultProfile (device)
|
|
@@ -85,8 +85,8 @@ function findDefaultProfile (device)
|
|
return nil
|
|
end
|
|
|
|
- for p in device:iterate_params("EnumProfile") do
|
|
- local profile = parseParam(p, "EnumProfile")
|
|
+ for p in device:iterate_params ("EnumProfile") do
|
|
+ local profile = parseParam (p, "EnumProfile")
|
|
if profile.name == def_name then
|
|
return profile
|
|
end
|
|
@@ -189,7 +189,7 @@ function handleProfiles (device, new_device)
|
|
isProfilePersistent (device.properties, self.active_profiles[dev_id].name) and
|
|
def_profile ~= nil and
|
|
self.active_profiles[dev_id].name == def_profile.name
|
|
- then
|
|
+ then
|
|
local active_profile = self.active_profiles[dev_id].name
|
|
Log.info ("Device profile " .. active_profile .. " is persistent for " .. dev_name)
|
|
return
|
|
@@ -233,14 +233,14 @@ self.om = ObjectManager {
|
|
}
|
|
}
|
|
|
|
-self.om:connect("object-added", function (_, device)
|
|
+self.om:connect ("object-added", function(_, device)
|
|
device:connect ("params-changed", onDeviceParamsChanged)
|
|
handleProfiles (device, true)
|
|
end)
|
|
|
|
-self.om:connect("object-removed", function (_, device)
|
|
+self.om:connect ("object-removed", function(_, device)
|
|
local dev_id = device["bound-id"]
|
|
self.active_profiles[dev_id] = nil
|
|
end)
|
|
|
|
-self.om:activate()
|
|
+self.om:activate ()
|
|
diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua
|
|
index 99ad8473..035c3006 100644
|
|
--- a/src/scripts/policy-node.lua
|
|
+++ b/src/scripts/policy-node.lua
|
|
@@ -18,6 +18,7 @@ self.scanning = false
|
|
self.pending_rescan = false
|
|
self.events_skipped = false
|
|
self.pending_error_timer = nil
|
|
+self.filters_api = Plugin.find("filters-api")
|
|
|
|
function rescan()
|
|
for si in linkables_om:iterate() do
|
|
@@ -437,9 +438,21 @@ function findDefaultLinkable (si)
|
|
local si_props = si.properties
|
|
local target_direction = getTargetDirection(si_props)
|
|
local def_node_id = getDefaultNode(si_props, target_direction)
|
|
- return linkables_om:lookup {
|
|
+ local si_target = linkables_om:lookup {
|
|
Constraint { "node.id", "=", tostring(def_node_id) }
|
|
}
|
|
+
|
|
+ -- get origin filter from default target if filters API is enabled
|
|
+ if self.filters_api ~= nil and si_target ~= nil then
|
|
+ local target_node_id = si_target.properties["node.id"]
|
|
+ target_node_id = self.filters_api:call("get-filter-from-target",
|
|
+ target_direction, target_node_id)
|
|
+ si_target = linkables_om:lookup {
|
|
+ Constraint { "node.id", "=", tostring(target_node_id) }
|
|
+ }
|
|
+ end
|
|
+
|
|
+ return si_target
|
|
end
|
|
|
|
function checkPassthroughCompatibility (si, si_target)
|
|
@@ -468,12 +481,19 @@ function findBestLinkable (si)
|
|
} do
|
|
local si_target_props = si_target.properties
|
|
local si_target_node_id = si_target_props["node.id"]
|
|
+ local si_target_node = si:get_associated_proxy ("node")
|
|
+ local si_target_link_group = si_target_node.properties["node.link-group"]
|
|
local priority = tonumber(si_target_props["priority.session"]) or 0
|
|
|
|
Log.debug(string.format("Looking at: %s (%s)",
|
|
tostring(si_target_props["node.name"]),
|
|
tostring(si_target_node_id)))
|
|
|
|
+ -- Never use a filter as a best linkable if filters API is loaded
|
|
+ if self.filters_api ~= nil and si_target_link_group ~= nil then
|
|
+ goto skip_linkable
|
|
+ end
|
|
+
|
|
if not canLink (si_props, si_target) then
|
|
Log.debug("... cannot link, skip linkable")
|
|
goto skip_linkable
|
|
@@ -517,6 +537,25 @@ function findBestLinkable (si)
|
|
tostring(target_picked.properties["node.name"]),
|
|
tostring(target_picked.properties["node.id"]),
|
|
tostring(target_can_passthrough)))
|
|
+
|
|
+ -- get origin filter from target if filters API is enabled
|
|
+ if self.filters_api ~= nil and target_picked ~= nil then
|
|
+ local target_node_id = target_picked.properties["node.id"]
|
|
+ target_node_id = self.filters_api:call("get-filter-from-target",
|
|
+ target_direction, target_node_id)
|
|
+ target_picked = linkables_om:lookup {
|
|
+ Constraint { "node.id", "=", tostring(target_node_id) }
|
|
+ }
|
|
+ if target_picked == nil then
|
|
+ return nil, nil
|
|
+ end
|
|
+ target_can_passthrough = checkPassthroughCompatibility (si, target_picked)
|
|
+ Log.info(string.format("... best filter picked: %s (%s), can_passthrough:%s",
|
|
+ tostring(target_picked.properties["node.name"]),
|
|
+ tostring(target_picked.properties["node.id"]),
|
|
+ tostring(target_can_passthrough)))
|
|
+ end
|
|
+
|
|
return target_picked, target_can_passthrough
|
|
else
|
|
return nil, nil
|
|
@@ -564,6 +603,28 @@ function lookupLink (si_id, si_target_id)
|
|
return link
|
|
end
|
|
|
|
+function checkFilter (si, handle_nonstreams)
|
|
+ -- handle all filter nodes if filters API is not loaded
|
|
+ if self.filters_api == nil then
|
|
+ return true
|
|
+ end
|
|
+
|
|
+ -- always handle filters if handle_nonstreams is true, even if it is disabled
|
|
+ if handle_nonstreams then
|
|
+ return true
|
|
+ end
|
|
+
|
|
+ -- always return true if this is not a filter
|
|
+ local node = si:get_associated_proxy ("node")
|
|
+ local link_group = node.properties["node.link-group"]
|
|
+ if link_group == nil then
|
|
+ return true
|
|
+ end
|
|
+
|
|
+ local direction = getTargetDirection(si.properties)
|
|
+ return self.filters_api:call("is-filter-enabled", direction, link_group)
|
|
+end
|
|
+
|
|
function checkLinkable(si, handle_nonstreams)
|
|
-- only handle stream session items
|
|
local si_props = si.properties
|
|
@@ -572,6 +633,11 @@ function checkLinkable(si, handle_nonstreams)
|
|
return false
|
|
end
|
|
|
|
+ -- check filters
|
|
+ if not checkFilter (si, handle_nonstreams) then
|
|
+ return false
|
|
+ end
|
|
+
|
|
-- Determine if we can handle item by this policy
|
|
if endpoints_om:get_n_objects () > 0 and
|
|
si_props["item.factory.name"] == "si-audio-adapter" then
|
|
@@ -652,6 +718,34 @@ function checkFollowDefault (si, si_target, has_node_defined_target)
|
|
end
|
|
end
|
|
|
|
+function findFilterTarget (si)
|
|
+ -- always return nil if filters API is not loaded
|
|
+ if self.filters_api == nil then
|
|
+ return nil, false
|
|
+ end
|
|
+
|
|
+ -- always return nil if this is not a filter
|
|
+ local node = si:get_associated_proxy ("node")
|
|
+ local link_group = node.properties["node.link-group"]
|
|
+ if link_group == nil then
|
|
+ return nil, false
|
|
+ end
|
|
+
|
|
+ -- get the filter target
|
|
+ local direction = getTargetDirection (si.properties)
|
|
+ local target_json = self.filters_api:call("get-filter-target", direction, link_group)
|
|
+ if target_json == nil then
|
|
+ return nil, false
|
|
+ end
|
|
+ target = target_json:parse()
|
|
+
|
|
+ Log.info (".. filter target ID is " .. tostring(target.bound_id) ..
|
|
+ " (" .. tostring (target.exclusive) .. ")")
|
|
+ return linkables_om:lookup {
|
|
+ Constraint { "node.id", "=", tostring(target.bound_id) }
|
|
+ }, target.exclusive
|
|
+end
|
|
+
|
|
function handleLinkable (si)
|
|
if checkPending () then
|
|
return
|
|
@@ -683,11 +777,23 @@ function handleLinkable (si)
|
|
local si_target, has_defined_target, has_node_defined_target
|
|
= findDefinedTarget(si_props)
|
|
local can_passthrough = si_target and canPassthrough(si, si_target)
|
|
-
|
|
if si_target and si_must_passthrough and not can_passthrough then
|
|
si_target = nil
|
|
end
|
|
|
|
+ -- find filter target (always returns nil for non filters)
|
|
+ if si_target == nil then
|
|
+ si_target, exclusive = findFilterTarget(si)
|
|
+ -- don't fallback if filter target is not found and exclusive is true
|
|
+ if si_target == nil and exclusive then
|
|
+ return
|
|
+ end
|
|
+ local can_passthrough = si_target and canPassthrough(si, si_target)
|
|
+ if si_target and si_must_passthrough and not can_passthrough then
|
|
+ si_target = nil
|
|
+ end
|
|
+ end
|
|
+
|
|
-- if the client has seen a target that we haven't yet prepared, schedule
|
|
-- a rescan one more time and hope for the best
|
|
local si_id = si.id
|
|
@@ -876,6 +982,17 @@ if config.follow and default_nodes ~= nil then
|
|
end)
|
|
end
|
|
|
|
+-- listen for filter node changes
|
|
+if self.filters_api ~= nil then
|
|
+ self.filters_api:connect("changed", function ()
|
|
+ -- unlink all the filters and schedule rescan
|
|
+ for si in linkables_om:iterate { Constraint { "node.link-group", "+" } } do
|
|
+ unhandleLinkable (si)
|
|
+ end
|
|
+ scheduleRescan ()
|
|
+ end)
|
|
+end
|
|
+
|
|
-- listen for target.node metadata changes if config.move is enabled
|
|
if config.move then
|
|
metadata_om:connect("object-added", function (om, metadata)
|