policy: refactor/improve policy-node & session items to fix linking to monitors
* populate most session item properties from create-item.lua to keep things more compact and readable * use a standard naming scheme for the session item properties * use session item properties instead of node properties in policy-node.lua * improve policy-node's performance by converting the properties dictionary less times for each session item * refactor some policy logic and make things slighly more readable * change the accepted values for 'context' in wp_si_linkable_get_ports(); use "input" and "output" to keep things clear, because the previous use of NULL and "reverse" were implying that a node has only one "standard" direction, but this is complicated for sinks w/ monitors and duplex nodes * allow using monitors (which are Audio/Sink nodes in fact) as sources * treat Audio/Duplex nodes as sinks, like p-m-s does * respect the "stream.capture.sink" property of streams Fixes #66
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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], "
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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)));
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user