New wireplumber config
This commit is contained in:
94
.config/wireplumber/scripts/lib/common-utils.lua
Normal file
94
.config/wireplumber/scripts/lib/common-utils.lua
Normal 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
|
||||
74
.config/wireplumber/scripts/lib/device-info-cache.lua
Normal file
74
.config/wireplumber/scripts/lib/device-info-cache.lua
Normal 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
|
||||
501
.config/wireplumber/scripts/lib/filter-utils.lua
Normal file
501
.config/wireplumber/scripts/lib/filter-utils.lua
Normal 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
|
||||
431
.config/wireplumber/scripts/lib/linking-utils.lua
Normal file
431
.config/wireplumber/scripts/lib/linking-utils.lua
Normal 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
|
||||
197
.config/wireplumber/scripts/lib/monitor-utils.lua
Normal file
197
.config/wireplumber/scripts/lib/monitor-utils.lua
Normal 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
|
||||
18
.config/wireplumber/scripts/lib/node-utils.lua
Normal file
18
.config/wireplumber/scripts/lib/node-utils.lua
Normal 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
|
||||
Reference in New Issue
Block a user