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

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