1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009 |
- -- WirePlumber
- --
- -- Copyright © 2020 Collabora Ltd.
- -- @author Julian Bouzas <julian.bouzas@collabora.com>
- --
- -- SPDX-License-Identifier: MIT
- -- Receive script arguments from config.lua
- local config = ... or {}
- -- ensure config.move and config.follow are not nil
- config.move = config.move or false
- config.follow = config.follow or false
- config.filter_forward_format = config["filter.forward-format"] or false
- local self = {}
- self.scanning = false
- self.pending_rescan = false
- self.events_skipped = false
- self.pending_error_timer = nil
- function rescan()
- for si in linkables_om:iterate() do
- handleLinkable (si)
- end
- end
- function scheduleRescan ()
- if self.scanning then
- self.pending_rescan = true
- return
- end
- self.scanning = true
- rescan ()
- self.scanning = false
- if self.pending_rescan then
- self.pending_rescan = false
- Core.sync(function ()
- scheduleRescan ()
- end)
- 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 config.move 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 = config.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
- if not si_props or (si_props["item.node.type"] ~= "stream"
- and not handle_nonstreams) then
- return false
- end
- -- Determine if we can handle item by this policy
- if endpoints_om:get_n_objects () > 0 and
- si_props["item.factory.name"] == "si-audio-adapter" then
- return false
- end
- 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
- scheduleRescan ()
- 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 config.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
- function handleLinkable (si)
- if checkPending () then
- return
- end
- local valid, si_props = checkLinkable(si)
- if not valid then
- return
- end
- -- check if we need to link this node at all
- local autoconnect = parseBool(si_props["node.autoconnect"])
- if not autoconnect then
- Log.debug (si, tostring(si_props["node.name"]) .. " does not need to be autoconnected")
- return
- end
- 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
- scheduleRescan()
- return
- end
- -- find fallback target
- if not si_target and (reconnect or not has_defined_target) then
- si_target, can_passthrough = findUndefinedTarget(si)
- end
- -- 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 ((link:get_active_features() & Feature.SessionItem.ACTIVE) == 0) then
- -- Link not yet activated. We don't want to remove it now, as that
- -- may cause problems. Instead, give up for now. A rescan is scheduled
- -- once the link activates.
- Log.info (link, "Link to be moved was not activated, will wait for it.")
- return
- end
- si_flags[si_id].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")
- return
- 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[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
- end
- function unhandleLinkable (si)
- local valid, si_props = checkLinkable(si, true)
- if not valid then
- return
- end
- Log.info (si, string.format("unhandling item: %s (%s)",
- tostring(si_props["node.name"]), tostring(si_props["node.id"])))
- -- 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[in_id] and si_flags[in_id].peer_id == out_id then
- si_flags[in_id].peer_id = nil
- elseif in_id == si.id and
- si_flags[out_id] and si_flags[out_id].peer_id == in_id then
- si_flags[out_id].peer_id = nil
- end
- silink:remove ()
- Log.info (silink, "... link removed")
- end
- end
- si_flags[si.id] = nil
- 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 },
- }
- }
- -- listen for default node changes if config.follow is enabled
- if config.follow and default_nodes ~= nil then
- default_nodes:connect("changed", function ()
- scheduleRescan ()
- end)
- end
- -- listen for target.node metadata changes if config.move is enabled
- if config.move then
- metadata_om:connect("object-added", function (om, metadata)
- metadata:connect("changed", function (m, subject, key, t, value)
- if key == "target.node" or key == "target.object" then
- scheduleRescan ()
- end
- end)
- end)
- end
- function findAssociatedLinkGroupNode (si)
- local si_props = si.properties
- local node = si:get_associated_proxy ("node")
- local link_group = node.properties["node.link-group"]
- if link_group == nil then
- return nil
- end
- -- get the associated media class
- local assoc_direction = getTargetDirection(si_props)
- local assoc_media_class =
- si_props["media.type"] ..
- (assoc_direction == "input" and "/Sink" or "/Source")
- -- find the linkable with same link group and matching assoc media class
- for assoc_si in linkables_om:iterate() do
- local assoc_node = assoc_si:get_associated_proxy ("node")
- local assoc_link_group = assoc_node.properties["node.link-group"]
- if assoc_link_group == link_group and
- assoc_media_class == assoc_node.properties["media.class"] then
- return assoc_si
- end
- end
- return nil
- end
- function onLinkGroupPortsStateChanged (si, old_state, new_state)
- local new_str = tostring(new_state)
- local si_props = si.properties
- -- only handle items with configured ports state
- if new_str ~= "configured" then
- return
- end
- Log.info (si, "ports format changed on " .. si_props["node.name"])
- -- find associated device
- local si_device = findAssociatedLinkGroupNode (si)
- if si_device ~= nil then
- local device_node_name = si_device.properties["node.name"]
- -- get the stream format
- local f, m = si:get_ports_format()
- -- unregister the device
- Log.info (si_device, "unregistering " .. device_node_name)
- si_device:remove()
- -- set new format in the device
- Log.info (si_device, "setting new format in " .. device_node_name)
- si_device:set_ports_format(f, m, function (item, e)
- if e ~= nil then
- Log.warning (item, "failed to configure ports in " ..
- device_node_name .. ": " .. e)
- end
- -- register back the device
- Log.info (item, "registering " .. device_node_name)
- item:register()
- end)
- 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)
- -- only listen for ports state changed on audio filter streams
- if si_flags[si.id].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
- Log.info (si, "listening ports state changed on " .. si_props["node.name"])
- end
- end
- linkables_om:connect("object-added", function (om, si)
- local si_props = si.properties
- -- Forward filters ports format to associated virtual devices if enabled
- if config.filter_forward_format then
- checkFiltersPortsState (si)
- end
- if si_props["item.node.type"] ~= "stream" then
- scheduleRescan ()
- else
- handleLinkable (si)
- end
- end)
- linkables_om:connect("object-removed", function (om, si)
- unhandleLinkable (si)
- scheduleRescan ()
- end)
- devices_om:connect("object-added", function (om, device)
- device:connect("params-changed", function (d, param_name)
- scheduleRescan ()
- end)
- end)
- metadata_om:activate()
- endpoints_om:activate()
- clients_om:activate()
- linkables_om:activate()
- pending_linkables_om:activate()
- links_om:activate()
- devices_om:activate()
|