remove "virtual items" scripts, m-si-audio-virtual and related tests

This commit is contained in:
Ashok Sidipotu 2024-06-12 10:08:26 +05:30 committed by George Kiagiadakis
parent f6b77c7456
commit 5948539551
15 changed files with 7 additions and 1143 deletions

View file

@ -68,16 +68,6 @@ shared_library(
dependencies : [wp_dep, pipewire_dep],
)
shared_library(
'wireplumber-module-si-audio-virtual',
[
'module-si-audio-virtual.c',
],
install : true,
install_dir : wireplumber_module_dir,
dependencies : [wp_dep, pipewire_dep],
)
shared_library(
'wireplumber-module-si-node',
[

View file

@ -1,368 +0,0 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/param/format.h>
#include <spa/param/audio/raw.h>
#include <spa/param/param.h>
WP_DEFINE_LOCAL_LOG_TOPIC ("m-si-audio-virtual")
#define SI_FACTORY_NAME "si-audio-virtual"
struct _WpSiAudioVirtual
{
WpSessionItem parent;
/* configuration */
gchar name[96];
gchar media_class[32];
WpDirection direction;
gchar role[32];
guint priority;
gboolean disable_dsp;
/* activation */
WpNode *node;
WpSiAdapter *adapter;
};
static void si_audio_virtual_linkable_init (WpSiLinkableInterface * iface);
static void si_audio_virtual_adapter_init (WpSiAdapterInterface * iface);
G_DECLARE_FINAL_TYPE(WpSiAudioVirtual, si_audio_virtual, WP,
SI_AUDIO_VIRTUAL, WpSessionItem)
G_DEFINE_TYPE_WITH_CODE (WpSiAudioVirtual, si_audio_virtual,
WP_TYPE_SESSION_ITEM,
G_IMPLEMENT_INTERFACE (WP_TYPE_SI_LINKABLE,
si_audio_virtual_linkable_init)
G_IMPLEMENT_INTERFACE (WP_TYPE_SI_ADAPTER, si_audio_virtual_adapter_init))
static void
si_audio_virtual_init (WpSiAudioVirtual * self)
{
}
static void
si_audio_virtual_reset (WpSessionItem * item)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
/* deactivate first */
wp_object_deactivate (WP_OBJECT (self),
WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);
/* reset */
self->name[0] = '\0';
self->media_class[0] = '\0';
self->direction = WP_DIRECTION_INPUT;
self->role[0] = '\0';
self->priority = 0;
self->disable_dsp = FALSE;
WP_SESSION_ITEM_CLASS (si_audio_virtual_parent_class)->reset (item);
}
static gboolean
si_audio_virtual_configure (WpSessionItem * item, WpProperties *p)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
g_autoptr (WpProperties) si_props = wp_properties_ensure_unique_owner (p);
const gchar *str;
/* reset previous config */
si_audio_virtual_reset (item);
str = wp_properties_get (si_props, "name");
if (!str)
return FALSE;
strncpy (self->name, str, sizeof (self->name) - 1);
str = wp_properties_get (si_props, "media.class");
if (!str)
return FALSE;
strncpy (self->media_class, str, sizeof (self->media_class) - 1);
if (strstr (self->media_class, "Source") ||
strstr (self->media_class, "Output"))
self->direction = WP_DIRECTION_OUTPUT;
wp_properties_set (si_props, "item.node.direction",
self->direction == WP_DIRECTION_OUTPUT ? "output" : "input");
str = wp_properties_get (si_props, "role");
if (str) {
strncpy (self->role, str, sizeof (self->role) - 1);
} else {
strncpy (self->role, "Unknown", sizeof (self->role) - 1);
wp_properties_set (si_props, "role", self->role);
}
str = wp_properties_get (si_props, "priority");
if (str && sscanf(str, "%u", &self->priority) != 1)
return FALSE;
if (!str)
wp_properties_setf (si_props, "priority", "%u", self->priority);
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));
return TRUE;
}
static gpointer
si_audio_virtual_get_associated_proxy (WpSessionItem * item, GType proxy_type)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
return wp_session_item_get_associated_proxy (
WP_SESSION_ITEM (self->adapter), proxy_type);
}
static void
si_audio_virtual_disable_active (WpSessionItem *si)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);
g_clear_object (&self->adapter);
g_clear_object (&self->node);
wp_object_update_features (WP_OBJECT (self), 0,
WP_SESSION_ITEM_FEATURE_ACTIVE);
}
static void
si_audio_virtual_disable_exported (WpSessionItem *si)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);
wp_object_update_features (WP_OBJECT (self), 0,
WP_SESSION_ITEM_FEATURE_EXPORTED);
}
static void
on_adapter_activate_done (WpObject * adapter, GAsyncResult * res,
WpTransition * transition)
{
WpSiAudioVirtual *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
if (!wp_object_activate_finish (adapter, res, &error)) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
wp_object_update_features (WP_OBJECT (self),
WP_SESSION_ITEM_FEATURE_ACTIVE, 0);
}
static void
on_adapter_port_state_changed (WpSiAdapter *item,
WpSiAdapterPortsState old_state, WpSiAdapterPortsState new_state,
WpSiAudioVirtual *self)
{
g_signal_emit_by_name (self, "adapter-ports-state-changed", old_state,
new_state);
}
static void
on_node_activate_done (WpObject * node, GAsyncResult * res,
WpTransition * transition)
{
WpSiAudioVirtual *self = wp_transition_get_source_object (transition);
g_autoptr (GError) error = NULL;
g_autoptr (WpCore) core = NULL;
g_autoptr (WpProperties) props = NULL;
if (!wp_object_activate_finish (node, res, &error)) {
wp_transition_return_error (transition, g_steal_pointer (&error));
return;
}
/* create adapter */
core = wp_object_get_core (WP_OBJECT (self));
self->adapter = WP_SI_ADAPTER (wp_session_item_make (core,
"si-audio-adapter"));
if (!self->adapter) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"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);
/* configure adapter */
props = wp_properties_new_empty ();
wp_properties_setf (props, "item.node", "%p", node);
wp_properties_set (props, "name", self->name);
wp_properties_set (props, "media.class", "Audio/Sink");
wp_properties_set (props, "item.features.no-format", "true");
wp_properties_set (props, "item.features.monitor", "true");
if (self->disable_dsp)
wp_properties_set (props, "item.features.no-dsp", "true");
if (!wp_session_item_configure (WP_SESSION_ITEM (self->adapter),
g_steal_pointer (&props))) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"si-audio-virtual: could not configure si-audio-adapter"));
}
/* activate adapter */
wp_object_activate (WP_OBJECT (self->adapter), WP_SESSION_ITEM_FEATURE_ACTIVE,
NULL, (GAsyncReadyCallback) on_adapter_activate_done, transition);
}
static void
si_audio_virtual_enable_active (WpSessionItem *si, WpTransition *transition)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
g_autofree gchar *name = g_strdup_printf ("control.%s", self->name);
g_autofree gchar *desc = g_strdup_printf ("%s %s Virtual", self->role,
(self->direction == WP_DIRECTION_OUTPUT) ? "Capture" : "Playback");
g_autofree gchar *media = g_strdup_printf ("Audio/%s",
(self->direction == WP_DIRECTION_OUTPUT) ? "Source" : "Sink");
const gchar *passive =
(self->direction == WP_DIRECTION_OUTPUT) ? "in" : "out";
if (!wp_session_item_is_configured (si)) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"si-audio-virtual: item is not configured"));
return;
}
/* create the node */
self->node = wp_node_new_from_factory (core, "adapter",
wp_properties_new (
PW_KEY_NODE_NAME, name,
PW_KEY_MEDIA_CLASS, media,
PW_KEY_FACTORY_NAME, "support.null-audio-sink",
PW_KEY_NODE_DESCRIPTION, desc,
PW_KEY_NODE_AUTOCONNECT, "true",
PW_KEY_NODE_PASSIVE, passive,
"monitor.channel-volumes", "true",
"wireplumber.is-virtual", "true",
NULL));
if (!self->node) {
wp_transition_return_error (transition,
g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
"si-audio-virtual: could not create null-audio-sink node"));
return;
}
/* activate node */
wp_object_activate (WP_OBJECT (self->node),
WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL | WP_NODE_FEATURE_PORTS, NULL,
(GAsyncReadyCallback) on_node_activate_done, transition);
}
static void
si_audio_virtual_enable_exported (WpSessionItem *si, WpTransition *transition)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (si);
wp_object_update_features (WP_OBJECT (self),
WP_SESSION_ITEM_FEATURE_EXPORTED, 0);
}
static void
si_audio_virtual_class_init (WpSiAudioVirtualClass * klass)
{
WpSessionItemClass *si_class = (WpSessionItemClass *) klass;
si_class->reset = si_audio_virtual_reset;
si_class->configure = si_audio_virtual_configure;
si_class->get_associated_proxy = si_audio_virtual_get_associated_proxy;
si_class->disable_active = si_audio_virtual_disable_active;
si_class->disable_exported = si_audio_virtual_disable_exported;
si_class->enable_active = si_audio_virtual_enable_active;
si_class->enable_exported = si_audio_virtual_enable_exported;
}
static GVariant *
si_audio_virtual_get_ports (WpSiLinkable * item, const gchar * context)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
return wp_si_linkable_get_ports (WP_SI_LINKABLE (self->adapter), context);
}
static void
si_audio_virtual_linkable_init (WpSiLinkableInterface * iface)
{
iface->get_ports = si_audio_virtual_get_ports;
}
static WpSiAdapterPortsState
si_audio_virtual_get_ports_state (WpSiAdapter * item)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
return wp_si_adapter_get_ports_state (self->adapter);
}
static WpSpaPod *
si_audio_virtual_get_ports_format (WpSiAdapter * item, const gchar **mode)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
return wp_si_adapter_get_ports_format (self->adapter, mode);
}
static void
si_audio_virtual_set_ports_format (WpSiAdapter * item, WpSpaPod *f,
const gchar *mode, GAsyncReadyCallback callback, gpointer data)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
wp_si_adapter_set_ports_format (self->adapter, f, mode, callback, data);
}
static gboolean
si_audio_virtual_set_ports_format_finish (WpSiAdapter * item,
GAsyncResult * res, GError ** error)
{
WpSiAudioVirtual *self = WP_SI_AUDIO_VIRTUAL (item);
return wp_si_adapter_set_ports_format_finish (self->adapter, res, error);
}
static void
si_audio_virtual_adapter_init (WpSiAdapterInterface * iface)
{
iface->get_ports_state = si_audio_virtual_get_ports_state;
iface->get_ports_format = si_audio_virtual_get_ports_format;
iface->set_ports_format = si_audio_virtual_set_ports_format;
iface->set_ports_format_finish = si_audio_virtual_set_ports_format_finish;
}
WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
return G_OBJECT (wp_si_factory_new_simple (SI_FACTORY_NAME,
si_audio_virtual_get_type ()));
}

