Moving files:

This commit is contained in:
Antoine Phan
2024-07-27 00:06:16 +07:00
parent 9716a8df5a
commit 0cd53e0b65
260 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Traverse through all the possible targets to pick up target node.
lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-best-target",
after = { "linking/find-defined-target",
"linking/find-filter-target",
"linking/find-media-role-target",
"linking/find-default-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
local target_direction = cutils.getTargetDirection (si_props)
local target_picked = nil
local target_can_passthrough = false
local target_priority = 0
local target_plugged = 0
log:info (si, string.format ("handling item: %s (%s)",
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
for target in om:iterate {
type = "SiLinkable",
Constraint { "item.node.type", "=", "device" },
Constraint { "item.node.direction", "=", target_direction },
Constraint { "media.type", "=", si_props ["media.type"] },
} do
local target_props = target.properties
local target_node_id = target_props ["node.id"]
local si_target_node = target:get_associated_proxy ("node")
local si_target_link_group = si_target_node.properties ["node.link-group"]
local priority = tonumber (target_props ["priority.session"]) or 0
log:debug (string.format ("Looking at: %s (%s)",
tostring (target_props ["node.name"]),
tostring (target_node_id)))
-- Skip smart filters as best target
if si_target_link_group ~= nil and
futils.is_filter_smart (target_direction, si_target_link_group) then
Log.debug ("... ignoring smart filter as best target")
goto skip_linkable
end
if not lutils.canLink (si_props, target) then
log:debug ("... cannot link, skip linkable")
goto skip_linkable
end
if not lutils.haveAvailableRoutes (target_props) then
log:debug ("... does not have routes, skip linkable")
goto skip_linkable
end
local passthrough_compatible, can_passthrough =
lutils.checkPassthroughCompatibility (si, target)
if not passthrough_compatible then
log:debug ("... passthrough is not compatible, skip linkable")
goto skip_linkable
end
local plugged = tonumber (target_props ["item.plugged.usec"]) or 0
log:debug ("... priority:" .. tostring (priority) .. ", plugged:" .. tostring (plugged))
-- (target_picked == NULL) --> make sure atleast one target is picked.
-- (priority > target_priority) --> pick the highest priority linkable(node)
-- target.
-- (priority == target_priority and plugged > target_plugged) --> pick the
-- latest connected/plugged(in time) linkable(node) target.
if (target_picked == nil or
priority > target_priority or
(priority == target_priority and plugged > target_plugged)) then
log:debug ("... picked")
target_picked = target
target_can_passthrough = can_passthrough
target_priority = priority
target_plugged = plugged
end
::skip_linkable::
end
if target_picked then
log:info (si,
string.format ("... best target picked: %s (%s), can_passthrough:%s",
tostring (target_picked.properties ["node.name"]),
tostring (target_picked.properties ["node.id"]),
tostring (target_can_passthrough)))
si_flags.can_passthrough = target_can_passthrough
event:set_data ("target", target_picked)
end
end
}:register ()

View File

@@ -0,0 +1,58 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if default nodes can be picked up as target node.
lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-default-target",
after = { "linking/find-defined-target",
"linking/find-filter-target",
"linking/find-media-role-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
local target_picked = false
log:info (si, string.format ("handling item: %s (%s)",
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
target = lutils.findDefaultLinkable (si)
local can_passthrough, passthrough_compatible
if target then
passthrough_compatible, can_passthrough =
lutils.checkPassthroughCompatibility (si, target)
if lutils.canLink (si_props, target) and passthrough_compatible then
target_picked = true;
end
end
if target_picked then
log:info (si,
string.format ("... default target picked: %s (%s), can_passthrough:%s",
tostring (target.properties ["node.name"]),
tostring (target.properties ["node.id"]),
tostring (can_passthrough)))
si_flags.can_passthrough = can_passthrough
event:set_data ("target", target)
end
end
}:register ()

View File

@@ -0,0 +1,131 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is defined explicitly.
-- This defination can be done in two ways.
-- 1. "node.target"/"target.object" in the node properties
-- 2. "target.node"/"target.object" in the default metadata
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-defined-target",
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
local metadata = Settings.get_boolean ("linking.allow-moving-streams") and
cutils.get_default_metadata_object ()
local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
local dont_move = cutils.parseBool (si_props ["node.dont-move"])
local target_key
local target_value = nil
local node_defined = false
local target_picked = nil
if si_props ["target.object"] ~= nil then
target_value = si_props ["target.object"]
target_key = "object.serial"
node_defined = true
elseif si_props ["node.target"] ~= nil then
target_value = si_props ["node.target"]
target_key = "node.id"
node_defined = true
end
if metadata and not dont_move then
local id = metadata:find (si_props ["node.id"], "target.object")
if id ~= nil then
target_value = id
target_key = "object.serial"
node_defined = false
else
id = metadata:find (si_props ["node.id"], "target.node")
if id ~= nil then
target_value = id
target_key = "node.id"
node_defined = false
end
end
end
if target_value == "-1" then
target_picked = false
target = nil
elseif target_value and tonumber (target_value) then
target = om:lookup {
type = "SiLinkable",
Constraint { target_key, "=", target_value },
}
if target and lutils.canLink (si_props, target) then
target_picked = true
end
elseif target_value then
for lnkbl in om:iterate { type = "SiLinkable" } do
local target_props = lnkbl.properties
if (target_props ["node.name"] == target_value or
target_props ["object.path"] == target_value) and
target_props ["item.node.direction"] == cutils.getTargetDirection (si_props) and
lutils.canLink (si_props, lnkbl) then
target_picked = true
target = lnkbl
break
end
end
end
local can_passthrough, passthrough_compatible
if target then
passthrough_compatible, can_passthrough =
lutils.checkPassthroughCompatibility (si, target)
if not passthrough_compatible then
target = nil
end
end
si_flags.has_defined_target = false
if target_picked and target then
log:info (si,
string.format ("... defined target picked: %s (%s), can_passthrough:%s",
tostring (target.properties ["node.name"]),
tostring (target.properties ["node.id"]),
tostring (can_passthrough)))
si_flags.has_node_defined_target = node_defined
si_flags.can_passthrough = can_passthrough
si_flags.has_defined_target = true
event:set_data ("target", target)
elseif target_value and dont_fallback then
-- send error to client and destroy node if linger is not set
local linger = cutils.parseBool (si_props ["node.linger"])
if not linger then
local node = si:get_associated_proxy ("node")
lutils.sendClientError (event, node, -2, "defined target not found")
node:request_destroy ()
log:info(si, "... destroyed node as defined target was not found")
else
log:info(si, "... waiting for defined target as dont-fallback is set")
end
event:stop_processing ()
end
end
}:register ()

View File

@@ -0,0 +1,92 @@
-- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is a filter target.
lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")
function findFilterTarget (si, om)
local node = si:get_associated_proxy ("node")
local link_group = node.properties ["node.link-group"]
local target_id = -1
-- return nil if session item is not a filter node
if link_group == nil then
return nil, false
end
-- return nil if filter is not smart
local direction = cutils.getTargetDirection (si.properties)
if not futils.is_filter_smart (direction, link_group) then
return nil, false
end
-- get the filter target
return futils.get_filter_target (direction, link_group), true
end
SimpleEventHook {
name = "linking/find-filter-target",
after = "linking/find-defined-target",
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
local target_picked = false
local allow_fallback
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
target, is_smart_filter = findFilterTarget (si, om)
local can_passthrough, passthrough_compatible
if target then
passthrough_compatible, can_passthrough =
lutils.checkPassthroughCompatibility (si, target)
if lutils.canLink (si_props, target) and passthrough_compatible then
target_picked = true
end
end
if target_picked and target then
log:info (si,
string.format ("... filter target picked: %s (%s), can_passthrough:%s",
tostring (target.properties ["node.name"]),
tostring (target.properties ["node.id"]),
tostring (can_passthrough)))
si_flags.can_passthrough = can_passthrough
event:set_data ("target", target)
elseif is_smart_filter and dont_fallback then
-- send error to client and destroy node if linger is not set
local linger = cutils.parseBool (si_props ["node.linger"])
if not linger then
local node = si:get_associated_proxy ("node")
lutils.sendClientError (event, node, -2, "smart filter defined target not found")
node:request_destroy ()
log:info(si, "... destroyed node as smart filter defined target was not found")
else
log:info(si, "... waiting for smart filter defined target as dont-fallback is set")
end
event:stop_processing ()
end
end
}:register ()

View File

@@ -0,0 +1,70 @@
-- WirePlumber
--
-- Copyright © 2024 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Select the media role target
cutils = require("common-utils")
lutils = require("linking-utils")
log = Log.open_topic("s-linking")
SimpleEventHook {
name = "linking/find-media-role-target",
after = { "linking/find-defined-target",
"linking/find-filter-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local _, om, si, si_props, _, target =
lutils:unwrap_select_target_event (event)
local target_direction = cutils.getTargetDirection (si_props)
local media_role = si_props["media.role"]
-- bypass the hook if the target is already picked up or if the role is not
-- defined
if target or media_role == nil then
return
end
log:info (si, string.format ("handling item %d: %s (%s) role (%s)", si.id,
tostring (si_props ["node.name"]), tostring (si_props ["node.id"]), media_role))
for si_target in om:iterate {
type = "SiLinkable",
Constraint { "item.node.direction", "=", target_direction },
Constraint { "device.intended-roles", "+" },
Constraint { "media.type", "=", si_props["media.type"] },
} do
local roles_json = si_target.properties["device.intended-roles"]
local roles_table = Json.Raw(roles_json):parse()
for _, target_role in ipairs(roles_table) do
if target_role == media_role then
target = si_target
break
end
end
if target then
break
end
end
-- set target
if target ~= nil then
log:info(si,
string.format("... media role target picked: %s (%s)",
tostring(target.properties["node.name"]),
tostring(target.properties["node.id"])))
event:set_data("target", target)
end
end
}:register()

View File

@@ -0,0 +1,37 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- example of a user injectible hook to link a node to a custom target
lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/sample-find-user-target",
before = "linking/find-defined-target",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
log:info (si, "in find-user-target")
-- implement logic here to find a suitable target
-- store the found target on the event,
-- the next hooks will take care of linking
event:set_data ("target", target)
end
}:register ()

View File

@@ -0,0 +1,92 @@
-- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Check if the target node is a filter target.
lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/get-filter-from-target",
after = { "linking/find-defined-target",
"linking/find-filter-target",
"linking/find-media-role-target",
"linking/find-default-target",
"linking/find-best-target" },
before = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target was not found or if it is a role-based policy target
if target == nil or lutils.is_role_policy_target (si_props, target.properties) then
return
end
-- bypass the hook if the session item is a smart filter
local node = si:get_associated_proxy ("node")
local node_props = node.properties
local link_group = node_props ["node.link-group"]
local target_direction = cutils.getTargetDirection (si.properties)
if link_group ~= nil and
futils.is_filter_smart (target_direction, link_group) then
return
end
-- bypass the hook if target is defined, is a filter and is targetable
local target_node = target:get_associated_proxy ("node")
local target_node_props = target_node.properties
local target_link_group = target_node_props ["node.link-group"]
if target_link_group ~= nil and si_flags.has_defined_target then
if futils.is_filter_smart (target_direction, target_link_group) and
not futils.is_filter_disabled (target_direction, target_link_group) and
futils.is_filter_targetable (target_direction, target_link_group) then
return
end
end
-- Get the filter from the given target if it exists, otherwise get the
-- default filter, but only if target was not defined
local media_type = si_props["media.type"]
local filter_target = futils.get_filter_from_target (target_direction, media_type, target)
if filter_target ~= nil then
target = filter_target
log:info (si, "... got filter for given target")
elseif filter_target == nil and not si_flags.has_defined_target then
filter_target = futils.get_filter_from_target (target_direction, media_type, nil)
if filter_target ~= nil then
target = filter_target
log:info (si, "... got default filter for given target")
end
end
local can_passthrough, passthrough_compatible
if target ~= nil then
passthrough_compatible, can_passthrough =
lutils.checkPassthroughCompatibility (si, target)
if lutils.canLink (si_props, target) and passthrough_compatible then
target_picked = true;
end
end
if target_picked then
log:info (si,
string.format ("... target picked: %s (%s), can_passthrough:%s",
tostring (target.properties ["node.name"]),
tostring (target.properties ["node.id"]),
tostring (can_passthrough)))
si_flags.can_passthrough = can_passthrough
event:set_data ("target", target)
end
end
}:register ()

View File

@@ -0,0 +1,157 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Links a session item to the target that has been previously selected.
-- This is meant to be the last hook in the select-target chain.
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")
AsyncEventHook {
name = "linking/link-target",
after = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
steps = {
start = {
next = "none",
execute = function (event, transition)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
if not target then
-- bypass the hook, nothing to link to.
transition:advance ()
return
end
local target_props = target.properties
local out_item = nil
local in_item = nil
local si_link = nil
local passthrough = si_flags.can_passthrough
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
local exclusive = cutils.parseBool (si_props ["node.exclusive"])
-- break rescan if tried more than 5 times with same target
if si_flags.failed_peer_id ~= nil and
si_flags.failed_peer_id == target.id and
si_flags.failed_count ~= nil and
si_flags.failed_count > 5 then
transition:return_error ("tried to link on last rescan, not retrying "
.. tostring (si_link))
return
end
if si_props["item.node.direction"] == "output" then
-- playback
out_item = si
in_item = target
else
-- capture
in_item = si
out_item = target
end
local is_role_policy_link = lutils.is_role_policy_target (si_props, target_props)
log:info (si,
string.format ("link %s <-> %s passthrough:%s, exclusive:%s, media role link:%s",
tostring (si_props ["node.name"]),
tostring (target_props ["node.name"]),
tostring (passthrough),
tostring (exclusive),
tostring (is_role_policy_link)))
-- create and configure link
si_link = SessionItem ("si-standard-link")
if not si_link:configure {
["out.item"] = out_item,
["in.item"] = in_item,
["passthrough"] = passthrough,
["exclusive"] = exclusive,
["out.item.port.context"] = "output",
["in.item.port.context"] = "input",
["media.role"] = si_props["media.role"],
["target.media.class"] = target_props["media.class"],
["policy.role-based.priority"] = target_props["policy.role-based.priority"],
["policy.role-based.action.same-priority"] = target_props["policy.role-based.action.same-priority"],
["policy.role-based.action.lower-priority"] = target_props["policy.role-based.action.lower-priority"],
["is.role.policy.link"] = is_role_policy_link,
["main.item.id"] = si.id,
["target.item.id"] = target.id,
} then
transition:return_error ("failed to configure si-standard-link "
.. tostring (si_link))
return
end
local ids = {si.id, target.id}
si_link:connect("link-error", function (_, error_msg)
for _, id in ipairs (ids) do
local si = om:lookup {
Constraint { "id", "=", id, type = "gobject" },
}
if si then
local node = si:get_associated_proxy ("node")
lutils.sendClientError(event, node, -32, error_msg)
end
end
end)
-- register
si_flags.was_handled = true
si_flags.peer_id = target.id
si_flags.failed_peer_id = target.id
if si_flags.failed_count ~= nil then
si_flags.failed_count = si_flags.failed_count + 1
else
si_flags.failed_count = 1
end
si_link:register ()
log:debug (si_link, "registered link between "
.. tostring (si) .. " and " .. tostring (target))
-- only activate non role-based policy links because their activation is
-- handled by rescan-media-role-links.lua
if not is_role_policy_link then
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
if e then
transition:return_error (tostring (l) .. " link failed: "
.. tostring (e))
if si_flags ~= nil then
si_flags.peer_id = nil
end
l:remove ()
else
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:debug (l, "activated link between "
.. tostring (si) .. " and " .. tostring (target))
transition:advance ()
end
end)
else
lutils.updatePriorityMediaRoleLink(si_link)
transition:advance ()
end
end,
},
},
}:register ()

View File

@@ -0,0 +1,125 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- remove the existing link if needed, check the properties of target, which
-- indicate it is not available for linking. If no target is available, send
-- down an error to the corresponding client.
lutils = require ("linking-utils")
cutils = require ("common-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/prepare-link",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, _, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
local si_id = si.id
local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
local exclusive = cutils.parseBool (si_props ["node.exclusive"])
local si_must_passthrough = cutils.parseBool (si_props ["item.node.encoded-only"])
log:info (si, string.format ("handling item %d: %s (%s)", si_id,
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
-- Check if item is linked to proper target, otherwise re-link
if si_flags.peer_id then
if target and si_flags.peer_id == target.id then
log:info (si, "... already linked to proper target")
-- Check this also here, in case in default targets changed
if Settings.get_boolean ("linking.follow-default-target") and
si_flags.has_node_defined_target then
lutils.checkFollowDefault (si, target)
end
target = nil
goto done
end
local link = lutils.lookupLink (si_id, si_flags.peer_id)
if reconnect then
if link ~= nil then
-- remove old link
if ((link:get_active_features () & Feature.SessionItem.ACTIVE) == 0)
then
-- remove also not yet activated links: they might never become
-- active, and we need not wait for it to become active
log:warning (link, "Link was not activated before removing")
end
si_flags.peer_id = nil
link:remove ()
log:info (si, "... moving to new target")
end
else
if link ~= nil then
log:info (si, "... dont-reconnect, not moving")
goto done
end
end
end
-- if the stream has dont-reconnect and was already linked before,
-- don't link it to a new target
if not reconnect and si_flags.was_handled then
target = nil
goto done
end
-- check target's availability
if target then
local target_is_linked, target_is_exclusive = lutils.isLinked (target)
if target_is_exclusive then
log:info (si, "... target is linked exclusively")
target = nil
end
if target_is_linked then
if exclusive or si_must_passthrough then
log:info (si, "... target is already linked, cannot link exclusively")
target = nil
else
-- disable passthrough, we can live without it
si_flags.can_passthrough = false
end
end
end
if not target then
log:info (si, "... target not found, reconnect:" .. tostring (reconnect))
local node = si:get_associated_proxy ("node")
if reconnect and si_flags.was_handled then
log:info (si, "... waiting reconnect")
return
end
local linger = cutils.parseBool (si_props ["node.linger"])
if linger then
log:info (si, "... node linger")
return
end
lutils.sendClientError (event, node, -2,
reconnect and "no target node available" or "target not found")
if not reconnect then
log:info (si, "... destroy node")
node:request_destroy ()
end
end
::done::
event:set_data ("target", target)
end
}:register ()

View File

@@ -0,0 +1,203 @@
-- WirePlumber
--
-- Copyright © 2024 Collabora Ltd.
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
--
-- 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 simplyfies 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()

View File

@@ -0,0 +1,235 @@
-- WirePlumber
--
-- Copyright © 2020-2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Handle new linkables and trigger rescanning of the graph.
-- Rescan the graph by pushing new select-target events for
-- all linkables that need to be linked
-- Cleanup links when the linkables they are associated with are removed.
-- Also, cleanup flags attached to linkables.
lutils = require ("linking-utils")
cutils = require ("common-utils")
futils = require ("filter-utils")
log = Log.open_topic ("s-linking")
handles = {}
function checkFilter (si, om, handle_nonstreams)
-- always handle filters if handle_nonstreams is true, even if it is disabled
if handle_nonstreams then
return true
end
-- always return true if this is not a filter
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
if link_group == nil then
return true
end
local direction = cutils.getTargetDirection (si.properties)
-- always handle filters that are not smart
if not futils.is_filter_smart (direction, link_group) then
return true
end
-- dont handle smart filters that are disabled
return not futils.is_filter_disabled (direction, link_group)
end
function checkLinkable (si, om, handle_nonstreams)
local si_props = si.properties
-- 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
-- check filters
if not checkFilter (si, om, handle_nonstreams) then
return false, si_props
end
return true, si_props
end
function unhandleLinkable (si, om)
local si_id = si.id
local valid, si_props = checkLinkable (si, om, true)
if not valid then
return
end
log:info (si, string.format ("unhandling item %d", si_id))
-- iterate over all the links in the graph and
-- remove any links associated with this item
for silink in om:iterate { type = "SiLink" } 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
local in_flags = lutils:get_flags (in_id)
local out_flags = lutils:get_flags (out_id)
if out_id == si_id and in_flags.peer_id == out_id then
in_flags.peer_id = nil
elseif in_id == si_id and out_flags.peer_id == in_id then
out_flags.peer_id = nil
end
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
lutils.clearPriorityMediaRoleLink(silink)
end
silink:remove ()
log:info (silink, "... link removed")
end
end
lutils:clear_flags (si_id)
end
SimpleEventHook {
name = "linking/linkable-removed",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-removed" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
},
execute = function (event)
local si = event:get_subject ()
local source = event:get_source ()
local om = source:call ("get-object-manager", "session-item")
unhandleLinkable (si, om)
end
}:register ()
function handleLinkables (source)
local om = source:call ("get-object-manager", "session-item")
for si in om:iterate { type = "SiLinkable" } do
local valid, si_props = checkLinkable (si, om)
if not valid then
goto skip_linkable
end
-- check if we need to link this node at all
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
if not autoconnect then
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
goto skip_linkable
end
-- push event to find target and link
source:call ("push-event", "select-target", si, nil)
::skip_linkable::
end
end
SimpleEventHook {
name = "linking/rescan",
interests = {
EventInterest {
Constraint { "event.type", "=", "rescan-for-linking" },
},
},
execute = function (event)
local source = event:get_source ()
local om = source:call ("get-object-manager", "session-item")
log:info ("rescanning...")
-- always unlink all filters that are smart and disabled
for si in om:iterate {
type = "SiLinkable",
Constraint { "node.link-group", "+" },
} do
local node = si:get_associated_proxy ("node")
local link_group = node.properties["node.link-group"]
local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) and
futils.is_filter_disabled (direction, link_group) then
unhandleLinkable (si, om)
end
end
handleLinkables (source)
end
}:register ()
SimpleEventHook {
name = "linking/rescan-trigger",
interests = {
-- on linkable added or removed, where linkable is adapter or plain node
EventInterest {
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
Constraint { "event.session-item.interface", "=", "linkable" },
},
-- on device Routes changed
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "c", "Route", "EnumRoute" },
},
-- on any "default" target changed
EventInterest {
Constraint { "event.type", "=", "metadata-changed" },
Constraint { "metadata.name", "=", "default" },
Constraint { "event.subject.key", "c", "default.audio.source",
"default.audio.sink", "default.video.source" },
},
},
execute = function (event)
local source = event:get_source ()
source:call ("schedule-rescan", "linking")
end
}:register ()
SimpleEventHook {
name = "linking/rescan-trigger-on-filters-metadata-changed",
interests = {
EventInterest {
Constraint { "event.type", "=", "metadata-changed" },
Constraint { "metadata.name", "=", "filters" },
},
},
execute = function (event)
local source = event:get_source ()
source:call ("schedule-rescan", "linking")
end
}:register ()
function handleMoveSetting (enable)
if (not handles.move_hook) and (enable == true) then
handles.move_hook = SimpleEventHook {
name = "linking/rescan-trigger-on-target-metadata-changed",
interests = {
EventInterest {
Constraint { "event.type", "=", "metadata-changed" },
Constraint { "metadata.name", "=", "default" },
Constraint { "event.subject.key", "c", "target.object", "target.node" },
},
},
execute = function (event)
local source = event:get_source ()
source:call ("schedule-rescan", "linking")
end
}
handles.move_hook:register()
elseif (handles.move_hook) and (enable == false) then
handles.move_hook:remove ()
handles.move_hook = nil
end
end
Settings.subscribe ("linking.allow-moving-streams", function ()
handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))
end)
handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))