wireplumber/src/scripts/linking/rescan.lua
Torkel Niklasson 6ebf81453c rescan: Optimize linking for simple stream nodes
Add immediate linking for common cases to reduce latency when new
audio/video streams are added. The full rescan mechanism is preserved
and still triggers for all session-item-added events via the
rescan-trigger hook.
2025-11-28 11:05:34 +00:00

332 lines
9.9 KiB
Lua

-- 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 = {}
handles.rescan_enabled = true
handles.timeout_source = nil
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:get_property ("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)
-- For the rest of them, only handle stream session items
if si:get_property ("item.node.type") ~= "stream" and
not handle_nonstreams then
return false
end
-- check filters
if not checkFilter (si, om, handle_nonstreams) then
return false
end
return true
end
function unhandleLinkable (si, om)
if not checkLinkable (si, om, true) then
return
end
local si_id = si.id
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 silink_props = silink.properties
local out_id = silink_props:get_int ("out.item.id")
local in_id = silink_props:get_int ("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 silink_props:get_boolean ("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 ()
-- Handle newly added linkable immediately without waiting for full rescan
-- Only for simple cases where we know it won't affect other parts of the graph
SimpleEventHook {
name = "linking/linkable-added-immediate",
before = "linking/rescan-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-added" },
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")
if not checkLinkable (si, om, false) then
return
end
-- Don't handle immediately if this is a smart filter that could affect other nodes
local node = si:get_associated_proxy ("node")
local link_group = node:get_property ("node.link-group")
if link_group then
local direction = cutils.getTargetDirection (si.properties)
if futils.is_filter_smart (direction, link_group) then
-- Smart filters need full rescan to handle cascading effects
return
end
end
-- Only handle if autoconnect is enabled
local autoconnect = si:get_property ("node.autoconnect")
if autoconnect ~= "true" then
return
end
-- Check if this is a simple stream (most common case)
-- Don't handle device nodes or special nodes that might become default targets
if si:get_property ("item.node.type") ~= "stream" then
return
end
-- Push select-target event immediately for simple stream case
source:call ("push-event", "select-target", si, nil)
end
}:register ()
function handleLinkables (source)
local om = source:call ("get-object-manager", "session-item")
for si in om:iterate { type = "SiLinkable" } do
if not checkLinkable (si, om) then
goto skip_linkable
end
-- Get properties
local si_props = si.properties
-- check if we need to link this node at all
local autoconnect = si_props:get_boolean ("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:get_property ("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" },
},
-- on any "filters" metadata changed
EventInterest {
Constraint { "event.type", "=", "metadata-changed" },
Constraint { "metadata.name", "=", "filters" },
},
},
execute = function (event)
if handles.rescan_enabled then
local source = event:get_source ()
source:call ("schedule-rescan", "linking")
end
end
}:register ()
SimpleEventHook {
name = "linking/session-item-added",
before = "linking/rescan-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-added" },
},
},
execute = function (event)
-- clear timeout source, if any
if handles.timeout_source ~= nil then
handles.timeout_source:destroy ()
handles.timeout_source = nil
end
-- Always enable rescan when any node is added
handles.rescan_enabled = true
end
}:register ()
-- Stop rescan for 2 seconds if BT item was removed. This avoids audio
-- being played on internal nodes for a few seconds while the BT device is
-- switching profiles.
SimpleEventHook {
name = "linking/bluez-session-item-removed",
before = "linking/rescan-trigger",
interests = {
EventInterest {
Constraint { "event.type", "=", "session-item-removed" },
Constraint { "device.api", "=", "bluez5" },
},
},
execute = function (event)
local si = event:get_subject ()
local source = event:get_source ()
-- clear timeout source, if any
if handles.timeout_source ~= nil then
handles.timeout_source:destroy ()
handles.timeout_source = nil
end
-- disable rescan
handles.rescan_enabled = false
-- re-enable rescan after 2 seconds
handles.timeout_source = Core.timeout_add (2000, function()
handles.timeout_source = nil
handles.rescan_enabled = true
source:call ("schedule-rescan", "linking")
end)
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"))