Files
wireplumber/src/scripts/linking/rescan-virtual-links.lua
George Kiagiadakis 43aa2d4952 scripts: don't use 'local' for file-wide scoped variables
Since all scripts run in a sandbox with their own global environment,
it means that they don't interfere with each other's global variables.
Therefore, all file-wide variables can be declared global without
any change in behavior. In my understanding, it is better to do so
because this means that any code accessing those variables is going
to access them directly from the global environment table with a simple
lookup rather than having each variable referenced in the local closure
of each function separately.
2023-09-29 23:13:28 +03:00

255 lines
8.0 KiB
Lua

-- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
-- @author Julian Bouzas <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
putils = require ("linking-utils")
log = Log.open_topic ("s-linking")
defaults = {}
defaults.duck_level = 0.3
defaults.roles = Json.Object {}
config = {}
config.duck_level = Conf.get_value_float ("wireplumber.settings",
"linking.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.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 ()