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:
George Kiagiadakis
2021-10-08 00:09:42 +03:00
parent fb28b076a1
commit e76c67c45c
13 changed files with 374 additions and 489 deletions

View File

@@ -25,12 +25,9 @@ struct _WpSiAudioAdapter
/* configuration */ /* configuration */
WpNode *node; WpNode *node;
WpPort *port; /* only used for passthrough or convert mode */ WpPort *port; /* only used for passthrough or convert mode */
gchar name[96];
gchar media_class[32];
gboolean control_port; gboolean control_port;
gboolean monitor; gboolean monitor;
gboolean disable_dsp; gboolean disable_dsp;
WpDirection direction;
WpDirection portconfig_direction; WpDirection portconfig_direction;
gboolean is_device; gboolean is_device;
gboolean dont_remix; gboolean dont_remix;
@@ -66,12 +63,10 @@ si_audio_adapter_reset (WpSessionItem * item)
/* reset */ /* reset */
g_clear_object (&self->node); g_clear_object (&self->node);
g_clear_object (&self->port); g_clear_object (&self->port);
self->name[0] = '\0';
self->media_class[0] = '\0';
self->control_port = FALSE; self->control_port = FALSE;
self->monitor = FALSE; self->monitor = FALSE;
self->disable_dsp = FALSE; self->disable_dsp = FALSE;
self->portconfig_direction = self->direction = WP_DIRECTION_INPUT; self->portconfig_direction = WP_DIRECTION_INPUT;
self->is_device = FALSE; self->is_device = FALSE;
self->dont_remix = FALSE; self->dont_remix = FALSE;
self->is_autoconnect = FALSE; self->is_autoconnect = FALSE;
@@ -93,84 +88,45 @@ si_audio_adapter_configure (WpSessionItem * item, WpProperties *p)
WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item); WpSiAudioAdapter *self = WP_SI_AUDIO_ADAPTER (item);
g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p); g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
WpNode *node = NULL; WpNode *node = NULL;
g_autoptr (WpProperties) node_props = NULL;
const gchar *str; const gchar *str;
/* reset previous config */ /* reset previous config */
si_audio_adapter_reset (item); 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)) if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node))
return FALSE; 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); 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); 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); 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_properties_setf (si_props, "is.device", "%u", self->is_device); wp_session_item_set_properties (item, g_steal_pointer (&si_props));
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));
return TRUE; 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_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
g_autoptr (WpIterator) it = NULL; g_autoptr (WpIterator) it = NULL;
g_auto (GValue) val = G_VALUE_INIT; g_auto (GValue) val = G_VALUE_INIT;
WpDirection direction = self->direction; WpDirection direction;
guint32 node_id; guint32 node_id;
/* context can only be NULL or "reverse" */ if (!g_strcmp0 (context, "output")) {
if (!g_strcmp0 (context, "reverse")) { direction = WP_DIRECTION_OUTPUT;
direction = (self->direction == WP_DIRECTION_INPUT) ?
WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT;
} }
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 */ /* on any other context, return an empty list of ports */
return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0);
} }

View File

@@ -119,8 +119,7 @@ si_audio_endpoint_configure (WpSessionItem * item, WpProperties *p)
if (!str) if (!str)
wp_properties_setf (si_props, "priority", "%u", self->priority); wp_properties_setf (si_props, "priority", "%u", self->priority);
wp_properties_set (si_props, "si.factory.name", SI_FACTORY_NAME); wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
wp_properties_setf (si_props, "is.device", "%u", FALSE);
wp_session_item_set_properties (WP_SESSION_ITEM (self), wp_session_item_set_properties (WP_SESSION_ITEM (self),
g_steal_pointer (&si_props)); g_steal_pointer (&si_props));
return TRUE; 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_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
g_autoptr (WpIterator) it = NULL; g_autoptr (WpIterator) it = NULL;
g_auto (GValue) val = G_VALUE_INIT; g_auto (GValue) val = G_VALUE_INIT;
WpDirection direction = self->direction; WpDirection direction;
guint32 node_id; guint32 node_id;
/* context can only be either NULL or "reverse" */ if (!g_strcmp0 (context, "output")) {
if (!g_strcmp0 (context, "reverse")) { direction = WP_DIRECTION_OUTPUT;
direction = (self->direction == WP_DIRECTION_INPUT) ?
WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT;
} }
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 */ /* on any other context, return an empty list of ports */
return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0);
} }

