New wireplumber config

This commit is contained in:
2024-07-26 06:23:40 -04:00
parent f9f2119beb
commit 9716a8df5a
99 changed files with 7572 additions and 5398 deletions

View File

@@ -0,0 +1,94 @@
-- WirePlumber
-- Copyright © 2022 Collabora Ltd.
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
-- SPDX-License-Identifier: MIT
-- Script is a Lua Module of common Lua utility functions
local cutils = {}
function cutils.parseBool (var)
return var and (var:lower () == "true" or var == "1")
end
function cutils.parseParam (param, id)
local props = param:parse ()
if props.pod_type == "Object" and props.object_id == id then
return props.properties
else
return nil
end
end
function cutils.mediaClassToDirection (media_class)
if media_class:find ("Sink") or
media_class:find ("Input") or
media_class:find ("Duplex") then
return "input"
elseif media_class:find ("Source") or media_class:find ("Output") then
return "output"
else
return nil
end
end
function cutils.getTargetDirection (properties)
local target_direction = nil
if properties ["item.node.direction"] == "output" or
(properties ["item.node.direction"] == "input" and
cutils.parseBool (properties ["stream.capture.sink"])) then
target_direction = "input"
else
target_direction = "output"
end
return target_direction
end
local default_nodes = Plugin.find ("default-nodes-api")
function cutils.getDefaultNode (properties, target_direction)
local target_media_class =
properties ["media.type"] ..
(target_direction == "input" and "/Sink" or "/Source")
if not default_nodes then
default_nodes = Plugin.find ("default-nodes-api")
end
return default_nodes:call ("get-default-node", target_media_class)
end
cutils.source_plugin = nil
cutils.object_managers = {}
function cutils.get_object_manager (name)
cutils.source_plugin = cutils.source_plugin or
Plugin.find ("standard-event-source")
cutils.object_managers [name] = cutils.object_managers [name] or
cutils.source_plugin:call ("get-object-manager", name)
return cutils.object_managers [name]
end
function cutils.get_default_metadata_object ()
return cutils.get_object_manager ("metadata"):lookup {
Constraint { "metadata.name", "=", "default" },
}
end
function cutils.arrayContains (a, value)
for _, v in ipairs (a) do
if v == value then
return true
end
end
return false
end
function cutils.get_application_name ()
return Core.get_properties()["application.name"] or "WirePlumber"
end
return cutils

View File

@@ -0,0 +1,74 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
local module = {
-- table of device info
dev_infos = {},
}
SimpleEventHook {
name = "lib/device-info-cache/cleanup",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-removed" },
},
},
execute = function (event)
local props = event:get_properties ()
local device_id = props ["object.serial"]
Log.trace ("cleaning up dev_info for object.serial = " .. device_id)
module.dev_infos [device_id] = nil
end
}:register()
function module.get_device_info (self, device)
local device_properties = device.properties
local device_id = device_properties ["object.serial"]
local dev_info = self.dev_infos [device_id]
-- new device
if not dev_info then
local device_name = device_properties ["device.name"]
if not device_name then
Log.critical (device, "invalid device.name")
return nil
end
Log.trace (device, string.format (
"create dev_info for '%s', object.serial = %s", device_name, device_id))
dev_info = {
name = device_name,
active_profile = -1,
route_infos = {},
}
self.dev_infos [device_id] = dev_info
end
return dev_info
end
function module.find_route_info (dev_info, route, return_new)
local ri = dev_info.route_infos [route.index]
if not ri and return_new then
ri = {
index = route.index,
name = route.name,
direction = route.direction,
devices = route.devices or {},
profiles = route.profiles,
priority = route.priority or 0,
available = route.available or "unknown",
prev_available = route.available or "unknown",
active = false,
prev_active = false,
save = false,
}
end
return ri
end
return module

View File

