diff --git a/modules/module-si-audio-adapter.c b/modules/module-si-audio-adapter.c index 80ef31f5..a7eb62c7 100644 --- a/modules/module-si-audio-adapter.c +++ b/modules/module-si-audio-adapter.c @@ -25,12 +25,9 @@ struct _WpSiAudioAdapter /* configuration */ WpNode *node; WpPort *port; /* only used for passthrough or convert mode */ - gchar name[96]; - gchar media_class[32]; gboolean control_port; gboolean monitor; gboolean disable_dsp; - WpDirection direction; WpDirection portconfig_direction; gboolean is_device; gboolean dont_remix; @@ -66,12 +63,10 @@ si_audio_adapter_reset (WpSessionItem * item) /* reset */ g_clear_object (&self->node); g_clear_object (&self->port); - self->name[0] = '\0'; - self->media_class[0] = '\0'; self->control_port = FALSE; self->monitor = FALSE; self->disable_dsp = FALSE; - self->portconfig_direction = self->direction = WP_DIRECTION_INPUT; + self->portconfig_direction = WP_DIRECTION_INPUT; self->is_device = FALSE; self->dont_remix = FALSE; self->is_autoconnect = FALSE; @@ -93,84 +88,45 @@ si_audio_adapter_configure (WpSessionItem * item, WpProperties *p) WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item); g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p); WpNode *node = NULL; - g_autoptr (WpProperties) node_props = NULL; const gchar *str; /* reset previous config */ si_audio_adapter_reset (item); - str = wp_properties_get (si_props, "node"); + str = wp_properties_get (si_props, "item.node"); if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node)) return FALSE; - node_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (node)); + str = wp_properties_get (si_props, PW_KEY_MEDIA_CLASS); + if (!str) + return FALSE; + if ((strstr (str, "Source") || strstr (str, "Output")) + && !strstr (str, "Virtual")) { + self->portconfig_direction = WP_DIRECTION_OUTPUT; + } - str = wp_properties_get (node_props, PW_KEY_STREAM_DONT_REMIX); + str = wp_properties_get (si_props, "item.features.control-port"); + self->control_port = str && pw_properties_parse_bool (str); + + str = wp_properties_get (si_props, "item.features.monitor"); + self->monitor = str && pw_properties_parse_bool (str); + + str = wp_properties_get (si_props, "item.features.no-dsp"); + self->disable_dsp = str && pw_properties_parse_bool (str); + + str = wp_properties_get (si_props, "item.node.type"); + self->is_device = !g_strcmp0 (str, "device"); + + str = wp_properties_get (si_props, PW_KEY_STREAM_DONT_REMIX); self->dont_remix = str && pw_properties_parse_bool (str); - str = wp_properties_get (node_props, PW_KEY_NODE_AUTOCONNECT); + str = wp_properties_get (si_props, PW_KEY_NODE_AUTOCONNECT); self->is_autoconnect = str && pw_properties_parse_bool (str); - str = wp_properties_get (si_props, "name"); - if (str) { - strncpy (self->name, str, sizeof (self->name) - 1); - } else { - str = wp_properties_get (node_props, PW_KEY_NODE_NAME); - if (G_LIKELY (str)) - strncpy (self->name, str, sizeof (self->name) - 1); - else - strncpy (self->name, "Unknown", sizeof (self->name) - 1); - wp_properties_set (si_props, "name", self->name); - } - - str = wp_properties_get (si_props, "media.class"); - if (str) { - strncpy (self->media_class, str, sizeof (self->media_class) - 1); - } else { - str = wp_properties_get (node_props, PW_KEY_MEDIA_CLASS); - if (G_LIKELY (str)) - strncpy (self->media_class, str, sizeof (self->media_class) - 1); - else - strncpy (self->media_class, "Unknown", sizeof (self->media_class) - 1); - wp_properties_set (si_props, "media.class", self->media_class); - } - self->is_device = strstr (self->media_class, "Stream") == NULL; - - if (strstr (self->media_class, "Source") || - strstr (self->media_class, "Output")) { - self->direction = WP_DIRECTION_OUTPUT; - self->portconfig_direction = (strstr (self->media_class, "Virtual")) ? - WP_DIRECTION_INPUT : self->direction; - } - wp_properties_setf (si_props, "direction", "%u", self->direction); - - str = wp_properties_get (si_props, "enable.control.port"); - if (str && sscanf(str, "%u", &self->control_port) != 1) - return FALSE; - if (!str) - wp_properties_setf (si_props, "enable.control.port", "%u", - self->control_port); - - str = wp_properties_get (si_props, "enable.monitor"); - if (str && sscanf(str, "%u", &self->monitor) != 1) - return FALSE; - if (!str) - wp_properties_setf (si_props, "enable.monitor", "%u", self->monitor); - - str = wp_properties_get (si_props, "disable.dsp"); - if (str && sscanf(str, "%u", &self->disable_dsp) != 1) - return FALSE; - if (!str) - wp_properties_setf (si_props, "disable.dsp", "%u", self->disable_dsp); - self->node = g_object_ref (node); - wp_properties_set (si_props, "si.factory.name", SI_FACTORY_NAME); - wp_properties_setf (si_props, "is.device", "%u", self->is_device); - wp_properties_setf (si_props, "dont.remix", "%u", self->dont_remix); - wp_properties_setf (si_props, "is.autoconnect", "%u", self->is_autoconnect); - wp_session_item_set_properties (WP_SESSION_ITEM (self), - g_steal_pointer (&si_props)); + wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME); + wp_session_item_set_properties (item, g_steal_pointer (&si_props)); return TRUE; } @@ -611,15 +567,16 @@ si_audio_adapter_get_ports (WpSiLinkable * item, const gchar * context) g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY); g_autoptr (WpIterator) it = NULL; g_auto (GValue) val = G_VALUE_INIT; - WpDirection direction = self->direction; + WpDirection direction; guint32 node_id; - /* context can only be NULL or "reverse" */ - if (!g_strcmp0 (context, "reverse")) { - direction = (self->direction == WP_DIRECTION_INPUT) ? - WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT; + if (!g_strcmp0 (context, "output")) { + direction = WP_DIRECTION_OUTPUT; } - else if (context != NULL) { + else if (!g_strcmp0 (context, "input")) { + direction = WP_DIRECTION_INPUT; + } + else { /* on any other context, return an empty list of ports */ return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); } diff --git a/modules/module-si-audio-endpoint.c b/modules/module-si-audio-endpoint.c index 5db56234..2ffd7960 100644 --- a/modules/module-si-audio-endpoint.c +++ b/modules/module-si-audio-endpoint.c @@ -119,8 +119,7 @@ si_audio_endpoint_configure (WpSessionItem * item, WpProperties *p) if (!str) wp_properties_setf (si_props, "priority", "%u", self->priority); - wp_properties_set (si_props, "si.factory.name", SI_FACTORY_NAME); - wp_properties_setf (si_props, "is.device", "%u", FALSE); + wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME); wp_session_item_set_properties (WP_SESSION_ITEM (self), g_steal_pointer (&si_props)); return TRUE; @@ -356,15 +355,16 @@ si_audio_endpoint_get_ports (WpSiLinkable * item, const gchar * context) g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY); g_autoptr (WpIterator) it = NULL; g_auto (GValue) val = G_VALUE_INIT; - WpDirection direction = self->direction; + WpDirection direction; guint32 node_id; - /* context can only be either NULL or "reverse" */ - if (!g_strcmp0 (context, "reverse")) { - direction = (self->direction == WP_DIRECTION_INPUT) ? - WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT; + if (!g_strcmp0 (context, "output")) { + direction = WP_DIRECTION_OUTPUT; } - else if (context != NULL) { + else if (!g_strcmp0 (context, "input")) { + direction = WP_DIRECTION_INPUT; + } + else { /* on any other context, return an empty list of ports */ return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); } diff --git a/modules/module-si-node.c b/modules/module-si-node.c index f550f1b1..35075a9e 100644 --- a/modules/module-si-node.c +++ b/modules/module-si-node.c @@ -19,9 +19,6 @@ struct _WpSiNode /* configuration */ WpNode *node; - gchar name[96]; - gchar media_class[32]; - WpDirection direction; }; static void si_node_linkable_init (WpSiLinkableInterface * iface); @@ -45,9 +42,6 @@ si_node_reset (WpSessionItem * item) /* reset */ g_clear_object (&self->node); - self->name[0] = '\0'; - self->media_class[0] = '\0'; - self->direction = WP_DIRECTION_INPUT; WP_SESSION_ITEM_CLASS (si_node_parent_class)->reset (item); } @@ -58,50 +52,18 @@ si_node_configure (WpSessionItem * item, WpProperties *p) WpSiNode *self = WP_SI_NODE (item); g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p); WpNode *node = NULL; - g_autoptr (WpProperties) node_props = NULL; const gchar *str; /* reset previous config */ si_node_reset (item); - str = wp_properties_get (si_props, "node"); + str = wp_properties_get (si_props, "item.node"); if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node)) return FALSE; - node_props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (node)); - - str = wp_properties_get (si_props, "name"); - if (str) { - strncpy (self->name, str, sizeof (self->name) - 1); - } else { - str = wp_properties_get (node_props, PW_KEY_NODE_NAME); - if (G_LIKELY (str)) - strncpy (self->name, str, sizeof (self->name) - 1); - else - strncpy (self->name, "Unknown", sizeof (self->name) - 1); - wp_properties_set (si_props, "name", self->name); - } - - str = wp_properties_get (si_props, "media.class"); - if (str) { - strncpy (self->media_class, str, sizeof (self->media_class) - 1); - } else { - str = wp_properties_get (node_props, PW_KEY_MEDIA_CLASS); - if (G_LIKELY (str)) - strncpy (self->media_class, str, sizeof (self->media_class) - 1); - else - strncpy (self->media_class, "Unknown", sizeof (self->media_class) - 1); - wp_properties_set (si_props, "media.class", self->media_class); - } - - if (strstr (self->media_class, "Source") || - strstr (self->media_class, "Output")) - self->direction = WP_DIRECTION_OUTPUT; - wp_properties_setf (si_props, "direction", "%u", self->direction); - self->node = g_object_ref (node); - wp_properties_set (si_props, "si.factory.name", SI_FACTORY_NAME); + wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME); wp_session_item_set_properties (WP_SESSION_ITEM (self), g_steal_pointer (&si_props)); return TRUE; @@ -188,15 +150,16 @@ si_node_get_ports (WpSiLinkable * item, const gchar * context) g_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY); g_autoptr (WpIterator) it = NULL; g_auto (GValue) val = G_VALUE_INIT; - WpDirection direction = self->direction; + WpDirection direction; guint32 node_id; - /* context can only be NULL, "reverse" */ - if (!g_strcmp0 (context, "reverse")) { - direction = (self->direction == WP_DIRECTION_INPUT) ? - WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT; + if (!g_strcmp0 (context, "output")) { + direction = WP_DIRECTION_OUTPUT; } - else if (context != NULL) { + else if (!g_strcmp0 (context, "input")) { + direction = WP_DIRECTION_INPUT; + } + else { /* on any other context, return an empty list of ports */ return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); } diff --git a/modules/module-si-standard-link.c b/modules/module-si-standard-link.c index ce977baa..fbd7b80e 100644 --- a/modules/module-si-standard-link.c +++ b/modules/module-si-standard-link.c @@ -117,7 +117,7 @@ si_standard_link_configure (WpSessionItem * item, WpProperties * p) g_weak_ref_set(&self->out_item, out_item); g_weak_ref_set(&self->in_item, in_item); - wp_properties_set (si_props, "si.factory.name", SI_FACTORY_NAME); + wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME); wp_session_item_set_properties (WP_SESSION_ITEM (self), g_steal_pointer (&si_props)); return TRUE; @@ -402,7 +402,7 @@ configure_and_link (WpSiStandardLink *self, WpSiAdapter *main, } else { const gchar *str = NULL; gboolean disable_dsp = FALSE; - str = wp_session_item_get_property (WP_SESSION_ITEM (main), "disable.dsp"); + str = wp_session_item_get_property (WP_SESSION_ITEM (main), "item.features.no-dsp"); disable_dsp = str && pw_properties_parse_bool (str); wp_si_adapter_set_ports_format (main, NULL, disable_dsp ? "passthrough" : "dsp", @@ -427,21 +427,21 @@ configure_and_link_adapters (WpSiStandardLink *self, WpTransition *transition) g_return_if_fail (si_out); g_return_if_fail (si_in); - str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "is.device"); - out_is_device = str && pw_properties_parse_bool (str); - str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "is.device"); - in_is_device = str && pw_properties_parse_bool (str); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "item.node.type"); + out_is_device = !g_strcmp0 (str, "device"); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "item.node.type"); + in_is_device = !g_strcmp0 (str, "device"); - str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "si.factory.name"); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "item.factory.name"); out_is_device = (str && !g_strcmp0 (str, "si-audio-endpoint") && !in_is_device) || out_is_device; - str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "si.factory.name"); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "item.factory.name"); in_is_device = (str && !g_strcmp0 (str, "si-audio-endpoint") && !out_is_device) || in_is_device; - str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "dont.remix"); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "stream.dont-remix"); out_dont_remix = str && pw_properties_parse_bool (str); - str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "dont.remix"); + str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "stream.dont-remix"); in_dont_remix = str && pw_properties_parse_bool (str); wp_debug_object (self, "out [device:%d, dont_remix %d], " diff --git a/src/config/policy.lua.d/10-default-policy.lua b/src/config/policy.lua.d/10-default-policy.lua index f022ddc6..124c4bc4 100644 --- a/src/config/policy.lua.d/10-default-policy.lua +++ b/src/config/policy.lua.d/10-default-policy.lua @@ -6,6 +6,11 @@ default_policy.policy = { ["move"] = true, -- moves session items when metadata target.node changes ["follow"] = true, -- moves session items to the default device when it has changed + -- 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 + ["audio.no-dsp"] = false, + -- how much to lower the volume of lower priority streams when ducking -- note that this is a linear volume modifier (not cubic as in pulseaudio) ["duck.level"] = 0.3, @@ -32,7 +37,7 @@ function default_policy.enable() load_script("static-endpoints.lua", default_policy.endpoints) -- Create items for nodes that appear in the graph - load_script("create-item.lua") + load_script("create-item.lua", default_policy.policy) -- Link nodes to each other to make media flow in the graph load_script("policy-node.lua", default_policy.policy) diff --git a/src/scripts/create-item.lua b/src/scripts/create-item.lua index e4bf80d7..93e37d7d 100644 --- a/src/scripts/create-item.lua +++ b/src/scripts/create-item.lua @@ -5,8 +5,54 @@ -- -- SPDX-License-Identifier: MIT +-- Receive script arguments from config.lua +local config = ... + items = {} +function configProperties(node) + local np = node.properties + local properties = { + ["item.node"] = node, + ["item.plugged.usec"] = GLib.get_monotonic_time(), + ["item.features.no-dsp"] = config["audio.no-dsp"], + ["item.features.monitor"] = true, + ["item.features.control-port"] = false, + ["node.id"] = node["bound-id"], + ["client.id"] = np["client.id"], + ["object.path"] = np["object.path"], + } + + for k, v in pairs(np) do + if k:find("^node") or k:find("^stream") or k:find("^media") then + properties[k] = v + end + end + + local media_class = properties["media.class"] or "" + + if not properties["media.type"] then + for _, i in ipairs({ "Audio", "Video", "Midi" }) do + if media_class:find(i) then + properties["media.type"] = i + break + end + end + end + + properties["item.node.type"] = + media_class:find("^Stream/") and "stream" or "device" + + if media_class:find("Sink") or + media_class:find("Input") or + media_class:find("Duplex") then + properties["item.node.direction"] = "input" + elseif media_class:find("Source") or media_class:find("Output") then + properties["item.node.direction"] = "output" + end + return properties +end + function addItem (node, item_type) local id = node["bound-id"] @@ -14,12 +60,7 @@ function addItem (node, item_type) items[id] = SessionItem ( item_type ) -- configure item - if not items[id]:configure { - ["node"] = node, - ["enable.monitor"] = true, - ["disable.dsp"] = false, - ["item.plugged.usec"] = GLib.get_monotonic_time(), - } then + if not items[id]:configure(configProperties(node)) then Log.warning(items[id], "failed to configure item for node " .. tostring(id)) return end diff --git a/src/scripts/policy-endpoint-client.lua b/src/scripts/policy-endpoint-client.lua index 907f3ec7..20332f57 100644 --- a/src/scripts/policy-endpoint-client.lua +++ b/src/scripts/policy-endpoint-client.lua @@ -67,27 +67,17 @@ function createLink (si, si_target_ep) local media_class = node.properties["media.class"] local target_media_class = si_target_ep.properties["media.class"] local out_item = nil - local out_context = nil local in_item = nil - local in_context = nil if string.find (media_class, "Input") or string.find (media_class, "Sink") then -- capture out_item = si_target_ep in_item = si - if string.find (target_media_class, "Input") or - string.find (target_media_class, "Sink") then - out_context = "reverse" - end else -- playback out_item = si in_item = si_target_ep - if string.find (target_media_class, "Output") or - string.find (target_media_class, "Source") then - in_context = "reverse" - end end Log.info (string.format("link %s <-> %s", @@ -99,8 +89,8 @@ function createLink (si, si_target_ep) if not si_link:configure { ["out.item"] = out_item, ["in.item"] = in_item, - ["out.item.port.context"] = out_context, - ["in.item.port.context"] = in_context, + ["out.item.port.context"] = "output", + ["in.item.port.context"] = "input", ["is.policy.endpoint.client.link"] = true, ["media.role"] = si_target_ep.properties["role"], ["target.media.class"] = target_media_class, @@ -216,7 +206,7 @@ siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} silinkables_om = ObjectManager { Interest { type = "SiLinkable", -- only handle si-audio-adapter and si-node Constraint { - "si.factory.name", "c", "si-audio-adapter", "si-node", type = "pw-global" }, + "item.factory.name", "c", "si-audio-adapter", "si-node", type = "pw-global" }, } } silinks_om = ObjectManager { Interest { type = "SiLink", diff --git a/src/scripts/policy-endpoint-device.lua b/src/scripts/policy-endpoint-device.lua index 07f2fc8f..6e4b9ea7 100644 --- a/src/scripts/policy-endpoint-device.lua +++ b/src/scripts/policy-endpoint-device.lua @@ -59,21 +59,17 @@ function createLink (si_ep, si_target) local target_node = si_target:get_associated_proxy ("node") local target_media_class = target_node.properties["media.class"] local out_item = nil - local out_context = nil local in_item = nil - local in_context = nil if string.find (target_media_class, "Input") or string.find (target_media_class, "Sink") then -- capture in_item = si_target out_item = si_ep - out_context = "reverse" else -- playback in_item = si_ep out_item = si_target - in_context = "reverse" end Log.info (string.format("link %s <-> %s", @@ -85,8 +81,8 @@ function createLink (si_ep, si_target) if not si_link:configure { ["out.item"] = out_item, ["in.item"] = in_item, - ["out.item.port.context"] = out_context, - ["in.item.port.context"] = in_context, + ["out.item.port.context"] = "output", + ["in.item.port.context"] = "input", ["passive"] = true, ["is.policy.endpoint.device.link"] = true, } then @@ -196,16 +192,21 @@ end default_nodes = Plugin.find("default-nodes-api") siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} -silinkables_om = ObjectManager { Interest { type = "SiLinkable", - -- only handle device si-audio-adapter items - Constraint { "si.factory.name", "=", "si-audio-adapter", type = "pw-global" }, - Constraint { "is.device", "=", true, type = "pw-global" }, +silinkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle device si-audio-adapter items + Constraint { "item.factory.name", "=", "si-audio-adapter", type = "pw-global" }, + Constraint { "item.node.type", "=", "device", type = "pw-global" }, + } +} +silinks_om = ObjectManager { + Interest { + type = "SiLink", + -- only handle links created by this policy + Constraint { "is.policy.endpoint.device.link", "=", true, type = "pw-global" }, } } -silinks_om = ObjectManager { Interest { type = "SiLink", - -- only handle links created by this policy - Constraint { "is.policy.endpoint.device.link", "=", true, type = "pw-global" }, -} } -- listen for default node changes if config.follow is enabled if config.follow then diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua index 1f9afcb1..84b907a5 100644 --- a/src/scripts/policy-node.lua +++ b/src/scripts/policy-node.lua @@ -14,46 +14,35 @@ config.follow = config.follow or false local pending_rescan = false -function createLink (si, si_target) - local node = si:get_associated_proxy ("node") - local target_node = si_target:get_associated_proxy ("node") - local media_class = node.properties["media.class"] - local target_media_class = target_node.properties["media.class"] - local out_item = nil - local out_context = nil - local in_item = nil - local in_context = nil +function parseBool(var) + return var and (var == "true" or var == "1") +end - if string.find (media_class, "Input") or - string.find (media_class, "Sink") then - -- capture - out_item = si_target - in_item = si - if string.find (target_media_class, "Input") or - string.find (target_media_class, "Sink") then - out_context = "reverse" - end - else +function createLink (si, si_target) + local out_item = nil + local in_item = nil + + if si.properties["item.node.direction"] == "output" then -- playback out_item = si in_item = si_target - if string.find (target_media_class, "Output") or - string.find (target_media_class, "Source") then - in_context = "reverse" - end + else + -- capture + in_item = si + out_item = si_target end Log.info (string.format("link %s <-> %s", - tostring(node.properties["node.name"]), - tostring(target_node.properties["node.name"]))) + tostring(si.properties["node.name"]), + tostring(si_target.properties["node.name"]))) -- create and configure link local si_link = SessionItem ( "si-standard-link" ) if not si_link:configure { ["out.item"] = out_item, ["in.item"] = in_item, - ["out.item.port.context"] = out_context, - ["in.item.port.context"] = in_context, + ["out.item.port.context"] = "output", + ["in.item.port.context"] = "input", ["is.policy.item.link"] = true, } then Log.warning (si_link, "failed to configure si-standard-link") @@ -74,286 +63,245 @@ function createLink (si, si_target) end) end -function directionEqual (mc_a, mc_b) - return (string.find (mc_a, "Input") or string.find (mc_a, "Sink")) == - (string.find (mc_b, "Input") or string.find (mc_b, "Sink")) -end +function canLink (properties, si_target) + local target_properties = si_target.properties -function canLinkCheck (node_link_group, si_target, hops) - local target_node = si_target:get_associated_proxy ("node") - local target_node_link_group = target_node.properties["node.link-group"] - local targer_media_class = target_node.properties["media.class"] - - if hops == 8 then + -- nodes must have the same media type + if properties["media.type"] ~= target_properties["media.type"] then return false end - -- allow linking if target has not link-group property - if not target_node_link_group then - return true + -- nodes must have opposite direction, or otherwise they must be both input + -- and the target must have a monitor (so the target will be used as a source) + local function isMonitor(properties) + return properties["item.node.direction"] == "input" and + parseBool(properties["item.features.monitor"]) and + properties["item.factory.name"] == "si-audio-adapter" end - -- do not allow linking if target has the same link-group - if node_link_group == target_node_link_group then - return false - end - - -- make sure target is not linked with another node with same link group - for si_n in silinkables_om:iterate() do - if si_n.id ~= si_target.id then - local n = si_n:get_associated_proxy ("node") - local n_media_class = n.properties["media.class"] - if directionEqual (n_media_class, targer_media_class) then - local n_link_group = n.properties["node.link-group"] - if n_link_group ~= nil and n_link_group == target_node_link_group then - - -- iterate peers and return false if one of them cannot link - for silink in silinks_om:iterate() do - local out_id = tonumber(silink.properties["out.item.id"]) - local in_id = tonumber(silink.properties["in.item.id"]) - if out_id == si_n.id or in_id == si_n.id then - local is_out = out_id == si_n.id and true or false - for peer in silinkables_om:iterate() do - if peer.id == (is_out and in_id or out_id) then - if not canLinkCheck (node_link_group, si_peer, hops + 1) then - return false - end - end - end - end - end - - end - end - end - end - - return true -end - -function isMonitorNode (node) - local stream_monitor = node.properties["stream.monitor"] - if stream_monitor then - return stream_monitor == "true" or stream_monitor == "1" - end - return false -end - -function canLink (node, si_target) - local target_node = si_target:get_associated_proxy ("node") - local target_media_class = target_node.properties["media.class"] - local media_class = node.properties["media.class"] - - -- Make sure node is a monitor if we want to link with same media class - if media_class == target_media_class and not isMonitorNode (node) then + if properties["item.node.direction"] == target_properties["item.node.direction"] + and not isMonitor(target_properties) then return false end -- check link group - local node_link_group = node.properties["node.link-group"] - if node_link_group ~= nil then - return canLinkCheck (node_link_group, si_target, 0) - else - return true - end -end + local function canLinkGroupCheck (link_group, si_target, hops) + local target_props = si_target.properties + local target_link_group = target_props["node.link-group"] -function findTargetByTargetNodeMetadata (node) - local node_id = node['bound-id'] - local metadata = metadata_om:lookup() - if metadata then - local value = metadata:find(node_id, "target.node") - if value then - for si_target in silinkables_om:iterate() do - local target_node = si_target:get_associated_proxy ("node") - if target_node["bound-id"] == tonumber(value) and - canLink (node, si_target) then - return si_target - end - end + if hops == 8 then + return false end - end - return nil -end -function findTargetByNodeTargetProperty (node) - local target_id_str = node.properties["node.target"] - if target_id_str then - for si_target in silinkables_om:iterate() do - local target_node = si_target:get_associated_proxy ("node") - local target_props = target_node.properties - if (target_node["bound-id"] == tonumber(target_id_str) or - target_props["node.name"] == target_id_str or - target_props["object.path"] == target_id_str) and - canLink (node, si_target) then - return si_target - end + -- allow linking if target has no link-group property + if not target_link_group then + return true end - end - return nil -end -function findTargetByDefaultNode (node, target_media_class) - local def_id = default_nodes:call("get-default-node", target_media_class) - if def_id ~= Id.INVALID then - for si_target in silinkables_om:iterate() do - local target_node = si_target:get_associated_proxy ("node") - if target_node["bound-id"] == def_id and - canLink (node, si_target) then - return si_target - end + -- do not allow linking if target has the same link-group + if link_group == target_link_group then + return false end - end - return nil -end -function findTargetByFirstAvailable (node, target_media_class) - for si_target in silinkables_om:iterate() do - local target_node = si_target:get_associated_proxy ("node") - if target_node.properties["media.class"] == target_media_class and - canLink (node, si_target) then - return si_target - end - end - return nil -end - -function findDefinedTarget (node) - local si_target = findTargetByTargetNodeMetadata (node) - if not si_target then - si_target = findTargetByNodeTargetProperty (node) - end - return si_target -end - -function findUndefinedTarget (node) - local target_class_assoc = { - ["Stream/Input/Audio"] = "Audio/Source", - ["Stream/Output/Audio"] = "Audio/Sink", - ["Stream/Input/Video"] = "Video/Source", - } - local si_target = nil - - local media_class = node.properties["media.class"] - local target_media_class = target_class_assoc[media_class] - if target_media_class then - si_target = findTargetByDefaultNode (node, target_media_class) - if not si_target then - si_target = findTargetByFirstAvailable (node, target_media_class) - end - end - return si_target -end - -function getSiLinkAndSiPeer (si, target_media_class) - for silink in silinks_om:iterate() do - local out_id = tonumber(silink.properties["out.item.id"]) - local in_id = tonumber(silink.properties["in.item.id"]) - if out_id == si.id or in_id == si.id then - local is_out = out_id == si.id and true or false - for peer in silinkables_om:iterate() do - if peer.id == (is_out and in_id or out_id) then - local peer_node = peer:get_associated_proxy ("node") - local peer_media_class = peer_node.properties["media.class"] - if peer_media_class == target_media_class then - return silink, peer + -- make sure target is not linked with another node with same link group + -- start by locating other nodes in the target's link-group, in opposite direction + for n in linkables_om:iterate { + Constraint { "id", "!", si_target.id, type = "gobject" }, + Constraint { "item.node.direction", "!", target_props["item.node.direction"] }, + Constraint { "node.link-group", "=", target_link_group }, + } do + -- iterate their peers and return false if one of them cannot link + for silink in links_om:iterate() do + local out_id = tonumber(silink.properties["out.item.id"]) + local in_id = tonumber(silink.properties["in.item.id"]) + if out_id == n.id or in_id == n.id then + local peer_id = (out_id == n.id) and in_id or out_id + local peer = linkables_om:lookup { + Constraint { "id", "=", peer_id, type = "gobject" }, + } + if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then + return false end end end end + return true + end + + local link_group = properties["node.link-group"] + if link_group then + return canLinkGroupCheck (link_group, si_target, 0) + end + return true +end + +-- Try to locate a valid target node that was explicitly defined by the user +-- Use the target.node metadata, if config.move is enabled, +-- then use the node.target property that was set on the node +-- `properties` must be the properties dictionary of the session item +-- that is currently being handled +function findDefinedTarget (properties) + local function findTargetByTargetNodeMetadata (properties) + local node_id = properties["node.id"] + local metadata = metadata_om:lookup() + local target_id = metadata and metadata:find(node_id, "target.node") or nil + if target_id and tonumber(target_id) > 0 then + local si_target = linkables_om:lookup { + Constraint { "node.id", "=", target_id }, + } + if si_target and canLink (properties, si_target) then + return si_target + end + end + return nil + end + + local function findTargetByNodeTargetProperty (properties) + local target_id = properties["node.target"] + if target_id then + for si_target in linkables_om:iterate() do + local target_props = si_target.properties + if (target_props["node.id"] == target_id or + target_props["node.name"] == target_id or + target_props["object.path"] == target_id) and + canLink (properties, si_target) then + return si_target + end + end + end + return nil + end + + return (config.move and findTargetByTargetNodeMetadata (properties) or nil) + or findTargetByNodeTargetProperty (properties) +end + +-- Try to locate a valid target node that was NOT explicitly defined by the user +-- `properties` must be the properties dictionary of the session item +-- that is currently being handled +function findUndefinedTarget (properties) + local function findTargetByDefaultNode (properties, target_direction) + local target_media_class = + properties["media.type"] .. + (target_direction == "input" and "/Sink" or "/Source") + local def_id = default_nodes:call("get-default-node", target_media_class) + if def_id ~= Id.INVALID then + local si_target = linkables_om:lookup { + Constraint { "node.id", "=", def_id }, + } + if si_target and canLink (properties, si_target) then + return si_target + end + end + return nil + end + + local function findTargetByFirstAvailable (properties, target_direction) + for si_target in linkables_om:iterate { + Constraint { "item.node.type", "=", "device" }, + Constraint { "item.node.direction", "=", target_direction }, + Constraint { "media.type", "=", properties["media.type"] }, + } do + if canLink (properties, si_target) then + return si_target + end + end + return nil + end + + local target_direction = nil + if properties["item.node.direction"] == "output" or + (properties["item.node.direction"] == "input" and + parseBool(properties["stream.capture.sink"])) then + target_direction = "input" + else + target_direction = "output" + end + + return findTargetByDefaultNode (properties, target_direction) + or findTargetByFirstAvailable (properties, target_direction) +end + +function getSiLinkAndSiPeer (si, si_props) + local self_id_key = (si_props["item.node.direction"] == "output") and + "out.item.id" or "in.item.id" + local peer_id_key = (si_props["item.node.direction"] == "output") and + "in.item.id" or "out.item.id" + local silink = links_om:lookup { Constraint { self_id_key, "=", si.id } } + if silink then + local peer_id = tonumber(silink.properties[peer_id_key]) + local peer = linkables_om:lookup { + Constraint { "id", "=", peer_id, type = "gobject" }, + } + return silink, peer end return nil, nil end -function isSiLinkableValid (si) - -- only handle session items that has a node associated proxy - local node = si:get_associated_proxy ("node") - if not node or not node.properties then - return false - end - +function checkLinkable(si) -- only handle stream session items - local media_class = node.properties["media.class"] - if not media_class or not string.find (media_class, "Stream") then + local si_props = si.properties + if not si_props or si_props["item.node.type"] ~= "stream" then return false end -- Determine if we can handle item by this policy - local media_role = node.properties["media.role"] - if siendpoints_om:get_n_objects () > 0 and media_role ~= nil then + local media_role = si_props["media.role"] + if endpoints_om:get_n_objects () > 0 and media_role ~= nil then return false end - return true + return true, si_props end -function getNodeAutoconnect (node) - local auto_connect = node.properties["node.autoconnect"] - if auto_connect then - return (auto_connect == "true" or auto_connect == "1") - end - return false -end - -function getNodeReconnect (node) - local dont_reconnect = node.properties["node.dont-reconnect"] - if dont_reconnect then - return not (dont_reconnect == "true" or dont_reconnect == "1") - end - return true -end - -function handleSiLinkable (si) - -- check if item is valid - if not isSiLinkableValid (si) then +function handleLinkable (si) + local valid, si_props = checkLinkable(si) + if not valid then return end - local node = si:get_associated_proxy ("node") - local media_class = node.properties["media.class"] or "" - Log.info (si, "handling item " .. tostring(node.properties["node.name"])) - - local autoconnect = getNodeAutoconnect (node) + -- check if we need to link this node at all + local autoconnect = parseBool(si_props["node.autoconnect"]) if not autoconnect then - Log.info (si, "node does not need to be autoconnected") + Log.debug (si, tostring(si_props["node.name"]) .. " does not need to be autoconnected") return end + Log.info (si, "handling item: " .. tostring(si_props["node.name"])) + -- get reconnect - local reconnect = getNodeReconnect (node) + local reconnect = not parseBool(si_props["node.dont-reconnect"]) -- find target - local si_target = findDefinedTarget (node) + local si_target = findDefinedTarget (si_props) if not si_target and not reconnect then - Log.info (si, "removing item and node") - si:remove() + Log.info (si, "... destroy node") + local node = si:get_associated_proxy ("node") node:request_destroy() return elseif not si_target and reconnect then - si_target = findUndefinedTarget (node) + si_target = findUndefinedTarget (si_props) end if not si_target then - Log.info (si, "target not found") + Log.info (si, "... target not found") return end -- Check if item is linked to proper target, otherwise re-link - local target_node = si_target:get_associated_proxy ("node") - local target_media_class = target_node.properties["media.class"] or "" - local si_link, si_peer = getSiLinkAndSiPeer (si, target_media_class) + local si_link, si_peer = getSiLinkAndSiPeer (si, si_props) if si_link then if si_peer and si_peer.id == si_target.id then - Log.debug (si, "already linked to proper target") + Log.debug (si, "... already linked to proper target") return end - -- only remove old link if active, otherwise schedule pending rescan + -- only remove old link if active, otherwise schedule rescan if ((si_link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then si_link:remove () - Log.info (si, "moving to new target") + Log.info (si, "... moving to new target") else pending_rescan = true - Log.info (si, "scheduled pending rescan") + Log.info (si, "... scheduled rescan") return end end @@ -362,64 +310,69 @@ function handleSiLinkable (si) createLink (si, si_target) end -function unhandleSiLinkable (si) - -- check if item is valid - if not isSiLinkableValid (si) then +function unhandleLinkable (si) + local valid, si_props = checkLinkable(si) + if not valid then return end - local node = si:get_associated_proxy ("node") - Log.info (si, "unhandling item " .. tostring(node.properties["node.name"])) + Log.info (si, "unhandling item: " .. tostring(si_props["node.name"])) -- remove any links associated with this item - for silink in silinks_om:iterate() do + for silink in links_om:iterate() do local out_id = tonumber (silink.properties["out.item.id"]) local in_id = tonumber (silink.properties["in.item.id"]) if out_id == si.id or in_id == si.id then silink:remove () - Log.info (silink, "link removed") + Log.info (silink, "... link removed") end end end -function reevaluateSiLinkables () - for si in silinkables_om:iterate() do - handleSiLinkable (si) +function rescan() + for si in linkables_om:iterate() do + handleLinkable (si) end -- if pending_rescan, re-evaluate after sync if pending_rescan then pending_rescan = false Core.sync (function (c) - reevaluateSiLinkables () + rescan() end) end end default_nodes = Plugin.find("default-nodes-api") + metadata_om = ObjectManager { Interest { type = "metadata", Constraint { "metadata.name", "=", "default" }, } } -siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} -silinkables_om = ObjectManager { Interest { type = "SiLinkable", - -- only handle si-audio-adapter and si-node - Constraint { - "si.factory.name", "c", "si-audio-adapter", "si-node", type = "pw-global" }, + +endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } } + +linkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle si-audio-adapter and si-node + Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, + } +} + +links_om = ObjectManager { + Interest { + type = "SiLink", + -- only handle links created by this policy + Constraint { "is.policy.item.link", "=", true }, } } -silinks_om = ObjectManager { Interest { type = "SiLink", - -- only handle links created by this policy - Constraint { "is.policy.item.link", "=", true, type = "pw-global" }, -} } -- listen for default node changes if config.follow is enabled if config.follow then - default_nodes:connect("changed", function (p) - reevaluateSiLinkables () - end) + default_nodes:connect("changed", rescan) end -- listen for target.node metadata changes if config.move is enabled @@ -427,22 +380,21 @@ if config.move then metadata_om:connect("object-added", function (om, metadata) metadata:connect("changed", function (m, subject, key, t, value) if key == "target.node" then - reevaluateSiLinkables () + rescan() end end) end) end -silinkables_om:connect("objects-changed", function (om) - reevaluateSiLinkables () +linkables_om:connect("objects-changed", function (om) + rescan() end) -silinkables_om:connect("object-removed", function (om, si) - unhandleSiLinkable (si) - reevaluateSiLinkables () +linkables_om:connect("object-removed", function (om, si) + unhandleLinkable (si) end) metadata_om:activate() -siendpoints_om:activate() -silinkables_om:activate() -silinks_om:activate() +endpoints_om:activate() +linkables_om:activate() +links_om:activate() diff --git a/tests/modules/si-audio-adapter.c b/tests/modules/si-audio-adapter.c index 31e607c6..d9f008e0 100644 --- a/tests/modules/si-audio-adapter.c +++ b/tests/modules/si-audio-adapter.c @@ -76,7 +76,8 @@ test_si_audio_adapter_configure_activate (TestFixture * f, /* configure */ { WpProperties *props = wp_properties_new_empty (); - wp_properties_setf (props, "node", "%p", node); + wp_properties_setf (props, "item.node", "%p", node); + wp_properties_set (props, "media.class", "Audio/Source"); g_assert_true (wp_session_item_configure (adapter, props)); g_assert_true (wp_session_item_is_configured (adapter)); } @@ -86,25 +87,7 @@ test_si_audio_adapter_configure_activate (TestFixture * f, const gchar *str = NULL; g_autoptr (WpProperties) props = wp_session_item_get_properties (adapter); g_assert_nonnull (props); - str = wp_properties_get (props, "name"); - g_assert_nonnull (str); - g_assert_cmpstr ("audiotestsrc.adapter", ==, str); - str = wp_properties_get (props, "media.class"); - g_assert_nonnull (str); - g_assert_cmpstr ("Audio/Source", ==, str); - str = wp_properties_get (props, "direction"); - g_assert_nonnull (str); - g_assert_cmpstr ("1", ==, str); - str = wp_properties_get (props, "enable.control.port"); - g_assert_nonnull (str); - g_assert_cmpstr ("0", ==, str); - str = wp_properties_get (props, "enable.monitor"); - g_assert_nonnull (str); - g_assert_cmpstr ("0", ==, str); - str = wp_properties_get (props, "is.device"); - g_assert_nonnull (str); - g_assert_cmpstr ("1", ==, str); - str = wp_properties_get (props, "si.factory.name"); + str = wp_properties_get (props, "item.factory.name"); g_assert_nonnull (str); g_assert_cmpstr ("si-audio-adapter", ==, str); } diff --git a/tests/modules/si-audio-endpoint.c b/tests/modules/si-audio-endpoint.c index 26c507c4..e360771a 100644 --- a/tests/modules/si-audio-endpoint.c +++ b/tests/modules/si-audio-endpoint.c @@ -79,7 +79,7 @@ test_si_audio_endpoint_configure_activate (TestFixture * f, str = wp_properties_get (props, "direction"); g_assert_nonnull (str); g_assert_cmpstr ("1", ==, str); - str = wp_properties_get (props, "si.factory.name"); + str = wp_properties_get (props, "item.factory.name"); g_assert_nonnull (str); g_assert_cmpstr ("si-audio-endpoint", ==, str); } diff --git a/tests/modules/si-node.c b/tests/modules/si-node.c index 986048fe..997993ed 100644 --- a/tests/modules/si-node.c +++ b/tests/modules/si-node.c @@ -89,7 +89,7 @@ test_si_node_configure_activate (TestFixture * f, gconstpointer user_data) { WpProperties *props = wp_properties_new_empty (); - wp_properties_setf (props, "node", "%p", node); + wp_properties_setf (props, "item.node", "%p", node); wp_properties_set (props, "media.class", data->media_class); g_assert_true (wp_session_item_configure (item, props)); g_assert_true (wp_session_item_is_configured (item)); @@ -99,16 +99,10 @@ test_si_node_configure_activate (TestFixture * f, gconstpointer user_data) const gchar *str = NULL; g_autoptr (WpProperties) props = wp_session_item_get_properties (item); g_assert_nonnull (props); - str = wp_properties_get (props, "name"); - g_assert_nonnull (str); - g_assert_cmpstr (data->name, ==, str); str = wp_properties_get (props, "media.class"); g_assert_nonnull (str); g_assert_cmpstr (data->expected_media_class, ==, str); - str = wp_properties_get (props, "direction"); - g_assert_nonnull (str); - g_assert_cmpstr (data->expected_direction == 0 ? "0" : "1", ==, str); - str = wp_properties_get (props, "si.factory.name"); + str = wp_properties_get (props, "item.factory.name"); g_assert_nonnull (str); g_assert_cmpstr ("si-node", ==, str); } @@ -132,7 +126,8 @@ test_si_node_configure_activate (TestFixture * f, gconstpointer user_data) { guint32 node_id, port_id, channel; g_autoptr (GVariant) v = - wp_si_linkable_get_ports (WP_SI_LINKABLE (item), NULL); + wp_si_linkable_get_ports (WP_SI_LINKABLE (item), + (data->expected_direction == WP_DIRECTION_INPUT) ? "input" : "output"); g_assert_true (g_variant_is_of_type (v, G_VARIANT_TYPE ("a(uuu)"))); g_assert_cmpint (g_variant_n_children (v), ==, 1); @@ -165,16 +160,10 @@ test_si_node_configure_activate (TestFixture * f, gconstpointer user_data) const gchar *str = NULL; g_autoptr (WpProperties) props = wp_session_item_get_properties (item); g_assert_nonnull (props); - str = wp_properties_get (props, "name"); - g_assert_nonnull (str); - g_assert_cmpstr (data->name, ==, str); str = wp_properties_get (props, "media.class"); g_assert_nonnull (str); g_assert_cmpstr (data->expected_media_class, ==, str); - str = wp_properties_get (props, "direction"); - g_assert_nonnull (str); - g_assert_cmpstr (data->expected_direction == 0 ? "0" : "1", ==, str); - str = wp_properties_get (props, "si.factory.name"); + str = wp_properties_get (props, "item.factory.name"); g_assert_nonnull (str); g_assert_cmpstr ("si-node", ==, str); } diff --git a/tests/modules/si-standard-link.c b/tests/modules/si-standard-link.c index 9bb27798..f88845b2 100644 --- a/tests/modules/si-standard-link.c +++ b/tests/modules/si-standard-link.c @@ -17,7 +17,8 @@ typedef struct { } TestFixture; static WpSessionItem * -load_node (TestFixture * f, const gchar * factory, const gchar * media_class) +load_node (TestFixture * f, const gchar * factory, const gchar * media_class, + const gchar * type) { g_autoptr (WpNode) node = NULL; g_autoptr (WpSessionItem) adapter = NULL; @@ -45,8 +46,9 @@ load_node (TestFixture * f, const gchar * factory, const gchar * media_class) /* configure */ { WpProperties *props = wp_properties_new_empty (); - wp_properties_setf (props, "node", "%p", node); + wp_properties_setf (props, "item.node", "%p", node); wp_properties_set (props, "media.class", media_class); + wp_properties_set (props, "item.node.type", type); g_assert_true (wp_session_item_configure (adapter, props)); g_assert_true (wp_session_item_is_configured (adapter)); } @@ -92,9 +94,9 @@ test_si_standard_link_setup (TestFixture * f, gconstpointer user_data) } if (test_is_spa_lib_installed (&f->base, "audiotestsrc")) - f->src_item = load_node (f, "audiotestsrc", "Stream/Output/Audio"); + f->src_item = load_node (f, "audiotestsrc", "Stream/Output/Audio", "stream"); if (test_is_spa_lib_installed (&f->base, "support.null-audio-sink")) - f->sink_item = load_node (f, "support.null-audio-sink", "Audio/Sink"); + f->sink_item = load_node (f, "support.null-audio-sink", "Audio/Sink", "device"); } static void @@ -131,6 +133,8 @@ test_si_standard_link_main (TestFixture * f, gconstpointer user_data) g_autoptr (WpProperties) props = wp_properties_new_empty (); wp_properties_setf (props, "out.item", "%p", f->src_item); wp_properties_setf (props, "in.item", "%p", f->sink_item); + wp_properties_set (props, "out.item.port.context", "output"); + wp_properties_set (props, "in.item.port.context", "input"); g_assert_true (wp_session_item_configure (link, g_steal_pointer (&props))); }