scripts: use the event stack to handle virtual session items
This removes both the policy-virtual-client.lua and policy-virtual-device.lua scripts, and creates a new linking/find-virtual-target.lua script to link clients with virtual session items if one of them can be found. In addition to this, this patch also ports the policy-virtual-client-links.lua into a new scripts/rescan-virtual-links.lua to use the event stack. The idea is for the scripts/link-target.lua to create all links but only activate non virtual links, and for the scripts/rescan-virtual-links.lua to activate/deactivate virtual links based on role priorities.
This commit is contained in:
@@ -90,7 +90,8 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
|
||||
if (strstr (self->media_class, "Source") ||
|
||||
strstr (self->media_class, "Output"))
|
||||
self->direction = WP_DIRECTION_OUTPUT;
|
||||
wp_properties_setf (si_props, "direction", "%u", self->direction);
|
||||
wp_properties_set (si_props, "item.node.direction",
|
||||
self->direction == WP_DIRECTION_OUTPUT ? "output" : "input");
|
||||
|
||||
str = wp_properties_get (si_props, "role");
|
||||
if (str) {
|
||||
@@ -109,6 +110,10 @@ si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
|
||||
str = wp_properties_get (si_props, "item.features.no-dsp");
|
||||
self->disable_dsp = str && pw_properties_parse_bool (str);
|
||||
|
||||
/* We always want virtual sources to autoconnect */
|
||||
wp_properties_set (si_props, PW_KEY_NODE_AUTOCONNECT, "true");
|
||||
wp_properties_set (si_props, "media.type", "Audio");
|
||||
|
||||
wp_properties_set (si_props, "item.factory.name", SI_FACTORY_NAME);
|
||||
wp_session_item_set_properties (WP_SESSION_ITEM (self),
|
||||
g_steal_pointer (&si_props));
|
||||
@@ -193,6 +198,21 @@ on_node_activate_done (WpObject * node, GAsyncResult * res,
|
||||
"si-audio-virtual: could not create si-audio-adapter"));
|
||||
}
|
||||
|
||||
/* Set node.id and node.name properties in this session item */
|
||||
{
|
||||
g_autoptr (WpProperties) si_props = wp_session_item_get_properties (
|
||||
WP_SESSION_ITEM (self));
|
||||
g_autoptr (WpProperties) new_props = wp_properties_new_empty ();
|
||||
guint32 node_id = wp_proxy_get_bound_id (WP_PROXY (node));
|
||||
wp_properties_setf (new_props, "node.id", "%u", node_id);
|
||||
wp_properties_set (new_props, "node.name",
|
||||
wp_pipewire_object_get_property (WP_PIPEWIRE_OBJECT (node),
|
||||
PW_KEY_NODE_NAME));
|
||||
wp_properties_update (si_props, new_props);
|
||||
wp_session_item_set_properties (WP_SESSION_ITEM (self),
|
||||
g_steal_pointer (&si_props));
|
||||
}
|
||||
|
||||
/* Forward adapter-ports-state-changed signal */
|
||||
g_signal_connect_object (self->adapter, "adapter-ports-state-changed",
|
||||
G_CALLBACK (on_adapter_port_state_changed), self, 0);
|
||||
@@ -243,6 +263,7 @@ si_audio_virtual_enable_active (WpSessionItem *si, WpTransition *transition)
|
||||
PW_KEY_MEDIA_CLASS, media,
|
||||
PW_KEY_FACTORY_NAME, "support.null-audio-sink",
|
||||
PW_KEY_NODE_DESCRIPTION, desc,
|
||||
PW_KEY_NODE_AUTOCONNECT, "true",
|
||||
"monitor.channel-volumes", "true",
|
||||
"wireplumber.is-virtual", "true",
|
||||
NULL));
|
||||
|
@@ -221,14 +221,16 @@ wireplumber.components = [
|
||||
{ name = default-nodes/find-best-default-node.lua, type = script/lua, priority = 100 }
|
||||
{ name = default-nodes/select-default-nodes.lua, type = script/lua, priority = 100 }
|
||||
|
||||
{ name = linking/find-best-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/find-virtual-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/find-defined-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/find-default-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/find-best-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/link-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/prepare-link.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/filter-forward-format.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/find-default-target.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/rescan.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/move-follow.lua, type = script/lua, priority = 100 }
|
||||
{ name = linking/rescan-virtual-links.lua, type = script/lua, priority = 100 }
|
||||
|
||||
{ name = device/apply-routes.lua, type = script/lua, priority = 100 }
|
||||
{ name = device/find-best-profile.lua, type = script/lua, priority = 100 }
|
||||
@@ -243,6 +245,7 @@ wireplumber.components = [
|
||||
{ name = client/access-default.lua, type = script/lua, priority = 100 }
|
||||
|
||||
{ name = node/create-item.lua, type = script/lua, priority = 100 }
|
||||
{ name = node/create-virtual-item.lua, type = script/lua, priority = 100 }
|
||||
{ name = node/suspend-node.lua, type = script/lua, priority = 100 }
|
||||
{ name = node/state-stream.lua, type = script/lua, priority = 100 }
|
||||
]
|
||||
|
@@ -24,6 +24,12 @@ end
|
||||
|
||||
function cutils.getTargetDirection (properties)
|
||||
local target_direction = nil
|
||||
|
||||
-- retrun same direction for si-audio-virtual session items
|
||||
if properties ["item.factory.name"] == "si-audio-virtual" then
|
||||
return properties ["item.node.direction"]
|
||||
end
|
||||
|
||||
if properties ["item.node.direction"] == "output" or
|
||||
(properties ["item.node.direction"] == "input" and
|
||||
cutils.parseBool (properties ["stream.capture.sink"])) then
|
||||
|
@@ -134,8 +134,6 @@ function putils.canLink (properties, si_target)
|
||||
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
|
||||
@@ -143,10 +141,20 @@ function putils.canLink (properties, si_target)
|
||||
properties ["item.factory.name"] == "si-audio-adapter"
|
||||
end
|
||||
|
||||
if properties ["item.factory.name"] == "si-audio-virtual" then
|
||||
-- virtual nodes must have the same direction, unless the target is monitor
|
||||
if properties ["item.node.direction"] ~= target_props ["item.node.direction"]
|
||||
and not isMonitor (target_props) then
|
||||
return false
|
||||
end
|
||||
else
|
||||
-- nodes must have opposite direction, or otherwise they must be both input
|
||||
-- and the target must have a monitor (so the target will be used as a source)
|
||||
if properties ["item.node.direction"] == target_props ["item.node.direction"]
|
||||
and not isMonitor (target_props) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- check link group
|
||||
local function canLinkGroupCheck(link_group, si_target, hops)
|
||||
@@ -305,8 +313,6 @@ linkables_om:activate ()
|
||||
links_om = ObjectManager {
|
||||
Interest {
|
||||
type = "SiLink",
|
||||
-- only handle links created by this policy
|
||||
Constraint { "is.policy.item.link", "=", true },
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ local config = require ("policy-config")
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/find-defined-target",
|
||||
after = "linking/find-virtual-target",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
|
124
src/scripts/linking/find-virtual-target.lua
Normal file
124
src/scripts/linking/find-virtual-target.lua
Normal file
@@ -0,0 +1,124 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Select the virtual target based on roles
|
||||
|
||||
local putils = require ("policy-utils")
|
||||
|
||||
local defaults = {}
|
||||
defaults.roles = Json.Object {}
|
||||
|
||||
local config = {}
|
||||
config.roles = Conf.get_section (
|
||||
"virtual-item-roles", defaults.roles):parse ()
|
||||
|
||||
function findRole(role, tmc)
|
||||
if role and not config.roles[role] then
|
||||
-- find the role with matching alias
|
||||
for r, p in pairs(config.roles) do
|
||||
-- default media class can be overridden in the role config data
|
||||
mc = p["media.class"] or "Audio/Sink"
|
||||
if (type(p.alias) == "table" and tmc == mc) then
|
||||
for i = 1, #(p.alias), 1 do
|
||||
if role == p.alias[i] then
|
||||
return r
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- otherwise get the lowest priority role
|
||||
local lowest_priority_p = nil
|
||||
local lowest_priority_r = nil
|
||||
for r, p in pairs(config.roles) do
|
||||
mc = p["media.class"] or "Audio/Sink"
|
||||
if tmc == mc and (lowest_priority_p == nil or
|
||||
p.priority < lowest_priority_p.priority) then
|
||||
lowest_priority_p = p
|
||||
lowest_priority_r = r
|
||||
end
|
||||
end
|
||||
return lowest_priority_r
|
||||
end
|
||||
return role
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "linking/find-virtual-target",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "select-target" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source, om, si, si_props, si_flags, target =
|
||||
putils:unwrap_find_target_event (event)
|
||||
local target_class_assoc = {
|
||||
["Stream/Input/Audio"] = "Audio/Source",
|
||||
["Stream/Output/Audio"] = "Audio/Sink",
|
||||
["Stream/Input/Video"] = "Video/Source",
|
||||
}
|
||||
local node = si:get_associated_proxy ("node")
|
||||
local highest_priority = -1
|
||||
local target = nil
|
||||
local role = node.properties["media.role"] or "Default"
|
||||
|
||||
-- bypass the hook if the target is already picked up
|
||||
if target then
|
||||
return
|
||||
end
|
||||
|
||||
-- dont use virtual target for any si-audio-virtual
|
||||
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||
return
|
||||
end
|
||||
|
||||
Log.info (si, string.format ("handling item: %s (%s)",
|
||||
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||
|
||||
-- get target media class
|
||||
local target_media_class = target_class_assoc[si_props ["media.class"]]
|
||||
if not target_media_class then
|
||||
Log.info (si, "target media class not found")
|
||||
return
|
||||
end
|
||||
|
||||
-- find highest priority virtual by role
|
||||
local media_role = findRole (role, target_media_class)
|
||||
if media_role == nil then
|
||||
Log.info (si, "media role not found")
|
||||
return
|
||||
end
|
||||
|
||||
for si_virtual in om:iterate {
|
||||
Constraint { "role", "=", media_role, type = "pw-global" },
|
||||
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
|
||||
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
} do
|
||||
local priority = tonumber(si_virtual.properties["priority"])
|
||||
if priority > highest_priority then
|
||||
highest_priority = priority
|
||||
target = si_virtual
|
||||
end
|
||||
end
|
||||
|
||||
local can_passthrough, passthrough_compatible
|
||||
if target then
|
||||
passthrough_compatible, can_passthrough =
|
||||
putils.checkPassthroughCompatibility (si, target)
|
||||
|
||||
if not passthrough_compatible then
|
||||
target = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- set target
|
||||
if target ~= nil then
|
||||
si_flags.can_passthrough = can_passthrough
|
||||
event:set_data ("target", target)
|
||||
end
|
||||
end
|
||||
}:register ()
|
@@ -20,7 +20,7 @@ AsyncEventHook {
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "link_activated",
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source, om, si, si_props, si_flags, target =
|
||||
putils:unwrap_find_target_event (event)
|
||||
@@ -53,6 +53,17 @@ AsyncEventHook {
|
||||
.. tostring (si_link))
|
||||
end
|
||||
|
||||
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||
if si_props ["item.node.direction"] == "output" then
|
||||
-- playback
|
||||
out_item = target
|
||||
in_item = si
|
||||
else
|
||||
-- capture
|
||||
in_item = target
|
||||
out_item = si
|
||||
end
|
||||
else
|
||||
if si_props ["item.node.direction"] == "output" then
|
||||
-- playback
|
||||
out_item = si
|
||||
@@ -62,12 +73,18 @@ AsyncEventHook {
|
||||
in_item = si
|
||||
out_item = target
|
||||
end
|
||||
end
|
||||
|
||||
local is_virtual_client_link = target_props ["item.factory.name"] == "si-audio-virtual"
|
||||
|
||||
Log.info (si,
|
||||
string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s",
|
||||
string.format ("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s, virtual-client:%s",
|
||||
tostring (si_props ["node.name"]),
|
||||
tostring (target_props ["node.name"]),
|
||||
tostring (passive), tostring (passthrough), tostring (exclusive)))
|
||||
tostring (passive),
|
||||
tostring (passthrough),
|
||||
tostring (exclusive),
|
||||
tostring (is_virtual_client_link)))
|
||||
|
||||
-- create and configure link
|
||||
si_link = SessionItem ("si-standard-link")
|
||||
@@ -79,7 +96,11 @@ AsyncEventHook {
|
||||
["exclusive"] = exclusive,
|
||||
["out.item.port.context"] = "output",
|
||||
["in.item.port.context"] = "input",
|
||||
["is.policy.item.link"] = true,
|
||||
["media.role"] = target_props["role"],
|
||||
["target.media.class"] = target_props["media.class"],
|
||||
["is.virtual.client.link"] = is_virtual_client_link,
|
||||
["main.item.id"] = si.id,
|
||||
["target.item.id"] = target.id,
|
||||
} then
|
||||
transition:return_error ("failed to configure si-standard-link "
|
||||
.. tostring (si_link))
|
||||
@@ -119,7 +140,12 @@ AsyncEventHook {
|
||||
end
|
||||
si_link:register ()
|
||||
|
||||
-- activate
|
||||
Log.info (si_link, "registered virtual si-standard-link between "
|
||||
.. tostring (si).." and ".. tostring(target))
|
||||
|
||||
-- only activate non virtual links because virtual links activation is
|
||||
-- handled by rescan-virtual-links.lua
|
||||
if not is_virtual_client_link then
|
||||
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
||||
if e then
|
||||
transition:return_error ("failed to activate si-standard-link: "
|
||||
@@ -130,33 +156,21 @@ AsyncEventHook {
|
||||
l:remove ()
|
||||
else
|
||||
si_flags.si_link = si_link
|
||||
si_flags.failed_peer_id = nil
|
||||
if si_flags.peer_id == nil then
|
||||
si_flags.peer_id = target.id
|
||||
end
|
||||
si_flags.failed_count = 0
|
||||
|
||||
Log.info (si_link, "activated si-standard-link between "
|
||||
.. tostring (si).." and ".. tostring(target))
|
||||
|
||||
transition:advance ()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
},
|
||||
link_activated = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source, om, si, si_props, si_flags, target =
|
||||
putils:unwrap_find_target_event (event)
|
||||
if not target then
|
||||
-- bypass the hook, nothing to link to.
|
||||
else
|
||||
transition:advance ()
|
||||
return
|
||||
end
|
||||
|
||||
if si_flags ~= nil then
|
||||
si_flags.failed_peer_id = nil
|
||||
if si_flags.peer_id == nil then
|
||||
si_flags.peer_id = si_target.id
|
||||
end
|
||||
si_flags.failed_count = 0
|
||||
end
|
||||
Log.info (si_flags.si_link, "activated si-standard-link between "
|
||||
.. tostring (si).." and "..tostring(si_target))
|
||||
|
||||
transition:advance ()
|
||||
end,
|
||||
},
|
||||
},
|
||||
|
254
src/scripts/linking/rescan-virtual-links.lua
Normal file
254
src/scripts/linking/rescan-virtual-links.lua
Normal file
@@ -0,0 +1,254 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2023 Collabora Ltd.
|
||||
-- @author Julian Bouzas <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
local putils = require ("policy-utils")
|
||||
|
||||
local defaults = {}
|
||||
defaults.duck_level = 0.3
|
||||
defaults.roles = Json.Object {}
|
||||
|
||||
local config = {}
|
||||
config.duck_level = Conf.get_value_float ("wireplumber.settings",
|
||||
"policy.default.duck-level", defaults.duck_level)
|
||||
config.roles = Conf.get_section (
|
||||
"virtual-item-roles", defaults.roles):parse ()
|
||||
|
||||
-- enable ducking if mixer-api is loaded
|
||||
mixer_api = Plugin.find("mixer-api")
|
||||
|
||||
function findRole (role)
|
||||
if role and not config.roles[role] then
|
||||
for r, p in pairs(config.roles) do
|
||||
if type(p.alias) == "table" then
|
||||
for i = 1, #(p.alias), 1 do
|
||||
if role == p.alias[i] then
|
||||
return r
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return role
|
||||
end
|
||||
|
||||
function getRolePriority (role)
|
||||
local r = role and config.roles[role] or nil
|
||||
return r and r.priority or 0
|
||||
end
|
||||
|
||||
function getAction (dominant_role, other_role)
|
||||
-- default to "mix" if the role is not configured
|
||||
if not dominant_role or not config.roles[dominant_role] then
|
||||
return "mix"
|
||||
end
|
||||
|
||||
local role_config = config.roles[dominant_role]
|
||||
return role_config["action." .. other_role]
|
||||
or role_config["action.default"]
|
||||
or "mix"
|
||||
end
|
||||
|
||||
function restoreVolume (om, role, media_class)
|
||||
if not mixer_api then return end
|
||||
|
||||
local si_v = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
Constraint { "media.role", "=", role, type = "pw" },
|
||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||
}
|
||||
|
||||
if si_v then
|
||||
local n = si_v:get_associated_proxy ("node")
|
||||
if n then
|
||||
Log.debug(si_v, "restore role " .. role)
|
||||
mixer_api:call("set-volume", n["bound-id"], {
|
||||
monitorVolume = 1.0,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function duckVolume (om, role, media_class)
|
||||
if not mixer_api then return end
|
||||
|
||||
local si_v = om:lookup {
|
||||
type = "SiLinkable",
|
||||
Constraint { "item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
Constraint { "media.role", "=", role, type = "pw" },
|
||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||
}
|
||||
|
||||
if si_v then
|
||||
local n = si_v:get_associated_proxy ("node")
|
||||
if n then
|
||||
Log.debug(si_v, "duck role " .. role)
|
||||
mixer_api:call("set-volume", n["bound-id"], {
|
||||
monitorVolume = config.duck_level,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function getSuspendPlaybackFromMetadata (om)
|
||||
local suspend = false
|
||||
local metadata = om:lookup {
|
||||
type = "metadata",
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
}
|
||||
if metadata then
|
||||
local value = metadata:find(0, "suspend.playback")
|
||||
if value then
|
||||
suspend = value == "1" and true or false
|
||||
end
|
||||
end
|
||||
return suspend
|
||||
end
|
||||
|
||||
AsyncEventHook {
|
||||
name = "linking/rescan-virtual-links",
|
||||
interests = {
|
||||
EventInterest {
|
||||
-- on virtual client link added and removed
|
||||
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||
Constraint { "event.session-item.interface", "=", "link" },
|
||||
Constraint { "is.virtual.client.link", "=", true },
|
||||
},
|
||||
EventInterest {
|
||||
-- on default metadata suspend.playback changed
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "=", "suspend.playback" },
|
||||
}
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local source = event:get_source ()
|
||||
local om = source:call ("get-object-manager", "session-item")
|
||||
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||
local suspend = getSuspendPlaybackFromMetadata (metadata_om)
|
||||
local pending_activations = 0
|
||||
local links = {
|
||||
["Audio/Source"] = {},
|
||||
["Audio/Sink"] = {},
|
||||
["Video/Source"] = {},
|
||||
}
|
||||
|
||||
-- gather info about links
|
||||
Log.info ("Rescanning virtual si-standard-link links...")
|
||||
for silink in om:iterate {
|
||||
type = "SiLink",
|
||||
Constraint { "is.virtual.client.link", "=", true },
|
||||
} do
|
||||
|
||||
-- deactivate all links if suspend playback metadata is present
|
||||
if suspend then
|
||||
silink:deactivate (Feature.SessionItem.ACTIVE)
|
||||
end
|
||||
|
||||
local props = silink.properties
|
||||
local role = props["media.role"]
|
||||
local target_class = props["target.media.class"]
|
||||
local plugged = props["item.plugged.usec"]
|
||||
local active = ((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
|
||||
if links[target_class] then
|
||||
table.insert(links[target_class], {
|
||||
silink = silink,
|
||||
role = findRole (role),
|
||||
active = active,
|
||||
priority = getRolePriority (role),
|
||||
plugged = plugged and tonumber(plugged) or 0
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function onVirtualLinkActivated (l, e)
|
||||
local si_id = tonumber (l.properties ["main.item.id"])
|
||||
local target_id = tonumber (l.properties ["target.item.id"])
|
||||
local si_flags = putils:get_flags (si_id)
|
||||
|
||||
if e then
|
||||
Log.warning (l, "failed to activate virtual si-standard-link: " .. e)
|
||||
if si_flags ~= nil then
|
||||
si_flags.peer_id = nil
|
||||
end
|
||||
l:remove ()
|
||||
else
|
||||
Log.info (l, "virtual si-standard-link activated successfully")
|
||||
si_flags.si_link = l
|
||||
si_flags.failed_peer_id = nil
|
||||
if si_flags.peer_id == nil then
|
||||
si_flags.peer_id = target_id
|
||||
end
|
||||
si_flags.failed_count = 0
|
||||
end
|
||||
|
||||
-- advance only when all pending activations are completed
|
||||
pending_activations = pending_activations - 1
|
||||
if pending_activations <= 0 then
|
||||
Log.info ("All virtual si-standard-links activated")
|
||||
transition:advance ()
|
||||
end
|
||||
end
|
||||
|
||||
local function compareLinks(l1, l2)
|
||||
return (l1.priority > l2.priority) or
|
||||
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
|
||||
end
|
||||
|
||||
for media_class, v in pairs(links) do
|
||||
-- sort on priority and stream creation time
|
||||
table.sort(v, compareLinks)
|
||||
|
||||
-- apply actions
|
||||
local first_link = v[1]
|
||||
if first_link then
|
||||
for i = 2, #v, 1 do
|
||||
local action = getAction(first_link.role, v[i].role)
|
||||
if action == "cork" then
|
||||
if v[i].active then
|
||||
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
|
||||
end
|
||||
elseif action == "mix" then
|
||||
if not v[i].active and not suspend then
|
||||
pending_activations = pending_activations + 1
|
||||
v[i].silink:activate (Feature.SessionItem.ACTIVE,
|
||||
onVirtualLinkActivated)
|
||||
end
|
||||
restoreVolume(om, v[i].role, media_class)
|
||||
elseif action == "duck" then
|
||||
if not v[i].active and not suspend then
|
||||
pending_activations = pending_activations + 1
|
||||
v[i].silink:activate (Feature.SessionItem.ACTIVE,
|
||||
onVirtualLinkActivated)
|
||||
end
|
||||
duckVolume (om, v[i].role, media_class)
|
||||
else
|
||||
Log.warning("Unknown action: " .. action)
|
||||
end
|
||||
end
|
||||
|
||||
if not first_link.active and not suspend then
|
||||
pending_activations = pending_activations + 1
|
||||
first_link.silink:activate(Feature.SessionItem.ACTIVE,
|
||||
onVirtualLinkActivated)
|
||||
end
|
||||
restoreVolume (om, first_link.role, media_class)
|
||||
end
|
||||
end
|
||||
|
||||
-- just advance transition if no pending activations are needed
|
||||
if pending_activations <= 0 then
|
||||
Log.info ("All virtual si-standard-links rescanned")
|
||||
transition:advance ()
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
}:register ()
|
@@ -14,16 +14,17 @@ local putils = require ("policy-utils")
|
||||
local cutils = require ("common-utils")
|
||||
|
||||
function checkLinkable (si, om, 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
|
||||
|
||||
-- Always handle si-audio-virtual session items
|
||||
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||
return true, si_props
|
||||
end
|
||||
|
||||
-- Determine if we can handle item by this policy
|
||||
if si_props ["item.factory.name"] == "si-audio-virtual" then
|
||||
return false
|
||||
-- For the rest of them, only handle stream session items
|
||||
if not si_props or (si_props ["item.node.type"] ~= "stream"
|
||||
and not handle_nonstreams) then
|
||||
return false, si_props
|
||||
end
|
||||
|
||||
return true, si_props
|
||||
@@ -116,7 +117,7 @@ SimpleEventHook {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node", "si-audio-virtual" },
|
||||
},
|
||||
-- on device Routes changed
|
||||
EventInterest {
|
||||
|
@@ -1,232 +0,0 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
local defaults = {}
|
||||
defaults.duck_level = 0.3
|
||||
defaults.roles = Json.Object {}
|
||||
|
||||
local config = {}
|
||||
config.duck_level = Conf.get_value_float ("wireplumber.settings",
|
||||
"policy.default.duck-level", defaults.duck_level)
|
||||
config.roles = Conf.get_section (
|
||||
"virtual-item-roles", defaults.roles):parse ()
|
||||
|
||||
function findRole(role)
|
||||
if role and not config.roles[role] then
|
||||
for r, p in pairs(config.roles) do
|
||||
if type(p.alias) == "table" then
|
||||
for i = 1, #(p.alias), 1 do
|
||||
if role == p.alias[i] then
|
||||
return r
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return role
|
||||
end
|
||||
|
||||
function priorityForRole(role)
|
||||
local r = role and config.roles[role] or nil
|
||||
return r and r.priority or 0
|
||||
end
|
||||
|
||||
function getAction(dominant_role, other_role)
|
||||
-- default to "mix" if the role is not configured
|
||||
if not dominant_role or not config.roles[dominant_role] then
|
||||
return "mix"
|
||||
end
|
||||
|
||||
local role_config = config.roles[dominant_role]
|
||||
return role_config["action." .. other_role]
|
||||
or role_config["action.default"]
|
||||
or "mix"
|
||||
end
|
||||
|
||||
function restoreVolume(role, media_class)
|
||||
if not mixer_api then return end
|
||||
|
||||
local si_v = virtuals_om:lookup {
|
||||
Constraint { "media.role", "=", role, type = "pw" },
|
||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||
}
|
||||
|
||||
if si_v then
|
||||
local n = si_v:get_associated_proxy ("node")
|
||||
if n then
|
||||
Log.debug(si_v, "restore role " .. role)
|
||||
mixer_api:call("set-volume", n["bound-id"], {
|
||||
monitorVolume = 1.0,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function duck(role, media_class)
|
||||
if not mixer_api then return end
|
||||
|
||||
local si_v = virtuals_om:lookup {
|
||||
Constraint { "media.role", "=", role, type = "pw" },
|
||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
||||
}
|
||||
|
||||
if si_v then
|
||||
local n = si_v:get_associated_proxy ("node")
|
||||
if n then
|
||||
Log.debug(si_v, "duck role " .. role)
|
||||
mixer_api:call("set-volume", n["bound-id"], {
|
||||
monitorVolume = config.duck_level,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function getSuspendPlaybackMetadata ()
|
||||
local suspend = false
|
||||
local metadata = metadata_om:lookup()
|
||||
if metadata then
|
||||
local value = metadata:find(0, "suspend.playback")
|
||||
if value then
|
||||
suspend = value == "1" and true or false
|
||||
end
|
||||
end
|
||||
return suspend
|
||||
end
|
||||
|
||||
function rescan()
|
||||
local links = {
|
||||
["Audio/Source"] = {},
|
||||
["Audio/Sink"] = {},
|
||||
["Video/Source"] = {},
|
||||
}
|
||||
|
||||
Log.info("Rescan virtual links")
|
||||
|
||||
-- deactivate all links if suspend playback metadata is present
|
||||
local suspend = getSuspendPlaybackMetadata()
|
||||
for silink in silinks_om:iterate() do
|
||||
if suspend then
|
||||
silink:deactivate(Feature.SessionItem.ACTIVE)
|
||||
end
|
||||
end
|
||||
|
||||
-- gather info about links
|
||||
for silink in silinks_om:iterate() do
|
||||
local props = silink.properties
|
||||
local role = props["media.role"]
|
||||
local target_class = props["target.media.class"]
|
||||
local plugged = props["item.plugged.usec"]
|
||||
local active =
|
||||
((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
|
||||
if links[target_class] then
|
||||
table.insert(links[target_class], {
|
||||
silink = silink,
|
||||
role = findRole(role),
|
||||
active = active,
|
||||
priority = priorityForRole(role),
|
||||
plugged = plugged and tonumber(plugged) or 0
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function compareLinks(l1, l2)
|
||||
return (l1.priority > l2.priority) or
|
||||
((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
|
||||
end
|
||||
|
||||
for media_class, v in pairs(links) do
|
||||
-- sort on priority and stream creation time
|
||||
table.sort(v, compareLinks)
|
||||
|
||||
-- apply actions
|
||||
local first_link = v[1]
|
||||
if first_link then
|
||||
for i = 2, #v, 1 do
|
||||
local action = getAction(first_link.role, v[i].role)
|
||||
if action == "cork" then
|
||||
if v[i].active then
|
||||
v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
|
||||
end
|
||||
elseif action == "mix" then
|
||||
if not v[i].active and not suspend then
|
||||
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
||||
end
|
||||
restoreVolume(v[i].role, media_class)
|
||||
elseif action == "duck" then
|
||||
if not v[i].active and not suspend then
|
||||
v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
||||
end
|
||||
duck(v[i].role, media_class)
|
||||
else
|
||||
Log.warning("Unknown action: " .. action)
|
||||
end
|
||||
end
|
||||
|
||||
if not first_link.active and not suspend then
|
||||
first_link.silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
|
||||
end
|
||||
restoreVolume(first_link.role, media_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
pending_ops = 0
|
||||
pending_rescan = false
|
||||
|
||||
function pendingOperation()
|
||||
pending_ops = pending_ops + 1
|
||||
return function()
|
||||
pending_ops = pending_ops - 1
|
||||
if pending_ops == 0 and pending_rescan then
|
||||
pending_rescan = false
|
||||
rescan()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function maybeRescan()
|
||||
if pending_ops == 0 then
|
||||
rescan()
|
||||
else
|
||||
pending_rescan = true
|
||||
end
|
||||
end
|
||||
|
||||
silinks_om = ObjectManager {
|
||||
Interest {
|
||||
type = "SiLink",
|
||||
Constraint { "is.policy.virtual.client.link", "=", true },
|
||||
},
|
||||
}
|
||||
silinks_om:connect("objects-changed", maybeRescan)
|
||||
silinks_om:activate()
|
||||
|
||||
-- enable ducking if mixer-api is loaded
|
||||
mixer_api = Plugin.find("mixer-api")
|
||||
if mixer_api then
|
||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
||||
Constraint {
|
||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
}
|
||||
}
|
||||
virtuals_om:activate()
|
||||
end
|
||||
|
||||
metadata_om = ObjectManager {
|
||||
Interest {
|
||||
type = "metadata",
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
}
|
||||
}
|
||||
metadata_om:connect("object-added", function (om, metadata)
|
||||
metadata:connect("changed", function (m, subject, key, t, value)
|
||||
if key == "suspend.playback" then
|
||||
maybeRescan()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
metadata_om:activate()
|
@@ -1,268 +0,0 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- Receive script arguments from config.lua
|
||||
|
||||
local defaults = {}
|
||||
defaults.roles = Json.Object {}
|
||||
|
||||
local config = {}
|
||||
config.roles = Conf.get_section (
|
||||
"virtual-item-roles", defaults.roles):parse ()
|
||||
|
||||
local self = {}
|
||||
self.scanning = false
|
||||
self.pending_rescan = false
|
||||
|
||||
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 findRole(role, tmc)
|
||||
if role and not config.roles[role] then
|
||||
-- find the role with matching alias
|
||||
for r, p in pairs(config.roles) do
|
||||
-- default media class can be overridden in the role config data
|
||||
mc = p["media.class"] or "Audio/Sink"
|
||||
if (type(p.alias) == "table" and tmc == mc) then
|
||||
for i = 1, #(p.alias), 1 do
|
||||
if role == p.alias[i] then
|
||||
return r
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- otherwise get the lowest priority role
|
||||
local lowest_priority_p = nil
|
||||
local lowest_priority_r = nil
|
||||
for r, p in pairs(config.roles) do
|
||||
mc = p["media.class"] or "Audio/Sink"
|
||||
if tmc == mc and (lowest_priority_p == nil or
|
||||
p.priority < lowest_priority_p.priority) then
|
||||
lowest_priority_p = p
|
||||
lowest_priority_r = r
|
||||
end
|
||||
end
|
||||
return lowest_priority_r
|
||||
end
|
||||
return role
|
||||
end
|
||||
|
||||
function findTargetVirtual (node, media_class, role)
|
||||
local target_class_assoc = {
|
||||
["Stream/Input/Audio"] = "Audio/Source",
|
||||
["Stream/Output/Audio"] = "Audio/Sink",
|
||||
["Stream/Input/Video"] = "Video/Source",
|
||||
}
|
||||
local media_role = nil
|
||||
local highest_priority = -1
|
||||
local target = nil
|
||||
|
||||
-- get target media class
|
||||
local target_media_class = target_class_assoc[media_class]
|
||||
if not target_media_class then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- find highest priority virtual by role
|
||||
media_role = findRole(role, target_media_class)
|
||||
for si_target_ep in virtuals_om:iterate {
|
||||
Constraint { "role", "=", media_role, type = "pw-global" },
|
||||
Constraint { "media.class", "=", target_media_class, type = "pw-global" },
|
||||
} do
|
||||
local priority = tonumber(si_target_ep.properties["priority"])
|
||||
if priority > highest_priority then
|
||||
highest_priority = priority
|
||||
target = si_target_ep
|
||||
end
|
||||
end
|
||||
|
||||
return target
|
||||
end
|
||||
|
||||
function createLink (si, si_target_ep)
|
||||
local out_item = nil
|
||||
local in_item = nil
|
||||
local si_props = si.properties
|
||||
local target_ep_props = si_target_ep.properties
|
||||
|
||||
if si_props["item.node.direction"] == "output" then
|
||||
-- playback
|
||||
out_item = si
|
||||
in_item = si_target_ep
|
||||
else
|
||||
-- capture
|
||||
out_item = si_target_ep
|
||||
in_item = si
|
||||
end
|
||||
|
||||
Log.info (string.format("link %s <-> %s",
|
||||
tostring(si_props["node.name"]),
|
||||
tostring(target_ep_props["name"])))
|
||||
|
||||
-- create and configure link
|
||||
local si_link = SessionItem ( "si-standard-link" )
|
||||
if not si_link:configure {
|
||||
["out.item"] = out_item,
|
||||
["in.item"] = in_item,
|
||||
["out.item.port.context"] = "output",
|
||||
["in.item.port.context"] = "input",
|
||||
["is.policy.virtual.client.link"] = true,
|
||||
["media.role"] = target_ep_props["role"],
|
||||
["target.media.class"] = target_ep_props["media.class"],
|
||||
["item.plugged.usec"] = si_props["item.plugged.usec"],
|
||||
} then
|
||||
Log.warning (si_link, "failed to configure si-standard-link")
|
||||
return
|
||||
end
|
||||
|
||||
-- register
|
||||
si_link:register()
|
||||
end
|
||||
|
||||
function checkLinkable (si)
|
||||
-- only handle session items that has a node associated proxy
|
||||
local node = si:get_associated_proxy ("node")
|
||||
if not node or not node.properties then
|
||||
return false
|
||||
end
|
||||
|
||||
-- only handle stream session items
|
||||
local media_class = node.properties["media.class"]
|
||||
if not media_class or not string.find (media_class, "Stream") then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Determine if we can handle item by this policy
|
||||
if virtuals_om:get_n_objects () == 0 then
|
||||
Log.debug (si, "item won't be handled by this policy")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function handleLinkable (si)
|
||||
if not checkLinkable (si) then
|
||||
return
|
||||
end
|
||||
|
||||
local node = si:get_associated_proxy ("node")
|
||||
local media_class = node.properties["media.class"] or ""
|
||||
local media_role = node.properties["media.role"] or "Default"
|
||||
Log.info (si, "handling item " .. tostring(node.properties["node.name"]) ..
|
||||
" with role " .. media_role)
|
||||
|
||||
-- find proper target virtual
|
||||
local si_target_ep = findTargetVirtual (node, media_class, media_role)
|
||||
if not si_target_ep then
|
||||
Log.info (si, "... target virtual not found")
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if item is linked to proper target, otherwise re-link
|
||||
for link in links_om:iterate() do
|
||||
local out_id = tonumber(link.properties["out.item.id"])
|
||||
local in_id = tonumber(link.properties["in.item.id"])
|
||||
if out_id == si.id or in_id == si.id then
|
||||
local is_out = out_id == si.id and true or false
|
||||
for peer_ep in virtuals_om:iterate() do
|
||||
if peer_ep.id == (is_out and in_id or out_id) then
|
||||
|
||||
if peer_ep.id == si_target_ep.id then
|
||||
Log.info (si, "... already linked to proper target virtual")
|
||||
return
|
||||
end
|
||||
|
||||
-- remove old link if active, otherwise schedule rescan
|
||||
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
|
||||
link:remove ()
|
||||
Log.info (si, "... moving to new target")
|
||||
else
|
||||
scheduleRescan ()
|
||||
Log.info (si, "... scheduled rescan")
|
||||
return
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create new link
|
||||
createLink (si, si_target_ep)
|
||||
end
|
||||
|
||||
function unhandleLinkable (si)
|
||||
if not checkLinkable (si) then
|
||||
return
|
||||
end
|
||||
|
||||
local node = si:get_associated_proxy ("node")
|
||||
Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
|
||||
|
||||
-- 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
|
||||
silink:remove ()
|
||||
Log.info (silink, "... link removed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
||||
Constraint {
|
||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
}
|
||||
}
|
||||
linkables_om = ObjectManager { Interest { type = "SiLinkable",
|
||||
-- only handle si-audio-adapter and si-node
|
||||
Constraint {
|
||||
"item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
|
||||
Constraint {
|
||||
"active-features", "!", 0, type = "gobject" },
|
||||
}
|
||||
}
|
||||
links_om = ObjectManager { Interest { type = "SiLink",
|
||||
-- only handle links created by this policy
|
||||
Constraint { "is.policy.virtual.client.link", "=", true, type = "pw-global" },
|
||||
} }
|
||||
|
||||
linkables_om:connect("objects-changed", function (om)
|
||||
scheduleRescan ()
|
||||
end)
|
||||
|
||||
linkables_om:connect("object-removed", function (om, si)
|
||||
unhandleLinkable (si)
|
||||
end)
|
||||
|
||||
virtuals_om:activate()
|
||||
linkables_om:activate()
|
||||
links_om:activate()
|
@@ -1,238 +0,0 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
local defaults = {}
|
||||
defaults.follow = true
|
||||
|
||||
local config = {}
|
||||
config.follow = Conf.get_value_boolean ("wireplumber.settings",
|
||||
"policy.default.follow", defaults.follow)
|
||||
|
||||
local self = {}
|
||||
self.scanning = false
|
||||
self.pending_rescan = false
|
||||
|
||||
function rescan ()
|
||||
-- check virtuals and register new links
|
||||
for si_v in virtuals_om:iterate() do
|
||||
handleVirtual (si_v)
|
||||
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 findTargetByDefaultNode (target_media_class)
|
||||
local def_id = default_nodes:call("get-default-node", target_media_class)
|
||||
if def_id ~= Id.INVALID then
|
||||
for si_target in linkables_om:iterate() do
|
||||
local target_node = si_target:get_associated_proxy ("node")
|
||||
if target_node["bound-id"] == def_id then
|
||||
return si_target
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function findTargetByFirstAvailable (target_media_class)
|
||||
for si_target in linkables_om:iterate() do
|
||||
local target_node = si_target:get_associated_proxy ("node")
|
||||
if target_node.properties["media.class"] == target_media_class then
|
||||
return si_target
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function findUndefinedTarget (si_ep)
|
||||
local media_class = si_ep.properties["media.class"]
|
||||
local target_class_assoc = {
|
||||
["Audio/Source"] = "Audio/Source",
|
||||
["Audio/Sink"] = "Audio/Sink",
|
||||
["Video/Source"] = "Video/Source",
|
||||
}
|
||||
local target_media_class = target_class_assoc[media_class]
|
||||
if not target_media_class then
|
||||
return nil
|
||||
end
|
||||
|
||||
local si_target = findTargetByDefaultNode (target_media_class)
|
||||
if not si_target then
|
||||
si_target = findTargetByFirstAvailable (target_media_class)
|
||||
end
|
||||
return si_target
|
||||
end
|
||||
|
||||
function createLink (si_ep, si_target)
|
||||
local out_item = nil
|
||||
local in_item = nil
|
||||
local ep_props = si_ep.properties
|
||||
local target_props = si_target.properties
|
||||
|
||||
if target_props["item.node.direction"] == "input" then
|
||||
-- playback
|
||||
out_item = si_ep
|
||||
in_item = si_target
|
||||
else
|
||||
-- capture
|
||||
in_item = si_ep
|
||||
out_item = si_target
|
||||
end
|
||||
|
||||
Log.info (string.format("link %s <-> %s",
|
||||
ep_props["name"],
|
||||
target_props["node.name"]))
|
||||
|
||||
-- create and configure link
|
||||
local si_link = SessionItem ( "si-standard-link" )
|
||||
if not si_link:configure {
|
||||
["out.item"] = out_item,
|
||||
["in.item"] = in_item,
|
||||
["out.item.port.context"] = "output",
|
||||
["in.item.port.context"] = "input",
|
||||
["passive"] = true,
|
||||
["is.policy.virtual.device.link"] = true,
|
||||
} then
|
||||
Log.warning (si_link, "failed to configure si-standard-link")
|
||||
return
|
||||
end
|
||||
|
||||
-- register
|
||||
si_link:register ()
|
||||
|
||||
-- activate
|
||||
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
||||
if e then
|
||||
Log.warning (l, "failed to activate si-standard-link: " .. tostring(e))
|
||||
l:remove ()
|
||||
else
|
||||
Log.info (l, "activated si-standard-link")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function handleVirtual (si_ep)
|
||||
Log.info (si_ep, "handling virtual " .. si_ep.properties["name"])
|
||||
|
||||
-- find proper target item
|
||||
local si_target = findUndefinedTarget (si_ep)
|
||||
if not si_target then
|
||||
Log.info (si_ep, "... target item not found")
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if item is linked to proper target, otherwise re-link
|
||||
for link in links_om:iterate() do
|
||||
local out_id = tonumber(link.properties["out.item.id"])
|
||||
local in_id = tonumber(link.properties["in.item.id"])
|
||||
if out_id == si_ep.id or in_id == si_ep.id then
|
||||
local is_out = out_id == si_ep.id and true or false
|
||||
for peer in linkables_om:iterate() do
|
||||
if peer.id == (is_out and in_id or out_id) then
|
||||
|
||||
if peer.id == si_target.id then
|
||||
Log.info (si_ep, "... already linked to proper target")
|
||||
return
|
||||
end
|
||||
|
||||
-- remove old link if active, otherwise schedule rescan
|
||||
if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
|
||||
link:remove ()
|
||||
Log.info (si_ep, "... moving to new target")
|
||||
else
|
||||
scheduleRescan ()
|
||||
Log.info (si_ep, "... scheduled rescan")
|
||||
return
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create new link
|
||||
createLink (si_ep, si_target)
|
||||
end
|
||||
|
||||
function unhandleLinkable (si)
|
||||
si_props = si.properties
|
||||
|
||||
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
|
||||
silink:remove ()
|
||||
Log.info (silink, "... link removed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
default_nodes = Plugin.find("default-nodes-api")
|
||||
virtuals_om = ObjectManager { Interest { type = "SiLinkable",
|
||||
Constraint {
|
||||
"item.factory.name", "=", "si-audio-virtual", type = "pw-global" },
|
||||
}
|
||||
}
|
||||
linkables_om = ObjectManager {
|
||||
Interest {
|
||||
type = "SiLinkable",
|
||||
-- only handle device si-audio-adapter items
|
||||
Constraint { "item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
|
||||
Constraint { "item.node.type", "=", "device", type = "pw-global" },
|
||||
Constraint { "active-features", "!", 0, type = "gobject" },
|
||||
}
|
||||
}
|
||||
links_om = ObjectManager {
|
||||
Interest {
|
||||
type = "SiLink",
|
||||
-- only handle links created by this policy
|
||||
Constraint { "is.policy.virtual.device.link", "=", true, type = "pw-global" },
|
||||
}
|
||||
}
|
||||
|
||||
-- listen for default node changes if "follow" setting is enabled
|
||||
if config.follow then
|
||||
default_nodes:connect("changed", function (p)
|
||||
scheduleRescan ()
|
||||
end)
|
||||
end
|
||||
|
||||
linkables_om:connect("objects-changed", function (om)
|
||||
scheduleRescan ()
|
||||
end)
|
||||
|
||||
virtuals_om:connect("object-added", function (om)
|
||||
scheduleRescan ()
|
||||
end)
|
||||
|
||||
linkables_om:connect("object-removed", function (om, si)
|
||||
unhandleLinkable (si)
|
||||
end)
|
||||
|
||||
virtuals_om:activate()
|
||||
linkables_om:activate()
|
||||
links_om:activate()
|
@@ -81,9 +81,9 @@ test_si_audio_virtual_configure_activate (TestFixture * f,
|
||||
str = wp_properties_get (props, "name");
|
||||
g_assert_nonnull (str);
|
||||
g_assert_cmpstr ("virtual", ==, str);
|
||||
str = wp_properties_get (props, "direction");
|
||||
str = wp_properties_get (props, "item.node.direction");
|
||||
g_assert_nonnull (str);
|
||||
g_assert_cmpstr ("1", ==, str);
|
||||
g_assert_cmpstr ("output", ==, str);
|
||||
str = wp_properties_get (props, "item.factory.name");
|
||||
g_assert_nonnull (str);
|
||||
g_assert_cmpstr ("si-audio-virtual", ==, str);
|
||||
|
Reference in New Issue
Block a user