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:
Julian Bouzas
2023-02-03 12:09:26 -05:00
parent 9ee0f096ec
commit d2123827f7
14 changed files with 496 additions and 804 deletions

View File

@@ -90,7 +90,8 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
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);
wp_properties_set (si_props, "item.node.direction",
self->direction == WP_DIRECTION_OUTPUT ? "output" : "input");
str = wp_properties_get (si_props, "role");
if (str) {
@@ -109,6 +110,10 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
str = wp_properties_get (si_props, "item.features.no-dsp");
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_session_item_set_properties (WP_SESSION_ITEM (self),
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"));
}
/* 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 */
g_signal_connect_object (self->adapter, "adapter-ports-state-changed",
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_FACTORY_NAME, "support.null-audio-sink",
PW_KEY_NODE_DESCRIPTION, desc,
PW_KEY_NODE_AUTOCONNECT, "true",
"monitor.channel-volumes", "true",
"wireplumber.is-virtual", "true",
NULL));

View File

@@ -221,14 +221,16 @@ wireplumber.components = [
{ 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 = 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-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/prepare-link.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/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/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 = 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/state-stream.lua, type = script/lua, priority = 100 }
]

View File

@@ -24,6 +24,12 @@ end
function cutils.getTargetDirection (properties)
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
(properties ["item.node.direction"] == "input" and
cutils.parseBool (properties ["stream.capture.sink"])) then

View File

@@ -134,8 +134,6 @@ function putils.canLink (properties, si_target)
return false
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)
return properties ["item.node.direction"] == "input" and
parseBool (properties ["item.features.monitor"]) and
@@ -143,10 +141,20 @@ function putils.canLink (properties, si_target)
properties ["item.factory.name"] == "si-audio-adapter"
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"]
and not isMonitor (target_props) then
return false
end
end
-- check link group
local function canLinkGroupCheck(link_group, si_target, hops)
@@ -305,8 +313,6 @@ linkables_om:activate ()
links_om = ObjectManager {
Interest {
type = "SiLink",
-- only handle links created by this policy
Constraint { "is.policy.item.link", "=", true },
}
}

View File

@@ -15,6 +15,7 @@ local config = require ("policy-config")
SimpleEventHook {
name = "linking/find-defined-target",
after = "linking/find-virtual-target",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },

View 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 ()

View File

@@ -20,7 +20,7 @@ AsyncEventHook {
},
steps = {
start = {
next = "link_activated",
next = "none",
execute = function (event, transition)
local source, om, si, si_props, si_flags, target =
putils:unwrap_find_target_event (event)
@@ -53,6 +53,17 @@ AsyncEventHook {
.. tostring (si_link))
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
-- playback
out_item = si
@@ -62,12 +73,18 @@ AsyncEventHook {
in_item = si
out_item = target
end
end
local is_virtual_client_link = target_props ["item.factory.name"] == "si-audio-virtual"
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 (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
si_link = SessionItem ("si-standard-link")
@@ -79,7 +96,11 @@ AsyncEventHook {
["exclusive"] = exclusive,
["out.item.port.context"] = "output",
["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
transition:return_error ("failed to configure si-standard-link "
.. tostring (si_link))
@@ -119,7 +140,12 @@ AsyncEventHook {
end
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)
if e then
transition:return_error ("failed to activate si-standard-link: "
@@ -130,33 +156,21 @@ AsyncEventHook {
l:remove ()
else
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 ()
end
end)
end,
},
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.
else
transition:advance ()
return
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,
},
},

View 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 ()

View File

@@ -14,16 +14,17 @@ local putils = require ("policy-utils")
local cutils = require ("common-utils")
function checkLinkable (si, om, handle_nonstreams)
-- only handle stream session items
local si_props = si.properties
if not si_props or (si_props ["item.node.type"] ~= "stream"
and not handle_nonstreams) then
return false
-- Always handle si-audio-virtual session items
if si_props ["item.factory.name"] == "si-audio-virtual" then
return true, si_props
end
-- Determine if we can handle item by this policy
if si_props ["item.factory.name"] == "si-audio-virtual" then
return false
-- For the rest of them, only handle stream session items
if not si_props or (si_props ["item.node.type"] ~= "stream"
and not handle_nonstreams) then
return false, si_props
end
return true, si_props
@@ -116,7 +117,7 @@ SimpleEventHook {
EventInterest {
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
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
EventInterest {

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -81,9 +81,9 @@ test_si_audio_virtual_configure_activate (TestFixture * f,
str = wp_properties_get (props, "name");
g_assert_nonnull (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_cmpstr ("1", ==, str);
g_assert_cmpstr ("output", ==, str);
str = wp_properties_get (props, "item.factory.name");
g_assert_nonnull (str);
g_assert_cmpstr ("si-audio-virtual", ==, str);