scripts: use the event stack to handle virtual session items
This removes both the policy-virtual-client.lua and policy-virtual-device.lua scripts, and creates a new linking/find-virtual-target.lua script to link clients with virtual session items if one of them can be found. In addition to this, this patch also ports the policy-virtual-client-links.lua into a new scripts/rescan-virtual-links.lua to use the event stack. The idea is for the scripts/link-target.lua to create all links but only activate non virtual links, and for the scripts/rescan-virtual-links.lua to activate/deactivate virtual links based on role priorities.
This commit is contained in:
@@ -90,7 +90,8 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
|
|||||||
if (strstr (self->media_class, "Source") ||
|
if (strstr (self->media_class, "Source") ||
|
||||||
strstr (self->media_class, "Output"))
|
strstr (self->media_class, "Output"))
|
||||||
self->direction = WP_DIRECTION_OUTPUT;
|
self->direction = WP_DIRECTION_OUTPUT;
|
||||||
wp_properties_setf (si_props, "direction", "%u", self->direction);
|
wp_properties_set (si_props, "item.node.direction",
|
||||||
|
self->direction == WP_DIRECTION_OUTPUT ? "output" : "input");
|
||||||
|
|
||||||
str = wp_properties_get (si_props, "role");
|
str = wp_properties_get (si_props, "role");
|
||||||
if (str) {
|
if (str) {
|
||||||
@@ -109,6 +110,10 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
|
|||||||
str = wp_properties_get (si_props, "item.features.no-dsp");
|
str = wp_properties_get (si_props, "item.features.no-dsp");
|
||||||
self->disable_dsp = str && pw_properties_parse_bool (str);
|
self->disable_dsp = str && pw_properties_parse_bool (str);
|
||||||
|
|
||||||
|
/* We always want virtual sources to autoconnect */
|
||||||
|
wp_properties_set (si_props, PW_KEY_NODE_AUTOCONNECT, "true");
|
||||||
|
wp_properties_set (si_props, "media.type", "Audio");
|
||||||
|
|
||||||
wp_properties_set (si_props, "item.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));
|
||||||
@@ -193,6 +198,21 @@ on_node_activate_done (WpObject * node, GAsyncResult * res,
|
|||||||
"si-audio-virtual: could not create si-audio-adapter"));
|
"si-audio-virtual: could not create si-audio-adapter"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Set node.id and node.name properties in this session item */
|
||||||
|
{
|
||||||
|
g_autoptr (WpProperties) si_props = wp_session_item_get_properties (
|
||||||
|
WP_SESSION_ITEM (self));
|
||||||
|
g_autoptr (WpProperties) new_props = wp_properties_new_empty ();
|
||||||
|
guint32 node_id = wp_proxy_get_bound_id (WP_PROXY (node));
|
||||||
|
wp_properties_setf (new_props, "node.id", "%u", node_id);
|
||||||
|
wp_properties_set (new_props, "node.name",
|
||||||
|
wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node),
|
||||||
|
PW_KEY_NODE_NAME));
|
||||||
|
wp_properties_update (si_props, new_props);
|
||||||
|
wp_session_item_set_properties (WP_SESSION_ITEM (self),
|
||||||
|
g_steal_pointer (&si_props));
|
||||||
|
}
|
||||||
|
|
||||||
/* Forward adapter-ports-state-changed signal */
|
/* Forward adapter-ports-state-changed signal */
|
||||||
g_signal_connect_object (self->adapter, "adapter-ports-state-changed",
|
g_signal_connect_object (self->adapter, "adapter-ports-state-changed",
|
||||||
G_CALLBACK (on_adapter_port_state_changed), self, 0);
|
G_CALLBACK (on_adapter_port_state_changed), self, 0);
|
||||||
@@ -243,6 +263,7 @@ si_audio_virtual_enable_active (WpSessionItem *si, WpTransition *transition)
|
|||||||
PW_KEY_MEDIA_CLASS, media,
|
PW_KEY_MEDIA_CLASS, media,
|
||||||
PW_KEY_FACTORY_NAME, "support.null-audio-sink",
|
PW_KEY_FACTORY_NAME, "support.null-audio-sink",
|
||||||
PW_KEY_NODE_DESCRIPTION, desc,
|
PW_KEY_NODE_DESCRIPTION, desc,
|
||||||
|
PW_KEY_NODE_AUTOCONNECT, "true",
|
||||||
"monitor.channel-volumes", "true",
|
"monitor.channel-volumes", "true",
|
||||||
"wireplumber.is-virtual", "true",
|
"wireplumber.is-virtual", "true",
|
||||||
NULL));
|
NULL));
|
||||||
|
@@ -221,14 +221,16 @@ wireplumber.components = [
|
|||||||
{ name = default-nodes/find-best-default-node.lua, type = script/lua, priority = 100 }
|
{ name = default-nodes/find-best-default-node.lua, type = script/lua, priority = 100 }
|
||||||
{ name = default-nodes/select-default-nodes.lua, type = script/lua, priority = 100 }
|
{ name = default-nodes/select-default-nodes.lua, type = script/lua, priority = 100 }
|
||||||
|
|
||||||
{ name = linking/find-best-target.lua, type = script/lua, priority = 100 }
|
{ name = linking/find-virtual-target.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/find-defined-target.lua, type = script/lua, priority = 100 }
|
{ name = linking/find-defined-target.lua, type = script/lua, priority = 100 }
|
||||||
|
{ name = linking/find-default-target.lua, type = script/lua, priority = 100 }
|
||||||
|
{ name = linking/find-best-target.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/link-target.lua, type = script/lua, priority = 100 }
|
{ name = linking/link-target.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/prepare-link.lua, type = script/lua, priority = 100 }
|
{ name = linking/prepare-link.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/filter-forward-format.lua, type = script/lua, priority = 100 }
|
{ name = linking/filter-forward-format.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/find-default-target.lua, type = script/lua, priority = 100 }
|
|
||||||
{ name = linking/rescan.lua, type = script/lua, priority = 100 }
|
{ name = linking/rescan.lua, type = script/lua, priority = 100 }
|
||||||
{ name = linking/move-follow.lua, type = script/lua, priority = 100 }
|
{ name = linking/move-follow.lua, type = script/lua, priority = 100 }
|
||||||
|
{ name = linking/rescan-virtual-links.lua, type = script/lua, priority = 100 }
|
||||||
|
|
||||||
{ name = device/apply-routes.lua, type = script/lua, priority = 100 }
|
{ name = device/apply-routes.lua, type = script/lua, priority = 100 }
|
||||||
{ name = device/find-best-profile.lua, type = script/lua, priority = 100 }
|
{ name = device/find-best-profile.lua, type = script/lua, priority = 100 }
|
||||||
@@ -243,6 +245,7 @@ wireplumber.components = [
|
|||||||
{ name = client/access-default.lua, type = script/lua, priority = 100 }
|
{ name = client/access-default.lua, type = script/lua, priority = 100 }
|
||||||
|
|
||||||
{ name = node/create-item.lua, type = script/lua, priority = 100 }
|
{ name = node/create-item.lua, type = script/lua, priority = 100 }
|
||||||
|
{ name = node/create-virtual-item.lua, type = script/lua, priority = 100 }
|
||||||
{ name = node/suspend-node.lua, type = script/lua, priority = 100 }
|
{ name = node/suspend-node.lua, type = script/lua, priority = 100 }
|
||||||
{ name = node/state-stream.lua, type = script/lua, priority = 100 }
|
{ name = node/state-stream.lua, type = script/lua, priority = 100 }
|
||||||
]
|
]
|
||||||
|
@@ -24,6 +24,12 @@ end
|
|||||||
|
|
||||||
function cutils.getTargetDirection (properties)
|
function cutils.getTargetDirection (properties)
|
||||||
local target_direction = nil
|
local target_direction = nil
|
||||||
|
|
||||||
|
-- retrun same direction for si-audio-virtual session items
|
||||||
|
if properties ["item.factory.name"] == "si-audio-virtual" then
|
||||||
|
return properties ["item.node.direction"]
|
||||||
|
end
|
||||||
|
|
||||||
if properties ["item.node.direction"] == "output" or
|
if properties ["item.node.direction"] == "output" or
|
||||||
(properties ["item.node.direction"] == "input" and
|
(properties ["item.node.direction"] == "input" and
|
||||||
cutils.parseBool (properties ["stream.capture.sink"])) then
|
cutils.parseBool (properties ["stream.capture.sink"])) then
|
||||||
|
@@ -134,8 +134,6 @@ function putils.canLink (properties, si_target)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 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)
|
local function isMonitor(properties)
|
||||||
return properties ["item.node.direction"] == "input" and
|
return properties ["item.node.direction"] == "input" and
|
||||||
parseBool (properties ["item.features.monitor"]) and
|
parseBool (properties ["item.features.monitor"]) and
|
||||||
@@ -143,10 +141,20 @@ function putils.canLink (properties, si_target)
|
|||||||
properties ["item.factory.name"] == "si-audio-adapter"
|
properties ["item.factory.name"] == "si-audio-adapter"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if properties ["item.factory.name"] == "si-audio-virtual" then
|
||||||
|
-- virtual nodes must have the same direction, unless the target is monitor
|
||||||
|
if properties ["item.node.direction"] ~= target_props ["item.node.direction"]
|
||||||
|
and not isMonitor (target_props) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- 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)
|
||||||
if properties ["item.node.direction"] == target_props ["item.node.direction"]
|
if properties ["item.node.direction"] == target_props ["item.node.direction"]
|
||||||
and not isMonitor (target_props) then
|
and not isMonitor (target_props) then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- check link group
|
-- check link group
|
||||||
local function canLinkGroupCheck(link_group, si_target, hops)
|
local function canLinkGroupCheck(link_group, si_target, hops)
|
||||||
@@ -305,8 +313,6 @@ linkables_om:activate ()
|
|||||||
links_om = ObjectManager {
|
links_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
type = "SiLink",
|
type = "SiLink",
|
||||||
-- only handle links created by this policy
|
|
||||||
Constraint { "is.policy.item.link", "=", true },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ local config = require ("policy-config")
|
|||||||
|
|
||||||
SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "linking/find-defined-target",
|
name = "linking/find-defined-target",
|
||||||
|
after = "linking/find-virtual-target",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "select-target" },
|
Constraint { "event.type", "=", "select-target" },
|
||||||
|
124
src/scripts/linking/find-virtual-target.lua
Normal file
124
src/scripts/linking/find-virtual-target.lua
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Select the virtual target based on roles
|
||||||
|
|
||||||
|
local putils = require ("policy-utils")
|
||||||
|
|
||||||
|
local defaults = {}
|
||||||
|
defaults.roles = Json.Object {}
|
||||||
|
|
||||||
|
local config = {}
|
||||||
|
config.roles = Conf.get_section (
|
||||||
|
"virtual-item-roles", defaults.roles):parse ()
|
||||||
|
|
||||||
|
function findRole(role, tmc)
|
||||||
|
if role and not config.roles[role] then
|
||||||
|
-- find the role with matching alias
|
||||||
|
for r, p in pairs(config.roles) do
|
||||||
|
-- default media class can be overridden in the role config data
|
||||||
|
mc = p["media.class"] or "Audio/Sink"
|
||||||
|
if (type(p.alias) == "table" and tmc == mc) then
|
||||||
|
for i = 1, #(p.alias), 1 do
|
||||||
|
if role == p.alias[i] then
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- otherwise get the lowest priority role
|
||||||
|
local lowest_priority_p = nil
|
||||||
|
local lowest_priority_r = nil
|
||||||
|
for r, p in pairs(config.roles) do
|
||||||
|
mc = p["media.class"] or "Audio/Sink"
|
||||||
|
if tmc == mc and (lowest_priority_p == nil or
|
||||||
|
p.priority < lowest_priority_p.priority) then
|
||||||
|
lowest_priority_p = p
|
||||||
|
lowest_priority_r = r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return lowest_priority_r
|
||||||
|
end
|
||||||
|
return role
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-virtual-target",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-target" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source, om, si, si_props, si_flags, target =
|
||||||
|
putils:unwrap_find_target_event (event)
|
||||||
|
local target_class_assoc = {
|
||||||
|
["Stream/Input/Audio"] = "Audio/Source",
|
||||||
|
["Stream/Output/Audio"] = "Audio/Sink",
|
||||||
|
["Stream/Input/Video"] = "Video/Source",
|
||||||
|
}
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
local highest_priority = -1
|
||||||
|
local target = nil
|
||||||
|
local role = node.properties["media.role"] or "Default"
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dont use virtual target for any si-audio-virtual
|
||||||
|
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.info (si, string.format ("handling item: %s (%s)",
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
-- get target media class
|
||||||
|
local target_media_class = target_class_assoc[si_props ["media.class"]]
|
||||||
|
if not target_media_class then
|
||||||
|
Log.info (si, "target media class not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find highest priority virtual by role
|
||||||
|
local media_role = findRole (role, target_media_class)
|
||||||
|
if media_role == nil then
|
||||||
|
Log.info (si, "media role not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for si_virtual in om:iterate {
|
||||||
|
Constraint { "role", "=", media_role, type = "pw-global" },
|
||||||
|
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
|
||||||
|
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||||
|
} do
|
||||||
|
local priority = tonumber(si_virtual.properties["priority"])
|
||||||
|
if priority > highest_priority then
|
||||||
|
highest_priority = priority
|
||||||
|
target = si_virtual
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local can_passthrough, passthrough_compatible
|
||||||
|
if target then
|
||||||
|
passthrough_compatible, can_passthrough =
|
||||||
|
putils.checkPassthroughCompatibility (si, target)
|
||||||
|
|
||||||
|
if not passthrough_compatible then
|
||||||
|
target = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set target
|
||||||
|
if target ~= nil then
|
||||||
|
si_flags.can_passthrough = can_passthrough
|
||||||
|
event:set_data ("target", target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
@@ -20,7 +20,7 @@ AsyncEventHook {
|
|||||||
},
|
},
|
||||||
steps = {
|
steps = {
|
||||||
start = {
|
start = {
|
||||||
next = "link_activated",
|
next = "none",
|
||||||
execute = function (event, transition)
|
execute = function (event, transition)
|
||||||
local source, om, si, si_props, si_flags, target =
|
local source, om, si, si_props, si_flags, target =
|
||||||
putils:unwrap_find_target_event (event)
|
putils:unwrap_find_target_event (event)
|
||||||
@@ -53,6 +53,17 @@ AsyncEventHook {
|
|||||||
.. tostring (si_link))
|
.. tostring (si_link))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||||
|
if si_props ["item.node.direction"] == "output" then
|
||||||
|
-- playback
|
||||||
|
out_item = target
|
||||||
|
in_item = si
|
||||||
|
else
|
||||||
|
-- capture
|
||||||
|
in_item = target
|
||||||
|
out_item = si
|
||||||
|
end
|
||||||
|
else
|
||||||
if si_props ["item.node.direction"] == "output" then
|
if si_props ["item.node.direction"] == "output" then
|
||||||
-- playback
|
-- playback
|
||||||
out_item = si
|
out_item = si
|
||||||
@@ -62,12 +73,18 @@ AsyncEventHook {
|
|||||||
in_item = si
|
in_item = si
|
||||||
out_item = target
|
out_item = target
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_virtual_client_link = target_props ["item.factory.name"] == "si-audio-virtual"
|
||||||
|
|
||||||
Log.info (si,
|
Log.info (si,
|
||||||
string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s",
|
string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s, virtual-client:%s",
|
||||||
tostring (si_props ["node.name"]),
|
tostring (si_props ["node.name"]),
|
||||||
tostring (target_props ["node.name"]),
|
tostring (target_props ["node.name"]),
|
||||||
tostring (passive), tostring (passthrough), tostring (exclusive)))
|
tostring (passive),
|
||||||
|
tostring (passthrough),
|
||||||
|
tostring (exclusive),
|
||||||
|
tostring (is_virtual_client_link)))
|
||||||
|
|
||||||
-- create and configure link
|
-- create and configure link
|
||||||
si_link = SessionItem ("si-standard-link")
|
si_link = SessionItem ("si-standard-link")
|
||||||
@@ -79,7 +96,11 @@ AsyncEventHook {
|
|||||||
["exclusive"] = exclusive,
|
["exclusive"] = exclusive,
|
||||||
["out.item.port.context"] = "output",
|
["out.item.port.context"] = "output",
|
||||||
["in.item.port.context"] = "input",
|
["in.item.port.context"] = "input",
|
||||||
["is.policy.item.link"] = true,
|
["media.role"] = target_props["role"],
|
||||||
|
["target.media.class"] = target_props["media.class"],
|
||||||
|
["is.virtual.client.link"] = is_virtual_client_link,
|
||||||
|
["main.item.id"] = si.id,
|
||||||
|
["target.item.id"] = target.id,
|
||||||
} then
|
} then
|
||||||
transition:return_error ("failed to configure si-standard-link "
|
transition:return_error ("failed to configure si-standard-link "
|
||||||
.. tostring (si_link))
|
.. tostring (si_link))
|
||||||
@@ -119,7 +140,12 @@ AsyncEventHook {
|
|||||||
end
|
end
|
||||||
si_link:register ()
|
si_link:register ()
|
||||||
|
|
||||||
-- activate
|
Log.info (si_link, "registered virtual si-standard-link between "
|
||||||
|
.. tostring (si).." and ".. tostring(target))
|
||||||
|
|
||||||
|
-- only activate non virtual links because virtual links activation is
|
||||||
|
-- handled by rescan-virtual-links.lua
|
||||||
|
if not is_virtual_client_link then
|
||||||
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
||||||
if e then
|
if e then
|
||||||
transition:return_error ("failed to activate si-standard-link: "
|
transition:return_error ("failed to activate si-standard-link: "
|
||||||
@@ -130,33 +156,21 @@ AsyncEventHook {
|
|||||||
l:remove ()
|
l:remove ()
|
||||||
else
|
else
|
||||||
si_flags.si_link = si_link
|
si_flags.si_link = si_link
|
||||||
|
si_flags.failed_peer_id = nil
|
||||||
|
if si_flags.peer_id == nil then
|
||||||
|
si_flags.peer_id = target.id
|
||||||
|
end
|
||||||
|
si_flags.failed_count = 0
|
||||||
|
|
||||||
|
Log.info (si_link, "activated si-standard-link between "
|
||||||
|
.. tostring (si).." and ".. tostring(target))
|
||||||
|
|
||||||
transition:advance ()
|
transition:advance ()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end,
|
else
|
||||||
},
|
|
||||||
link_activated = {
|
|
||||||
next = "none",
|
|
||||||
execute = function (event, transition)
|
|
||||||
local source, om, si, si_props, si_flags, target =
|
|
||||||
putils:unwrap_find_target_event (event)
|
|
||||||
if not target then
|
|
||||||
-- bypass the hook, nothing to link to.
|
|
||||||
transition:advance ()
|
transition:advance ()
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if si_flags ~= nil then
|
|
||||||
si_flags.failed_peer_id = nil
|
|
||||||
if si_flags.peer_id == nil then
|
|
||||||
si_flags.peer_id = si_target.id
|
|
||||||
end
|
|
||||||
si_flags.failed_count = 0
|
|
||||||
end
|
|
||||||
Log.info (si_flags.si_link, "activated si-standard-link between "
|
|
||||||
.. tostring (si).." and "..tostring(si_target))
|
|
||||||
|
|
||||||
transition:advance ()
|
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
254
src/scripts/linking/rescan-virtual-links.lua
Normal file
254
src/scripts/linking/rescan-virtual-links.lua
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Julian Bouzas <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
local putils = require ("policy-utils")
|
||||||
|
|
||||||
|
local defaults = {}
|
||||||
|
defaults.duck_level = 0.3
|
||||||
|
defaults.roles = Json.Object {}
|
||||||
|
|
||||||
|
local config = {}
|
||||||
|
config.duck_level = Conf.get_value_float ("wireplumber.settings",
|
||||||
|
"policy.default.duck-level", defaults.duck_level)
|
||||||
|
config.roles = Conf.get_section (
|
||||||
|
"virtual-item-roles", defaults.roles):parse ()
|
||||||
|
|
||||||
|
-- enable ducking if mixer-api is loaded
|
||||||
|
mixer_api = Plugin.find("mixer-api")
|
||||||
|
|
||||||
|
function findRole (role)
|
||||||
|
if role and not config.roles[role] then
|
||||||
|
for r, p in pairs(config.roles) do
|
||||||
|
if type(p.alias) == "table" then
|
||||||
|
for i = 1, #(p.alias), 1 do
|
||||||
|
if role == p.alias[i] then
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return role
|
||||||
|
end
|
||||||
|
|
||||||
|
function getRolePriority (role)
|
||||||
|
local r = role and config.roles[role] or nil
|
||||||
|
return r and r.priority or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function getAction (dominant_role, other_role)
|
||||||
|
-- default to "mix" if the role is not configured
|
||||||
|
if not dominant_role or not config.roles[dominant_role] then
|
||||||
|
return "mix"
|
||||||
|
end
|
||||||
|
|
||||||
|
local role_config = config.roles[dominant_role]
|
||||||
|
return role_config["action." .. other_role]
|
||||||
|
or role_config["action.default"]
|
||||||
|
or "mix"
|
||||||
|
end
|
||||||
|
|
||||||
|
function restoreVolume (om, role, media_class)
|
||||||
|
if not mixer_api then return end
|
||||||
|
|
||||||
|
local si_v = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||||
|
Constraint { "media.role", "=", role, type = "pw" },
|
||||||
|
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||||
|
}
|
||||||
|
|
||||||
|
if si_v then
|
||||||
|
local n = si_v:get_associated_proxy ("node")
|
||||||
|
if n then
|
||||||
|
Log.debug(si_v, "restore role " .. role)
|
||||||
|
mixer_api:call("set-volume", n["bound-id"], {
|
||||||
|
monitorVolume = 1.0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function duckVolume (om, role, media_class)
|
||||||
|
if not mixer_api then return end
|
||||||
|
|
||||||
|
local si_v = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||||
|
Constraint { "media.role", "=", role, type = "pw" },
|
||||||
|
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||||
|
}
|
||||||
|
|
||||||
|
if si_v then
|
||||||
|
local n = si_v:get_associated_proxy ("node")
|
||||||
|
if n then
|
||||||
|
Log.debug(si_v, "duck role " .. role)
|
||||||
|
mixer_api:call("set-volume", n["bound-id"], {
|
||||||
|
monitorVolume = config.duck_level,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function getSuspendPlaybackFromMetadata (om)
|
||||||
|
local suspend = false
|
||||||
|
local metadata = om:lookup {
|
||||||
|
type = "metadata",
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
}
|
||||||
|
if metadata then
|
||||||
|
local value = metadata:find(0, "suspend.playback")
|
||||||
|
if value then
|
||||||
|
suspend = value == "1" and true or false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return suspend
|
||||||
|
end
|
||||||
|
|
||||||
|
AsyncEventHook {
|
||||||
|
name = "linking/rescan-virtual-links",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
-- on virtual client link added and removed
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "link" },
|
||||||
|
Constraint { "is.virtual.client.link", "=", true },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
-- on default metadata suspend.playback changed
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "=", "suspend.playback" },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
steps = {
|
||||||
|
start = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||||
|
local suspend = getSuspendPlaybackFromMetadata (metadata_om)
|
||||||
|
local pending_activations = 0
|
||||||
|
local links = {
|
||||||
|
["Audio/Source"] = {},
|
||||||
|
["Audio/Sink"] = {},
|
||||||
|
["Video/Source"] = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- gather info about links
|
||||||
|
Log.info ("Rescanning virtual si-standard-link links...")
|
||||||
|
for silink in om:iterate {
|
||||||
|
type = "SiLink",
|
||||||
|
Constraint { "is.virtual.client.link", "=", true },
|
||||||
|
} do
|
||||||
|
|
||||||
|
-- deactivate all links if suspend playback metadata is present
|
||||||
|
if suspend then
|
||||||
|
silink:deactivate (Feature.SessionItem.ACTIVE)
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = silink.properties
|
||||||
|
local role = props["media.role"]
|
||||||
|
local target_class = props["target.media.class"]
|
||||||
|
local plugged = props["item.plugged.usec"]
|
||||||
|
local active = ((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
|
||||||
|
if links[target_class] then
|
||||||
|
table.insert(links[target_class], {
|
||||||
|
silink = silink,
|
||||||
|
role = findRole (role),
|
||||||
|
active = active,
|
||||||
|
priority = getRolePriority (role),
|
||||||
|
plugged = plugged and tonumber(plugged) or 0
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function onVirtualLinkActivated (l, e)
|
||||||
|
local si_id = tonumber (l.properties ["main.item.id"])
|
||||||
|
local target_id = tonumber (l.properties ["target.item.id"])
|
||||||
|
local si_flags = putils:get_flags (si_id)
|
||||||
|
|
||||||
|
if e then
|
||||||
|
Log.warning (l, "failed to activate virtual si-standard-link: " .. e)
|
||||||
|
if si_flags ~= nil then
|
||||||
|
si_flags.peer_id = nil
|
||||||
|
end
|
||||||
|
l:remove ()
|
||||||
|
else
|
||||||
|
Log.info (l, "virtual si-standard-link activated successfully")
|
||||||
|
si_flags.si_link = l
|
||||||
|
si_flags.failed_peer_id = nil
|
||||||
|
if si_flags.peer_id == nil then
|
||||||
|
si_flags.peer_id = target_id
|
||||||
|
end
|
||||||
|
si_flags.failed_count = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- advance only when all pending activations are completed
|
||||||
|
pending_activations = pending_activations - 1
|
||||||
|
if pending_activations <= 0 then
|
||||||
|
Log.info ("All virtual si-standard-links activated")
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function compareLinks(l1, l2)
|
||||||
|
return (l1.priority > l2.priority) or
|
||||||
|
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
|
||||||
|
end
|
||||||
|
|
||||||
|
for media_class, v in pairs(links) do
|
||||||
|
-- sort on priority and stream creation time
|
||||||
|
table.sort(v, compareLinks)
|
||||||
|
|
||||||
|
-- apply actions
|
||||||
|
local first_link = v[1]
|
||||||
|
if first_link then
|
||||||
|
for i = 2, #v, 1 do
|
||||||
|
local action = getAction(first_link.role, v[i].role)
|
||||||
|
if action == "cork" then
|
||||||
|
if v[i].active then
|
||||||
|
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
|
||||||
|
end
|
||||||
|
elseif action == "mix" then
|
||||||
|
if not v[i].active and not suspend then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
v[i].silink:activate (Feature.SessionItem.ACTIVE,
|
||||||
|
onVirtualLinkActivated)
|
||||||
|
end
|
||||||
|
restoreVolume(om, v[i].role, media_class)
|
||||||
|
elseif action == "duck" then
|
||||||
|
if not v[i].active and not suspend then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
v[i].silink:activate (Feature.SessionItem.ACTIVE,
|
||||||
|
onVirtualLinkActivated)
|
||||||
|
end
|
||||||
|
duckVolume (om, v[i].role, media_class)
|
||||||
|
else
|
||||||
|
Log.warning("Unknown action: " .. action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not first_link.active and not suspend then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
first_link.silink:activate(Feature.SessionItem.ACTIVE,
|
||||||
|
onVirtualLinkActivated)
|
||||||
|
end
|
||||||
|
restoreVolume (om, first_link.role, media_class)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- just advance transition if no pending activations are needed
|
||||||
|
if pending_activations <= 0 then
|
||||||
|
Log.info ("All virtual si-standard-links rescanned")
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:register ()
|
@@ -14,16 +14,17 @@ local putils = require ("policy-utils")
|
|||||||
local cutils = require ("common-utils")
|
local cutils = require ("common-utils")
|
||||||
|
|
||||||
function checkLinkable (si, om, handle_nonstreams)
|
function checkLinkable (si, om, handle_nonstreams)
|
||||||
-- only handle stream session items
|
|
||||||
local si_props = si.properties
|
local si_props = si.properties
|
||||||
if not si_props or (si_props ["item.node.type"] ~= "stream"
|
|
||||||
and not handle_nonstreams) then
|
-- Always handle si-audio-virtual session items
|
||||||
return false
|
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||||
|
return true, si_props
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Determine if we can handle item by this policy
|
-- For the rest of them, only handle stream session items
|
||||||
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
if not si_props or (si_props ["item.node.type"] ~= "stream"
|
||||||
return false
|
and not handle_nonstreams) then
|
||||||
|
return false, si_props
|
||||||
end
|
end
|
||||||
|
|
||||||
return true, si_props
|
return true, si_props
|
||||||
@@ -116,7 +117,7 @@ SimpleEventHook {
|
|||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node", "si-audio-virtual" },
|
||||||
},
|
},
|
||||||
-- on device Routes changed
|
-- on device Routes changed
|
||||||
EventInterest {
|
EventInterest {
|
||||||
|
@@ -1,232 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local defaults = {}
|
|
||||||
defaults.duck_level = 0.3
|
|
||||||
defaults.roles = Json.Object {}
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
config.duck_level = Conf.get_value_float ("wireplumber.settings",
|
|
||||||
"policy.default.duck-level", defaults.duck_level)
|
|
||||||
config.roles = Conf.get_section (
|
|
||||||
"virtual-item-roles", defaults.roles):parse ()
|
|
||||||
|
|
||||||
function findRole(role)
|
|
||||||
if role and not config.roles[role] then
|
|
||||||
for r, p in pairs(config.roles) do
|
|
||||||
if type(p.alias) == "table" then
|
|
||||||
for i = 1, #(p.alias), 1 do
|
|
||||||
if role == p.alias[i] then
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return role
|
|
||||||
end
|
|
||||||
|
|
||||||
function priorityForRole(role)
|
|
||||||
local r = role and config.roles[role] or nil
|
|
||||||
return r and r.priority or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function getAction(dominant_role, other_role)
|
|
||||||
-- default to "mix" if the role is not configured
|
|
||||||
if not dominant_role or not config.roles[dominant_role] then
|
|
||||||
return "mix"
|
|
||||||
end
|
|
||||||
|
|
||||||
local role_config = config.roles[dominant_role]
|
|
||||||
return role_config["action." .. other_role]
|
|
||||||
or role_config["action.default"]
|
|
||||||
or "mix"
|
|
||||||
end
|
|
||||||
|
|
||||||
function restoreVolume(role, media_class)
|
|
||||||
if not mixer_api then return end
|
|
||||||
|
|
||||||
local si_v = virtuals_om:lookup {
|
|
||||||
Constraint { "media.role", "=", role, type = "pw" },
|
|
||||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
|
||||||
}
|
|
||||||
|
|
||||||
if si_v then
|
|
||||||
local n = si_v:get_associated_proxy ("node")
|
|
||||||
if n then
|
|
||||||
Log.debug(si_v, "restore role " .. role)
|
|
||||||
mixer_api:call("set-volume", n["bound-id"], {
|
|
||||||
monitorVolume = 1.0,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function duck(role, media_class)
|
|
||||||
if not mixer_api then return end
|
|
||||||
|
|
||||||
local si_v = virtuals_om:lookup {
|
|
||||||
Constraint { "media.role", "=", role, type = "pw" },
|
|
||||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
|
||||||
}
|
|
||||||
|
|
||||||
if si_v then
|
|
||||||
local n = si_v:get_associated_proxy ("node")
|
|
||||||
if n then
|
|
||||||
Log.debug(si_v, "duck role " .. role)
|
|
||||||
mixer_api:call("set-volume", n["bound-id"], {
|
|
||||||
monitorVolume = config.duck_level,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function getSuspendPlaybackMetadata ()
|
|
||||||
local suspend = false
|
|
||||||
local metadata = metadata_om:lookup()
|
|
||||||
if metadata then
|
|
||||||
local value = metadata:find(0, "suspend.playback")
|
|
||||||
if value then
|
|
||||||
suspend = value == "1" and true or false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return suspend
|
|
||||||
end
|
|
||||||
|
|
||||||
function rescan()
|
|
||||||
local links = {
|
|
||||||
["Audio/Source"] = {},
|
|
||||||
["Audio/Sink"] = {},
|
|
||||||
["Video/Source"] = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Rescan virtual links")
|
|
||||||
|
|
||||||
-- deactivate all links if suspend playback metadata is present
|
|
||||||
local suspend = getSuspendPlaybackMetadata()
|
|
||||||
for silink in silinks_om:iterate() do
|
|
||||||
if suspend then
|
|
||||||
silink:deactivate(Feature.SessionItem.ACTIVE)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- gather info about links
|
|
||||||
for silink in silinks_om:iterate() do
|
|
||||||
local props = silink.properties
|
|
||||||
local role = props["media.role"]
|
|
||||||
local target_class = props["target.media.class"]
|
|
||||||
local plugged = props["item.plugged.usec"]
|
|
||||||
local active =
|
|
||||||
((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
|
|
||||||
if links[target_class] then
|
|
||||||
table.insert(links[target_class], {
|
|
||||||
silink = silink,
|
|
||||||
role = findRole(role),
|
|
||||||
active = active,
|
|
||||||
priority = priorityForRole(role),
|
|
||||||
plugged = plugged and tonumber(plugged) or 0
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function compareLinks(l1, l2)
|
|
||||||
return (l1.priority > l2.priority) or
|
|
||||||
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
|
|
||||||
end
|
|
||||||
|
|
||||||
for media_class, v in pairs(links) do
|
|
||||||
-- sort on priority and stream creation time
|
|
||||||
table.sort(v, compareLinks)
|
|
||||||
|
|
||||||
-- apply actions
|
|
||||||
local first_link = v[1]
|
|
||||||
if first_link then
|
|
||||||
for i = 2, #v, 1 do
|
|
||||||
local action = getAction(first_link.role, v[i].role)
|
|
||||||
if action == "cork" then
|
|
||||||
if v[i].active then
|
|
||||||
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
|
|
||||||
end
|
|
||||||
elseif action == "mix" then
|
|
||||||
if not v[i].active and not suspend then
|
|
||||||
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
|
||||||
end
|
|
||||||
restoreVolume(v[i].role, media_class)
|
|
||||||
elseif action == "duck" then
|
|
||||||
if not v[i].active and not suspend then
|
|
||||||
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
|
||||||
end
|
|
||||||
duck(v[i].role, media_class)
|
|
||||||
else
|
|
||||||
Log.warning("Unknown action: " .. action)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not first_link.active and not suspend then
|
|
||||||
first_link.silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
|
||||||
end
|
|
||||||
restoreVolume(first_link.role, media_class)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
pending_ops = 0
|
|
||||||
pending_rescan = false
|
|
||||||
|
|
||||||
function pendingOperation()
|
|
||||||
pending_ops = pending_ops + 1
|
|
||||||
return function()
|
|
||||||
pending_ops = pending_ops - 1
|
|
||||||
if pending_ops == 0 and pending_rescan then
|
|
||||||
pending_rescan = false
|
|
||||||
rescan()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function maybeRescan()
|
|
||||||
if pending_ops == 0 then
|
|
||||||
rescan()
|
|
||||||
else
|
|
||||||
pending_rescan = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
silinks_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "SiLink",
|
|
||||||
Constraint { "is.policy.virtual.client.link", "=", true },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
silinks_om:connect("objects-changed", maybeRescan)
|
|
||||||
silinks_om:activate()
|
|
||||||
|
|
||||||
-- enable ducking if mixer-api is loaded
|
|
||||||
mixer_api = Plugin.find("mixer-api")
|
|
||||||
if mixer_api then
|
|
||||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
|
||||||
Constraint {
|
|
||||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
virtuals_om:activate()
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "metadata",
|
|
||||||
Constraint { "metadata.name", "=", "default" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metadata_om:connect("object-added", function (om, metadata)
|
|
||||||
metadata:connect("changed", function (m, subject, key, t, value)
|
|
||||||
if key == "suspend.playback" then
|
|
||||||
maybeRescan()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
metadata_om:activate()
|
|
@@ -1,268 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
-- Receive script arguments from config.lua
|
|
||||||
|
|
||||||
local defaults = {}
|
|
||||||
defaults.roles = Json.Object {}
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
config.roles = Conf.get_section (
|
|
||||||
"virtual-item-roles", defaults.roles):parse ()
|
|
||||||
|
|
||||||
local self = {}
|
|
||||||
self.scanning = false
|
|
||||||
self.pending_rescan = false
|
|
||||||
|
|
||||||
function rescan ()
|
|
||||||
for si in linkables_om:iterate() do
|
|
||||||
handleLinkable (si)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function scheduleRescan ()
|
|
||||||
if self.scanning then
|
|
||||||
self.pending_rescan = true
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self.scanning = true
|
|
||||||
rescan ()
|
|
||||||
self.scanning = false
|
|
||||||
|
|
||||||
if self.pending_rescan then
|
|
||||||
self.pending_rescan = false
|
|
||||||
Core.sync(function ()
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function findRole(role, tmc)
|
|
||||||
if role and not config.roles[role] then
|
|
||||||
-- find the role with matching alias
|
|
||||||
for r, p in pairs(config.roles) do
|
|
||||||
-- default media class can be overridden in the role config data
|
|
||||||
mc = p["media.class"] or "Audio/Sink"
|
|
||||||
if (type(p.alias) == "table" and tmc == mc) then
|
|
||||||
for i = 1, #(p.alias), 1 do
|
|
||||||
if role == p.alias[i] then
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- otherwise get the lowest priority role
|
|
||||||
local lowest_priority_p = nil
|
|
||||||
local lowest_priority_r = nil
|
|
||||||
for r, p in pairs(config.roles) do
|
|
||||||
mc = p["media.class"] or "Audio/Sink"
|
|
||||||
if tmc == mc and (lowest_priority_p == nil or
|
|
||||||
p.priority < lowest_priority_p.priority) then
|
|
||||||
lowest_priority_p = p
|
|
||||||
lowest_priority_r = r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return lowest_priority_r
|
|
||||||
end
|
|
||||||
return role
|
|
||||||
end
|
|
||||||
|
|
||||||
function findTargetVirtual (node, media_class, role)
|
|
||||||
local target_class_assoc = {
|
|
||||||
["Stream/Input/Audio"] = "Audio/Source",
|
|
||||||
["Stream/Output/Audio"] = "Audio/Sink",
|
|
||||||
["Stream/Input/Video"] = "Video/Source",
|
|
||||||
}
|
|
||||||
local media_role = nil
|
|
||||||
local highest_priority = -1
|
|
||||||
local target = nil
|
|
||||||
|
|
||||||
-- get target media class
|
|
||||||
local target_media_class = target_class_assoc[media_class]
|
|
||||||
if not target_media_class then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- find highest priority virtual by role
|
|
||||||
media_role = findRole(role, target_media_class)
|
|
||||||
for si_target_ep in virtuals_om:iterate {
|
|
||||||
Constraint { "role", "=", media_role, type = "pw-global" },
|
|
||||||
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
|
|
||||||
} do
|
|
||||||
local priority = tonumber(si_target_ep.properties["priority"])
|
|
||||||
if priority > highest_priority then
|
|
||||||
highest_priority = priority
|
|
||||||
target = si_target_ep
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return target
|
|
||||||
end
|
|
||||||
|
|
||||||
function createLink (si, si_target_ep)
|
|
||||||
local out_item = nil
|
|
||||||
local in_item = nil
|
|
||||||
local si_props = si.properties
|
|
||||||
local target_ep_props = si_target_ep.properties
|
|
||||||
|
|
||||||
if si_props["item.node.direction"] == "output" then
|
|
||||||
-- playback
|
|
||||||
out_item = si
|
|
||||||
in_item = si_target_ep
|
|
||||||
else
|
|
||||||
-- capture
|
|
||||||
out_item = si_target_ep
|
|
||||||
in_item = si
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.info (string.format("link %s <-> %s",
|
|
||||||
tostring(si_props["node.name"]),
|
|
||||||
tostring(target_ep_props["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"] = "output",
|
|
||||||
["in.item.port.context"] = "input",
|
|
||||||
["is.policy.virtual.client.link"] = true,
|
|
||||||
["media.role"] = target_ep_props["role"],
|
|
||||||
["target.media.class"] = target_ep_props["media.class"],
|
|
||||||
["item.plugged.usec"] = si_props["item.plugged.usec"],
|
|
||||||
} then
|
|
||||||
Log.warning (si_link, "failed to configure si-standard-link")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- register
|
|
||||||
si_link:register()
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
local media_class = node.properties["media.class"]
|
|
||||||
if not media_class or not string.find (media_class, "Stream") then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Determine if we can handle item by this policy
|
|
||||||
if virtuals_om:get_n_objects () == 0 then
|
|
||||||
Log.debug (si, "item won't be handled by this policy")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleLinkable (si)
|
|
||||||
if not checkLinkable (si) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local node = si:get_associated_proxy ("node")
|
|
||||||
local media_class = node.properties["media.class"] or ""
|
|
||||||
local media_role = node.properties["media.role"] or "Default"
|
|
||||||
Log.info (si, "handling item " .. tostring(node.properties["node.name"]) ..
|
|
||||||
" with role " .. media_role)
|
|
||||||
|
|
||||||
-- find proper target virtual
|
|
||||||
local si_target_ep = findTargetVirtual (node, media_class, media_role)
|
|
||||||
if not si_target_ep then
|
|
||||||
Log.info (si, "... target virtual not found")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if item is linked to proper target, otherwise re-link
|
|
||||||
for link in links_om:iterate() do
|
|
||||||
local out_id = tonumber(link.properties["out.item.id"])
|
|
||||||
local in_id = tonumber(link.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_ep in virtuals_om:iterate() do
|
|
||||||
if peer_ep.id == (is_out and in_id or out_id) then
|
|
||||||
|
|
||||||
if peer_ep.id == si_target_ep.id then
|
|
||||||
Log.info (si, "... already linked to proper target virtual")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- remove old link if active, otherwise schedule rescan
|
|
||||||
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
|
|
||||||
link:remove ()
|
|
||||||
Log.info (si, "... moving to new target")
|
|
||||||
else
|
|
||||||
scheduleRescan ()
|
|
||||||
Log.info (si, "... scheduled rescan")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create new link
|
|
||||||
createLink (si, si_target_ep)
|
|
||||||
end
|
|
||||||
|
|
||||||
function unhandleLinkable (si)
|
|
||||||
if not checkLinkable (si) then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local node = si:get_associated_proxy ("node")
|
|
||||||
Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
|
|
||||||
|
|
||||||
-- remove any links associated with this item
|
|
||||||
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")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
|
||||||
Constraint {
|
|
||||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
linkables_om = ObjectManager { Interest { type = "SiLinkable",
|
|
||||||
-- only handle si-audio-adapter and si-node
|
|
||||||
Constraint {
|
|
||||||
"item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
|
|
||||||
Constraint {
|
|
||||||
"active-features", "!", 0, type = "gobject" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
links_om = ObjectManager { Interest { type = "SiLink",
|
|
||||||
-- only handle links created by this policy
|
|
||||||
Constraint { "is.policy.virtual.client.link", "=", true, type = "pw-global" },
|
|
||||||
} }
|
|
||||||
|
|
||||||
linkables_om:connect("objects-changed", function (om)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
|
|
||||||
linkables_om:connect("object-removed", function (om, si)
|
|
||||||
unhandleLinkable (si)
|
|
||||||
end)
|
|
||||||
|
|
||||||
virtuals_om:activate()
|
|
||||||
linkables_om:activate()
|
|
||||||
links_om:activate()
|
|
@@ -1,238 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local defaults = {}
|
|
||||||
defaults.follow = true
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
config.follow = Conf.get_value_boolean ("wireplumber.settings",
|
|
||||||
"policy.default.follow", defaults.follow)
|
|
||||||
|
|
||||||
local self = {}
|
|
||||||
self.scanning = false
|
|
||||||
self.pending_rescan = false
|
|
||||||
|
|
||||||
function rescan ()
|
|
||||||
-- check virtuals and register new links
|
|
||||||
for si_v in virtuals_om:iterate() do
|
|
||||||
handleVirtual (si_v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function scheduleRescan ()
|
|
||||||
if self.scanning then
|
|
||||||
self.pending_rescan = true
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self.scanning = true
|
|
||||||
rescan ()
|
|
||||||
self.scanning = false
|
|
||||||
|
|
||||||
if self.pending_rescan then
|
|
||||||
self.pending_rescan = false
|
|
||||||
Core.sync(function ()
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function findTargetByDefaultNode (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 linkables_om:iterate() do
|
|
||||||
local target_node = si_target:get_associated_proxy ("node")
|
|
||||||
if target_node["bound-id"] == def_id then
|
|
||||||
return si_target
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function findTargetByFirstAvailable (target_media_class)
|
|
||||||
for si_target in linkables_om:iterate() do
|
|
||||||
local target_node = si_target:get_associated_proxy ("node")
|
|
||||||
if target_node.properties["media.class"] == target_media_class then
|
|
||||||
return si_target
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function findUndefinedTarget (si_ep)
|
|
||||||
local media_class = si_ep.properties["media.class"]
|
|
||||||
local target_class_assoc = {
|
|
||||||
["Audio/Source"] = "Audio/Source",
|
|
||||||
["Audio/Sink"] = "Audio/Sink",
|
|
||||||
["Video/Source"] = "Video/Source",
|
|
||||||
}
|
|
||||||
local target_media_class = target_class_assoc[media_class]
|
|
||||||
if not target_media_class then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local si_target = findTargetByDefaultNode (target_media_class)
|
|
||||||
if not si_target then
|
|
||||||
si_target = findTargetByFirstAvailable (target_media_class)
|
|
||||||
end
|
|
||||||
return si_target
|
|
||||||
end
|
|
||||||
|
|
||||||
function createLink (si_ep, si_target)
|
|
||||||
local out_item = nil
|
|
||||||
local in_item = nil
|
|
||||||
local ep_props = si_ep.properties
|
|
||||||
local target_props = si_target.properties
|
|
||||||
|
|
||||||
if target_props["item.node.direction"] == "input" then
|
|
||||||
-- playback
|
|
||||||
out_item = si_ep
|
|
||||||
in_item = si_target
|
|
||||||
else
|
|
||||||
-- capture
|
|
||||||
in_item = si_ep
|
|
||||||
out_item = si_target
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.info (string.format("link %s <-> %s",
|
|
||||||
ep_props["name"],
|
|
||||||
target_props["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"] = "output",
|
|
||||||
["in.item.port.context"] = "input",
|
|
||||||
["passive"] = true,
|
|
||||||
["is.policy.virtual.device.link"] = true,
|
|
||||||
} then
|
|
||||||
Log.warning (si_link, "failed to configure si-standard-link")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- register
|
|
||||||
si_link:register ()
|
|
||||||
|
|
||||||
-- activate
|
|
||||||
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
|
||||||
if e then
|
|
||||||
Log.warning (l, "failed to activate si-standard-link: " .. tostring(e))
|
|
||||||
l:remove ()
|
|
||||||
else
|
|
||||||
Log.info (l, "activated si-standard-link")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleVirtual (si_ep)
|
|
||||||
Log.info (si_ep, "handling virtual " .. si_ep.properties["name"])
|
|
||||||
|
|
||||||
-- find proper target item
|
|
||||||
local si_target = findUndefinedTarget (si_ep)
|
|
||||||
if not si_target then
|
|
||||||
Log.info (si_ep, "... target item not found")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if item is linked to proper target, otherwise re-link
|
|
||||||
for link in links_om:iterate() do
|
|
||||||
local out_id = tonumber(link.properties["out.item.id"])
|
|
||||||
local in_id = tonumber(link.properties["in.item.id"])
|
|
||||||
if out_id == si_ep.id or in_id == si_ep.id then
|
|
||||||
local is_out = out_id == si_ep.id and true or false
|
|
||||||
for peer in linkables_om:iterate() do
|
|
||||||
if peer.id == (is_out and in_id or out_id) then
|
|
||||||
|
|
||||||
if peer.id == si_target.id then
|
|
||||||
Log.info (si_ep, "... already linked to proper target")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- remove old link if active, otherwise schedule rescan
|
|
||||||
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
|
|
||||||
link:remove ()
|
|
||||||
Log.info (si_ep, "... moving to new target")
|
|
||||||
else
|
|
||||||
scheduleRescan ()
|
|
||||||
Log.info (si_ep, "... scheduled rescan")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create new link
|
|
||||||
createLink (si_ep, si_target)
|
|
||||||
end
|
|
||||||
|
|
||||||
function unhandleLinkable (si)
|
|
||||||
si_props = si.properties
|
|
||||||
|
|
||||||
Log.info (si, string.format("unhandling item: %s (%s)",
|
|
||||||
tostring(si_props["node.name"]), tostring(si_props["node.id"])))
|
|
||||||
|
|
||||||
-- remove any links associated with this item
|
|
||||||
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")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
default_nodes = Plugin.find("default-nodes-api")
|
|
||||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
|
||||||
Constraint {
|
|
||||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
linkables_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" },
|
|
||||||
Constraint { "active-features", "!", 0, type = "gobject" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
links_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "SiLink",
|
|
||||||
-- only handle links created by this policy
|
|
||||||
Constraint { "is.policy.virtual.device.link", "=", true, type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-- listen for default node changes if "follow" setting is enabled
|
|
||||||
if config.follow then
|
|
||||||
default_nodes:connect("changed", function (p)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
linkables_om:connect("objects-changed", function (om)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
|
|
||||||
virtuals_om:connect("object-added", function (om)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
|
|
||||||
linkables_om:connect("object-removed", function (om, si)
|
|
||||||
unhandleLinkable (si)
|
|
||||||
end)
|
|
||||||
|
|
||||||
virtuals_om:activate()
|
|
||||||
linkables_om:activate()
|
|
||||||
links_om:activate()
|
|
@@ -81,9 +81,9 @@ test_si_audio_virtual_configure_activate (TestFixture * f,
|
|||||||
str = wp_properties_get (props, "name");
|
str = wp_properties_get (props, "name");
|
||||||
g_assert_nonnull (str);
|
g_assert_nonnull (str);
|
||||||
g_assert_cmpstr ("virtual", ==, str);
|
g_assert_cmpstr ("virtual", ==, str);
|
||||||
str = wp_properties_get (props, "direction");
|
str = wp_properties_get (props, "item.node.direction");
|
||||||
g_assert_nonnull (str);
|
g_assert_nonnull (str);
|
||||||
g_assert_cmpstr ("1", ==, str);
|
g_assert_cmpstr ("output", ==, str);
|
||||||
str = wp_properties_get (props, "item.factory.name");
|
str = wp_properties_get (props, "item.factory.name");
|
||||||
g_assert_nonnull (str);
|
g_assert_nonnull (str);
|
||||||
g_assert_cmpstr ("si-audio-virtual", ==, str);
|
g_assert_cmpstr ("si-audio-virtual", ==, str);
|
||||||
|
Reference in New Issue
Block a user