-- WirePlumber
--
-- Copyright © 2024 Collabora Ltd.
-- @author Ashok Sidipotu <[email protected]>
--
-- SPDX-License-Identifier: MIT
lutils = require("linking-utils")
cutils = require("common-utils")
log = Log.open_topic("s-linking")
function restoreVolume (om, link)
setVolume(om, link, 1.0)
end
function duckVolume (om, link)
setVolume(om, link, Settings.get_float("linking.role-based.duck-level"))
end
function setVolume (om, link, level)
local lprops = link.properties
local media_role_si_id = nil
local dir = lprops ["item.node.direction"]
if dir == "output" then
media_role_si_id = lprops ["out.item.id"]
else
media_role_si_id = lprops ["in.item.id"]
end
local media_role_lnkbl = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "id", "=", media_role_si_id, type = "gobject" },
}
-- apply volume control on the stream node of the loopback module, instead of
-- the sink/source node as it simplifies the volume ducking and
-- restoration.
local media_role_other_lnkbl = om:lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.link-group", "=", media_role_lnkbl.properties ["node.link-group"] },
Constraint { "id", "!", media_role_lnkbl.id, type = "gobject" },
}
if media_role_other_lnkbl then
local n = media_role_other_lnkbl:get_associated_proxy("node")
if n then
log:info(string.format(".. %s volume of media role node \"%s(%d)\" to %f",
level < 1.0 and "duck" or "restore", n.properties ["node.name"],
n ["bound-id"], level))
local props = {
"Spa:Pod:Object:Param:Props",
"Props",
volume = level,
}
local param = Pod.Object(props)
n:set_param("Props", param)
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-media-role-links",
interests = {
EventInterest {
-- on media client link added and removed
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
Constraint { "event.session-item.interface", "=", "link" },
Constraint { "is.role.policy.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, om, _, si_props, _, _ =
lutils:unwrap_select_target_event(event)
local metadata_om = source:call("get-object-manager", "metadata")
local suspend = getSuspendPlaybackFromMetadata(metadata_om)
local pending_activations = 0
local mc = si_props ["target.media.class"]
local pmrl_active = nil
pmrl = lutils.getPriorityMediaRoleLink(mc)
log:debug("Rescanning media role links...")
local function onMediaRoleLinkActivated (l, e)
local si_id = tonumber(l.properties ["main.item.id"])
local target_id = tonumber(l.properties ["target.item.id"])
local si_flags = lutils:get_flags(si_id)
if e then
log:warning(l, "failed to activate media role link: " .. e)
if si_flags ~= nil then
si_flags.peer_id = nil
end
l:remove()
else
log:info(l, "media role 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 media role links activated")
transition:advance()
end
end
for link in om:iterate {
type = "SiLink",
Constraint { "is.role.policy.link", "=", true },
Constraint { "target.media.class", "=", mc },
} do
-- deactivate all links if suspend playback metadata is present
if suspend then
link:deactivate(Feature.SessionItem.ACTIVE)
end
local active = ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
log:debug(string.format(" .. looking at link(%d) active %s pmrl %s", link.id, tostring(active),
tostring(link == pmrl)))
if link == pmrl then
pmrl_active = active
restoreVolume(om, pmrl)
goto continue
end
local action = lutils.getAction(pmrl, link)
log:debug(string.format(" .. apply action(%s) on link(%d)", action, link.id, tostring(active)))
if action == "cork" then
if active then
link:deactivate(Feature.SessionItem.ACTIVE)
end
elseif action == "mix" then
if not active and not suspend then
pending_activations = pending_activations + 1
link:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
end
restoreVolume(om, link)
elseif action == "duck" then
if not active and not suspend then
pending_activations = pending_activations + 1
link:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
end
duckVolume(om, link)
else
log:warning("Unknown action: " .. action)
end
::continue::
end
if pmrl and not pmrl_active then
pending_activations = pending_activations + 1
pmrl:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
restoreVolume(om, pmrl)
end
-- just advance transition if no pending activations are needed
if pending_activations <= 0 then
log:debug("All media role links rescanned")
transition:advance()
end
end,
},
},
}:register()