@@ -0,0 +1,501 @@
-- WirePlumber
-- Copyright © 2023 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
-- SPDX-License-Identifier: MIT
-- Script is a Lua Module of filter Lua utility functions
local cutils = require ("common-utils")
local module = {
metadata = nil,
filters = {},
}
local function getFilterSmart (metadata, node)
-- Check metadata
if metadata ~= nil then
local id = node["bound-id"]
local value_str = metadata:find (id, "filter.smart")
if value_str ~= nil then
local json = Json.Raw (value_str)
if json:is_boolean() then
return json:parse()
end
end
end
-- Check node properties
local prop_str = node.properties ["filter.smart"]
if prop_str ~= nil then
return cutils.parseBool (prop_str)
end
-- Otherwise consider the filter not smart by default
return false
end
local function getFilterSmartName (metadata, node)
-- Check metadata
if metadata ~= nil then
local id = node["bound-id"]
local value_str = metadata:find (id, "filter.smart.name")
if value_str ~= nil then
local json = Json.Raw (value_str)
if json:is_string() then
return json:parse()
end
end
end
-- Check node properties
local prop_str = node.properties ["filter.smart.name"]
if prop_str ~= nil then
return prop_str
end
-- Otherwise use link group as name
return node.properties ["node.link-group"]
end
local function getFilterSmartDisabled (metadata, node)
-- Check metadata
if metadata ~= nil then
local id = node["bound-id"]
local value_str = metadata:find (id, "filter.smart.disabled")
if value_str ~= nil then
local json = Json.Raw (value_str)
if json:is_boolean() then
return json:parse()
end
end
end
-- Check node properties
local prop_str = node.properties ["filter.smart.disabled"]
if prop_str ~= nil then
return cutils.parseBool (prop_str)
end
-- Otherwise consider the filter not disabled by default
return false
end
local function getFilterSmartTargetable (metadata, node)
-- Check metadata
if metadata ~= nil then
local id = node["bound-id"]
local value_str = metadata:find (id, "filter.smart.targetable")
if value_str ~= nil then
local json = Json.Raw (value_str)
if json:is_boolean() then
return json:parse()
end
end
end
-- Check node properties
local prop_str = node.properties ["filter.smart.targetable"]
if prop_str ~= nil then
return cutils.parseBool (prop_str)
end
-- Otherwise consider the filter not targetable by default
return false
end
local function getFilterSmartTarget (metadata, node, om)
-- Check metadata and fallback to properties
local id = node["bound-id"]
local value_str = nil
if metadata ~= nil then
value_str = metadata:find (id, "filter.smart.target")
end
if value_str == nil then
value_str = node.properties ["filter.smart.target"]
if value_str == nil then
return nil
end
end
-- Parse match rules
local match_rules_json = Json.Raw (value_str)
if not match_rules_json:is_object () then
return nil
end
local match_rules = match_rules_json:parse ()
-- Find target
local target = nil
for si_target in om:iterate { type = "SiLinkable" } do
local n_target = si_target:get_associated_proxy ("node")
if n_target == nil then
goto skip_target
end
-- Target nodes cannot be smart filters
if n_target.properties ["node.link-group"] ~= nil and
getFilterSmart (metadata, n_target) then
goto skip_target
end
-- Make sure the target node properties match all rules
for key, val in pairs(match_rules) do
if n_target.properties[key] ~= tostring (val) then
goto skip_target
end
end
-- Target found
target = si_target
break;
::skip_target::
end
return target
end
local function getFilterSmartTargetless (metadata, node)
local id = node["bound-id"]
local value_str = nil
if metadata ~= nil then
value_str = metadata:find (id, "filter.smart.target")
end
if value_str == nil then
value_str = node.properties ["filter.smart.target"]
end
return value_str == nil
end
local function getFilterSmartBefore (metadata, node)
-- Check metadata and fallback to properties
local id = node["bound-id"]
local value_str = nil
if metadata ~= nil then
value_str = metadata:find (id, "filter.smart.before")
end
if value_str == nil then
value_str = node.properties ["filter.smart.before"]
if value_str == nil then
return nil
end
end
-- Parse
local before_json = Json.Raw (value_str)
if not before_json:is_array() then
return nil
end
return before_json:parse ()
end
local function getFilterSmartAfter (metadata, node)
-- Check metadata and fallback to properties
local id = node["bound-id"]
local value_str = nil
if metadata ~= nil then
value_str = metadata:find (id, "filter.smart.after")
end
if value_str == nil then
value_str = node.properties ["filter.smart.after"]
if value_str == nil then
return nil
end
end
-- Parse
local after_json = Json.Raw (value_str)
if not after_json:is_array() then
return nil
end
return after_json:parse ()
end
local function insertFilterSorted (curr_filters, filter)
local before_filters = {}
local after_filters = {}
local new_filters = {}
-- Check if the current filters need to be inserted before or after
for i, v in ipairs(curr_filters) do
local insert_before = true
local insert_after = false
if v.before ~= nil then
for j, b in ipairs(v.before) do
if filter.name == b then
insert_after = false
break
end
end
end
if v.after ~= nil then
for j, b in ipairs(v.after) do
if filter.name == b then
insert_before = false
break
end
end
end
if filter.before ~= nil then
for j, b in ipairs(filter.before) do
if v.name == b then
insert_after = true
end
end
end
if filter.after ~= nil then
for j, b in ipairs(filter.after) do
if v.name == b then
insert_before = true
end
end
end
if insert_before then
if insert_after then
Log.warning ("cyclic before/after found in filters " .. v.name .. " and " .. filter.name)
end
table.insert (before_filters, v)
else
table.insert (after_filters, v)
end
end
-- Add the filters to the new table stored
for i, v in ipairs(before_filters) do
table.insert (new_filters, v)
end
table.insert (new_filters, filter)
for i, v in ipairs(after_filters) do
table.insert (new_filters, v)
end
return new_filters
end
local function rescanFilters (om, metadata_om)
local metadata =
metadata_om:lookup { Constraint { "metadata.name", "=", "filters" } }
-- Always clear all filters data on rescan
module.filters = {}
Log.info ("rescanning filters...")
for si in om:iterate { type = "SiLinkable" } do
local filter = {}
local n = si:get_associated_proxy ("node")
if n == nil then
goto skip_linkable
end
-- Only handle nodes with link group (filters)
filter.link_group = n.properties ["node.link-group"]
if filter.link_group == nil then
goto skip_linkable
end
-- Only handle the main filter nodes
filter.media_class = n.properties ["media.class"]
if string.find (filter.media_class, "Stream") then
goto skip_linkable
end
-- Filter direction
if string.find (filter.media_class, "Audio/Sink") or
string.find (filter.media_class, "Video/Sink") then
filter.direction = "input"
else
filter.direction = "output"
end
-- Filter media type
filter.media_type = si.properties["media.type"]
-- Get filter properties
filter.smart = getFilterSmart (metadata, n)
filter.name = getFilterSmartName (metadata, n)
filter.disabled = getFilterSmartDisabled (metadata, n)
filter.targetable = getFilterSmartTargetable (metadata, n)
filter.target = getFilterSmartTarget (metadata, n, om)
filter.targetless = getFilterSmartTargetless (metadata, n)
filter.before = getFilterSmartBefore (metadata, n)
filter.after = getFilterSmartAfter (metadata, n)
-- Add the main and stream session items
filter.main_si = si
filter.stream_si = om:lookup {
type = "SiLinkable",
Constraint { "node.link-group", "=", filter.link_group },
Constraint { "media.class", "#", "Stream/*", type = "pw-global" }
}
-- Add the filter to the list sorted by before and after
module.filters = insertFilterSorted (module.filters, filter)
::skip_linkable::
end
end
SimpleEventHook {
name = "lib/filter-utils/rescan",
before = "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")
local metadata_om = source:call ("get-object-manager", "metadata")
rescanFilters (om, metadata_om)
end
}:register ()
function module.is_filter_smart (direction, link_group)
-- Make sure direction and link_group is valid
if direction == nil or link_group == nil then
return false
end
for i, v in ipairs(module.filters) do
if v.direction == direction and v.link_group == link_group then
return v.smart
end
end
return false
end
function module.is_filter_disabled (direction, link_group)
-- Make sure direction and link_group is valid
if direction == nil or link_group == nil then
return false
end
for i, v in ipairs(module.filters) do
if v.direction == direction and v.link_group == link_group then
return v.disabled
end
end
return false
end
function module.is_filter_targetable (direction, link_group)
-- Make sure direction and link_group is valid
if direction == nil or link_group == nil then
return false
end
for i, v in ipairs(module.filters) do
if v.direction == direction and v.link_group == link_group then
return v.targetable
end
end
return false
end
function module.get_filter_target (direction, link_group)
-- Make sure direction and link_group are valid
if direction == nil or link_group == nil then
return nil
end
-- Find the current filter
local filter = nil
local index = nil
for i, v in ipairs(module.filters) do
if v.direction == direction and
v.link_group == link_group and
not v.disabled and
v.smart then
filter = v
index = i
break
end
end
if filter == nil then
return nil
end
-- Return the next filter with matching target
for i, v in ipairs(module.filters) do
if v.direction == direction and
v.media_type == filter.media_type and
v.name ~= filter.name and
v.link_group ~= link_group and
not v.disabled and
v.smart and
((v.target == nil and filter.target == nil) or
(v.target ~= nil and filter.target ~= nil and v.target.id == filter.target.id)) and
i > index then
return v.main_si
end
end
-- Otherwise return the filter destination target
return filter.target
end
function module.get_filter_from_target (direction, media_type, si_target)
local target = si_target
-- Make sure direction and media_type are valid
if direction == nil or media_type == nil then
return nil
end
-- If si_target is a filter, find it and use its target
if si_target then
local target_node = si_target:get_associated_proxy ("node")
local target_link_group = target_node.properties ["node.link-group"]
if target_link_group ~= nil then
local filter = nil
for i, v in ipairs(module.filters) do
if v.direction == direction and
v.media_type == media_type and
v.link_group == target_link_group and
not v.disabled and
v.smart then
filter = v
break
end
end
if filter == nil then
return nil
end
target = filter.target
end
end
-- Find the first filter matching target
for i, v in ipairs(module.filters) do
if v.direction == direction and
v.media_type == media_type and
not v.disabled and
v.smart and
((v.target ~= nil and target ~= nil and v.target.id == target.id) or
(target == nil and v.targetless)) then
return v.main_si
end
end
return nil
end
return module

