-- WirePlumber
-- Copyright © 2023 Collabora Ltd.
-- @author Julian Bouzas <[email protected]>
-- 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