View File

@@ -19,9 +19,6 @@ struct _WpSiNode
/* configuration */ /* configuration */
WpNode *node; WpNode *node;
gchar name[96];
gchar media_class[32];
WpDirection direction;
}; };
static void si_node_linkable_init (WpSiLinkableInterface * iface); static void si_node_linkable_init (WpSiLinkableInterface * iface);
@@ -45,9 +42,6 @@ si_node_reset (WpSessionItem * item)
/* reset */ /* reset */
g_clear_object (&self->node); 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); 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); WpSiNode *self = WP_SI_NODE (item);
g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p); g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
WpNode *node = NULL; WpNode *node = NULL;
g_autoptr (WpProperties) node_props = NULL;
const gchar *str; const gchar *str;
/* reset previous config */ /* reset previous config */
si_node_reset (item); 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)) if (!str || sscanf(str, "%p", &node) != 1 || !WP_IS_NODE (node))
return FALSE; 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); 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), wp_session_item_set_properties (WP_SESSION_ITEM (self),
g_steal_pointer (&si_props)); g_steal_pointer (&si_props));
return TRUE; 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_auto (GVariantBuilder) b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_ARRAY);
g_autoptr (WpIterator) it = NULL; g_autoptr (WpIterator) it = NULL;
g_auto (GValue) val = G_VALUE_INIT; g_auto (GValue) val = G_VALUE_INIT;
WpDirection direction = self->direction; WpDirection direction;
guint32 node_id; guint32 node_id;
/* context can only be NULL, "reverse" */ if (!g_strcmp0 (context, "output")) {
if (!g_strcmp0 (context, "reverse")) { direction = WP_DIRECTION_OUTPUT;
direction = (self->direction == WP_DIRECTION_INPUT) ?
WP_DIRECTION_OUTPUT : WP_DIRECTION_INPUT;
} }
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 */ /* on any other context, return an empty list of ports */
return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0); return g_variant_new_array (G_VARIANT_TYPE ("(uuu)"), NULL, 0);
} }

View File