View File

@@ -0,0 +1,431 @@
-- WirePlumber
-- Copyright © 2022 Collabora Ltd.
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
-- SPDX-License-Identifier: MIT
-- Script is a Lua Module of linking Lua utility functions
local cutils = require ("common-utils")
local lutils = {
si_flags = {},
priority_media_role_link = {},
}
function lutils.get_flags (self, si_id)
if not self.si_flags [si_id] then
self.si_flags [si_id] = {}
end
return self.si_flags [si_id]
end
function lutils.clear_flags (self, si_id)
self.si_flags [si_id] = nil
end
function getprio (link)
return tonumber (link.properties ["policy.role-based.priority"]) or 0
end
function getplugged (link)
return tonumber (link.properties ["item.plugged.usec"]) or 0
end
function lutils.getAction (pmrl, link)
local props = pmrl.properties
if getprio (pmrl) == getprio (link) then
return props ["policy.role-based.action.same-priority"] or "mix"
else
return props ["policy.role-based.action.lower-priority"] or "mix"
end
end
-- if the link happens to be priority one, clear it and find the next
-- priority.
function lutils.clearPriorityMediaRoleLink (link)
local lprops = link.properties
local lmc = lprops ["target.media.class"]
pmrl = lutils.getPriorityMediaRoleLink (lmc)
-- only proceed if the link happens to be priority one.
if pmrl ~= link then
return
end
local prio_link = nil
local prio = 0
local plugged = 0
for l in cutils.get_object_manager ("session-item"):iterate {
type = "SiLink",
Constraint { "item.factory.name", "=", "si-standard-link", type = "pw-global" },
Constraint { "is.role.policy.link", "=", true },
Constraint { "target.media.class", "=", lmc },
} do
local props = l.properties
-- dont consider this link as it is about to be removed.
if pmrl == l then
goto continue
end
if getprio (link) > prio or
(getprio (link) == prio and getplugged (link) > plugged) then
prio = getprio (l)
plugged = getplugged (l)
prio_link = l
end
::continue::
end
if prio_link then
setPriorityMediaRoleLink (lmc, prio_link)
else
setPriorityMediaRoleLink (lmc, nil)
end
end
-- record priority media role link
function lutils.updatePriorityMediaRoleLink (link)
local lprops = link.properties
local mc = lprops ["target.media.class"]
if not lutils.priority_media_role_link [mc] then
setPriorityMediaRoleLink (mc, link)
return
end
pmrl = lutils.getPriorityMediaRoleLink (mc)
if getprio (link) > getprio (pmrl) or
(getprio (link) == getprio (pmrl) and getplugged (link) >= getplugged (pmrl)) then
setPriorityMediaRoleLink (mc, link)
end
end
function lutils.getPriorityMediaRoleLink (lmc)
return lutils.priority_media_role_link [lmc]
end
function setPriorityMediaRoleLink (lmc, link)
lutils.priority_media_role_link [lmc] = link
if link then
Log.debug (
string.format ("update priority link(%d) media role(\"%s\") priority(%d)",
link.id, link.properties ["media.role"], getprio (link)))
else
Log.debug ("clear priority media role")
end
end
function lutils.is_role_policy_target (si_props, target_props)
-- role-based policy links are those that link to targets with
-- policy.role-based.target = true, unless the stream is a monitor
-- (usually pavucontrol) or the stream is linking to the monitor ports
-- of a sink (both are "input")
return Core.test_feature ("hooks.linking.role-based.rescan")
and cutils.parseBool (target_props["policy.role-based.target"])
and not cutils.parseBool (si_props ["stream.monitor"])
and si_props["item.node.direction"] ~= target_props["item.node.direction"]
end
function lutils.unwrap_select_target_event (self, event)
local source = event:get_source ()
local si = event:get_subject ()
local target = event:get_data ("target")
local om = source:call ("get-object-manager", "session-item")
local si_id = si.id
return source, om, si, si.properties, self:get_flags (si_id), target
end
function lutils.canPassthrough (si, si_target)
local props = si.properties
local tprops = si_target.properties
-- both nodes must support encoded formats
if not cutils.parseBool (props ["item.node.supports-encoded-fmts"])
or not cutils.parseBool (tprops ["item.node.supports-encoded-fmts"]) then
return false
end
-- make sure that the nodes have at least one common non-raw format
local n1 = si:get_associated_proxy ("node")
local n2 = si_target:get_associated_proxy ("node")
for p1 in n1:iterate_params ("EnumFormat") do
local p1p = p1:parse ()
if p1p.properties.mediaSubtype ~= "raw" then
for p2 in n2:iterate_params ("EnumFormat") do
if p1:filter (p2) then
return true
end
end
end
end
return false
end
function lutils.checkFollowDefault (si, si_target)
-- If it got linked to the default target that is defined by node
-- props but not metadata, start ignoring the node prop from now on.
-- This is what Pulseaudio does.
--
-- Pulseaudio skips here filter streams (i->origin_sink and
-- o->destination_source set in PA). Pipewire does not have a flag
-- explicitly for this, but we can use presence of node.link-group.
local si_props = si.properties
local target_props = si_target.properties
local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
local is_filter = (si_props ["node.link-group"] ~= nil)
if reconnect and not is_filter then
local def_id = cutils.getDefaultNode (si_props,
cutils.getTargetDirection (si_props))
if target_props ["node.id"] == tostring (def_id) then
local metadata = cutils.get_default_metadata_object ()
-- Set target.node, for backward compatibility
metadata:set (tonumber
(si_props ["node.id"]), "target.node", "Spa:Id", "-1")
Log.info (si, "... set metadata to follow default")
end
end
end
function lutils.lookupLink (si_id, si_target_id)
local link = cutils.get_object_manager ("session-item"):lookup {
type = "SiLink",
Constraint { "out.item.id", "=", si_id },
Constraint { "in.item.id", "=", si_target_id }
}
if not link then
link = cutils.get_object_manager ("session-item"):lookup {
type = "SiLink",
Constraint { "in.item.id", "=", si_id },
Constraint { "out.item.id", "=", si_target_id }
}
end
return link
end
function lutils.isLinked (si_target)
local target_id = si_target.id
local linked = false
local exclusive = false
for l in cutils.get_object_manager ("session-item"):iterate {
type = "SiLink",
} do
local p = l.properties
local out_id = tonumber (p ["out.item.id"])
local in_id = tonumber (p ["in.item.id"])
linked = (out_id == target_id) or (in_id == target_id)
if linked then
exclusive = cutils.parseBool (p ["exclusive"]) or cutils.parseBool (p ["passthrough"])
break
end
end
return linked, exclusive
end
function lutils.getNodePeerId (node_id)
for l in cutils.get_object_manager ("link"):iterate() do
local p = l.properties
local in_id = tonumber(p["link.input.node"])
local out_id = tonumber(p["link.output.node"])
if in_id == node_id then
return out_id
elseif out_id == node_id then
return in_id
end
end
return nil
end
function lutils.canLink (properties, si_target)
local target_props = si_target.properties
-- nodes must have the same media type
if properties ["media.type"] ~= target_props ["media.type"] then
return false
end
local function isMonitor(properties)
return properties ["item.node.direction"] == "input" and
cutils.parseBool (properties ["item.features.monitor"]) and
not cutils.parseBool (properties ["item.features.no-dsp"]) and
properties ["item.factory.name"] == "si-audio-adapter"
end
-- nodes must have opposite direction, or otherwise they must be both input
-- and the target must have a monitor (so the target will be used as a source)
if properties ["item.node.direction"] == target_props ["item.node.direction"]
and not isMonitor (target_props) then
return false
end
-- check link group
local function canLinkGroupCheck(link_group, si_target, hops)
local target_props = si_target.properties
local target_link_group = target_props ["node.link-group"]
if hops == 8 then
return false
end
-- allow linking if target has no link-group property
if not target_link_group then
return true
end
-- do not allow linking if target has the same link-group
if link_group == target_link_group then
return false
end
-- make sure target is not linked with another node with same link group
-- start by locating other nodes in the target's link-group, in opposite direction
for n in cutils.get_object_manager ("session-item"):iterate {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "id", "!", si_target.id, type = "gobject" },
Constraint { "item.node.direction", "!", target_props ["item.node.direction"] },
Constraint { "node.link-group", "=", target_link_group },
} do
-- iterate their peers and return false if one of them cannot link
for silink in cutils.get_object_manager ("session-item"):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 == n.id or in_id == n.id then
local peer_id = (out_id == n.id) and in_id or out_id
local peer = cutils.get_object_manager ("session-item"):lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "id", "=", peer_id, type = "gobject" },
}
if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
return false
end
end
end
end
return true
end
local link_group = properties ["node.link-group"]
if link_group then
return canLinkGroupCheck (link_group, si_target, 0)
end
return true
end
function lutils.findDefaultLinkable (si)
local si_props = si.properties
local target_direction = cutils.getTargetDirection (si_props)
local def_node_id = cutils.getDefaultNode (si_props, target_direction)
return cutils.get_object_manager ("session-item"):lookup {
type = "SiLinkable",
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
Constraint { "node.id", "=", tostring (def_node_id) }
}
end
function lutils.checkPassthroughCompatibility (si, si_target)
local si_must_passthrough =
cutils.parseBool (si.properties ["item.node.encoded-only"])
local si_target_must_passthrough =
cutils.parseBool (si_target.properties ["item.node.encoded-only"])
local can_passthrough = lutils.canPassthrough (si, si_target)
if (si_must_passthrough or si_target_must_passthrough)
and not can_passthrough then
return false, can_passthrough
end
return true, can_passthrough
end
-- Does the target device have any active/available paths/routes to
-- the physical device(spkr/mic/cam)?
function lutils.haveAvailableRoutes (si_props)
local card_profile_device = si_props ["card.profile.device"]
local device_id = si_props ["device.id"]
local device = device_id and cutils.get_object_manager ("device"):lookup {
Constraint { "bound-id", "=", device_id, type = "gobject" },
}
if not card_profile_device or not device then
return true
end
local found = 0
local avail = 0
-- First check "SPA_PARAM_Route" if there are any active devices
-- in an active profile.
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
if (route.device ~= tonumber (card_profile_device)) then
goto skip_route
end
if (route.available == "no") then
return false
end
do return true end
::skip_route::
end
-- Second check "SPA_PARAM_EnumRoute" if there is any route that
-- is available if not active.
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
if not cutils.arrayContains
(route.devices, tonumber (card_profile_device)) then
goto skip_enum_route
end
found = found + 1;
if (route.available ~= "no") then
avail = avail + 1
end
::skip_enum_route::
end
if found == 0 then
return true
end
if avail > 0 then
return true
end
return false
end
function lutils.sendClientError (event, node, code, message)
local source = event:get_source ()
local client_id = node.properties ["client.id"]
if client_id then
local clients_om = source:call ("get-object-manager", "client")
local client = clients_om:lookup {
Constraint { "bound-id", "=", client_id, type = "gobject" }
}
if client then
client:send_error (node ["bound-id"], code, message)
end
end
end
return lutils

