diff --git a/lib/wp/event-hook.h b/lib/wp/event-hook.h index 2aae2c6c..28f21d35 100644 --- a/lib/wp/event-hook.h +++ b/lib/wp/event-hook.h @@ -30,8 +30,12 @@ typedef enum { WP_EVENT_HOOK_DEFAULT_PRIORITY_BASE = 0, - WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_TARGET_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_BASE, - WP_EVENT_HOOK_DEFAULT_PRIORITY_RESCAN_POLICY = WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_TARGET_SI + PRIORITY_STEP, + WP_EVENT_HOOK_DEFAULT_PRIORITY_LINK_TARGET_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_BASE, + WP_EVENT_HOOK_DEFAULT_PRIORITY_PREPARE_LINK_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_LINK_TARGET_SI + PRIORITY_STEP, + WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_BEST_TARGET_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_PREPARE_LINK_SI + PRIORITY_STEP, + WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_DEFAULT_TARGET_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_BEST_TARGET_SI + PRIORITY_STEP, + WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_DEFINED_TARGET_SI = WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_DEFAULT_TARGET_SI + PRIORITY_STEP, + WP_EVENT_HOOK_DEFAULT_PRIORITY_RESCAN_POLICY = WP_EVENT_HOOK_DEFAULT_PRIORITY_FIND_DEFINED_TARGET_SI + PRIORITY_STEP, WP_EVENT_HOOK_DEFAULT_PRIORITY_AFTER_EVENTS_DEFAULT_NODES_STATE_SAVE = WP_EVENT_HOOK_DEFAULT_PRIORITY_RESCAN_POLICY + PRIORITY_STEP, WP_EVENT_HOOK_DEFAULT_PRIORITY_RESCAN_DEFAULT_NODES = WP_EVENT_HOOK_DEFAULT_PRIORITY_AFTER_EVENTS_DEFAULT_NODES_STATE_SAVE + PRIORITY_STEP, diff --git a/modules/module-lua-scripting/api/api.c b/modules/module-lua-scripting/api/api.c index 7cf4166d..a9727b96 100644 --- a/modules/module-lua-scripting/api/api.c +++ b/modules/module-lua-scripting/api/api.c @@ -1675,8 +1675,8 @@ event_dispatcher_push_event (lua_State *L) g_autoptr (WpEventDispatcher) dispatcher = wp_event_dispatcher_get_instance (get_wp_core (L)); WpEvent *event = wp_event_new (type, priority, properties, source, subject); - wp_event_dispatcher_push_event (dispatcher, event); - wplua_pushboxed (L, WP_TYPE_EVENT, wp_event_ref (event)); + wp_event_dispatcher_push_event (dispatcher, wp_event_ref (event)); + wplua_pushboxed (L, WP_TYPE_EVENT, event); return 0; } diff --git a/src/config/wireplumber.conf.d/policy.conf b/src/config/wireplumber.conf.d/policy.conf index bea14156..d9bdfd27 100644 --- a/src/config/wireplumber.conf.d/policy.conf +++ b/src/config/wireplumber.conf.d/policy.conf @@ -21,6 +21,9 @@ wireplumber.components = [ # Link nodes to each other to make media flow in the graph { name = policy-node.lua , type = script/lua } + # policy hooks to link nodes to each other + { name = policy-hooks.lua , type = script/lua } + # Create endpoints statically at startup { name = static-endpoints.lua , type = script/lua } diff --git a/src/scripts/lib/common-utils.lua b/src/scripts/lib/common-utils.lua new file mode 100644 index 00000000..777a9a47 --- /dev/null +++ b/src/scripts/lib/common-utils.lua @@ -0,0 +1,37 @@ +-- WirePlumber + +-- Copyright © 2022 Collabora Ltd. +-- @author Ashok Sidipotu + +-- 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.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 + +function cutils.getDefaultNode (properties, target_direction) + local target_media_class = + properties ["media.type"] .. + (target_direction == "input" and "/Sink" or "/Source") + return default_nodes:call ("get-default-node", target_media_class) +end + +default_nodes = Plugin.find ("default-nodes-api") + +return cutils \ No newline at end of file diff --git a/src/scripts/lib/policy-utils.lua b/src/scripts/lib/policy-utils.lua new file mode 100644 index 00000000..906eec7e --- /dev/null +++ b/src/scripts/lib/policy-utils.lua @@ -0,0 +1,313 @@ +-- WirePlumber + +-- Copyright © 2022 Collabora Ltd. +-- @author Ashok Sidipotu + +-- SPDX-License-Identifier: MIT + +-- Script is a Lua Module of policy Lua utility functions + +local cutils = require ("common-utils") + +function parseBool (var) + return cutils.parseBool (var) +end + +local putils = {} + +putils.si_flags = {} + +function putils.get_flags (si_id) + if not putils.si_flags [si_id] then + putils.si_flags [si_id] = {} + end + + return putils.si_flags [si_id] +end + +function putils.set_flags (si_id, si_flags) + putils.si_flags [si_id] = si_flags +end + +function putils.canPassthrough (si, si_target) + local props = si.properties + local tprops = si_target.properties + -- both nodes must support encoded formats + if not parseBool (props ["item.node.supports-encoded-fmts"]) + or not 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 putils.checkFollowDefault (si, si_target, has_node_defined_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. + if not has_node_defined_target then + return + end + local si_props = si.properties + local target_props = si_target.properties + local reconnect = not parseBool (si_props ["node.dont-reconnect"]) + local is_filter = (si_props ["node.link-group"] ~= nil) + + if follow and default_nodes ~= nil and reconnect and not is_filter then + local def_id = getDefaultNode (si_props, + cutils.getTargetDirection (si_props)) + + if target_props ["node.id"] == tostring (def_id) then + local metadata = putils.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 putils.lookupLink (si_id, si_target_id) + local link = links_om:lookup { + Constraint { "out.item.id", "=", si_id }, + Constraint { "in.item.id", "=", si_target_id } + } + if not link then + link = links_om:lookup { + Constraint { "in.item.id", "=", si_id }, + Constraint { "out.item.id", "=", si_target_id } + } + end + return link +end + +function putils.isLinked (si_target) + local target_id = si_target.id + local linked = false + local exclusive = false + + for l in links_om:iterate () 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 = parseBool (p ["exclusive"]) or parseBool (p ["passthrough"]) + break + end + end + return linked, exclusive +end + +function putils.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 + + -- 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) + local function isMonitor(properties) + return properties ["item.node.direction"] == "input" and + parseBool (properties ["item.features.monitor"]) and + not parseBool (properties ["item.features.no-dsp"]) and + properties ["item.factory.name"] == "si-audio-adapter" + end + + 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 linkables_om:iterate { + 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 links_om:iterate () 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 = linkables_om:lookup { + 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 putils.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 linkables_om:lookup { + Constraint { "node.id", "=", tostring (def_node_id) } + } +end + +function putils.checkPassthroughCompatibility (si, si_target) + local si_must_passthrough = + parseBool (si.properties ["item.node.encoded-only"]) + local si_target_must_passthrough = + parseBool (si_target.properties ["item.node.encoded-only"]) + local can_passthrough = putils.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 putils.haveAvailableRoutes (si_props) + local card_profile_device = si_props ["card.profile.device"] + local device_id = si_props ["device.id"] + local device = device_id and devices_om: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 = 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 = parseParam (p, "EnumRoute") + if not route then + goto skip_enum_route + end + + if not 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 + +linkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle si-audio-adapter and si-node + Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, + Constraint { "active-features", "!", 0, type = "gobject" }, + } +} + +linkables_om:activate () + +metadata_om = ObjectManager { + Interest { + type = "metadata", + Constraint { "metadata.name", "=", "default" }, + } +} + +metadata_om:activate () + +links_om = ObjectManager { + Interest { + type = "SiLink", + -- only handle links created by this policy + Constraint { "is.policy.item.link", "=", true }, + } +} + +links_om:activate () + +function putils.get_default_metadata_object () + return metadata_om:lookup () +end + +return putils diff --git a/src/scripts/policy-hooks.lua b/src/scripts/policy-hooks.lua new file mode 100644 index 00000000..8886846c --- /dev/null +++ b/src/scripts/policy-hooks.lua @@ -0,0 +1,572 @@ +-- WirePlumber + +-- Copyright © 2022 Collabora Ltd. +-- @author Ashok Sidipotu + +-- SPDX-License-Identifier: MIT + +-- Script registers hooks needed to perform policy rescan. + +-- settings file: policy.conf + +local putils = require ("policy-utils") +local cutils = require ("common-utils") + +local move = Settings.get ("default-policy-move"):parse () or false + +function parseBool (var) + return cutils.parseBool (var) +end + +-- check if target node is defined explicitly. +-- This defination can be done in two ways. +-- 1. "node.target"/"target.object" in the node properties +-- 2. "target.node"/"target.object" in default metadata +function findDefinedTarget (event) + local si = event:get_subject () + local si_props = si.properties + local si_id = si.id; + local si_flags = putils.get_flags (si_id) + local si_target = nil + + Log.info (si, string.format ("handling item: %s (%s)", + tostring (si_props ["node.name"]), tostring (si_props ["node.id"]))) + + local metadata = move and putils.get_default_metadata_object () + local target_key + local target_value = nil + local node_defined = false + local target_picked = nil + + if si_props ["target.object"] ~= nil then + target_value = si_props ["target.object"] + target_key = "object.serial" + node_defined = true + elseif si_props ["node.target"] ~= nil then + target_value = si_props ["node.target"] + target_key = "node.id" + node_defined = true + end + + if metadata then + local id = metadata:find (si_props ["node.id"], "target.object") + if id ~= nil then + target_value = id + target_key = "object.serial" + node_defined = false + else + id = metadata:find (si_props ["node.id"], "target.node") + if id ~= nil then + target_value = id + target_key = "node.id" + node_defined = false + end + end + end + + if target_value == "-1" then + target_picked = false + si_target = nil + elseif target_value and tonumber (target_value) then + si_target = linkables_om:lookup { + Constraint { target_key, "=", target_value }, + } + if si_target and putils.canLink (si_props, si_target) then + target_picked = true + end + elseif target_value then + for si_target in linkables_om:iterate () do + local target_props = si_target.properties + if (target_props ["node.name"] == target_value or + target_props ["object.path"] == target_value) and + target_props ["item.node.direction"] == cutils.getTargetDirection (si_props) and + putils.canLink (si_props, si_target) then + target_picked = true + end + end + end + + local can_passthrough, passthrough_compatible + if si_target then + passthrough_compatible, can_passthrough = + putils.checkPassthroughCompatibility (si, si_target) + + if not passthrough_compatible then + si_target = nil + end + end + + -- if the client has seen a target that we haven't yet prepared, stop the + -- event and wait(for one time) for next rescan to happen and hope for the + -- best. + + if target_picked + and not si_target + and not si_flags.was_handled + and not si_flags.done_waiting then + Log.info (si, string.format ("... waiting for target %s (%s)", + tostring (si_target.properties ["node.name"]), + tostring (si_target.properties ["node.id"]))) + si_flags.done_waiting = true + event:stop_processing () + + elseif target_picked then + Log.info (si, + string.format ("... defined target picked: %s (%s), can_passthrough:%s", + tostring (si_target.properties ["node.name"]), + tostring (si_target.properties ["node.id"]), + tostring (can_passthrough))) + si_flags.si_target = si_target + si_flags.has_node_defined_target = node_defined + si_flags.can_passthrough = can_passthrough + else + si_flags.si_target = nil + si_flags.can_passthrough = nil + si_flags.has_node_defined_target = nil + end + + putils.set_flags (si_id, si_flags) +end + +-- check if default nodes can be picked up as target node. +function findDefaultTarget (event) + local si = event:get_subject () + local si_id = si.id + local si_flags = putils.get_flags (si_id) + local si_target = si_flags.si_target + + if si_target or (default_nodes == nil) then + -- bypass the hook as the target is already picked up. + return + end + + local si_props = si.properties + local target_picked = false + + Log.info (si, string.format ("handling item: %s (%s)", + tostring (si_props ["node.name"]), tostring (si_props ["node.id"]))) + + local si_target = putils.findDefaultLinkable (si) + + local can_passthrough, passthrough_compatible + if si_target then + passthrough_compatible, can_passthrough = + putils.checkPassthroughCompatibility (si, si_target) + if putils.canLink (si_props, si_target) and passthrough_compatible then + target_picked = true; + end + end + + if target_picked then + Log.info (si, + string.format ("... default target picked: %s (%s), can_passthrough:%s", + tostring (si_target.properties ["node.name"]), + tostring (si_target.properties ["node.id"]), + tostring (can_passthrough))) + si_flags.si_target = si_target + si_flags.can_passthrough = can_passthrough + else + si_flags.si_target = nil + si_flags.can_passthrough = nil + end + + putils.set_flags (si_id, si_flags) +end + +-- Traverse through all the possible targets to pick up target node. +function findBestTarget (event) + local si = event:get_subject () + local si_id = si.id + local si_flags = putils.get_flags (si_id) + local si_target = si_flags.si_target + + if si_target then + -- bypass the hook as the target is already picked up. + return + end + + local si_props = si.properties + local target_direction = putils.getTargetDirection (si_props) + local target_picked = nil + local target_can_passthrough = false + local target_priority = 0 + local target_plugged = 0 + + for si_target in linkables_om:iterate { + Constraint { "item.node.type", "=", "device" }, + Constraint { "item.node.direction", "=", target_direction }, + Constraint { "media.type", "=", si_props ["media.type"] }, + } do + local si_target_props = si_target.properties + local si_target_node_id = si_target_props ["node.id"] + local priority = tonumber (si_target_props ["priority.session"]) or 0 + + Log.debug (string.format ("Looking at: %s (%s)", + tostring (si_target_props ["node.name"]), + tostring (si_target_node_id))) + + if not putils.canLink (si_props, si_target) then + Log.debug ("... cannot link, skip linkable") + goto skip_linkable + end + + if not putils.haveAvailableRoutes (si_target_props) then + Log.debug ("... does not have routes, skip linkable") + goto skip_linkable + end + + local passthrough_compatible, can_passthrough = + putils.checkPassthroughCompatibility (si, si_target) + if not passthrough_compatible then + Log.debug ("... passthrough is not compatible, skip linkable") + goto skip_linkable + end + + local plugged = tonumber (si_target_props ["item.plugged.usec"]) or 0 + + Log.debug ("... priority:" .. tostring (priority) .. ", plugged:" .. tostring (plugged)) + + -- (target_picked == NULL) --> make sure atleast one target is picked. + -- (priority > target_priority) --> pick the highest priority linkable(node) + -- target. + -- (priority == target_priority and plugged > target_plugged) --> pick the + -- latest connected/plugged(in time) linkable(node) target. + if (target_picked == nil or + priority > target_priority or + (priority == target_priority and plugged > target_plugged)) then + Log.debug ("... picked") + target_picked = si_target + target_can_passthrough = can_passthrough + target_priority = priority + target_plugged = plugged + end + ::skip_linkable:: + end + + if target_picked then + Log.info (si, + string.format ("... best target picked: %s (%s), can_passthrough:%s", + tostring (target_picked.properties ["node.name"]), + tostring (target_picked.properties ["node.id"]), + tostring (target_can_passthrough))) + si_flags.si_target = target_picked + si_flags.can_passthrough = target_can_passthrough + else + si_flags.si_target = nil + si_flags.can_passthrough = nil + end + + putils.set_flags (si_id, si_flags) +end + +-- remove the existing link if needed, check the properties of target, which +-- indicate it is not available for linking. If no target is available, send +-- down an error to the corresponding client. +function prepareLink (event) + local si = event:get_subject () + local si_id = si.id + local si_flags = putils.get_flags (si_id) + local si_target = si_flags.si_target + local si_props = si.properties + + local reconnect = not parseBool (si_props ["node.dont-reconnect"]) + local exclusive = parseBool (si_props ["node.exclusive"]) + local si_must_passthrough = parseBool (si_props ["item.node.encoded-only"]) + + Log.info(si, string.format("handling item: %s (%s)", + tostring(si_props["node.name"]), tostring(si_props["node.id"]))) + + -- Check if item is linked to proper target, otherwise re-link + if si_flags.peer_id then + if si_target and si_flags.peer_id == si_target.id then + Log.debug (si, "... already linked to proper target") + -- Check this also here, in case in default targets changed + putils.checkFollowDefault (si, si_target, + si_flags.has_node_defined_target) + si_target = nil + goto done + end + + local link = putils.lookupLink (si_id, si_flags.peer_id) + if reconnect then + if link ~= nil then + -- remove old link + if ((link:get_active_features () & Feature.SessionItem.ACTIVE) == 0) + then + -- remove also not yet activated links: they might never become + -- active, and we need not wait for it to become active + Log.warning (link, "Link was not activated before removing") + end + si_flags.peer_id = nil + link:remove () + Log.info (si, "... moving to new target") + end + else + if link ~= nil then + Log.info (si, "... dont-reconnect, not moving") + goto done + end + end + end + + -- if the stream has dont-reconnect and was already linked before, + -- don't link it to a new target + if not reconnect and si_flags.was_handled then + si_target = nil + goto done + end + + -- check target's availability + if si_target then + local target_is_linked, target_is_exclusive = putils.isLinked (si_target) + if target_is_exclusive then + Log.info (si, "... target is linked exclusively") + si_target = nil + end + + if target_is_linked then + if exclusive or si_must_passthrough then + Log.info (si, "... target is already linked, cannot link exclusively") + si_target = nil + else + -- disable passthrough, we can live without it + si_flags.can_passthrough = false + end + end + end + + if not si_target then + Log.info (si, "... target not found, reconnect:" .. tostring (reconnect)) + + local node = si:get_associated_proxy ("node") + if not reconnect then + Log.info (si, "... destroy node") + node:request_destroy () + elseif si_flags.was_handled then + Log.info (si, "... waiting reconnect") + return + end + + local client_id = node.properties ["client.id"] + if client_id then + local client = clients_om:lookup { + Constraint { "bound-id", "=", client_id, type = "gobject" } + } + if client then + client:send_error (node ["bound-id"], -2, "no node available") + end + end + end + + ::done:: + si_flags.si_target = si_target + putils.set_flags (si_id, si_flags) +end + +function createLink (event) + local si = event:get_subject () + local si_id = si.id + local si_flags = putils.get_flags (si_id) + local si_target = si_flags.si_target + + if not si_target then + -- bypass the hook, nothing to link to. + return + end + + local si_props = si.properties + local target_props = si_target.properties + local out_item = nil + local in_item = nil + local si_link = nil + local passthrough = si_flags.can_passthrough + + local exclusive = parseBool (si_props ["node.exclusive"]) + local passive = parseBool (si_props ["node.passive"]) or + parseBool (target_props ["node.passive"]) + + -- break rescan if tried more than 5 times with same target + if si_flags.failed_peer_id ~= nil and + si_flags.failed_peer_id == si_target.id and + si_flags.failed_count ~= nil and + si_flags.failed_count > 5 then + Log.warning (si, "tried to link on last rescan, not retrying") + goto done + end + + if si_props ["item.node.direction"] == "output" then + -- playback + out_item = si + in_item = si_target + else + -- capture + in_item = si + out_item = si_target + end + + Log.info (si, + string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s", + tostring (si_props ["node.name"]), + tostring (target_props ["node.name"]), + tostring (passive), tostring (passthrough), tostring (exclusive))) + + -- create and configure link + si_link = SessionItem ("si-standard-link") + if not si_link:configure { + ["out.item"] = out_item, + ["in.item"] = in_item, + ["passive"] = passive, + ["passthrough"] = passthrough, + ["exclusive"] = exclusive, + ["out.item.port.context"] = "output", + ["in.item.port.context"] = "input", + ["is.policy.item.link"] = true, + } then + Log.warning (si_link, "failed to configure si-standard-link") + goto done + end + + si_link:connect("link-error", function (_, error_msg) + local ids = {si_id} + if si_flags[si_id] ~= nil then + table.insert (ids, si_flags[si_id].peer_id) + end + + for _, id in ipairs (ids) do + local si = linkables_om:lookup { + Constraint { "id", "=", id, type = "gobject" }, + } + if si then + local node = si:get_associated_proxy ("node") + local client_id = node.properties["client.id"] + if client_id then + local client = clients_om:lookup { + Constraint { "bound-id", "=", client_id, type = "gobject" } + } + if client then + Log.info (node, "sending client error: " .. error_msg) + client:send_error (node["bound-id"], -32, error_msg) + end + end + end + end + end) + + -- register + si_flags.peer_id = si_target.id + si_flags.failed_peer_id = si_target.id + if si_flags.failed_count ~= nil then + si_flags.failed_count = si_flags.failed_count + 1 + else + si_flags.failed_count = 1 + end + si_link:register () + + -- activate + si_link:activate (Feature.SessionItem.ACTIVE, function (l, e) + if e then + Log.info(l, "failed to activate si-standard-link: "..tostring(si).." error:".. tostring(e)) + if si_flags ~= nil then + si_flags.peer_id = nil + end + l:remove () + else + if si_flags ~= nil then + si_flags.failed_peer_id = nil + si_flags.failed_count = 0 + end + Log.info (l, "activated si-standard-link "..tostring(si)) + end + putils.set_flags (si_id, si_flags) + end) + + ::done:: + putils.set_flags (si_id, si_flags) +end + +linkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle si-audio-adapter and si-node + Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, + Constraint { "active-features", "!", 0, type = "gobject" }, + } +} + +linkables_om:activate () + +clients_om = ObjectManager { Interest { type = "client" } } + +clients_om:activate () + +default_nodes = Plugin.find ("default-nodes-api") + +SimpleEventHook { + name = "link-target-si@policy-node", + type = "after-events-with-event", + priority = "link-target-si", + interests = { + EventInterest { + Constraint { "event.type", "=", "find-target-si-and-link" }, + }, + }, + execute = function (event) + createLink (event) + end +}:register () + +SimpleEventHook { + name = "prepare-link-si@policy-node", + type = "after-events-with-event", + priority = "prepare-link-si", + interests = { + EventInterest { + Constraint { "event.type", "=", "find-target-si-and-link" }, + }, + }, + execute = function (event) + prepareLink (event) + end +}:register () + +SimpleEventHook { + name = "find-best-target-si@policy-node", + type = "after-events-with-event", + priority = "find-best-target-si", + interests = { + EventInterest { + Constraint { "event.type", "=", "find-target-si-and-link" }, + }, + }, + execute = function (event) + findBestTarget (event) + end +}:register () + +SimpleEventHook { + name = "find-default-target-si@policy-node", + type = "after-events-with-event", + priority = "find-default-target-si", + interests = { + EventInterest { + Constraint { "event.type", "=", "find-target-si-and-link" }, + }, + }, + execute = function (event) + findDefaultTarget (event) + end +}:register () + +SimpleEventHook { + name = "find-defined-target@policy-node", + type = "after-events-with-event", + priority = "find-defined-target-si", + interests = { + EventInterest { + Constraint { "event.type", "=", "find-target-si-and-link" }, + }, + }, + execute = function (event) + findDefinedTarget (event) + end +}:register () diff --git a/src/scripts/policy-node.lua b/src/scripts/policy-node.lua index 7f31f0c3..1d4fe062 100644 --- a/src/scripts/policy-node.lua +++ b/src/scripts/policy-node.lua @@ -20,152 +20,54 @@ local move = Settings.get ("default-policy-move"):parse() or false local follow = Settings.get ("default-policy-follow"):parse() or false local filter_forward_format = Settings.get ("filter.forward-format"):parse() or false -local self = {} -self.scanning = false -self.pending_rescan = false -self.events_skipped = false -self.pending_error_timer = nil +local putils = require ("policy-utils") +local cutils = require ("common-utils") + find_target_events = {} -function findTargetSiAndLink (si) +function parseBool (var) + return cutils.parseBool (var) +end - si_props = si.properties - - Log.info (si, string.format ("handling item: %s (%s)", - tostring (si_props ["node.name"]), tostring (si_props ["node.id"]))) - - ensureSiFlags (si) - - -- get other important node properties - local reconnect = not parseBool (si_props ["node.dont-reconnect"]) - local exclusive = parseBool (si_props ["node.exclusive"]) - local si_must_passthrough = parseBool (si_props ["item.node.encoded-only"]) - - -- find defined target - local si_target, has_defined_target, has_node_defined_target = findDefinedTarget (si_props) - local can_passthrough = si_target and canPassthrough (si, si_target) - - if si_target and si_must_passthrough and not can_passthrough then - si_target = nil - end - - -- if the client has seen a target that we haven't yet prepared, schedule - -- a rescan one more time and hope for the best - local si_id = si.id - if has_defined_target - and not si_target - and not si_flags [si_id].was_handled - and not si_flags [si_id].done_waiting then - Log.info (si, "... waiting for target") - si_flags [si_id].done_waiting = true - -- Event-Stack TBD: do we need to retain this call here? - rescan () +function unhandleLinkable (si) + local si_flags = putils.get_flags (si_id) + local valid, si_props = checkLinkable (si, true) + if not valid then return end - -- find fallback target - if not si_target and (reconnect or not has_defined_target) then - si_target, can_passthrough = findUndefinedTarget (si) - end + Log.info (si, string.format ("unhandling item: %s (%s)", + tostring (si_props ["node.name"]), tostring (si_props ["node.id"]))) - -- Check if item is linked to proper target, otherwise re-link - if si_flags [si_id].peer_id then - if si_target and si_flags [si_id].peer_id == si_target.id then - Log.debug (si, "... already linked to proper target") - -- Check this also here, in case in default targets changed - checkFollowDefault (si, si_target, has_node_defined_target) - return - end - local link = lookupLink (si_id, si_flags [si_id].peer_id) - if reconnect then - if link ~= nil then - -- remove old link if active, otherwise schedule rescan - if ((link:get_active_features () & Feature.SessionItem.ACTIVE) ~= 0) then - si_flags [si_id].peer_id = nil - link:remove () - Log.info (si, "... moving to new target") - else - -- Event-Stack TBD: do we need to retain this call here? - rescan () - Log.info (si, "... scheduled rescan") - return - end - end - else - if link ~= nil then - Log.info (si, "... dont-reconnect, not moving") - return + -- remove any links associated with this item + for silink in links_om:iterate () do + local out_id = tonumber (silink.properties ["out.item.id"]) + local in_id = tonumber (silink.properties ["in.item.id"]) + if out_id == si.id or in_id == si.id then + if out_id == si.id and + si_flags and si_flags.peer_id == out_id then + si_flags.peer_id = nil + elseif in_id == si.id and + si_flags and si_flags.peer_id == in_id then + si_flags.peer_id = nil end + silink:remove () + Log.info (silink, "... link removed") end end - -- if the stream has dont-reconnect and was already linked before, - -- don't link it to a new target - if not reconnect and si_flags [si.id].was_handled then - si_target = nil - end - - -- check target's availability - if si_target then - local target_is_linked, target_is_exclusive = isLinked (si_target) - if target_is_exclusive then - Log.info (si, "... target is linked exclusively") - si_target = nil - end - - if target_is_linked then - if exclusive or si_must_passthrough then - Log.info (si, "... target is already linked, cannot link exclusively") - si_target = nil - else - -- disable passthrough, we can live without it - can_passthrough = false - end - end - end - - if not si_target then - Log.info (si, "... target not found, reconnect:" .. tostring (reconnect)) - - local node = si:get_associated_proxy ("node") - if not reconnect then - Log.info (si, "... destroy node") - node:request_destroy () - elseif si_flags [si.id].was_handled then - Log.info (si, "... waiting reconnect") - return - end - - local client_id = node.properties ["client.id"] - if client_id then - local client = clients_om:lookup { - Constraint { "bound-id", "=", client_id, type = "gobject" } - } - if client then - client:send_error (node ["bound-id"], -2, "no node available") - end - end - else - createLink (si, si_target, can_passthrough, exclusive) - si_flags [si.id].was_handled = true - - checkFollowDefault (si, si_target, has_node_defined_target) - end + si_flags = nil + putils.set_flags (si_id, si_flags) end function handleLinkable (si) - if checkPending () then - return - end + local si_id = si.id; local valid, si_props = checkLinkable (si) if not valid then return end - props = {} - props ["event.subject.type"] = "linkable" - -- check if we need to link this node at all local autoconnect = parseBool (si_props ["node.autoconnect"]) if not autoconnect then @@ -173,548 +75,27 @@ function handleLinkable (si) return end - if not find_target_events [si.id] then - find_target_events [si.id] = {} - else - -- stop the processing of the old event, we are going to queue a new one any - -- way - find_target_events [si.id]:stop_processing () + if not find_target_events [si_id] then + find_target_events [si_id] = nil end - find_target_events [si.id] = EventDispatcher.push_event { - type = "find-target-si-and-link", priority = 10, properties = props, - subject = si } + if find_target_events [si_id] ~= nil then + -- stop the processing of the old event, we are going to queue a new one any + -- way + find_target_events [si_id]:stop_processing () + end + + find_target_events [si_id] = EventDispatcher.push_event { + type = "find-target-si-and-link", priority = 10, subject = si } end function rescan () Log.info ("rescanning..") for si in linkables_om:iterate () do - handleLinkable (si) + handleLinkable (si) end end -function parseBool (var) - return var and (var:lower () == "true" or var == "1") -end - -function createLink (si, si_target, passthrough, exclusive) - local out_item = nil - local in_item = nil - local si_props = si.properties - local target_props = si_target.properties - local si_id = si.id - - -- break rescan if tried more than 5 times with same target - if si_flags [si_id].failed_peer_id ~= nil and - si_flags [si_id].failed_peer_id == si_target.id and - si_flags [si_id].failed_count ~= nil and - si_flags [si_id].failed_count > 5 then - Log.warning (si, "tried to link on last rescan, not retrying") - return - end - - if si_props ["item.node.direction"] == "output" then - -- playback - out_item = si - in_item = si_target - else - -- capture - in_item = si - out_item = si_target - end - - local passive = parseBool (si_props ["node.passive"]) or - parseBool (target_props ["node.passive"]) - - Log.info ( - string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s", - tostring (si_props ["node.name"]), - tostring (target_props ["node.name"]), - tostring (passive), tostring (passthrough), tostring (exclusive))) - - -- create and configure link - local si_link = SessionItem ("si-standard-link") - if not si_link:configure { - ["out.item"] = out_item, - ["in.item"] = in_item, - ["passive"] = passive, - ["passthrough"] = passthrough, - ["exclusive"] = exclusive, - ["out.item.port.context"] = "output", - ["in.item.port.context"] = "input", - ["is.policy.item.link"] = true, - } then - Log.warning (si_link, "failed to configure si-standard-link") - return - end - - si_link:connect("link-error", function (_, error_msg) - local ids = {si_id} - if si_flags[si_id] ~= nil then - table.insert (ids, si_flags[si_id].peer_id) - end - - for _, id in ipairs (ids) do - local si = linkables_om:lookup { - Constraint { "id", "=", id, type = "gobject" }, - } - if si then - local node = si:get_associated_proxy ("node") - local client_id = node.properties["client.id"] - if client_id then - local client = clients_om:lookup { - Constraint { "bound-id", "=", client_id, type = "gobject" } - } - if client then - Log.info (node, "sending client error: " .. error_msg) - client:send_error (node["bound-id"], -32, error_msg) - end - end - end - end - end) - - -- register - si_flags [si_id].peer_id = si_target.id - si_flags [si_id].failed_peer_id = si_target.id - if si_flags [si_id].failed_count ~= nil then - si_flags [si_id].failed_count = si_flags [si_id].failed_count + 1 - else - si_flags [si_id].failed_count = 1 - end - si_link:register () - - -- activate - si_link:activate (Feature.SessionItem.ACTIVE, function (l, e) - if e then - Log.info (l, "failed to activate si-standard-link: " .. tostring (e)) - if si_flags [si_id] ~= nil then - si_flags [si_id].peer_id = nil - end - l:remove () - else - if si_flags [si_id] ~= nil then - si_flags [si_id].failed_peer_id = nil - si_flags [si_id].failed_count = 0 - end - Log.info (l, "activated si-standard-link") - end - scheduleRescan() - end) -end - -function isLinked (si_target) - local target_id = si_target.id - local linked = false - local exclusive = false - - for l in links_om:iterate () 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 = parseBool (p ["exclusive"]) or parseBool (p ["passthrough"]) - break - end - end - return linked, exclusive -end - -function canPassthrough (si, si_target) - -- both nodes must support encoded formats - if not parseBool (si.properties ["item.node.supports-encoded-fmts"]) - or not parseBool (si_target.properties ["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 canLink (properties, si_target) - local target_properties = si_target.properties - - -- nodes must have the same media type - if properties ["media.type"] ~= target_properties ["media.type"] then - return false - 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) - local function isMonitor(properties) - return properties ["item.node.direction"] == "input" and - parseBool (properties ["item.features.monitor"]) and - not parseBool (properties ["item.features.no-dsp"]) and - properties ["item.factory.name"] == "si-audio-adapter" - end - - if properties ["item.node.direction"] == target_properties ["item.node.direction"] - and not isMonitor (target_properties) 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 linkables_om:iterate { - 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 links_om:iterate () 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 = linkables_om:lookup { - 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 getTargetDirection (properties) - local target_direction = nil - if properties ["item.node.direction"] == "output" or - (properties ["item.node.direction"] == "input" and - parseBool (properties ["stream.capture.sink"])) then - target_direction = "input" - else - target_direction = "output" - end - return target_direction -end - -function getDefaultNode (properties, target_direction) - local target_media_class = - properties ["media.type"] .. - (target_direction == "input" and "/Sink" or "/Source") - return default_nodes:call ("get-default-node", target_media_class) -end - --- Try to locate a valid target node that was explicitly requsted by the --- client(node.target) or by the user(target.node) --- Use the target.node metadata, if "move" setting is enabled, --- then use the node.target property that was set on the node --- `properties` must be the properties dictionary of the session item --- that is currently being handled -function findDefinedTarget (properties) - local metadata = move and metadata_om:lookup () - local target_direction = getTargetDirection (properties) - local target_key - local target_value - local node_defined = false - - if properties ["target.object"] ~= nil then - target_value = properties ["target.object"] - target_key = "object.serial" - node_defined = true - elseif properties ["node.target"] ~= nil then - target_value = properties ["node.target"] - target_key = "node.id" - node_defined = true - end - - if metadata then - local id = metadata:find (properties ["node.id"], "target.object") - if id ~= nil then - target_value = id - target_key = "object.serial" - node_defined = false - else - id = metadata:find (properties ["node.id"], "target.node") - if id ~= nil then - target_value = id - target_key = "node.id" - node_defined = false - end - end - end - - if target_value == "-1" then - return nil, false, node_defined - end - - if target_value and tonumber (target_value) then - local si_target = linkables_om:lookup { - Constraint { target_key, "=", target_value }, - } - if si_target and canLink (properties, si_target) then - return si_target, true, node_defined - end - end - - if target_value then - for si_target in linkables_om:iterate () do - local target_props = si_target.properties - if (target_props ["node.name"] == target_value or - target_props ["object.path"] == target_value) and - target_props ["item.node.direction"] == target_direction and - canLink (properties, si_target) then - return si_target, true, node_defined - end - end - end - return nil, (target_value ~= nil), node_defined -end - -function parseParam (param, id) - local route = param:parse () - if route.pod_type == "Object" and route.object_id == id then - return route.properties - else - return nil - end -end - -function arrayContains (a, value) - for _, v in ipairs (a) do - if v == value then - return true - end - end - return false -end - --- Does the target device have any active/available paths/routes to --- the physical device(spkr/mic/cam)? -function haveAvailableRoutes (si_props) - local card_profile_device = si_props ["card.profile.device"] - local device_id = si_props ["device.id"] - local device = device_id and devices_om: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 = 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 = parseParam (p, "EnumRoute") - if not route then - goto skip_enum_route - end - - if not 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 findDefaultLinkable (si) - local si_props = si.properties - local target_direction = getTargetDirection (si_props) - local def_node_id = getDefaultNode (si_props, target_direction) - return linkables_om:lookup { - Constraint { "node.id", "=", tostring (def_node_id) } - } -end - -function checkPassthroughCompatibility (si, si_target) - local si_must_passthrough = parseBool (si.properties ["item.node.encoded-only"]) - local si_target_must_passthrough = parseBool (si_target.properties ["item.node.encoded-only"]) - local can_passthrough = 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 - -function findBestLinkable (si) - local si_props = si.properties - local target_direction = getTargetDirection (si_props) - local target_picked = nil - local target_can_passthrough = false - local target_priority = 0 - local target_plugged = 0 - - for si_target in linkables_om:iterate { - Constraint { "item.node.type", "=", "device" }, - Constraint { "item.node.direction", "=", target_direction }, - Constraint { "media.type", "=", si_props ["media.type"] }, - } do - local si_target_props = si_target.properties - local si_target_node_id = si_target_props ["node.id"] - local priority = tonumber (si_target_props ["priority.session"]) or 0 - - Log.debug (string.format ("Looking at: %s (%s)", - tostring (si_target_props ["node.name"]), - tostring (si_target_node_id))) - - if not canLink (si_props, si_target) then - Log.debug ("... cannot link, skip linkable") - goto skip_linkable - end - - if not haveAvailableRoutes (si_target_props) then - Log.debug ("... does not have routes, skip linkable") - goto skip_linkable - end - - local passthrough_compatible, can_passthrough = - checkPassthroughCompatibility (si, si_target) - if not passthrough_compatible then - Log.debug ("... passthrough is not compatible, skip linkable") - goto skip_linkable - end - - local plugged = tonumber (si_target_props ["item.plugged.usec"]) or 0 - - Log.debug ("... priority:" .. tostring (priority) .. ", plugged:" .. tostring (plugged)) - - -- (target_picked == NULL) --> make sure atleast one target is picked. - -- (priority > target_priority) --> pick the highest priority linkable(node) - -- target. - -- (priority == target_priority and plugged > target_plugged) --> pick the - -- latest connected/plugged(in time) linkable(node) target. - if (target_picked == nil or - priority > target_priority or - (priority == target_priority and plugged > target_plugged)) then - Log.debug ("... picked") - target_picked = si_target - target_can_passthrough = can_passthrough - target_priority = priority - target_plugged = plugged - end - ::skip_linkable:: - end - - if target_picked then - Log.info (string.format ("... best target picked: %s (%s), can_passthrough:%s", - tostring (target_picked.properties ["node.name"]), - tostring (target_picked.properties ["node.id"]), - tostring (target_can_passthrough))) - return target_picked, target_can_passthrough - else - return nil, nil - end -end - -function findUndefinedTarget (si) - -- Just find the best linkable if default nodes module is not loaded - if default_nodes == nil then - return findBestLinkable (si) - end - - -- Otherwise find the default linkable. If the default linkable is not - -- compatible, we find the best one instead. We return nil if the default - -- linkable does not exist. - local si_target = findDefaultLinkable (si) - if si_target then - local passthrough_compatible, can_passthrough = - checkPassthroughCompatibility (si, si_target) - if canLink (si.properties, si_target) and passthrough_compatible then - Log.info (string.format ("... default target picked: %s (%s), can_passthrough:%s", - tostring (si_target.properties ["node.name"]), - tostring (si_target.properties ["node.id"]), - tostring (can_passthrough))) - return si_target, can_passthrough - else - return findBestLinkable (si) - end - end - return nil, nil -end - -function lookupLink (si_id, si_target_id) - local link = links_om:lookup { - Constraint { "out.item.id", "=", si_id }, - Constraint { "in.item.id", "=", si_target_id } - } - if not link then - link = links_om:lookup { - Constraint { "in.item.id", "=", si_id }, - Constraint { "out.item.id", "=", si_target_id } - } - end - return link -end - function checkLinkable (si, handle_nonstreams) -- only handle stream session items local si_props = si.properties @@ -732,119 +113,6 @@ function checkLinkable (si, handle_nonstreams) return true, si_props end -si_flags = {} - -function checkPending () - local pending_linkables = pending_linkables_om:get_n_objects () - - -- We cannot process linkables if some of them are pending activation, - -- because linkables do not appear in the same order as nodes, - -- and we cannot resolve target node references until all linkables - -- have appeared. - - if self.pending_error_timer then - self.pending_error_timer:destroy () - self.pending_error_timer = nil - end - - if pending_linkables ~= 0 then - -- Wait for linkables to get it sync - Log.debug (string.format ("pending %d linkable not ready", - pending_linkables)) - self.events_skipped = true - - -- To make bugs in activation easier to debug, emit an error message - -- if they occur. policy-node should never be suspended for 20sec. - self.pending_error_timer = Core.timeout_add (20000, function () - self.pending_error_timer = nil - if pending_linkables ~= 0 then - Log.message (string.format ("%d pending linkable(s) not activated in 20sec. " - .. "This should never happen.", pending_linkables)) - end - end) - - return true - elseif self.events_skipped then - Log.debug ("pending linkables ready") - self.events_skipped = false - -- Event-Stack TBD: do we need to retain this call here? - rescan () - return true - end - - return false -end - -function checkFollowDefault (si, si_target, has_node_defined_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. - if not has_node_defined_target then - return - end - - local si_props = si.properties - local target_props = si_target.properties - local reconnect = not parseBool (si_props ["node.dont-reconnect"]) - local is_filter = (si_props ["node.link-group"] ~= nil) - - if follow and default_nodes ~= nil and reconnect and not is_filter then - local def_id = getDefaultNode (si_props, getTargetDirection (si_props)) - - if target_props ["node.id"] == tostring (def_id) then - local metadata = metadata_om:lookup () - -- 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 - -default_nodes = Plugin.find ("default-nodes-api") - -metadata_om = ObjectManager { - Interest { - type = "metadata", - Constraint { "metadata.name", "=", "default" }, - } -} - -endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } } - -clients_om = ObjectManager { Interest { type = "client" } } - -devices_om = ObjectManager { Interest { type = "device" } } - -linkables_om = ObjectManager { - Interest { - type = "SiLinkable", - -- only handle si-audio-adapter and si-node - Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, - Constraint { "active-features", "!", 0, type = "gobject" }, - } -} - -pending_linkables_om = ObjectManager { - Interest { - type = "SiLinkable", - -- only handle si-audio-adapter and si-node - Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, - Constraint { "active-features", "=", 0, type = "gobject" }, - } -} - -links_om = ObjectManager { - Interest { - type = "SiLink", - -- only handle links created by this policy - Constraint { "is.policy.item.link", "=", true }, - } -} - function findAssociatedLinkGroupNode (si) local si_props = si.properties local node = si:get_associated_proxy ("node") @@ -854,7 +122,7 @@ function findAssociatedLinkGroupNode (si) end -- get the associated media class - local assoc_direction = getTargetDirection (si_props) + local assoc_direction = cutils.getTargetDirection (si_props) local assoc_media_class = si_props ["media.type"] .. (assoc_direction == "input" and "/Sink" or "/Source") @@ -910,27 +178,21 @@ function onLinkGroupPortsStateChanged (si, old_state, new_state) end end -function ensureSiFlags (si) - -- prepare flags table - if not si_flags [si.id] then - si_flags [si.id] = {} - end -end - function checkFiltersPortsState (si) local si_props = si.properties local node = si:get_associated_proxy ("node") local link_group = node.properties ["node.link-group"] - - ensureSiFlags (si) + local si_id = si.id + local si_flags = putils.get_flags (si_id) -- only listen for ports state changed on audio filter streams - if si_flags [si.id].ports_state_signal ~= true and + if si_flags.ports_state_signal ~= true and si_props ["item.factory.name"] == "si-audio-adapter" and si_props ["item.node.type"] == "stream" and link_group ~= nil then si:connect ("adapter-ports-state-changed", onLinkGroupPortsStateChanged) - si_flags [si.id].ports_state_signal = true + si_flags.ports_state_signal = true + putils.set_flags (si_id, si_flags) Log.info (si, "listening ports state changed on " .. si_props ["node.name"]) end end @@ -961,22 +223,6 @@ SimpleEventHook { end }:register () -SimpleEventHook { - name = "find-target-si-and-link@policy-node", - type = "after-events-with-event", - priority = "find-target-si", - interests = { - EventInterest { - Constraint { "event.type", "=", "find-target-si-and-link" }, - Constraint { "event.subject.type", "=", "linkable" }, - }, - }, - execute = function (event) - local si = event:get_subject () - findTargetSiAndLink (si) - end -}:register () - SimpleEventHook { name = "linkable-removed@policy-node", type = "on-event", @@ -1087,14 +333,12 @@ if move then Constraint { "event.subject.type", "=", "metadata" }, Constraint { "metadata.name", "=", "default" }, Constraint { "event.subject.key", "=", "target.node" }, - Constraint { "event.subject.value", "!", "-1" }, }, EventInterest { Constraint { "event.type", "=", "object-changed" }, Constraint { "event.subject.type", "=", "metadata" }, Constraint { "metadata.name", "=", "default" }, Constraint { "event.subject.key", "=", "target.object" }, - Constraint { "event.subject.value", "!", "-1" }, }, }, execute = function () @@ -1103,10 +347,28 @@ if move then }:register () end -metadata_om:activate () +default_nodes = Plugin.find ("default-nodes-api") + +endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } } + +linkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle si-audio-adapter and si-node + Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, + Constraint { "active-features", "!", 0, type = "gobject" }, + } +} + +pending_linkables_om = ObjectManager { + Interest { + type = "SiLinkable", + -- only handle si-audio-adapter and si-node + Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" }, + Constraint { "active-features", "=", 0, type = "gobject" }, + } +} + endpoints_om:activate () -clients_om:activate () linkables_om:activate () pending_linkables_om:activate () -links_om:activate () -devices_om:activate () diff --git a/tests/wplua/scripts/lib/testlib.lua b/tests/wplua/scripts/lib/testlib.lua index 0ea3b5f1..414a9051 100644 --- a/tests/wplua/scripts/lib/testlib.lua +++ b/tests/wplua/scripts/lib/testlib.lua @@ -1,9 +1,26 @@ local testlib = {} +testlib.table1 = {} + +testlib.table1 ["test-key"] = "test-value" + function testlib.test_add_ten (x) return x + 10 end -Log.info("in testlib") +function testlib.get_empty_table (key) + testlib.table1 [key] = {} + return testlib.table1 [key] +end + +function testlib.set_table (key, value) + testlib.table1 [key] = value +end + +function testlib.get_table (key) + return testlib.table1 [key] +end + +Log.info ("in testlib") return testlib diff --git a/tests/wplua/scripts/require.lua b/tests/wplua/scripts/require.lua index a40a4ed5..783cacc4 100644 --- a/tests/wplua/scripts/require.lua +++ b/tests/wplua/scripts/require.lua @@ -1,8 +1,19 @@ -local testlib = require("testlib") +local testlib = require ("testlib") +assert (package.loaded ["testlib"] == testlib) -assert(type(testlib) == "table") -assert(package.loaded["testlib"] == testlib) +assert (type (testlib) == "table") local x = 1 -x = testlib.test_add_ten(x) -assert(x == 11) +x = testlib.test_add_ten (x) +assert (x == 11) + +local val_table = testlib.get_empty_table ("test-key") +assert (type (val_table) == "table") +val_table ["key"] = "value" +val_table.id = 100 + +testlib.set_table ("test-key", val_table) + +local val1 = testlib.get_table ("test-key") +assert (val1 ["key"] == "value") +assert (val1.id == 100)