View file

@ -544,13 +544,6 @@ configure_and_link_adapters (WpSiStandardLink *self, WpTransition *transition)
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.node.type");
in->is_device = !g_strcmp0 (str, "device");
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "item.factory.name");
out->is_device = (str && !g_strcmp0 (str, "si-audio-virtual") && !in->is_device)
|| out->is_device;
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "item.factory.name");
in->is_device = (str && !g_strcmp0 (str, "si-audio-virtual") && !out->is_device)
|| in->is_device;
str = wp_session_item_get_property (WP_SESSION_ITEM (out->si), "stream.dont-remix");
out->dont_remix = str && pw_properties_parse_bool (str);
str = wp_session_item_get_property (WP_SESSION_ITEM (in->si), "stream.dont-remix");

View file

@ -232,10 +232,6 @@ wireplumber.components = [
name = libwireplumber-module-si-standard-link, type = module
provides = si.standard-link
}
{
name = libwireplumber-module-si-audio-virtual, type = module
provides = si.audio-virtual
}
## API to access default nodes from scripts
{
@ -546,11 +542,6 @@ wireplumber.components = [
name = node/filter-forward-format.lua, type = script/lua
provides = hooks.filter.forward-format
}
{
name = node/create-virtual-item.lua, type = script/lua
provides = script.create-role-items
requires = [ si.audio-virtual ]
}
{
type = virtual, provides = policy.node
requires = [ hooks.node.create-session-item ]
@ -642,7 +633,6 @@ wireplumber.components = [
{
type = virtual, provides = policy.role-priority-system
requires = [ policy.standard,
script.create-role-items,
policy.linking.role-priority-system ]
}
## Load targets

View file

@ -1,95 +0,0 @@
## The WirePlumber virtual item configuration
virtual-items = {
## The list of virtual items to create
# virtual-item.capture = {
# media.class = "Audio/Source"
# role = "Capture"
# }
# virtual-item.multimedia = {
# media.class = "Audio/Sink"
# role = "Multimedia"
# }
# virtual-item.speech_low = {
# media.class = "Audio/Sink"
# role = "Speech-Low"
# }
# virtual-item.custom_low = {
# media.class = "Audio/Sink"
# role = "Custom-Low"
# }
# virtual-item.navigation = {
# media.class = "Audio/Sink"
# role = "Navigation"
# }
# virtual-item.speech_high = {
# media.class = "Audio/Sink"
# role = "Speech-High"
# }
# virtual-item.custom_high = {
# media.class = "Audio/Sink"
# role = "Custom-High"
# }
# virtual-item.communication = {
# media.class = "Audio/Sink"
# role = "Communication"
# }
# virtual-item.emergency = {
# media.class = "Audio/Sink"
# role = "Emergency"
# }
}
virtual-item-roles = {
## The list of virtual item roles to use
# Capture = {
# alias = [ "Multimedia", "Music", "Voice", "Capture" ]
# priority = 25
# action.default = "cork"
# action.capture = "mix"
# media.class = "Audio/Source"
# }
# Multimedia = {
# alias = [ "Movie" "Music" "Game" ]
# priority = 25
# action.default = "cork"
# }
# Speech-Low = {
# priority = 30
# action.default = "cork"
# action.Speech-Low = "mix"
# }
# Custom-Low = {
# priority = 35
# action.default = "cork"
# action.Custom-Low = "mix"
# }
# Navigation = {
# priority = 50
# action.default = "duck"
# action.Navigation = "mix"
# }
# Speech-High = {
# priority = 60
# action.default = "cork"
# action.Speech-High = "mix"
# }
# Custom-High = {
# priority = 65
# action.default = "cork"
# action.Custom-High = "mix"
# }
# Communication = {
# priority = 75
# action.default = "cork"
# action.Communication = "mix"
# }
# Emergency = {
# alias = [ "Alert" ]
# priority = 99
# action.default = "cork"
# action.Emergency = "mix"
# }
}

View file

@ -37,11 +37,6 @@ 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

View file

@ -249,19 +249,11 @@ function lutils.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
-- 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
-- check link group

View file

@ -1,121 +0,0 @@
-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT
--
-- Select the virtual target based on roles
lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")
config = {}
config.roles = Conf.get_section_as_object ("virtual-item-roles")
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 =
lutils:unwrap_select_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 %d: %s (%s)", si.id,
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 =
lutils.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 ()

View file

@ -124,8 +124,8 @@ AsyncEventHook {
log:debug (si_link, "registered link between "
.. tostring (si) .. " and " .. tostring (target))
-- only activate non virtual links because virtual links activation is
-- handled by rescan-virtual-links.lua
-- only activate non media role links because their activation is
-- handled by rescan-media-role-links.lua
if not is_media_role_link then
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
if e then

View file

@ -1,251 +0,0 @@
-- WirePlumber
--
-- Copyright © 2023 Collabora Ltd.
-- @author Julian Bouzas <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")
defaults = {}
defaults.duck_level = 0.3
config = {}
config.duck_level = defaults.duck_level -- FIXME
config.roles = Conf.get_section_as_object ("virtual-item-roles")
-- 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 = lutils: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.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 ()

View file

@ -43,11 +43,6 @@ end
function checkLinkable (si, om, handle_nonstreams)
local si_props = si.properties
-- Always handle si-audio-virtual session items
if si_props ["item.factory.name"] == "si-audio-virtual" then
return true, si_props
end
-- 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

View file

@ -1,42 +0,0 @@
-- WirePlumber
--
-- Copyright © 2021 Collabora Ltd.
-- @author Julian Bouzas <julian.bouzas@collabora.com>
--
-- SPDX-License-Identifier: MIT
-- Receive script arguments from config.lua
-- creates the virtual items defined in the JSON(virtual.conf)
log = Log.open_topic ("s-node")
config = {}
config.virtual_items = Conf.get_section_as_object ("virtual-items")
function createVirtualItem (factory_name, properties)
-- create virtual item
local si_v = SessionItem ( factory_name )
if not si_v then
log:warning (si_v, "could not create virtual item of type " .. factory_name)
return
end
-- configure virtual item
if not si_v:configure(properties) then
log:warning(si_v, "failed to configure virtual item " .. properties.name)
return
end
-- activate and register virtual item
si_v:activate (Features.ALL, function (item)
item:register ()
log:info(item, "registered virtual item " .. properties.name)
end)
end
for name, properties in pairs(config.virtual_items) do
properties["name"] = name
createVirtualItem ("si-audio-virtual", properties)
end

View file

@ -40,13 +40,6 @@ test(
env: common_env,
)
test(
'test-si-audio-virtual',
executable('test-si-audio-virtual', 'si-audio-virtual.c',
dependencies: common_deps),
env: common_env,
)
test(
'test-si-standard-link',
executable('test-si-standard-link', 'si-standard-link.c',

View file

@ -1,206 +0,0 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "../common/base-test-fixture.h"
typedef struct {
WpBaseTestFixture base;
} TestFixture;
static void
on_plugin_loaded (WpCore * core, GAsyncResult * res, TestFixture *f)
{
gboolean loaded;
GError *error = NULL;
loaded = wp_core_load_component_finish (core, res, &error);
g_assert_no_error (error);
g_assert_true (loaded);
g_main_loop_quit (f->base.loop);
}
static void
test_si_audio_virtual_setup (TestFixture * f, gconstpointer user_data)
{
wp_base_test_fixture_setup (&f->base, 0);
/* load modules */
{
g_autoptr (WpTestServerLocker) lock =
wp_test_server_locker_new (&f->base.server);
g_assert_nonnull (pw_context_load_module (f->base.server.context,
"libpipewire-module-spa-node-factory", NULL, NULL));
g_assert_nonnull (pw_context_load_module (f->base.server.context,
"libpipewire-module-adapter", NULL, NULL));
}
{
wp_core_load_component (f->base.core,
"libwireplumber-module-si-audio-adapter", "module", NULL, NULL, NULL,
(GAsyncReadyCallback) on_plugin_loaded, f);
g_main_loop_run (f->base.loop);
}
{
wp_core_load_component (f->base.core,
"libwireplumber-module-si-audio-virtual", "module", NULL, NULL, NULL,
(GAsyncReadyCallback) on_plugin_loaded, f);
g_main_loop_run (f->base.loop);
}
}
static void
test_si_audio_virtual_teardown (TestFixture * f, gconstpointer user_data)
{
wp_base_test_fixture_teardown (&f->base);
}
static void
test_si_audio_virtual_configure_activate (TestFixture * f,
gconstpointer user_data)
{
g_autoptr (WpSessionItem) item = NULL;
/* skip the test if null-audio-sink factory is not installed */
if (!test_is_spa_lib_installed (&f->base, "support.null-audio-sink")) {
g_test_skip ("The pipewire null-audio-sink factory was not found");
return;
}
/* create item */
item = wp_session_item_make (f->base.core, "si-audio-virtual");
g_assert_nonnull (item);
/* configure item */
{
WpProperties *props = wp_properties_new_empty ();
wp_properties_set (props, "name", "virtual");
wp_properties_set (props, "media.class", "Audio/Source");
g_assert_true (wp_session_item_configure (item, props));
g_assert_true (wp_session_item_is_configured (item));
}
{
const gchar *str = NULL;
g_autoptr (WpProperties) props = wp_session_item_get_properties (item);
g_assert_nonnull (props);
str = wp_properties_get (props, "name");
g_assert_nonnull (str);
g_assert_cmpstr ("virtual", ==, str);
str = wp_properties_get (props, "item.node.direction");
g_assert_nonnull (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);
}
/* activate item */
wp_object_activate (WP_OBJECT (item), WP_SESSION_ITEM_FEATURE_ACTIVE,
NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
g_main_loop_run (f->base.loop);
g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==,
WP_SESSION_ITEM_FEATURE_ACTIVE);
/* reset */
wp_session_item_reset (item);
g_assert_false (wp_session_item_is_configured (item));
}
static void
test_si_audio_virtual_export (TestFixture * f, gconstpointer user_data)
{
g_autoptr (WpSessionItem) item = NULL;
g_autoptr (WpObjectManager) clients_om = NULL;
g_autoptr (WpClient) self_client = NULL;
/* skip the test if null-audio-sink factory is not installed */
if (!test_is_spa_lib_installed (&f->base, "support.null-audio-sink")) {
g_test_skip ("The pipewire null-audio-sink factory was not found");
return;
}
clients_om = wp_object_manager_new ();
wp_object_manager_add_interest (clients_om, WP_TYPE_CLIENT, NULL);
wp_object_manager_request_object_features (clients_om,
WP_TYPE_CLIENT, WP_PROXY_FEATURE_BOUND);
g_signal_connect_swapped (clients_om, "objects-changed",
G_CALLBACK (g_main_loop_quit), f->base.loop);
wp_core_install_object_manager (f->base.core, clients_om);
g_main_loop_run (f->base.loop);
self_client = wp_object_manager_lookup (clients_om, WP_TYPE_CLIENT, NULL);
g_assert_nonnull (self_client);
/* create item */
item = wp_session_item_make (f->base.core, "si-audio-virtual");
g_assert_nonnull (item);
/* configure item */
{
WpProperties *props = wp_properties_new_empty ();
wp_properties_set (props, "name", "virtual");
wp_properties_set (props, "media.class", "Audio/Source");
g_assert_true (wp_session_item_configure (item, props));
g_assert_true (wp_session_item_is_configured (item));
}
/* activate item */
{
wp_object_activate (WP_OBJECT (item),
WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED,
NULL, (GAsyncReadyCallback) test_object_activate_finish_cb, f);
g_main_loop_run (f->base.loop);
g_assert_cmphex (wp_object_get_active_features (WP_OBJECT (item)), ==,
WP_SESSION_ITEM_FEATURE_ACTIVE | WP_SESSION_ITEM_FEATURE_EXPORTED);
}
{
g_autoptr (WpNode) n = NULL;
g_autoptr (WpProperties) props = NULL;
n = wp_session_item_get_associated_proxy (item, WP_TYPE_NODE);
g_assert_nonnull (n);
props = wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (n));
g_assert_nonnull (props);
g_assert_cmpstr (wp_properties_get (props, "media.class"), ==,
"Audio/Source");
}
/* reset */
wp_session_item_reset (item);
g_assert_false (wp_session_item_is_configured (item));
}
gint
main (gint argc, gchar *argv[])
{
g_test_init (&argc, &argv, NULL);
wp_init (WP_INIT_ALL);
/* configure-activate */
g_test_add ("/modules/si-audio-virtual/configure-activate",
TestFixture, NULL,
test_si_audio_virtual_setup,
test_si_audio_virtual_configure_activate,
test_si_audio_virtual_teardown);
/* export */
g_test_add ("/modules/si-audio-virtual/export",
TestFixture, NULL,
test_si_audio_virtual_setup,
test_si_audio_virtual_export,
test_si_audio_virtual_teardown);
return g_test_run ();
}

View file

@ -229,7 +229,6 @@ load_components (ScriptRunnerFixture *f, gconstpointer argv)
load_component (f, "libwireplumber-module-si-audio-adapter", "module");
load_component (f, "libwireplumber-module-si-standard-link", "module");
load_component (f, "libwireplumber-module-si-audio-virtual", "module");
load_component (f, "default-nodes/apply-default-node.lua", "script/lua");
load_component (f, "default-nodes/state-default-nodes.lua", "script/lua");