View File

@@ -0,0 +1,197 @@
-- WirePlumber
-- Copyright © 2023 Collabora Ltd.
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
-- SPDX-License-Identifier: MIT
-- Script is a Lua Module of monitor Lua utility functions
log = Log.open_topic ("s-monitors-utils")
local mutils = {
cam_data = {}
}
-- finds out if any of the managed objects(nodes of a device or devices of
-- device enumerator) has duplicate values
function mutils.find_duplicate (parent, id, property, value)
for i = 0, id - 1, 1 do
local obj = parent:get_managed_object (i)
if obj and obj.properties[property] == value then
return true
end
end
return false
end
function get_cam_data(self, dev_id)
if not self.cam_data[dev_id] then
self.cam_data[dev_id] = {}
self.cam_data[dev_id]["libcamera"] = {}
self.cam_data[dev_id]["v4l2"] = {}
end
return self.cam_data[dev_id]
end
function parse_devids_get_cam_data(self, devids)
local dev_ids_json = Json.Raw(devids)
local dev_ids_table = {}
if dev_ids_json:is_array() then
dev_ids_table = dev_ids_json:parse()
else
-- to maintain the backward compatibility with earlier pipewire versions.
for dev_id_str in devids:gmatch("%S+") do
local dev_id = tonumber(dev_id_str)
if dev_id then
table.insert(dev_ids_table, dev_id)
end
end
end
local dev_num = nil
-- `device.devids` is a json array of device numbers
for _, dev_id_str in ipairs(dev_ids_table) do
local dev_id = tonumber(dev_id_str)
if not dev_id then
log:notice ("invalid device number")
return
end
log:debug ("Working on device " .. dev_id)
local dev_cam_data = get_cam_data (self, dev_id)
if not dev_num then
dev_num = dev_id
if #dev_ids_table > 1 then
-- libcam node can some times use more tha one V4L2 devices, in this
-- case, return the first device id and mark rest of the them as peers
-- to the first one.
log:debug ("Device " .. dev_id .. " uses multi V4L2 devices")
dev_cam_data.uses_multi_v4l2_devices = true
end
else
log:debug ("Device " .. dev_id .. " is peer to " .. dev_num)
dev_cam_data.peer_id = dev_num
end
end
if dev_num then
return self.cam_data[dev_num], dev_num
end
end
function mutils.clear_cam_data (self, dev_num)
local dev_cam_data = self.cam_data[dev_num]
if not dev_cam_data then
return
end
if dev_cam_data.uses_multi_v4l2_devices then
for dev_id, cam_data_ in pairs(self.cam_data) do
if cam_data_.peer_id == dev_num then
log:debug("clear " .. dev_id .. " it is peer to " .. dev_num)
self.cam_data[dev_id] = nil
end
end
end
self.cam_data[dev_num] = nil
end
function mutils.create_cam_node(self, dev_num)
local api = nil
local cam_data = get_cam_data (self, dev_num)
if cam_data["v4l2"].enum_status and cam_data["libcamera"].enum_status then
if cam_data.uses_multi_v4l2_devices then
api = "libcamera"
elseif cam_data.peer_id ~= nil then
-- no need to create node for peer
log:notice ("timer expired for peer device " .. dev_num)
return
elseif cam_data.is_device_uvc then
api = "v4l2"
else
api = "libcamera"
end
else
api = cam_data["v4l2"].enum_status and "v4l2" or "libcamera"
end
log:info (string.format ("create \"%s\" node for device:%s", api,
cam_data.dev_path))
source = source or Plugin.find ("standard-event-source")
local e = source:call ("create-event", "create-" .. api .. "-device-node",
cam_data[api].parent, nil)
e:set_data ("factory", cam_data[api].factory)
e:set_data ("node-properties", cam_data[api].properties)
e:set_data ("node-sub-id", cam_data[api].id)
EventDispatcher.push_event (e)
self:clear_cam_data (dev_num)
end
-- arbitrates between v4l2 and libcamera on who gets to create the device node
-- for a device, logic is based on the device number of the device given by both
-- the parties.
function mutils.register_cam_node (self, parent, id, factory, properties)
local api = properties["device.api"]
local dev_ids = properties["device.devids"]
log:debug(api .. " reported " .. dev_ids)
local cam_data, dev_num = parse_devids_get_cam_data(self, dev_ids)
if not cam_data then
log:notice (string.format ("device numbers invalid for %s device:%s",
api, properties["device.name"]))
return false
end
-- only v4l2 can give this info
if properties["api.v4l2.cap.driver"] == "uvcvideo" then
log:debug ("Device " .. dev_num .. " is a UVC device")
cam_data.is_device_uvc = true
end
-- only v4l2 can give this info
if properties["api.v4l2.path"] then
cam_data.dev_path = properties["api.v4l2.path"]
end
local cam_api_data = cam_data[api]
cam_api_data.enum_status = true
-- cache info, it comes handy when creating node
cam_api_data.parent = parent
cam_api_data.id = id
cam_api_data.name = properties["device.name"]
cam_api_data.factory = factory
cam_api_data.properties = properties
local other_api = api == "v4l2" and "libcamera" or "v4l2"
if cam_api_data.enum_status and not cam_data[other_api].enum_status then
log:trace (string.format ("\"%s\" armed a timer for %d", api, dev_num))
cam_data.source = Core.timeout_add (
Settings.get_int ("monitor.camera-discovery-timeout"), function()
log:trace (string.format ("\"%s\" armed timer expired for %d", api, dev_num))
self:create_cam_node (dev_num)
cam_data.source = nil
end)
elseif cam_data.source then
log:trace (string.format ("\"%s\" disarmed timer for %d", api, dev_num))
cam_data.source:destroy ()
cam_data.source = nil
self:create_cam_node (dev_num)
else
log:notice (string.format ("\"%s\" calling after timer expiry for %d:%s%s",
api, dev_num, cam_data.dev_path,
(cam_data.is_device_uvc and "(uvc)" or "")))
end
return true
end
return mutils

View File

@@ -0,0 +1,18 @@
-- WirePlumber
--
-- Copyright © 2024 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
local module = {}
function module.get_session_priority (node_props)
local priority = node_props ["priority.session"]
-- fallback to driver priority if session priority is not set
if not priority then
priority = node_props ["priority.driver"]
end
return math.tointeger (priority) or 0
end
return module