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:
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 ()
|
Reference in New Issue
Block a user