@@ -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->out_item, out_item);
g_weak_ref_set(&self->in_item, in_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), wp_session_item_set_properties (WP_SESSION_ITEM (self),
g_steal_pointer (&si_props)); g_steal_pointer (&si_props));
return TRUE; return TRUE;
@@ -402,7 +402,7 @@ configure_and_link (WpSiStandardLink *self, WpSiAdapter *main,
} else { } else {
const gchar *str = NULL; const gchar *str = NULL;
gboolean disable_dsp = FALSE; 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); disable_dsp = str && pw_properties_parse_bool (str);
wp_si_adapter_set_ports_format (main, NULL, wp_si_adapter_set_ports_format (main, NULL,
disable_dsp ? "passthrough" : "dsp", 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_out);
g_return_if_fail (si_in); g_return_if_fail (si_in);
str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "is.device"); str = wp_session_item_get_property (WP_SESSION_ITEM (si_out), "item.node.type");
out_is_device = str && pw_properties_parse_bool (str); out_is_device = !g_strcmp0 (str, "device");
str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "is.device"); str = wp_session_item_get_property (WP_SESSION_ITEM (si_in), "item.node.type");
in_is_device = str && pw_properties_parse_bool (str); 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 && !g_strcmp0 (str, "si-audio-endpoint") && !in_is_device)
|| out_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 && !g_strcmp0 (str, "si-audio-endpoint") && !out_is_device)
|| in_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); 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); in_dont_remix = str && pw_properties_parse_bool (str);
wp_debug_object (self, "out [device:%d, dont_remix %d], " wp_debug_object (self, "out [device:%d, dont_remix %d], "

View File

@@ -6,6 +6,11 @@ default_policy.policy = {
["move"] = true, -- moves session items when metadata target.node changes ["move"] = true, -- moves session items when metadata target.node changes
["follow"] = true, -- moves session items to the default device when it has changed ["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 -- 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) -- note that this is a linear volume modifier (not cubic as in pulseaudio)
["duck.level"] = 0.3, ["duck.level"] = 0.3,
@@ -32,7 +37,7 @@ function default_policy.enable()
load_script("static-endpoints.lua", default_policy.endpoints) load_script("static-endpoints.lua", default_policy.endpoints)
-- Create items for nodes that appear in the graph -- 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 -- Link nodes to each other to make media flow in the graph
load_script("policy-node.lua", default_policy.policy) load_script("policy-node.lua", default_policy.policy)

View File

@@ -5,8 +5,54 @@
-- --
-- SPDX-License-Identifier: MIT -- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
local config = ...
items = {} 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) function addItem (node, item_type)
local id = node["bound-id"] local id = node["bound-id"]
@@ -14,12 +60,7 @@ function addItem (node, item_type)
items[id] = SessionItem ( item_type ) items[id] = SessionItem ( item_type )
-- configure item -- configure item
if not items[id]:configure { if not items[id]:configure(configProperties(node)) then
["node"] = node,
["enable.monitor"] = true,
["disable.dsp"] = false,
["item.plugged.usec"] = GLib.get_monotonic_time(),
} then
Log.warning(items[id], "failed to configure item for node " .. tostring(id)) Log.warning(items[id], "failed to configure item for node " .. tostring(id))
return return
end end

View File

@@ -67,27 +67,17 @@ function createLink (si, si_target_ep)
local media_class = node.properties["media.class"] local media_class = node.properties["media.class"]
local target_media_class = si_target_ep.properties["media.class"] local target_media_class = si_target_ep.properties["media.class"]
local out_item = nil local out_item = nil
local out_context = nil
local in_item = nil local in_item = nil
local in_context = nil
if string.find (media_class, "Input") or if string.find (media_class, "Input") or
string.find (media_class, "Sink") then string.find (media_class, "Sink") then
-- capture -- capture
out_item = si_target_ep out_item = si_target_ep
in_item = si in_item = si
if string.find (target_media_class, "Input") or
string.find (target_media_class, "Sink") then
out_context = "reverse"
end
else else
-- playback -- playback
out_item = si out_item = si
in_item = si_target_ep 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 end
Log.info (string.format("link %s <-> %s", Log.info (string.format("link %s <-> %s",
@@ -99,8 +89,8 @@ function createLink (si, si_target_ep)
if not si_link:configure { if not si_link:configure {
["out.item"] = out_item, ["out.item"] = out_item,
["in.item"] = in_item, ["in.item"] = in_item,
["out.item.port.context"] = out_context, ["out.item.port.context"] = "output",
["in.item.port.context"] = in_context, ["in.item.port.context"] = "input",
["is.policy.endpoint.client.link"] = true, ["is.policy.endpoint.client.link"] = true,
["media.role"] = si_target_ep.properties["role"], ["media.role"] = si_target_ep.properties["role"],
["target.media.class"] = target_media_class, ["target.media.class"] = target_media_class,
@@ -216,7 +206,7 @@ siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
silinkables_om = ObjectManager { Interest { type = "SiLinkable", silinkables_om = ObjectManager { Interest { type = "SiLinkable",
-- only handle si-audio-adapter and si-node -- only handle si-audio-adapter and si-node
Constraint { 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", silinks_om = ObjectManager { Interest { type = "SiLink",

View File

@@ -59,21 +59,17 @@ function createLink (si_ep, si_target)
local target_node = si_target:get_associated_proxy ("node") local target_node = si_target:get_associated_proxy ("node")
local target_media_class = target_node.properties["media.class"] local target_media_class = target_node.properties["media.class"]
local out_item = nil local out_item = nil
local out_context = nil
local in_item = nil local in_item = nil
local in_context = nil
if string.find (target_media_class, "Input") or if string.find (target_media_class, "Input") or
string.find (target_media_class, "Sink") then string.find (target_media_class, "Sink") then
-- capture -- capture
in_item = si_target in_item = si_target
out_item = si_ep out_item = si_ep
out_context = "reverse"
else else
-- playback -- playback
in_item = si_ep in_item = si_ep
out_item = si_target out_item = si_target
in_context = "reverse"
end end
Log.info (string.format("link %s <-> %s", Log.info (string.format("link %s <-> %s",
@@ -85,8 +81,8 @@ function createLink (si_ep, si_target)
if not si_link:configure { if not si_link:configure {
["out.item"] = out_item, ["out.item"] = out_item,
["in.item"] = in_item, ["in.item"] = in_item,
["out.item.port.context"] = out_context, ["out.item.port.context"] = "output",
["in.item.port.context"] = in_context, ["in.item.port.context"] = "input",
["passive"] = true, ["passive"] = true,
["is.policy.endpoint.device.link"] = true, ["is.policy.endpoint.device.link"] = true,
} then } then
@@ -196,16 +192,21 @@ end
default_nodes = Plugin.find("default-nodes-api") default_nodes = Plugin.find("default-nodes-api")
siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }} siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
silinkables_om = ObjectManager { Interest { type = "SiLinkable", silinkables_om = ObjectManager {
-- only handle device si-audio-adapter items Interest {
Constraint { "si.factory.name", "=", "si-audio-adapter", type = "pw-global" }, type = "SiLinkable",
Constraint { "is.device", "=", true, type = "pw-global" }, -- 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 -- listen for default node changes if config.follow is enabled
if config.follow then if config.follow then

View File

@@ -14,46 +14,35 @@ config.follow = config.follow or false
local pending_rescan = false local pending_rescan = false
function createLink (si, si_target) function parseBool(var)
local node = si:get_associated_proxy ("node") return var and (var == "true" or var == "1")
local target_node = si_target:get_associated_proxy ("node") end
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
if string.find (media_class, "Input") or function createLink (si, si_target)
string.find (media_class, "Sink") then local out_item = nil
-- capture local in_item = nil
out_item = si_target
in_item = si if si.properties["item.node.direction"] == "output" then
if string.find (target_media_class, "Input") or
string.find (target_media_class, "Sink") then
out_context = "reverse"
end
else
-- playback -- playback
out_item = si out_item = si
in_item = si_target in_item = si_target
if string.find (target_media_class, "Output") or else
string.find (target_media_class, "Source") then -- capture
in_context = "reverse" in_item = si
end out_item = si_target
end end
Log.info (string.format("link %s <-> %s", Log.info (string.format("link %s <-> %s",
tostring(node.properties["node.name"]), tostring(si.properties["node.name"]),
tostring(target_node.properties["node.name"]))) tostring(si_target.properties["node.name"])))
-- create and configure link -- create and configure link
local si_link = SessionItem ( "si-standard-link" ) local si_link = SessionItem ( "si-standard-link" )
if not si_link:configure { if not si_link:configure {
["out.item"] = out_item, ["out.item"] = out_item,
["in.item"] = in_item, ["in.item"] = in_item,
["out.item.port.context"] = out_context, ["out.item.port.context"] = "output",
["in.item.port.context"] = in_context, ["in.item.port.context"] = "input",
["is.policy.item.link"] = true, ["is.policy.item.link"] = true,
} then } then
Log.warning (si_link, "failed to configure si-standard-link") Log.warning (si_link, "failed to configure si-standard-link")
@@ -74,286 +63,245 @@ function createLink (si, si_target)
end) end)
end end
function directionEqual (mc_a, mc_b) function canLink (properties, si_target)
return (string.find (mc_a, "Input") or string.find (mc_a, "Sink")) == local target_properties = si_target.properties
(string.find (mc_b, "Input") or string.find (mc_b, "Sink"))
end
function canLinkCheck (node_link_group, si_target, hops) -- nodes must have the same media type
local target_node = si_target:get_associated_proxy ("node") if properties["media.type"] ~= target_properties["media.type"] then
local target_node_link_group = target_node.properties["node.link-group"]
local targer_media_class = target_node.properties["media.class"]
if hops == 8 then
return false return false
end end
-- allow linking if target has not link-group property -- nodes must have opposite direction, or otherwise they must be both input
if not target_node_link_group then -- and the target must have a monitor (so the target will be used as a source)
return true 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 end
-- do not allow linking if target has the same link-group if properties["item.node.direction"] == target_properties["item.node.direction"]
if node_link_group == target_node_link_group then and not isMonitor(target_properties) 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
return false return false
end end
-- check link group -- check link group
local node_link_group = node.properties["node.link-group"] local function canLinkGroupCheck (link_group, si_target, hops)
if node_link_group ~= nil then local target_props = si_target.properties
return canLinkCheck (node_link_group, si_target, 0) local target_link_group = target_props["node.link-group"]
else
return true
end
end
function findTargetByTargetNodeMetadata (node) if hops == 8 then
local node_id = node['bound-id'] return false
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
end end
end
return nil
end
function findTargetByNodeTargetProperty (node) -- allow linking if target has no link-group property
local target_id_str = node.properties["node.target"] if not target_link_group then
if target_id_str then return true
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
end end
end
return nil
end
function findTargetByDefaultNode (node, target_media_class) -- do not allow linking if target has the same link-group
local def_id = default_nodes:call("get-default-node", target_media_class) if link_group == target_link_group then
if def_id ~= Id.INVALID then return false
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
end end
end
return nil
end
function findTargetByFirstAvailable (node, target_media_class) -- make sure target is not linked with another node with same link group
for si_target in silinkables_om:iterate() do -- start by locating other nodes in the target's link-group, in opposite direction
local target_node = si_target:get_associated_proxy ("node") for n in linkables_om:iterate {
if target_node.properties["media.class"] == target_media_class and Constraint { "id", "!", si_target.id, type = "gobject" },
canLink (node, si_target) then Constraint { "item.node.direction", "!", target_props["item.node.direction"] },
return si_target Constraint { "node.link-group", "=", target_link_group },
end } do
end -- iterate their peers and return false if one of them cannot link
return nil for silink in links_om:iterate() do
end local out_id = tonumber(silink.properties["out.item.id"])
local in_id = tonumber(silink.properties["in.item.id"])
function findDefinedTarget (node) if out_id == n.id or in_id == n.id then
local si_target = findTargetByTargetNodeMetadata (node) local peer_id = (out_id == n.id) and in_id or out_id
if not si_target then local peer = linkables_om:lookup {
si_target = findTargetByNodeTargetProperty (node) Constraint { "id", "=", peer_id, type = "gobject" },
end }
return si_target if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
end return false
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
end end
end end
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 end
return nil, nil return nil, nil
end end
function isSiLinkableValid (si) function checkLinkable(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
-- only handle stream session items -- only handle stream session items
local media_class = node.properties["media.class"] local si_props = si.properties
if not media_class or not string.find (media_class, "Stream") then if not si_props or si_props["item.node.type"] ~= "stream" then
return false return false
end end
-- Determine if we can handle item by this policy -- Determine if we can handle item by this policy
local media_role = node.properties["media.role"] local media_role = si_props["media.role"]
if siendpoints_om:get_n_objects () > 0 and media_role ~= nil then if endpoints_om:get_n_objects () > 0 and media_role ~= nil then
return false return false
end end
return true return true, si_props
end end
function getNodeAutoconnect (node) function handleLinkable (si)
local auto_connect = node.properties["node.autoconnect"] local valid, si_props = checkLinkable(si)
if auto_connect then if not valid 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
return return
end end
local node = si:get_associated_proxy ("node") -- check if we need to link this node at all
local media_class = node.properties["media.class"] or "" local autoconnect = parseBool(si_props["node.autoconnect"])
Log.info (si, "handling item " .. tostring(node.properties["node.name"]))
local autoconnect = getNodeAutoconnect (node)
if not autoconnect then 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 return
end end
Log.info (si, "handling item: " .. tostring(si_props["node.name"]))
-- get reconnect -- get reconnect
local reconnect = getNodeReconnect (node) local reconnect = not parseBool(si_props["node.dont-reconnect"])
-- find target -- find target
local si_target = findDefinedTarget (node) local si_target = findDefinedTarget (si_props)
if not si_target and not reconnect then if not si_target and not reconnect then
Log.info (si, "removing item and node") Log.info (si, "... destroy node")
si:remove() local node = si:get_associated_proxy ("node")
node:request_destroy() node:request_destroy()
return return
elseif not si_target and reconnect then elseif not si_target and reconnect then
si_target = findUndefinedTarget (node) si_target = findUndefinedTarget (si_props)
end end
if not si_target then if not si_target then
Log.info (si, "target not found") Log.info (si, "... target not found")
return return
end end
-- Check if item is linked to proper target, otherwise re-link -- Check if item is linked to proper target, otherwise re-link
local target_node = si_target:get_associated_proxy ("node") local si_link, si_peer = getSiLinkAndSiPeer (si, si_props)
local target_media_class = target_node.properties["media.class"] or ""
local si_link, si_peer = getSiLinkAndSiPeer (si, target_media_class)
if si_link then if si_link then
if si_peer and si_peer.id == si_target.id 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 return
end 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 if ((si_link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
si_link:remove () si_link:remove ()
Log.info (si, "moving to new target") Log.info (si, "... moving to new target")
else else
pending_rescan = true pending_rescan = true
Log.info (si, "scheduled pending rescan") Log.info (si, "... scheduled rescan")
return return
end end
end end
@@ -362,64 +310,69 @@ function handleSiLinkable (si)
createLink (si, si_target) createLink (si, si_target)
end end
function unhandleSiLinkable (si) function unhandleLinkable (si)
-- check if item is valid local valid, si_props = checkLinkable(si)
if not isSiLinkableValid (si) then if not valid then
return return
end end
local node = si:get_associated_proxy ("node") Log.info (si, "unhandling item: " .. tostring(si_props["node.name"]))
Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
-- remove any links associated with this item -- 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 out_id = tonumber (silink.properties["out.item.id"])
local in_id = tonumber (silink.properties["in.item.id"]) local in_id = tonumber (silink.properties["in.item.id"])
if out_id == si.id or in_id == si.id then if out_id == si.id or in_id == si.id then
silink:remove () silink:remove ()
Log.info (silink, "link removed") Log.info (silink, "... link removed")
end end
end end
end end
function reevaluateSiLinkables () function rescan()
for si in silinkables_om:iterate() do for si in linkables_om:iterate() do
handleSiLinkable (si) handleLinkable (si)
end end
-- if pending_rescan, re-evaluate after sync -- if pending_rescan, re-evaluate after sync
if pending_rescan then if pending_rescan then
pending_rescan = false pending_rescan = false
Core.sync (function (c) Core.sync (function (c)
reevaluateSiLinkables () rescan()
end) end)
end end
end end
default_nodes = Plugin.find("default-nodes-api") default_nodes = Plugin.find("default-nodes-api")
metadata_om = ObjectManager { metadata_om = ObjectManager {
Interest { Interest {
type = "metadata", type = "metadata",
Constraint { "metadata.name", "=", "default" }, Constraint { "metadata.name", "=", "default" },
} }
} }
siendpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
silinkables_om = ObjectManager { Interest { type = "SiLinkable", endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } }
-- only handle si-audio-adapter and si-node
Constraint { linkables_om = ObjectManager {
"si.factory.name", "c", "si-audio-adapter", "si-node", type = "pw-global" }, 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 -- listen for default node changes if config.follow is enabled
if config.follow then if config.follow then
default_nodes:connect("changed", function (p) default_nodes:connect("changed", rescan)
reevaluateSiLinkables ()
end)
end end
-- listen for target.node metadata changes if config.move is enabled -- 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_om:connect("object-added", function (om, metadata)
metadata:connect("changed", function (m, subject, key, t, value) metadata:connect("changed", function (m, subject, key, t, value)
if key == "target.node" then if key == "target.node" then
reevaluateSiLinkables () rescan()
end end
end) end)
end) end)
end end
silinkables_om:connect("objects-changed", function (om) linkables_om:connect("objects-changed", function (om)
reevaluateSiLinkables () rescan()
end) end)
silinkables_om:connect("object-removed", function (om, si) linkables_om:connect("object-removed", function (om, si)
unhandleSiLinkable (si) unhandleLinkable (si)
reevaluateSiLinkables ()
end) end)
metadata_om:activate() metadata_om:activate()
siendpoints_om:activate() endpoints_om:activate()
silinkables_om:activate() linkables_om:activate()
silinks_om:activate() links_om:activate()

View File

@@ -76,7 +76,8 @@ test_si_audio_adapter_configure_activate (TestFixture * f,
/* configure */ /* configure */
{ {
WpProperties *props = wp_properties_new_empty (); 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_configure (adapter, props));
g_assert_true (wp_session_item_is_configured (adapter)); 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; const gchar *str = NULL;
g_autoptr (WpProperties) props = wp_session_item_get_properties (adapter); g_autoptr (WpProperties) props = wp_session_item_get_properties (adapter);
g_assert_nonnull (props); g_assert_nonnull (props);
str = wp_properties_get (props, "name"); str = wp_properties_get (props, "item.factory.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");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr ("si-audio-adapter", ==, str); g_assert_cmpstr ("si-audio-adapter", ==, str);
} }

View File

@@ -79,7 +79,7 @@ test_si_audio_endpoint_configure_activate (TestFixture * f,
str = wp_properties_get (props, "direction"); str = wp_properties_get (props, "direction");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr ("1", ==, 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_nonnull (str);
g_assert_cmpstr ("si-audio-endpoint", ==, str); g_assert_cmpstr ("si-audio-endpoint", ==, str);
} }

View File

@@ -89,7 +89,7 @@ test_si_node_configure_activate (TestFixture * f, gconstpointer user_data)
{ {
WpProperties *props = wp_properties_new_empty (); 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); wp_properties_set (props, "media.class", data->media_class);
g_assert_true (wp_session_item_configure (item, props)); g_assert_true (wp_session_item_configure (item, props));
g_assert_true (wp_session_item_is_configured (item)); 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; const gchar *str = NULL;
g_autoptr (WpProperties) props = wp_session_item_get_properties (item); g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
g_assert_nonnull (props); 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"); str = wp_properties_get (props, "media.class");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr (data->expected_media_class, ==, str); g_assert_cmpstr (data->expected_media_class, ==, str);
str = wp_properties_get (props, "direction"); str = wp_properties_get (props, "item.factory.name");
g_assert_nonnull (str);
g_assert_cmpstr (data->expected_direction == 0 ? "0" : "1", ==, str);
str = wp_properties_get (props, "si.factory.name");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr ("si-node", ==, 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; guint32 node_id, port_id, channel;
g_autoptr (GVariant) v = 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_true (g_variant_is_of_type (v, G_VARIANT_TYPE ("a(uuu)")));
g_assert_cmpint (g_variant_n_children (v), ==, 1); 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; const gchar *str = NULL;
g_autoptr (WpProperties) props = wp_session_item_get_properties (item); g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
g_assert_nonnull (props); 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"); str = wp_properties_get (props, "media.class");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr (data->expected_media_class, ==, str); g_assert_cmpstr (data->expected_media_class, ==, str);
str = wp_properties_get (props, "direction"); str = wp_properties_get (props, "item.factory.name");
g_assert_nonnull (str);
g_assert_cmpstr (data->expected_direction == 0 ? "0" : "1", ==, str);
str = wp_properties_get (props, "si.factory.name");
g_assert_nonnull (str); g_assert_nonnull (str);
g_assert_cmpstr ("si-node", ==, str); g_assert_cmpstr ("si-node", ==, str);
} }

View File

@@ -17,7 +17,8 @@ typedef struct {
} TestFixture; } TestFixture;
static WpSessionItem * 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 (WpNode) node = NULL;
g_autoptr (WpSessionItem) adapter = NULL; g_autoptr (WpSessionItem) adapter = NULL;
@@ -45,8 +46,9 @@ load_node (TestFixture * f, const gchar * factory, const gchar * media_class)
/* configure */ /* configure */
{ {
WpProperties *props = wp_properties_new_empty (); 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, "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_configure (adapter, props));
g_assert_true (wp_session_item_is_configured (adapter)); 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")) 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")) 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 static void
@@ -131,6 +133,8 @@ test_si_standard_link_main (TestFixture * f, gconstpointer user_data)
g_autoptr (WpProperties) props = wp_properties_new_empty (); g_autoptr (WpProperties) props = wp_properties_new_empty ();
wp_properties_setf (props, "out.item", "%p", f->src_item); wp_properties_setf (props, "out.item", "%p", f->src_item);
wp_properties_setf (props, "in.item", "%p", f->sink_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))); g_assert_true (wp_session_item_configure (link, g_steal_pointer (&props)));
} }