Google
Edit File: filter-utils.lua
-- 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