mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-01-13 01:00:17 +01:00
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.
332 lines
9.9 KiB
Lua
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"))
|