New wireplumber config
This commit is contained in:
@@ -1,88 +0,0 @@
|
|||||||
# WirePlumber daemon context configuration #
|
|
||||||
|
|
||||||
context.properties = {
|
|
||||||
## Properties to configure the PipeWire context and some modules
|
|
||||||
|
|
||||||
application.name = "WirePlumber Bluetooth"
|
|
||||||
log.level = 2
|
|
||||||
wireplumber.script-engine = lua-scripting
|
|
||||||
wireplumber.export-core = false
|
|
||||||
|
|
||||||
#mem.mlock-all = false
|
|
||||||
#support.dbus = true
|
|
||||||
}
|
|
||||||
|
|
||||||
context.spa-libs = {
|
|
||||||
#<factory-name regex> = <library-name>
|
|
||||||
#
|
|
||||||
# Used to find spa factory names. It maps an spa factory name
|
|
||||||
# regular expression to a library name that should contain
|
|
||||||
# that factory.
|
|
||||||
#
|
|
||||||
api.bluez5.* = bluez5/libspa-bluez5
|
|
||||||
audio.convert.* = audioconvert/libspa-audioconvert
|
|
||||||
support.* = support/libspa-support
|
|
||||||
}
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
#{ name = <module-name>
|
|
||||||
# [ args = { <key> = <value> ... } ]
|
|
||||||
# [ flags = [ [ ifexists ] [ nofail ] ]
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
# PipeWire modules to load.
|
|
||||||
# If ifexists is given, the module is ignored when it is not found.
|
|
||||||
# If nofail is given, module initialization failures are ignored.
|
|
||||||
#
|
|
||||||
|
|
||||||
# Uses RTKit to boost the data thread priority.
|
|
||||||
{ name = libpipewire-module-rt
|
|
||||||
args = {
|
|
||||||
nice.level = -11
|
|
||||||
#rt.prio = 88
|
|
||||||
#rt.time.soft = -1
|
|
||||||
#rt.time.hard = -1
|
|
||||||
}
|
|
||||||
flags = [ ifexists nofail ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# The native communication protocol.
|
|
||||||
{ name = libpipewire-module-protocol-native }
|
|
||||||
|
|
||||||
# Allows creating nodes that run in the context of the
|
|
||||||
# client. Is used by all clients that want to provide
|
|
||||||
# data to PipeWire.
|
|
||||||
{ name = libpipewire-module-client-node }
|
|
||||||
|
|
||||||
# Allows creating devices that run in the context of the
|
|
||||||
# client. Is used by the session manager.
|
|
||||||
{ name = libpipewire-module-client-device }
|
|
||||||
|
|
||||||
# Makes a factory for wrapping nodes in an adapter with a
|
|
||||||
# converter and resampler.
|
|
||||||
{ name = libpipewire-module-adapter }
|
|
||||||
|
|
||||||
# Allows applications to create metadata objects. It creates
|
|
||||||
# a factory for Metadata objects.
|
|
||||||
{ name = libpipewire-module-metadata }
|
|
||||||
|
|
||||||
# Provides factories to make session manager objects.
|
|
||||||
{ name = libpipewire-module-session-manager }
|
|
||||||
|
|
||||||
# Provides factories to make SPA node objects.
|
|
||||||
{ name = libpipewire-module-spa-node-factory }
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
#{ name = <component-name>, type = <component-type> }
|
|
||||||
#
|
|
||||||
# WirePlumber components to load
|
|
||||||
#
|
|
||||||
|
|
||||||
# The lua scripting engine
|
|
||||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
|
||||||
|
|
||||||
# The lua configuration file
|
|
||||||
# Other components are loaded from there
|
|
||||||
{ name = bluetooth.lua, type = config/lua }
|
|
||||||
]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
components = {}
|
|
||||||
|
|
||||||
function load_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_optional_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_pw_module(m)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_script(s, a)
|
|
||||||
if not components[s] then
|
|
||||||
components[s] = { s, type = "script/lua", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_monitor(s, a)
|
|
||||||
load_script("monitors/" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_access(s, a)
|
|
||||||
load_script("access/access-" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
bluez_midi_monitor = {}
|
|
||||||
bluez_midi_monitor.properties = {}
|
|
||||||
bluez_midi_monitor.rules = {}
|
|
||||||
|
|
||||||
function bluez_midi_monitor.enable()
|
|
||||||
if bluez_midi_monitor.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
load_monitor("bluez-midi", {
|
|
||||||
properties = bluez_midi_monitor.properties,
|
|
||||||
rules = bluez_midi_monitor.rules,
|
|
||||||
})
|
|
||||||
|
|
||||||
if bluez_midi_monitor.properties["with-logind"] then
|
|
||||||
load_optional_module("logind")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
bluez_monitor = {}
|
|
||||||
bluez_monitor.properties = {}
|
|
||||||
bluez_monitor.rules = {}
|
|
||||||
|
|
||||||
function bluez_monitor.enable()
|
|
||||||
if bluez_monitor.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
load_monitor("bluez", {
|
|
||||||
properties = bluez_monitor.properties,
|
|
||||||
rules = bluez_monitor.rules,
|
|
||||||
})
|
|
||||||
|
|
||||||
if bluez_monitor.properties["with-logind"] then
|
|
||||||
load_optional_module("logind")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
bluez_monitor.enabled = true
|
|
||||||
|
|
||||||
bluez_monitor.properties = {
|
|
||||||
-- These features do not work on all headsets, so they are enabled
|
|
||||||
-- by default based on the hardware database. They can also be
|
|
||||||
-- forced on/off for all devices by the following options:
|
|
||||||
|
|
||||||
-- ["bluez5.enable-sbc-xq"] = true,
|
|
||||||
-- ["bluez5.enable-msbc"] = true,
|
|
||||||
-- ["bluez5.enable-hw-volume"] = true,
|
|
||||||
|
|
||||||
-- See bluez-hardware.conf for the hardware database.
|
|
||||||
|
|
||||||
-- Enabled headset roles (default: [ hfp_hf hfp_ag ]), this
|
|
||||||
-- property only applies to native backend. Currently some headsets
|
|
||||||
-- (Sony WH-1000XM3) are not working with both hsp_ag and hfp_ag
|
|
||||||
-- enabled, disable either hsp_ag or hfp_ag to work around it.
|
|
||||||
--
|
|
||||||
-- Supported headset roles: hsp_hs (HSP Headset),
|
|
||||||
-- hsp_ag (HSP Audio Gateway),
|
|
||||||
-- hfp_hf (HFP Hands-Free),
|
|
||||||
-- hfp_ag (HFP Audio Gateway)
|
|
||||||
--["bluez5.headset-roles"] = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]",
|
|
||||||
|
|
||||||
-- Enabled A2DP codecs (default: all).
|
|
||||||
--["bluez5.codecs"] = "[ sbc sbc_xq aac ldac aptx aptx_hd aptx_ll aptx_ll_duplex faststream faststream_duplex ]",
|
|
||||||
|
|
||||||
-- HFP/HSP backend (default: native).
|
|
||||||
-- Available values: any, none, hsphfpd, ofono, native
|
|
||||||
--["bluez5.hfphsp-backend"] = "native",
|
|
||||||
|
|
||||||
-- HFP/HSP native backend modem (default: none).
|
|
||||||
-- Available values: none, any or the modem device string as found in
|
|
||||||
-- 'Device' property of org.freedesktop.ModemManager1.Modem interface
|
|
||||||
--["bluez5.hfphsp-backend-native-modem"] = "none",
|
|
||||||
|
|
||||||
-- HFP/HSP hardware offload SCO support (default: false).
|
|
||||||
--["bluez5.hw-offload-sco"] = false,
|
|
||||||
|
|
||||||
-- Properties for the A2DP codec configuration
|
|
||||||
--["bluez5.default.rate"] = 48000,
|
|
||||||
--["bluez5.default.channels"] = 2,
|
|
||||||
|
|
||||||
-- Register dummy AVRCP player, required for AVRCP volume function.
|
|
||||||
-- Disable if you are running mpris-proxy or equivalent.
|
|
||||||
--["bluez5.dummy-avrcp-player"] = true,
|
|
||||||
|
|
||||||
-- Opus Pro Audio mode settings
|
|
||||||
--["bluez5.a2dp.opus.pro.channels"] = 3, -- no. channels
|
|
||||||
--["bluez5.a2dp.opus.pro.coupled-streams"] = 1, -- no. joint stereo pairs, see RFC 7845 Sec. 5.1.1
|
|
||||||
--["bluez5.a2dp.opus.pro.locations"] = "FL,FR,LFE", -- audio locations
|
|
||||||
--["bluez5.a2dp.opus.pro.max-bitrate"] = 600000,
|
|
||||||
--["bluez5.a2dp.opus.pro.frame-dms"] = 50, -- frame duration in 1/10 ms: 25, 50, 100, 200, 400
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.channels"] = 1, -- same settings for the return direction
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.coupled-streams"] = 0,
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.locations"] = "FC",
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.max-bitrate"] = 160000,
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.frame-dms"] = 400,
|
|
||||||
|
|
||||||
-- Enable the logind module, which arbitrates which user will be allowed
|
|
||||||
-- to have bluetooth audio enabled at any given time (particularly useful
|
|
||||||
-- if you are using GDM as a display manager, as the gdm user also launches
|
|
||||||
-- pipewire and wireplumber).
|
|
||||||
-- This requires access to the D-Bus user session; disable if you are running
|
|
||||||
-- a system-wide instance of wireplumber.
|
|
||||||
["with-logind"] = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
bluez_monitor.rules = {
|
|
||||||
-- An array of matches/actions to evaluate.
|
|
||||||
{
|
|
||||||
-- Rules for matching a device or node. It is an array of
|
|
||||||
-- properties that all need to match the regexp. If any of the
|
|
||||||
-- matches work, the actions are executed for the object.
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- This matches all cards.
|
|
||||||
{ "device.name", "matches", "bluez_card.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
-- Apply properties on the matched object.
|
|
||||||
apply_properties = {
|
|
||||||
-- Auto-connect device profiles on start up or when only partial
|
|
||||||
-- profiles have connected. Disabled by default if the property
|
|
||||||
-- is not specified.
|
|
||||||
--["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
|
|
||||||
["bluez5.auto-connect"] = "[ hfp_hf hsp_hs a2dp_sink ]",
|
|
||||||
|
|
||||||
-- Hardware volume control (default: [ hfp_ag hsp_ag a2dp_source ])
|
|
||||||
--["bluez5.hw-volume"] = "[ hfp_hf hsp_hs a2dp_sink hfp_ag hsp_ag a2dp_source ]",
|
|
||||||
|
|
||||||
-- LDAC encoding quality
|
|
||||||
-- Available values: auto (Adaptive Bitrate, default)
|
|
||||||
-- hq (High Quality, 990/909kbps)
|
|
||||||
-- sq (Standard Quality, 660/606kbps)
|
|
||||||
-- mq (Mobile use Quality, 330/303kbps)
|
|
||||||
--["bluez5.a2dp.ldac.quality"] = "auto",
|
|
||||||
|
|
||||||
-- AAC variable bitrate mode
|
|
||||||
-- Available values: 0 (cbr, default), 1-5 (quality level)
|
|
||||||
--["bluez5.a2dp.aac.bitratemode"] = 0,
|
|
||||||
|
|
||||||
-- Profile connected first
|
|
||||||
-- Available values: a2dp-sink (default), headset-head-unit
|
|
||||||
--["device.profile"] = "a2dp-sink",
|
|
||||||
|
|
||||||
-- Opus Pro Audio encoding mode: audio, voip, lowdelay
|
|
||||||
--["bluez5.a2dp.opus.pro.application"] = "audio",
|
|
||||||
--["bluez5.a2dp.opus.pro.bidi.application"] = "audio",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all sources.
|
|
||||||
{ "node.name", "matches", "bluez_input.*" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- Matches all sinks.
|
|
||||||
{ "node.name", "matches", "bluez_output.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
--["node.nick"] = "My Node",
|
|
||||||
--["priority.driver"] = 100,
|
|
||||||
--["priority.session"] = 100,
|
|
||||||
--["node.pause-on-idle"] = false,
|
|
||||||
--["resample.quality"] = 4,
|
|
||||||
--["channelmix.normalize"] = false,
|
|
||||||
--["channelmix.mix-lfe"] = false,
|
|
||||||
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
|
|
||||||
--["monitor.channel-volumes"] = false,
|
|
||||||
|
|
||||||
-- Media source role, "input" or "playback"
|
|
||||||
-- Defaults to "playback", playing stream to speakers
|
|
||||||
-- Set to "input" to use as an input for apps
|
|
||||||
--["bluez5.media-source-role"] = "input",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
-- BLE MIDI is currently disabled by default, because it conflicts with
|
|
||||||
-- the SELinux policy on Fedora 37 and potentially other systems using
|
|
||||||
-- SELinux. For a workaround, see
|
|
||||||
-- https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/spa/plugins/bluez5/README-MIDI.md
|
|
||||||
bluez_midi_monitor.enabled = false
|
|
||||||
|
|
||||||
bluez_midi_monitor.properties = {
|
|
||||||
-- Enable the logind module, which arbitrates which user will be allowed
|
|
||||||
-- to have bluetooth audio enabled at any given time (particularly useful
|
|
||||||
-- if you are using GDM as a display manager, as the gdm user also launches
|
|
||||||
-- pipewire and wireplumber).
|
|
||||||
-- This requires access to the D-Bus user session; disable if you are running
|
|
||||||
-- a system-wide instance of wireplumber.
|
|
||||||
["with-logind"] = true,
|
|
||||||
|
|
||||||
-- List of MIDI server node names. Each node name given will create a new instance
|
|
||||||
-- of a BLE MIDI service. Typical BLE MIDI instruments have on service instance,
|
|
||||||
-- so adding more than one here may confuse some clients. The node property matching
|
|
||||||
-- rules below apply also to these servers.
|
|
||||||
--["servers"] = { "bluez_midi.server" },
|
|
||||||
}
|
|
||||||
|
|
||||||
bluez_midi_monitor.rules = {
|
|
||||||
-- An array of matches/actions to evaluate.
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all nodes.
|
|
||||||
{ "node.name", "matches", "bluez_midi.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
--["node.nick"] = "My Node",
|
|
||||||
--["priority.driver"] = 100,
|
|
||||||
--["priority.session"] = 100,
|
|
||||||
--["node.pause-on-idle"] = false,
|
|
||||||
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
|
|
||||||
--["monitor.channel-volumes"] = false,
|
|
||||||
--["node.latency-offset-msec"] = -10, -- delay (<0) input to reduce jitter
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
bluez_monitor.enable()
|
|
||||||
bluez_midi_monitor.enable()
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
components = {}
|
|
||||||
|
|
||||||
function load_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_optional_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_pw_module(m)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_script(s, a)
|
|
||||||
if not components[s] then
|
|
||||||
components[s] = { s, type = "script/lua", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_monitor(s, a)
|
|
||||||
load_script("monitors/" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_access(s, a)
|
|
||||||
load_script("access/access-" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
# WirePlumber daemon context configuration #
|
|
||||||
|
|
||||||
context.properties = {
|
|
||||||
## Properties to configure the PipeWire context and some modules
|
|
||||||
|
|
||||||
#application.name = WirePlumber
|
|
||||||
log.level = 2
|
|
||||||
wireplumber.script-engine = lua-scripting
|
|
||||||
|
|
||||||
#mem.mlock-all = false
|
|
||||||
#support.dbus = true
|
|
||||||
}
|
|
||||||
|
|
||||||
context.spa-libs = {
|
|
||||||
#<factory-name regex> = <library-name>
|
|
||||||
#
|
|
||||||
# Used to find spa factory names. It maps an spa factory name
|
|
||||||
# regular expression to a library name that should contain
|
|
||||||
# that factory.
|
|
||||||
#
|
|
||||||
api.alsa.* = alsa/libspa-alsa
|
|
||||||
api.v4l2.* = v4l2/libspa-v4l2
|
|
||||||
audio.convert.* = audioconvert/libspa-audioconvert
|
|
||||||
support.* = support/libspa-support
|
|
||||||
}
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
#{ name = <module-name>
|
|
||||||
# [ args = { <key> = <value> ... } ]
|
|
||||||
# [ flags = [ [ ifexists ] [ nofail ] ]
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
# PipeWire modules to load.
|
|
||||||
# If ifexists is given, the module is ignored when it is not found.
|
|
||||||
# If nofail is given, module initialization failures are ignored.
|
|
||||||
#
|
|
||||||
|
|
||||||
# The native communication protocol.
|
|
||||||
{ name = libpipewire-module-protocol-native }
|
|
||||||
|
|
||||||
# Allows creating nodes that run in the context of the
|
|
||||||
# client. Is used by all clients that want to provide
|
|
||||||
# data to PipeWire.
|
|
||||||
{ name = libpipewire-module-client-node }
|
|
||||||
|
|
||||||
# Allows creating devices that run in the context of the
|
|
||||||
# client. Is used by the session manager.
|
|
||||||
{ name = libpipewire-module-client-device }
|
|
||||||
|
|
||||||
# Makes a factory for wrapping nodes in an adapter with a
|
|
||||||
# converter and resampler.
|
|
||||||
{ name = libpipewire-module-adapter }
|
|
||||||
|
|
||||||
# Allows applications to create metadata objects. It creates
|
|
||||||
# a factory for Metadata objects.
|
|
||||||
{ name = libpipewire-module-metadata }
|
|
||||||
|
|
||||||
# Provides factories to make session manager objects.
|
|
||||||
{ name = libpipewire-module-session-manager }
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
#{ name = <component-name>, type = <component-type> }
|
|
||||||
#
|
|
||||||
# WirePlumber components to load
|
|
||||||
#
|
|
||||||
|
|
||||||
# The lua scripting engine
|
|
||||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
|
||||||
|
|
||||||
# The lua configuration file
|
|
||||||
# Other components are loaded from there
|
|
||||||
{ name = main.lua, type = config/lua }
|
|
||||||
]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
components = {}
|
|
||||||
|
|
||||||
function load_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_optional_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_pw_module(m)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_script(s, a)
|
|
||||||
if not components[s] then
|
|
||||||
components[s] = { s, type = "script/lua", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_monitor(s, a)
|
|
||||||
load_script("monitors/" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_access(s, a)
|
|
||||||
load_script("access/access-" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
default_access = {}
|
|
||||||
default_access.properties = {}
|
|
||||||
default_access.rules = {}
|
|
||||||
|
|
||||||
function default_access.enable()
|
|
||||||
if default_access.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
load_access("default", {
|
|
||||||
rules = default_access.rules
|
|
||||||
})
|
|
||||||
|
|
||||||
if default_access.properties["enable-flatpak-portal"] then
|
|
||||||
-- Enables portal permissions via org.freedesktop.impl.portal.PermissionStore
|
|
||||||
load_module("portal-permissionstore")
|
|
||||||
load_access("portal")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
alsa_monitor = {}
|
|
||||||
alsa_monitor.properties = {}
|
|
||||||
alsa_monitor.rules = {}
|
|
||||||
|
|
||||||
function alsa_monitor.enable()
|
|
||||||
if alsa_monitor.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The "reserve-device" module needs to be loaded for reservation to work
|
|
||||||
if alsa_monitor.properties["alsa.reserve"] then
|
|
||||||
load_module("reserve-device")
|
|
||||||
end
|
|
||||||
|
|
||||||
load_monitor("alsa", {
|
|
||||||
properties = alsa_monitor.properties,
|
|
||||||
rules = alsa_monitor.rules,
|
|
||||||
})
|
|
||||||
|
|
||||||
if alsa_monitor.properties["alsa.midi"] then
|
|
||||||
load_monitor("alsa-midi", {
|
|
||||||
properties = alsa_monitor.properties,
|
|
||||||
})
|
|
||||||
-- The "file-monitor-api" module needs to be loaded for MIDI device monitoring
|
|
||||||
if alsa_monitor.properties["alsa.midi.monitoring"] then
|
|
||||||
load_module("file-monitor-api")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
libcamera_monitor = {}
|
|
||||||
libcamera_monitor.properties = {}
|
|
||||||
libcamera_monitor.rules = {}
|
|
||||||
|
|
||||||
function libcamera_monitor.enable()
|
|
||||||
if libcamera_monitor.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
load_monitor("libcamera", {
|
|
||||||
properties = libcamera_monitor.properties,
|
|
||||||
rules = libcamera_monitor.rules,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
v4l2_monitor = {}
|
|
||||||
v4l2_monitor.properties = {}
|
|
||||||
v4l2_monitor.rules = {}
|
|
||||||
|
|
||||||
function v4l2_monitor.enable()
|
|
||||||
if v4l2_monitor.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
load_monitor("v4l2", {
|
|
||||||
properties = v4l2_monitor.properties,
|
|
||||||
rules = v4l2_monitor.rules,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
device_defaults = {}
|
|
||||||
device_defaults.enabled = true
|
|
||||||
|
|
||||||
device_defaults.properties = {
|
|
||||||
-- store preferences to the file system and restore them at startup;
|
|
||||||
-- when set to false, default nodes and routes are selected based on
|
|
||||||
-- their priorities and any runtime changes do not persist after restart
|
|
||||||
["use-persistent-storage"] = true,
|
|
||||||
|
|
||||||
-- the default volumes to apply to ACP device nodes, in the linear scale
|
|
||||||
--["default-volume"] = 0.064,
|
|
||||||
--["default-input-volume"] = 1.0,
|
|
||||||
|
|
||||||
-- Whether to auto-switch to echo cancel sink and source nodes or not
|
|
||||||
["auto-echo-cancel"] = true,
|
|
||||||
|
|
||||||
-- Sets the default echo-cancel-sink node name to automatically switch to
|
|
||||||
["echo-cancel-sink-name"] = "echo-cancel-sink",
|
|
||||||
|
|
||||||
-- Sets the default echo-cancel-source node name to automatically switch to
|
|
||||||
["echo-cancel-source-name"] = "echo-cancel-source",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Sets persistent device profiles that should never change when wireplumber is
|
|
||||||
-- running, even if a new profile with higher priority becomes available
|
|
||||||
device_defaults.persistent_profiles = {
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all devices
|
|
||||||
{ "device.name", "matches", "*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
profile_names = {
|
|
||||||
"off",
|
|
||||||
"pro-audio"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function device_defaults.enable()
|
|
||||||
if device_defaults.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Selects appropriate default nodes and enables saving and restoring them
|
|
||||||
load_module("default-nodes", device_defaults.properties)
|
|
||||||
|
|
||||||
-- Selects appropriate profile for devices
|
|
||||||
load_script("policy-device-profile.lua", {
|
|
||||||
persistent = device_defaults.persistent_profiles
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Selects appropriate device routes ("ports" in pulseaudio terminology)
|
|
||||||
-- and enables saving and restoring them together with
|
|
||||||
-- their properties (per-route/port volume levels, channel maps, etc)
|
|
||||||
load_script("policy-device-routes.lua", device_defaults.properties)
|
|
||||||
|
|
||||||
if device_defaults.properties["use-persistent-storage"] then
|
|
||||||
-- Enables functionality to save and restore default device profiles
|
|
||||||
load_module("default-profile")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
stream_defaults = {}
|
|
||||||
stream_defaults.enabled = true
|
|
||||||
|
|
||||||
stream_defaults.properties = {
|
|
||||||
-- whether to restore the last stream properties or not
|
|
||||||
["restore-props"] = true,
|
|
||||||
|
|
||||||
-- whether to restore the last stream target or not
|
|
||||||
["restore-target"] = true,
|
|
||||||
|
|
||||||
-- the default channel volume for new streams whose props were never saved
|
|
||||||
-- previously. This is only used if "restore-props" is set to true.
|
|
||||||
["default-channel-volume"] = 1.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_defaults.rules = {
|
|
||||||
-- Rules to override settings per node
|
|
||||||
-- {
|
|
||||||
-- matches = {
|
|
||||||
-- {
|
|
||||||
-- { "application.name", "matches", "pw-play" },
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
-- apply_properties = {
|
|
||||||
-- ["state.restore-props"] = false,
|
|
||||||
-- ["state.restore-target"] = false,
|
|
||||||
-- ["state.default-channel-volume"] = 0.5,
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
}
|
|
||||||
|
|
||||||
function stream_defaults.enable()
|
|
||||||
if stream_defaults.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Save and restore stream-specific properties
|
|
||||||
load_script("restore-stream.lua", {
|
|
||||||
properties = stream_defaults.properties,
|
|
||||||
rules = stream_defaults.rules,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
alsa_monitor.enabled = true
|
|
||||||
|
|
||||||
alsa_monitor.properties = {
|
|
||||||
-- Create a JACK device. This is not enabled by default because
|
|
||||||
-- it requires that the PipeWire JACK replacement libraries are
|
|
||||||
-- not used by the session manager, in order to be able to
|
|
||||||
-- connect to the real JACK server.
|
|
||||||
--["alsa.jack-device"] = false,
|
|
||||||
|
|
||||||
-- Reserve devices via org.freedesktop.ReserveDevice1 on D-Bus
|
|
||||||
-- Disable if you are running a system-wide instance, which
|
|
||||||
-- doesn't have access to the D-Bus user session
|
|
||||||
["alsa.reserve"] = true,
|
|
||||||
--["alsa.reserve.priority"] = -20,
|
|
||||||
--["alsa.reserve.application-name"] = "WirePlumber",
|
|
||||||
|
|
||||||
-- Enables MIDI functionality
|
|
||||||
["alsa.midi"] = true,
|
|
||||||
|
|
||||||
-- Enables monitoring of alsa MIDI devices
|
|
||||||
["alsa.midi.monitoring"] = true,
|
|
||||||
|
|
||||||
-- MIDI bridge node properties
|
|
||||||
["alsa.midi.node-properties"] = {
|
|
||||||
-- Name set for the node with ALSA MIDI ports
|
|
||||||
["node.name"] = "Midi-Bridge",
|
|
||||||
-- Removes longname/number from MIDI port names
|
|
||||||
--["api.alsa.disable-longname"] = true,
|
|
||||||
},
|
|
||||||
|
|
||||||
-- These properties override node defaults when running in a virtual machine.
|
|
||||||
-- The rules below still override those.
|
|
||||||
["vm.node.defaults"] = {
|
|
||||||
["api.alsa.period-size"] = 256,
|
|
||||||
["api.alsa.headroom"] = 8192,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
alsa_monitor.rules = {
|
|
||||||
-- An array of matches/actions to evaluate.
|
|
||||||
--
|
|
||||||
-- If you want to disable some devices or nodes, you can apply properties per device as the following example.
|
|
||||||
-- The name can be found by running pw-cli ls Device, or pw-cli dump Device
|
|
||||||
--{
|
|
||||||
-- matches = {
|
|
||||||
-- {
|
|
||||||
-- { "device.name", "matches", "name_of_some_disabled_card" },
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
-- apply_properties = {
|
|
||||||
-- ["device.disabled"] = true,
|
|
||||||
-- },
|
|
||||||
--}
|
|
||||||
{
|
|
||||||
-- Rules for matching a device or node. It is an array of
|
|
||||||
-- properties that all need to match the regexp. If any of the
|
|
||||||
-- matches work, the actions are executed for the object.
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- This matches all cards.
|
|
||||||
{ "device.name", "matches", "alsa_card.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
-- Apply properties on the matched object.
|
|
||||||
apply_properties = {
|
|
||||||
-- Use ALSA-Card-Profile devices. They use UCM or the profile
|
|
||||||
-- configuration to configure the device and mixer settings.
|
|
||||||
["api.alsa.use-acp"] = true,
|
|
||||||
|
|
||||||
-- Use UCM instead of profile when available. Can be
|
|
||||||
-- disabled to skip trying to use the UCM profile.
|
|
||||||
--["api.alsa.use-ucm"] = true,
|
|
||||||
|
|
||||||
-- Don't use the hardware mixer for volume control. It
|
|
||||||
-- will only use software volume. The mixer is still used
|
|
||||||
-- to mute unused paths based on the selected port.
|
|
||||||
--["api.alsa.soft-mixer"] = false,
|
|
||||||
|
|
||||||
-- Ignore decibel settings of the driver. Can be used to
|
|
||||||
-- work around buggy drivers that report wrong values.
|
|
||||||
--["api.alsa.ignore-dB"] = false,
|
|
||||||
|
|
||||||
-- The profile set to use for the device. Usually this is
|
|
||||||
-- "default.conf" but can be changed with a udev rule or here.
|
|
||||||
--["device.profile-set"] = "profileset-name",
|
|
||||||
|
|
||||||
-- The default active profile. Is by default set to "Off".
|
|
||||||
--["device.profile"] = "default profile name",
|
|
||||||
|
|
||||||
-- Automatically select the best profile. This is the
|
|
||||||
-- highest priority available profile. This is disabled
|
|
||||||
-- here and instead implemented in the session manager
|
|
||||||
-- where it can save and load previous preferences.
|
|
||||||
["api.acp.auto-profile"] = false,
|
|
||||||
|
|
||||||
-- Automatically switch to the highest priority available port.
|
|
||||||
-- This is disabled here and implemented in the session manager instead.
|
|
||||||
["api.acp.auto-port"] = false,
|
|
||||||
|
|
||||||
-- Other properties can be set here.
|
|
||||||
--["device.nick"] = "My Device",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all sources.
|
|
||||||
{ "node.name", "matches", "alsa_input.*" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- Matches all sinks.
|
|
||||||
{ "node.name", "matches", "alsa_output.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
--["node.nick"] = "My Node",
|
|
||||||
--["node.description"] = "My Node Description",
|
|
||||||
--["priority.driver"] = 100,
|
|
||||||
--["priority.session"] = 100,
|
|
||||||
--["node.pause-on-idle"] = false,
|
|
||||||
--["monitor.channel-volumes"] = false
|
|
||||||
--["resample.quality"] = 4,
|
|
||||||
--["resample.disable"] = false,
|
|
||||||
--["channelmix.normalize"] = false,
|
|
||||||
--["channelmix.mix-lfe"] = false,
|
|
||||||
--["channelmix.upmix"] = true,
|
|
||||||
--["channelmix.upmix-method"] = "psd", -- "none" or "simple"
|
|
||||||
--["channelmix.lfe-cutoff"] = 150,
|
|
||||||
--["channelmix.fc-cutoff"] = 12000,
|
|
||||||
--["channelmix.rear-delay"] = 12.0,
|
|
||||||
--["channelmix.stereo-widen"] = 0.0,
|
|
||||||
--["channelmix.hilbert-taps"] = 0,
|
|
||||||
--["channelmix.disable"] = false,
|
|
||||||
--["dither.noise"] = 0,
|
|
||||||
--["dither.method"] = "none", -- "rectangular", "triangular" or "shaped5"
|
|
||||||
--["audio.channels"] = 2,
|
|
||||||
--["audio.format"] = "S16LE",
|
|
||||||
--["audio.rate"] = 44100,
|
|
||||||
--["audio.allowed-rates"] = "32000,96000",
|
|
||||||
--["audio.position"] = "FL,FR",
|
|
||||||
--["api.alsa.period-size"] = 1024,
|
|
||||||
--["api.alsa.period-num"] = 2,
|
|
||||||
--["api.alsa.headroom"] = 0,
|
|
||||||
--["api.alsa.start-delay"] = 0,
|
|
||||||
--["api.alsa.disable-mmap"] = false,
|
|
||||||
--["api.alsa.disable-batch"] = false,
|
|
||||||
--["api.alsa.use-chmap"] = false,
|
|
||||||
--["api.alsa.multirate"] = true,
|
|
||||||
--["latency.internal.rate"] = 0
|
|
||||||
--["latency.internal.ns"] = 0
|
|
||||||
--["clock.name"] = "api.alsa.0"
|
|
||||||
--["session.suspend-timeout-seconds"] = 5, -- 0 disables suspend
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
default_access.enabled = true
|
|
||||||
|
|
||||||
default_access.properties = {
|
|
||||||
-- Enable the use of the flatpak portal integration.
|
|
||||||
-- Disable if you are running a system-wide instance, which
|
|
||||||
-- doesn't have access to the D-Bus user session
|
|
||||||
["enable-flatpak-portal"] = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
default_access.rules = {
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
{ "pipewire.access", "=", "flatpak" },
|
|
||||||
{ "media.category", "=", "Manager" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default_permissions = "all",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
{ "pipewire.access", "=", "flatpak" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default_permissions = "rx",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
{ "pipewire.access", "=", "restricted" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default_permissions = "rx",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
libcamera_monitor.enabled = true
|
|
||||||
|
|
||||||
libcamera_monitor.rules = {
|
|
||||||
-- An array of matches/actions to evaluate.
|
|
||||||
{
|
|
||||||
-- Rules for matching a device or node. It is an array of
|
|
||||||
-- properties that all need to match the regexp. If any of the
|
|
||||||
-- matches work, the actions are executed for the object.
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- This matches all cards.
|
|
||||||
{ "device.name", "matches", "libcamera_device.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
-- Apply properties on the matched object.
|
|
||||||
apply_properties = {
|
|
||||||
-- ["device.nick"] = "My Device",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all sources.
|
|
||||||
{ "node.name", "matches", "libcamera_input.*" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- Matches all sinks.
|
|
||||||
{ "node.name", "matches", "libcamera_output.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
--["node.nick"] = "My Node",
|
|
||||||
--["priority.driver"] = 100,
|
|
||||||
--["priority.session"] = 100,
|
|
||||||
--["node.pause-on-idle"] = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
v4l2_monitor.enabled = true
|
|
||||||
|
|
||||||
v4l2_monitor.rules = {
|
|
||||||
-- An array of matches/actions to evaluate.
|
|
||||||
{
|
|
||||||
-- Rules for matching a device or node. It is an array of
|
|
||||||
-- properties that all need to match the regexp. If any of the
|
|
||||||
-- matches work, the actions are executed for the object.
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- This matches all cards.
|
|
||||||
{ "device.name", "matches", "v4l2_device.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
-- Apply properties on the matched object.
|
|
||||||
apply_properties = {
|
|
||||||
-- ["device.nick"] = "My Device",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matches = {
|
|
||||||
{
|
|
||||||
-- Matches all sources.
|
|
||||||
{ "node.name", "matches", "v4l2_input.*" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- Matches all sinks.
|
|
||||||
{ "node.name", "matches", "v4l2_output.*" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apply_properties = {
|
|
||||||
--["node.nick"] = "My Node",
|
|
||||||
--["priority.driver"] = 100,
|
|
||||||
--["priority.session"] = 100,
|
|
||||||
--["node.pause-on-idle"] = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
-- Provide the "default" pw_metadata, which stores
|
|
||||||
-- dynamic properties of pipewire objects in RAM
|
|
||||||
load_module("metadata")
|
|
||||||
|
|
||||||
-- Default client access policy
|
|
||||||
default_access.enable()
|
|
||||||
|
|
||||||
-- Load devices
|
|
||||||
alsa_monitor.enable()
|
|
||||||
v4l2_monitor.enable()
|
|
||||||
libcamera_monitor.enable()
|
|
||||||
|
|
||||||
-- Track/store/restore user choices about devices
|
|
||||||
device_defaults.enable()
|
|
||||||
|
|
||||||
-- Track/store/restore user choices about streams
|
|
||||||
stream_defaults.enable()
|
|
||||||
|
|
||||||
-- Link nodes by stream role and device intended role
|
|
||||||
load_script("intended-roles.lua")
|
|
||||||
|
|
||||||
-- Automatically suspends idle nodes after 3 seconds
|
|
||||||
load_script("suspend-node.lua")
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# WirePlumber daemon context configuration #
|
|
||||||
|
|
||||||
context.properties = {
|
|
||||||
## Properties to configure the PipeWire context and some modules
|
|
||||||
|
|
||||||
application.name = "WirePlumber Policy"
|
|
||||||
log.level = 2
|
|
||||||
wireplumber.script-engine = lua-scripting
|
|
||||||
wireplumber.export-core = false
|
|
||||||
|
|
||||||
#mem.mlock-all = false
|
|
||||||
#support.dbus = true
|
|
||||||
}
|
|
||||||
|
|
||||||
context.spa-libs = {
|
|
||||||
#<factory-name regex> = <library-name>
|
|
||||||
#
|
|
||||||
# Used to find spa factory names. It maps an spa factory name
|
|
||||||
# regular expression to a library name that should contain
|
|
||||||
# that factory.
|
|
||||||
#
|
|
||||||
audio.convert.* = audioconvert/libspa-audioconvert
|
|
||||||
support.* = support/libspa-support
|
|
||||||
}
|
|
||||||
|
|
||||||
context.modules = [
|
|
||||||
#{ name = <module-name>
|
|
||||||
# [ args = { <key> = <value> ... } ]
|
|
||||||
# [ flags = [ [ ifexists ] [ nofail ] ]
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
# PipeWire modules to load.
|
|
||||||
# If ifexists is given, the module is ignored when it is not found.
|
|
||||||
# If nofail is given, module initialization failures are ignored.
|
|
||||||
#
|
|
||||||
|
|
||||||
# The native communication protocol.
|
|
||||||
{ name = libpipewire-module-protocol-native }
|
|
||||||
|
|
||||||
# Allows creating nodes that run in the context of the
|
|
||||||
# client. Is used by all clients that want to provide
|
|
||||||
# data to PipeWire.
|
|
||||||
{ name = libpipewire-module-client-node }
|
|
||||||
|
|
||||||
# Allows creating devices that run in the context of the
|
|
||||||
# client. Is used by the session manager.
|
|
||||||
{ name = libpipewire-module-client-device }
|
|
||||||
|
|
||||||
# Makes a factory for wrapping nodes in an adapter with a
|
|
||||||
# converter and resampler.
|
|
||||||
{ name = libpipewire-module-adapter }
|
|
||||||
|
|
||||||
# Allows applications to create metadata objects. It creates
|
|
||||||
# a factory for Metadata objects.
|
|
||||||
{ name = libpipewire-module-metadata }
|
|
||||||
|
|
||||||
# Provides factories to make session manager objects.
|
|
||||||
{ name = libpipewire-module-session-manager }
|
|
||||||
]
|
|
||||||
|
|
||||||
wireplumber.components = [
|
|
||||||
#{ name = <component-name>, type = <component-type> }
|
|
||||||
#
|
|
||||||
# WirePlumber components to load
|
|
||||||
#
|
|
||||||
|
|
||||||
# The lua scripting engine
|
|
||||||
{ name = libwireplumber-module-lua-scripting, type = module }
|
|
||||||
|
|
||||||
# The lua configuration file
|
|
||||||
# Other components are loaded from there
|
|
||||||
{ name = policy.lua, type = config/lua }
|
|
||||||
]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
components = {}
|
|
||||||
|
|
||||||
function load_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_optional_module(m, a)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libwireplumber-module-" .. m, type = "module", args = a, optional = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_pw_module(m)
|
|
||||||
assert(type(m) == "string", "module name is mandatory, bail out");
|
|
||||||
if not components[m] then
|
|
||||||
components[m] = { "libpipewire-module-" .. m, type = "pw_module" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_script(s, a)
|
|
||||||
if not components[s] then
|
|
||||||
components[s] = { s, type = "script/lua", args = a }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_monitor(s, a)
|
|
||||||
load_script("monitors/" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
|
|
||||||
function load_access(s, a)
|
|
||||||
load_script("access/access-" .. s .. ".lua", a)
|
|
||||||
end
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
default_policy = {}
|
|
||||||
default_policy.enabled = true
|
|
||||||
default_policy.properties = {}
|
|
||||||
default_policy.endpoints = {}
|
|
||||||
|
|
||||||
default_policy.policy = {
|
|
||||||
["move"] = true, -- moves session items when metadata target.node changes
|
|
||||||
["follow"] = true, -- moves session items to the default device when it has changed
|
|
||||||
|
|
||||||
-- Whether to forward the ports format of filter stream nodes to their
|
|
||||||
-- associated filter device nodes. This is needed for application to stream
|
|
||||||
-- surround audio if echo-cancel is enabled.
|
|
||||||
["filter.forward-format"] = false,
|
|
||||||
|
|
||||||
-- Set to 'true' to disable channel splitting & merging on nodes and enable
|
|
||||||
-- passthrough of audio in the same format as the format of the device.
|
|
||||||
-- Note that this breaks JACK support; it is generally not recommended
|
|
||||||
["audio.no-dsp"] = false,
|
|
||||||
|
|
||||||
-- how much to lower the volume of lower priority streams when ducking
|
|
||||||
-- note that this is a linear volume modifier (not cubic as in pulseaudio)
|
|
||||||
["duck.level"] = 0.3,
|
|
||||||
}
|
|
||||||
|
|
||||||
bluetooth_policy = {}
|
|
||||||
|
|
||||||
bluetooth_policy.policy = {
|
|
||||||
-- Whether to store state on the filesystem.
|
|
||||||
["use-persistent-storage"] = true,
|
|
||||||
|
|
||||||
-- Whether to use headset profile in the presence of an input stream.
|
|
||||||
["media-role.use-headset-profile"] = true,
|
|
||||||
|
|
||||||
-- Application names correspond to application.name in stream properties.
|
|
||||||
-- Applications which do not set media.role but which should be considered
|
|
||||||
-- for role based profile switching can be specified here.
|
|
||||||
["media-role.applications"] = {
|
|
||||||
"Firefox", "Chromium input", "Google Chrome input", "Brave input",
|
|
||||||
"Microsoft Edge input", "Vivaldi input", "ZOOM VoiceEngine",
|
|
||||||
"Telegram Desktop", "telegram-desktop", "linphone", "Mumble",
|
|
||||||
"WEBRTC VoiceEngine", "Skype", "Firefox Developer Edition",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function default_policy.enable()
|
|
||||||
if default_policy.enabled == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Session item factories, building blocks for the session management graph
|
|
||||||
-- Do not disable these unless you really know what you are doing
|
|
||||||
load_module("si-node")
|
|
||||||
load_module("si-audio-adapter")
|
|
||||||
load_module("si-standard-link")
|
|
||||||
load_module("si-audio-endpoint")
|
|
||||||
|
|
||||||
-- API to access default nodes from scripts
|
|
||||||
load_module("default-nodes-api")
|
|
||||||
|
|
||||||
-- API to access mixer controls, needed for volume ducking
|
|
||||||
load_module("mixer-api")
|
|
||||||
|
|
||||||
-- Create endpoints statically at startup
|
|
||||||
load_script("static-endpoints.lua", default_policy.endpoints)
|
|
||||||
|
|
||||||
-- Create items for nodes that appear in the graph
|
|
||||||
load_script("create-item.lua", default_policy.policy)
|
|
||||||
|
|
||||||
-- Link nodes to each other to make media flow in the graph
|
|
||||||
load_script("policy-node.lua", default_policy.policy)
|
|
||||||
|
|
||||||
-- Link client nodes with endpoints to make media flow in the graph
|
|
||||||
load_script("policy-endpoint-client.lua", default_policy.policy)
|
|
||||||
load_script("policy-endpoint-client-links.lua", default_policy.policy)
|
|
||||||
|
|
||||||
-- Link endpoints with device nodes to make media flow in the graph
|
|
||||||
load_script("policy-endpoint-device.lua", default_policy.policy)
|
|
||||||
|
|
||||||
-- Switch bluetooth profile based on media.role
|
|
||||||
load_script("policy-bluetooth.lua", bluetooth_policy.policy)
|
|
||||||
end
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
-- uncomment to enable role-based endpoints
|
|
||||||
-- this is not yet ready for desktop use
|
|
||||||
--
|
|
||||||
--[[
|
|
||||||
|
|
||||||
default_policy.policy.roles = {
|
|
||||||
["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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
default_policy.endpoints = {
|
|
||||||
["endpoint.capture"] = {
|
|
||||||
["media.class"] = "Audio/Source",
|
|
||||||
["role"] = "Capture",
|
|
||||||
},
|
|
||||||
["endpoint.multimedia"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Multimedia",
|
|
||||||
},
|
|
||||||
["endpoint.speech_low"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Speech-Low",
|
|
||||||
},
|
|
||||||
["endpoint.custom_low"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Custom-Low",
|
|
||||||
},
|
|
||||||
["endpoint.navigation"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Navigation",
|
|
||||||
},
|
|
||||||
["endpoint.speech_high"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Speech-High",
|
|
||||||
},
|
|
||||||
["endpoint.custom_high"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Custom-High",
|
|
||||||
},
|
|
||||||
["endpoint.communication"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Communication",
|
|
||||||
},
|
|
||||||
["endpoint.emergency"] = {
|
|
||||||
["media.class"] = "Audio/Sink",
|
|
||||||
["role"] = "Emergency",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]]--
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
default_policy.enable()
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local config = ... or {}
|
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function rulesGetDefaultPermissions(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.default_permissions then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
return r.default_permissions
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
clients_om = ObjectManager {
|
|
||||||
Interest { type = "client" }
|
|
||||||
}
|
|
||||||
|
|
||||||
clients_om:connect("object-added", function (om, client)
|
|
||||||
local id = client["bound-id"]
|
|
||||||
local properties = client["properties"]
|
|
||||||
|
|
||||||
local perms = rulesGetDefaultPermissions(properties)
|
|
||||||
|
|
||||||
if perms then
|
|
||||||
Log.info(client, "Granting permissions to client " .. id .. ": " .. perms)
|
|
||||||
client:update_permissions { ["any"] = perms }
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
clients_om:activate()
|
|
||||||
89
.config/wireplumber/scripts/client/access-default.lua
Normal file
89
.config/wireplumber/scripts/client/access-default.lua
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-client")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("access.rules")
|
||||||
|
|
||||||
|
function getAccess (properties)
|
||||||
|
local access = properties["pipewire.access"]
|
||||||
|
local client_access = properties["pipewire.client.access"]
|
||||||
|
local is_flatpak = properties["pipewire.sec.flatpak"]
|
||||||
|
|
||||||
|
if is_flatpak then
|
||||||
|
client_access = "flatpak"
|
||||||
|
end
|
||||||
|
|
||||||
|
if client_access == nil then
|
||||||
|
return access
|
||||||
|
elseif access == "unrestricted" or access == "default" then
|
||||||
|
if client_access ~= "unrestricted" then
|
||||||
|
return client_access
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return access
|
||||||
|
end
|
||||||
|
|
||||||
|
function getDefaultPermissions (properties)
|
||||||
|
local access = properties["access"]
|
||||||
|
local media_category = properties["media.category"]
|
||||||
|
|
||||||
|
if access == "flatpak" and media_category == "Manager" then
|
||||||
|
return "all", "flatpak-manager"
|
||||||
|
elseif access == "flatpak" or access == "restricted" then
|
||||||
|
return "rx", access
|
||||||
|
elseif access == "default" then
|
||||||
|
return "all", access
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function getPermissions (properties)
|
||||||
|
if config.rules then
|
||||||
|
local mprops, matched = JsonUtils.match_rules_update_properties (
|
||||||
|
config.rules, properties)
|
||||||
|
if (matched > 0 and mprops["default_permissions"]) then
|
||||||
|
return mprops["default_permissions"], mprops["access"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
clients_om = ObjectManager {
|
||||||
|
Interest { type = "client" }
|
||||||
|
}
|
||||||
|
|
||||||
|
clients_om:connect("object-added", function (om, client)
|
||||||
|
local id = client["bound-id"]
|
||||||
|
local properties = client["properties"]
|
||||||
|
local access = getAccess (properties)
|
||||||
|
|
||||||
|
properties["access"] = access
|
||||||
|
|
||||||
|
local perms, effective_access = getPermissions (properties)
|
||||||
|
if perms == nil then
|
||||||
|
perms, effective_access = getDefaultPermissions (properties)
|
||||||
|
end
|
||||||
|
if effective_access == nil then
|
||||||
|
effective_access = access
|
||||||
|
end
|
||||||
|
|
||||||
|
if perms ~= nil then
|
||||||
|
log:info(client, "Granting permissions to client " .. id .. " (access " ..
|
||||||
|
effective_access .. "): " .. perms)
|
||||||
|
client:update_permissions { ["any"] = perms }
|
||||||
|
client:update_properties { ["pipewire.access.effective"] = effective_access }
|
||||||
|
else
|
||||||
|
log:debug(client, "No rule for client " .. id .. " (access " .. access .. ")")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
clients_om:activate()
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
MEDIA_ROLE_NONE = 0
|
MEDIA_ROLE_NONE = 0
|
||||||
MEDIA_ROLE_CAMERA = 1 << 0
|
MEDIA_ROLE_CAMERA = 1 << 0
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-client")
|
||||||
|
|
||||||
function hasPermission (permissions, app_id, lookup)
|
function hasPermission (permissions, app_id, lookup)
|
||||||
if permissions then
|
if permissions then
|
||||||
for key, values in pairs(permissions) do
|
for key, values in pairs(permissions) do
|
||||||
@@ -28,7 +30,7 @@ end
|
|||||||
|
|
||||||
function setPermissions (client, allow_client, allow_nodes)
|
function setPermissions (client, allow_client, allow_nodes)
|
||||||
local client_id = client["bound-id"]
|
local client_id = client["bound-id"]
|
||||||
Log.info(client, "Granting ALL access to client " .. client_id)
|
log:info(client, "Granting ALL access to client " .. client_id)
|
||||||
|
|
||||||
-- Update permissions on client
|
-- Update permissions on client
|
||||||
client:update_permissions { [client_id] = allow_client and "all" or "-" }
|
client:update_permissions { [client_id] = allow_client and "all" or "-" }
|
||||||
@@ -50,18 +52,18 @@ function updateClientPermissions (client, permissions)
|
|||||||
-- Make sure the client is not the portal itself
|
-- Make sure the client is not the portal itself
|
||||||
str_prop = client.properties["pipewire.access.portal.is_portal"]
|
str_prop = client.properties["pipewire.access.portal.is_portal"]
|
||||||
if str_prop == "yes" then
|
if str_prop == "yes" then
|
||||||
Log.info (client, "client is the portal itself")
|
log:info (client, "client is the portal itself")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Make sure the client has a portal app Id
|
-- Make sure the client has a portal app Id
|
||||||
str_prop = client.properties["pipewire.access.portal.app_id"]
|
str_prop = client.properties["pipewire.access.portal.app_id"]
|
||||||
if str_prop == nil then
|
if str_prop == nil then
|
||||||
Log.info (client, "Portal managed client did not set app_id")
|
log:info (client, "Portal managed client did not set app_id")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if str_prop == "" then
|
if str_prop == "" then
|
||||||
Log.info (client, "Ignoring portal check for non-sandboxed client")
|
log:info (client, "Ignoring portal check for non-sandboxed client")
|
||||||
setPermissions (client, true, true)
|
setPermissions (client, true, true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -70,19 +72,19 @@ function updateClientPermissions (client, permissions)
|
|||||||
-- Make sure the client has portal media roles
|
-- Make sure the client has portal media roles
|
||||||
str_prop = client.properties["pipewire.access.portal.media_roles"]
|
str_prop = client.properties["pipewire.access.portal.media_roles"]
|
||||||
if str_prop == nil then
|
if str_prop == nil then
|
||||||
Log.info (client, "Portal managed client did not set media_roles")
|
log:info (client, "Portal managed client did not set media_roles")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
media_roles = parseMediaRoles (str_prop)
|
media_roles = parseMediaRoles (str_prop)
|
||||||
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
|
if (media_roles & MEDIA_ROLE_CAMERA) == 0 then
|
||||||
Log.info (client, "Ignoring portal check for clients without camera role")
|
log:info (client, "Ignoring portal check for clients without camera role")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update permissions
|
-- Update permissions
|
||||||
allowed = hasPermission (permissions, app_id, "yes")
|
allowed = hasPermission (permissions, app_id, "yes")
|
||||||
|
|
||||||
Log.info (client, "setting permissions: " .. tostring(allowed))
|
log:info (client, "setting permissions: " .. tostring(allowed))
|
||||||
setPermissions (client, allowed, allowed)
|
setPermissions (client, allowed, allowed)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -133,7 +135,7 @@ else
|
|||||||
-- Otherwise, just set all permissions to all portal clients
|
-- Otherwise, just set all permissions to all portal clients
|
||||||
clients_om:connect("object-added", function (om, client)
|
clients_om:connect("object-added", function (om, client)
|
||||||
local id = client["bound-id"]
|
local id = client["bound-id"]
|
||||||
Log.info(client, "Granting ALL access to client " .. id)
|
log:info(client, "Granting ALL access to client " .. id)
|
||||||
client:update_permissions { ["any"] = "all" }
|
client:update_permissions { ["any"] = "all" }
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
87
.config/wireplumber/scripts/client/access-snap.lua
Normal file
87
.config/wireplumber/scripts/client/access-snap.lua
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
-- Manage snap audio permissions
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Canonical Ltd.
|
||||||
|
-- @author Sergio Costas Rodriguez <sergio.costas@canonical.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
function removeClientPermissionsForOtherClients (client)
|
||||||
|
-- Remove access to any other clients, but allow all the process of the
|
||||||
|
-- same snap to access their elements
|
||||||
|
local client_id = client.properties["pipewire.snap.id"]
|
||||||
|
for snap_client in clients_snap:iterate() do
|
||||||
|
local snap_client_id = snap_client.properties["pipewire.snap.id"]
|
||||||
|
if snap_client_id ~= client_id then
|
||||||
|
client:update_permissions { [snap_client["bound-id"]] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for no_snap_client in clients_no_snap:iterate() do
|
||||||
|
client:update_permissions { [no_snap_client["bound-id"]] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateClientPermissions (client)
|
||||||
|
-- Remove access to Audio/Sources and Audio/Sinks based on snap permissions
|
||||||
|
for node in nodes_om:iterate() do
|
||||||
|
local node_id = node["bound-id"]
|
||||||
|
local property = "pipewire.snap.audio.playback"
|
||||||
|
|
||||||
|
if node.properties["media.class"] == "Audio/Source" then
|
||||||
|
property = "pipewire.snap.audio.record"
|
||||||
|
end
|
||||||
|
|
||||||
|
if client.properties[property] ~= "true" then
|
||||||
|
client:update_permissions { [node_id] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
clients_snap = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "client",
|
||||||
|
Constraint { "pipewire.snap.id", "+", type = "pw"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients_no_snap = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "client",
|
||||||
|
Constraint { "pipewire.snap.id", "-", type = "pw"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "node",
|
||||||
|
Constraint { "media.class", "matches", "Audio/*"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients_snap:connect("object-added", function (om, client)
|
||||||
|
-- If a new snap client is added, adjust its permissions
|
||||||
|
updateClientPermissions (client)
|
||||||
|
removeClientPermissionsForOtherClients (client)
|
||||||
|
end)
|
||||||
|
|
||||||
|
clients_no_snap:connect("object-added", function (om, client)
|
||||||
|
-- If a new, non-snap client is added,
|
||||||
|
-- remove access to it from other snaps
|
||||||
|
client_id = client["bound-id"]
|
||||||
|
for snap_client in clients_snap:iterate() do
|
||||||
|
if client.properties["pipewire.snap.id"] ~= nil then
|
||||||
|
snap_client:update_permissions { [client_id] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
nodes_om:connect("object-added", function (om, node)
|
||||||
|
-- If a new Audio/Sink or Audio/Source node is added,
|
||||||
|
-- adjust the permissions in the snap clients
|
||||||
|
for client in clients_snap:iterate() do
|
||||||
|
updateClientPermissions (client)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
clients_snap:activate()
|
||||||
|
clients_no_snap:activate()
|
||||||
|
nodes_om:activate()
|
||||||
@@ -1,129 +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 config = ... or {}
|
|
||||||
|
|
||||||
items = {}
|
|
||||||
|
|
||||||
function configProperties(node)
|
|
||||||
local np = node.properties
|
|
||||||
local properties = {
|
|
||||||
["item.node"] = node,
|
|
||||||
["item.plugged.usec"] = GLib.get_monotonic_time(),
|
|
||||||
["item.features.no-dsp"] = config["audio.no-dsp"],
|
|
||||||
["item.features.monitor"] = true,
|
|
||||||
["item.features.control-port"] = false,
|
|
||||||
["node.id"] = node["bound-id"],
|
|
||||||
["client.id"] = np["client.id"],
|
|
||||||
["object.path"] = np["object.path"],
|
|
||||||
["object.serial"] = np["object.serial"],
|
|
||||||
["target.object"] = np["target.object"],
|
|
||||||
["priority.session"] = np["priority.session"],
|
|
||||||
["device.id"] = np["device.id"],
|
|
||||||
["card.profile.device"] = np["card.profile.device"],
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v in pairs(np) do
|
|
||||||
if k:find("^node") or k:find("^stream") or k:find("^media") then
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local media_class = properties["media.class"] or ""
|
|
||||||
|
|
||||||
if not properties["media.type"] then
|
|
||||||
for _, i in ipairs({ "Audio", "Video", "Midi" }) do
|
|
||||||
if media_class:find(i) then
|
|
||||||
properties["media.type"] = i
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
properties["item.node.type"] =
|
|
||||||
media_class:find("^Stream/") and "stream" or "device"
|
|
||||||
|
|
||||||
if media_class:find("Sink") or
|
|
||||||
media_class:find("Input") or
|
|
||||||
media_class:find("Duplex") then
|
|
||||||
properties["item.node.direction"] = "input"
|
|
||||||
elseif media_class:find("Source") or media_class:find("Output") then
|
|
||||||
properties["item.node.direction"] = "output"
|
|
||||||
end
|
|
||||||
return properties
|
|
||||||
end
|
|
||||||
|
|
||||||
function addItem (node, item_type)
|
|
||||||
local id = node["bound-id"]
|
|
||||||
local item
|
|
||||||
|
|
||||||
-- create item
|
|
||||||
item = SessionItem ( item_type )
|
|
||||||
items[id] = item
|
|
||||||
|
|
||||||
-- configure item
|
|
||||||
if not item:configure(configProperties(node)) then
|
|
||||||
Log.warning(item, "failed to configure item for node " .. tostring(id))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
item:register ()
|
|
||||||
|
|
||||||
-- activate item
|
|
||||||
items[id]:activate (Features.ALL, function (item, e)
|
|
||||||
if e then
|
|
||||||
Log.message(item, "failed to activate item: " .. tostring(e));
|
|
||||||
if item then
|
|
||||||
item:remove ()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
Log.info(item, "activated item for node " .. tostring(id))
|
|
||||||
|
|
||||||
-- Trigger object managers to update status
|
|
||||||
item:remove ()
|
|
||||||
if item["active-features"] ~= 0 then
|
|
||||||
item:register ()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
nodes_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
|
||||||
},
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
|
||||||
},
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
|
||||||
Constraint { "wireplumber.is-endpoint", "-", type = "pw" },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes_om:connect("object-added", function (om, node)
|
|
||||||
local media_class = node.properties['media.class']
|
|
||||||
if string.find (media_class, "Audio") then
|
|
||||||
addItem (node, "si-audio-adapter")
|
|
||||||
else
|
|
||||||
addItem (node, "si-node")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
nodes_om:connect("object-removed", function (om, node)
|
|
||||||
local id = node["bound-id"]
|
|
||||||
if items[id] then
|
|
||||||
items[id]:remove ()
|
|
||||||
items[id] = nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
nodes_om:activate()
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-default-nodes")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "default-nodes/apply-default-node",
|
||||||
|
after = { "default-nodes/find-best-default-node",
|
||||||
|
"default-nodes/find-selected-default-node",
|
||||||
|
"default-nodes/find-stored-default-node" },
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-default-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local def_node_type = props ["default-node.type"]
|
||||||
|
local selected_node = event:get_data ("selected-node")
|
||||||
|
|
||||||
|
local om = source:call ("get-object-manager", "metadata")
|
||||||
|
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||||
|
|
||||||
|
if selected_node then
|
||||||
|
local key = "default." .. def_node_type
|
||||||
|
|
||||||
|
log:info ("set default node for " .. key .. " " .. selected_node)
|
||||||
|
|
||||||
|
metadata:set (0, key, "Spa:String:JSON",
|
||||||
|
Json.Object { ["name"] = selected_node }:to_string ())
|
||||||
|
else
|
||||||
|
metadata:set (0, "default." .. def_node_type, nil, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-default-nodes")
|
||||||
|
|
||||||
|
nutils = require ("node-utils")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "default-nodes/find-best-default-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-default-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local available_nodes = event:get_data ("available-nodes")
|
||||||
|
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||||
|
local selected_node = event:get_data ("selected-node")
|
||||||
|
|
||||||
|
available_nodes = available_nodes and available_nodes:parse ()
|
||||||
|
if not available_nodes then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, node_props in ipairs (available_nodes) do
|
||||||
|
-- Highest priority node wins
|
||||||
|
local priority = nutils.get_session_priority (node_props)
|
||||||
|
|
||||||
|
if priority > selected_prio or selected_node == nil then
|
||||||
|
selected_prio = priority
|
||||||
|
selected_node = node_props ["node.name"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event:set_data ("selected-node-priority", selected_prio)
|
||||||
|
event:set_data ("selected-node", selected_node)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- hook to make sure the user prefered device(default.configured.*) in other
|
||||||
|
-- words currently selected device is given higher priority
|
||||||
|
|
||||||
|
-- state-default-nodes.lua also does find out the default node out of the user
|
||||||
|
-- preferences(current and past), however it doesnt give any higher priority to
|
||||||
|
-- the currently selected device.
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-default-nodes")
|
||||||
|
|
||||||
|
nutils = require ("node-utils")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "default-nodes/find-selected-default-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-default-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local available_nodes = event:get_data ("available-nodes")
|
||||||
|
|
||||||
|
available_nodes = available_nodes and available_nodes:parse ()
|
||||||
|
if not available_nodes then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||||
|
local selected_node = event:get_data ("selected-node")
|
||||||
|
|
||||||
|
local source = event:get_source ()
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local def_node_type = props ["default-node.type"]
|
||||||
|
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||||
|
local metadata = metadata_om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||||
|
local obj = metadata:find (0, "default.configured." .. def_node_type)
|
||||||
|
|
||||||
|
if not obj then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local json = Json.Raw (obj)
|
||||||
|
local current_configured_node = json:parse ().name
|
||||||
|
|
||||||
|
for _, node_props in ipairs (available_nodes) do
|
||||||
|
local name = node_props ["node.name"]
|
||||||
|
local priority = nutils.get_session_priority (node_props)
|
||||||
|
|
||||||
|
if current_configured_node == name then
|
||||||
|
priority = 30000 + priority
|
||||||
|
|
||||||
|
if priority > selected_prio then
|
||||||
|
|
||||||
|
selected_prio = priority
|
||||||
|
selected_node = name
|
||||||
|
|
||||||
|
event:set_data ("selected-node-priority", selected_prio)
|
||||||
|
event:set_data ("selected-node", selected_node)
|
||||||
|
end
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
179
.config/wireplumber/scripts/default-nodes/rescan.lua
Normal file
179
.config/wireplumber/scripts/default-nodes/rescan.lua
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- looks for changes in user-preferences and devices added/removed and schedules
|
||||||
|
-- rescan and pushes "select-default-node" event for each of the media_classes
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-default-nodes")
|
||||||
|
|
||||||
|
-- looks for changes in user-preferences and devices added/removed and schedules
|
||||||
|
-- rescan
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "default-nodes/rescan-trigger",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
Constraint { "media.class", "#", "Audio/*" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
Constraint { "media.class", "#", "Video/*" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||||
|
"default.configured.audio.source", "default.configured.video.source"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed"},
|
||||||
|
Constraint { "event.subject.param-id", "c", "Route", "EnumRoute"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
source:call ("schedule-rescan", "default-nodes")
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
-- pushes "select-default-node" event for each of the media_classes
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "default-nodes/rescan",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "rescan-for-default-nodes" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local si_om = source:call ("get-object-manager", "session-item")
|
||||||
|
local devices_om = source:call ("get-object-manager", "device")
|
||||||
|
|
||||||
|
log:trace ("re-evaluating default nodes")
|
||||||
|
|
||||||
|
-- Audio Sink
|
||||||
|
pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.sink", "in", {
|
||||||
|
"Audio/Sink", "Audio/Duplex"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Audio Source
|
||||||
|
pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.source", "out", {
|
||||||
|
"Audio/Source", "Audio/Source/Virtual", "Audio/Duplex", "Audio/Sink"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Video Source
|
||||||
|
pushSelectDefaultNodeEvent (source, si_om, devices_om, "video.source", "out", {
|
||||||
|
"Video/Source", "Video/Source/Virtual"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
function pushSelectDefaultNodeEvent (source, si_om, devices_om, def_node_type,
|
||||||
|
port_direction, media_classes)
|
||||||
|
local nodes =
|
||||||
|
collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
|
||||||
|
local event = source:call ("create-event", "select-default-node", nil, {
|
||||||
|
["default-node.type"] = def_node_type,
|
||||||
|
})
|
||||||
|
event:set_data ("available-nodes", Json.Array (nodes))
|
||||||
|
EventDispatcher.push_event (event)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return an array table where each element is another table containing all the
|
||||||
|
-- node properties of all the nodes that can be selected for a given media class
|
||||||
|
-- set and direction
|
||||||
|
function collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
|
||||||
|
local collected = {}
|
||||||
|
|
||||||
|
for linkable in si_om:iterate {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "media.class", "c", table.unpack (media_classes) },
|
||||||
|
} do
|
||||||
|
local linkable_props = linkable.properties
|
||||||
|
local node = linkable:get_associated_proxy ("node")
|
||||||
|
|
||||||
|
-- check that the node has ports in the requested direction
|
||||||
|
if not node:lookup_port {
|
||||||
|
Constraint { "port.direction", "=", port_direction }
|
||||||
|
} then
|
||||||
|
goto next_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check that the node has available routes,
|
||||||
|
-- if it is associated to a real device
|
||||||
|
if not nodeHasAvailableRoutes (node, devices_om) then
|
||||||
|
goto next_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert (collected, Json.Object (node.properties))
|
||||||
|
|
||||||
|
::next_linkable::
|
||||||
|
end
|
||||||
|
|
||||||
|
return collected
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If the node has an associated device, verify that it has an available
|
||||||
|
-- route. Some UCM profiles expose all paths (headphones, HDMI, etc) as nodes,
|
||||||
|
-- even though they may not be connected... See #145
|
||||||
|
function nodeHasAvailableRoutes (node, devices_om)
|
||||||
|
local properties = node.properties
|
||||||
|
local device_id = properties ["device.id"]
|
||||||
|
local cpd = properties ["card.profile.device"]
|
||||||
|
|
||||||
|
if not device_id or not cpd then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the device
|
||||||
|
local device = devices_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", device_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if not device then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if the current device route supports the node card device profile
|
||||||
|
for r in device:iterate_params ("Route") do
|
||||||
|
local route = r:parse ()
|
||||||
|
local route_props = route.properties
|
||||||
|
if route_props.device == tonumber (cpd) then
|
||||||
|
if route_props.available == "no" then
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if available routes support the node card device profile
|
||||||
|
local found = 0
|
||||||
|
for r in device:iterate_params ("EnumRoute") do
|
||||||
|
local route = r:parse ()
|
||||||
|
local route_props = route.properties
|
||||||
|
if type (route_props.devices) == "table" then
|
||||||
|
for _, i in ipairs (route_props.devices) do
|
||||||
|
if i == tonumber (cpd) then
|
||||||
|
found = found + 1
|
||||||
|
if route_props.available ~= "no" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The node is part of a profile without routes so we assume it
|
||||||
|
-- is available. This can happen for Pro Audio profiles
|
||||||
|
if found == 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- the script states the default nodes from the user preferences, it has hooks
|
||||||
|
-- which stores the user preferences(it stores not just the current preference
|
||||||
|
-- but all the previous preferences) in to the state file, retrives them from
|
||||||
|
-- state file during the bootup, finally it has a hook which finds a default
|
||||||
|
-- node out of the user preferences
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-default-nodes")
|
||||||
|
|
||||||
|
nutils = require ("node-utils")
|
||||||
|
|
||||||
|
-- the state storage
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
|
||||||
|
find_stored_default_node_hook = SimpleEventHook {
|
||||||
|
name = "default-nodes/find-stored-default-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-default-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local available_nodes = event:get_data ("available-nodes")
|
||||||
|
local selected_prio = event:get_data ("selected-node-priority") or 0
|
||||||
|
local selected_node = event:get_data ("selected-node")
|
||||||
|
|
||||||
|
available_nodes = available_nodes and available_nodes:parse ()
|
||||||
|
if not available_nodes then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local stored = collectStored (props ["default-node.type"])
|
||||||
|
|
||||||
|
-- Check if any of the available nodes matches any of the configured
|
||||||
|
for _, node_props in ipairs (available_nodes) do
|
||||||
|
local name = node_props ["node.name"]
|
||||||
|
|
||||||
|
for i, v in ipairs (stored) do
|
||||||
|
if name == v then
|
||||||
|
local priority = nutils.get_session_priority (node_props)
|
||||||
|
priority = priority + 20001 - i
|
||||||
|
|
||||||
|
if priority > selected_prio then
|
||||||
|
selected_prio = priority
|
||||||
|
selected_node = name
|
||||||
|
end
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if selected_node then
|
||||||
|
event:set_data ("selected-node-priority", selected_prio)
|
||||||
|
event:set_data ("selected-node", selected_node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
store_configured_default_nodes_hook = SimpleEventHook {
|
||||||
|
name = "default-nodes/store-configured-default-nodes",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "c", "default.configured.audio.sink",
|
||||||
|
"default.configured.audio.source", "default.configured.video.source"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local props = event:get_properties ()
|
||||||
|
-- get the part after "default.configured." (= 19 chars)
|
||||||
|
local def_node_type = props ["event.subject.key"]:sub (20)
|
||||||
|
local new_value = props ["event.subject.value"]
|
||||||
|
local new_stored = {}
|
||||||
|
|
||||||
|
if new_value then
|
||||||
|
new_value = Json.Raw (new_value):parse () ["name"]
|
||||||
|
end
|
||||||
|
|
||||||
|
if new_value then
|
||||||
|
local stored = collectStored (def_node_type)
|
||||||
|
local pos = #stored + 1
|
||||||
|
|
||||||
|
-- find if the curent configured value is already in the stack
|
||||||
|
for i, v in ipairs (stored) do
|
||||||
|
if v == new_value then
|
||||||
|
pos = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- insert at the top and shift the remaining to fill the gap
|
||||||
|
new_stored [1] = new_value
|
||||||
|
if pos > 1 then
|
||||||
|
table.move (stored, 1, pos-1, 2, new_stored)
|
||||||
|
end
|
||||||
|
if pos < #stored then
|
||||||
|
table.move (stored, pos+1, #stored, pos+1, new_stored)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
updateStored (def_node_type, new_stored)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- set initial values
|
||||||
|
metadata_added_hook = SimpleEventHook {
|
||||||
|
name = "default-nodes/metadata-added",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-added" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local types = { "audio.sink", "audio.source", "video.source" }
|
||||||
|
local source = event:get_source ()
|
||||||
|
local om = source:call ("get-object-manager", "metadata")
|
||||||
|
local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } }
|
||||||
|
|
||||||
|
for _, t in ipairs (types) do
|
||||||
|
local v = state_table ["default.configured." .. t]
|
||||||
|
if v then
|
||||||
|
metadata:set (0, "default.configured." .. t, "Spa:String:JSON",
|
||||||
|
Json.Object { ["name"] = v }:to_string ())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Collect all the previously configured node names from the state file
|
||||||
|
function collectStored (def_node_type)
|
||||||
|
local stored = {}
|
||||||
|
local key_base = "default.configured." .. def_node_type
|
||||||
|
local key = key_base
|
||||||
|
|
||||||
|
local index = 0
|
||||||
|
repeat
|
||||||
|
local v = state_table [key]
|
||||||
|
table.insert (stored, v)
|
||||||
|
key = key_base .. "." .. tostring (index)
|
||||||
|
index = index + 1
|
||||||
|
until v == nil
|
||||||
|
|
||||||
|
return stored
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store the given node names in the state file
|
||||||
|
function updateStored (def_node_type, stored)
|
||||||
|
local key_base = "default.configured." .. def_node_type
|
||||||
|
local key = key_base
|
||||||
|
|
||||||
|
local index = 0
|
||||||
|
for _, v in ipairs (stored) do
|
||||||
|
state_table [key] = v
|
||||||
|
key = key_base .. "." .. tostring (index)
|
||||||
|
index = index + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- erase the rest, if any
|
||||||
|
repeat
|
||||||
|
local v = state_table [key]
|
||||||
|
state_table [key] = nil
|
||||||
|
key = key_base .. "." .. tostring (index)
|
||||||
|
index = index + 1
|
||||||
|
until v == nil
|
||||||
|
|
||||||
|
state:save_after_timeout (state_table)
|
||||||
|
end
|
||||||
|
|
||||||
|
function toggleState (enable)
|
||||||
|
if enable and not state then
|
||||||
|
state = State ("default-nodes")
|
||||||
|
state_table = state:load ()
|
||||||
|
find_stored_default_node_hook:register ()
|
||||||
|
store_configured_default_nodes_hook:register ()
|
||||||
|
metadata_added_hook:register ()
|
||||||
|
elseif not enable and state then
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
find_stored_default_node_hook:remove ()
|
||||||
|
store_configured_default_nodes_hook:remove ()
|
||||||
|
metadata_added_hook:remove ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Settings.subscribe ("node.restore-default-targets", function ()
|
||||||
|
toggleState (Settings.get_boolean ("node.restore-default-targets"))
|
||||||
|
end)
|
||||||
|
toggleState (Settings.get_boolean ("node.restore-default-targets"))
|
||||||
59
.config/wireplumber/scripts/device/apply-profile.lua
Normal file
59
.config/wireplumber/scripts/device/apply-profile.lua
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- apply the selected profile to the device
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
AsyncEventHook {
|
||||||
|
name = "device/apply-profile",
|
||||||
|
after = { "device/find-stored-profile", "device/find-preferred-profile", "device/find-best-profile" },
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-profile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps = {
|
||||||
|
start = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local profile = event:get_data ("selected-profile")
|
||||||
|
local dev_name = device.properties ["device.name"] or ""
|
||||||
|
|
||||||
|
if not profile then
|
||||||
|
log:info (device, "No profile found to set on " .. dev_name)
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
local active_profile = cutils.parseParam (p, "Profile")
|
||||||
|
if active_profile.index == tonumber(profile.index) then
|
||||||
|
log:info (device, "Profile " .. profile.name .. " is already set on " .. dev_name)
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local param = Pod.Object {
|
||||||
|
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||||
|
index = tonumber(profile.index),
|
||||||
|
}
|
||||||
|
log:info (device, "Setting profile " .. profile.name .. " on " .. dev_name)
|
||||||
|
device:set_param ("Profile", param)
|
||||||
|
|
||||||
|
-- FIXME: add cancellability
|
||||||
|
-- sync on the pipewire connection to ensure that the param
|
||||||
|
-- has been configured on the remote device object
|
||||||
|
Core.sync (function ()
|
||||||
|
transition:advance ()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}:register()
|
||||||
107
.config/wireplumber/scripts/device/apply-routes.lua
Normal file
107
.config/wireplumber/scripts/device/apply-routes.lua
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021-2022 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- Based on default-routes.c from pipewire-media-session
|
||||||
|
-- Copyright © 2020 Wim Taymans
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Set the Route param as part of the "select-routes" event run
|
||||||
|
|
||||||
|
devinfo = require ("device-info-cache")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
AsyncEventHook {
|
||||||
|
name = "device/apply-routes",
|
||||||
|
after = { "device/find-stored-routes",
|
||||||
|
"device/find-best-routes",
|
||||||
|
"device/apply-route-props" },
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-routes" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps = {
|
||||||
|
start = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local selected_routes = event:get_data ("selected-routes")
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
assert (dev_info)
|
||||||
|
|
||||||
|
if not selected_routes then
|
||||||
|
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for device_id, route in pairs (selected_routes) do
|
||||||
|
-- JSON to lua table
|
||||||
|
route = Json.Raw (route):parse ()
|
||||||
|
|
||||||
|
-- steal the props
|
||||||
|
local props = route.props or {}
|
||||||
|
|
||||||
|
-- replace with the full route info
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route)
|
||||||
|
if not route_info then
|
||||||
|
goto skip_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ensure default values
|
||||||
|
local is_input = (route_info.direction == "Input")
|
||||||
|
props.mute = props.mute or false
|
||||||
|
props.channelVolumes = props.channelVolumes or
|
||||||
|
{ is_input and Settings.get_float ("device.routes.default-source-volume")
|
||||||
|
or Settings.get_float ("device.routes.default-sink-volume") }
|
||||||
|
|
||||||
|
-- prefix the props with correct IDs to create a Pod.Object
|
||||||
|
table.insert (props, 1, "Spa:Pod:Object:Param:Props")
|
||||||
|
table.insert (props, 2, "Route")
|
||||||
|
|
||||||
|
-- convert arrays to Spa Pod
|
||||||
|
if props.channelVolumes then
|
||||||
|
table.insert (props.channelVolumes, 1, "Spa:Float")
|
||||||
|
props.channelVolumes = Pod.Array (props.channelVolumes)
|
||||||
|
end
|
||||||
|
if props.channelMap then
|
||||||
|
table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
|
||||||
|
props.channelMap = Pod.Array (props.channelMap)
|
||||||
|
end
|
||||||
|
if props.iec958Codecs then
|
||||||
|
table.insert (props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
|
||||||
|
props.iec958Codecs = Pod.Array (props.iec958Codecs)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- construct Route param
|
||||||
|
local param = Pod.Object {
|
||||||
|
"Spa:Pod:Object:Param:Route", "Route",
|
||||||
|
index = route_info.index,
|
||||||
|
device = device_id,
|
||||||
|
props = Pod.Object (props),
|
||||||
|
save = route_info.save,
|
||||||
|
}
|
||||||
|
|
||||||
|
log:debug (param,
|
||||||
|
string.format ("setting route(%s) on for device(%s)(%s)",
|
||||||
|
route_info.name, dev_info.name, tostring (device)))
|
||||||
|
|
||||||
|
device:set_param ("Route", param)
|
||||||
|
|
||||||
|
::skip_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- FIXME: add cancellability
|
||||||
|
-- sync on the pipewire connection to ensure that the params
|
||||||
|
-- have been configured on the remote device object
|
||||||
|
Core.sync (function ()
|
||||||
|
transition:advance ()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}:register()
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021 Asymptotic Inc.
|
||||||
|
-- @author Sanchayan Maity <sanchayan@asymptotic.io>
|
||||||
|
--
|
||||||
|
-- Based on bt-profile-switch.lua in tests/examples
|
||||||
|
-- Copyright © 2021 George Kiagiadakis
|
||||||
|
--
|
||||||
|
-- Based on bluez-autoswitch in media-session
|
||||||
|
-- Copyright © 2021 Pauli Virtanen
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- This script is charged to automatically change BT profiles on a device. If a
|
||||||
|
-- client is linked to the device's loopback source node, the associated BT
|
||||||
|
-- device profile is automatically switched to HSP/HFP. If there is no clients
|
||||||
|
-- linked to the device's loopback source node, the BT device profile is
|
||||||
|
-- switched back to A2DP profile.
|
||||||
|
--
|
||||||
|
-- We switch to the highest priority profile that has an Input route available.
|
||||||
|
-- The reason for this is that we may have microphone enabled with non-HFP
|
||||||
|
-- codecs eg. Faststream.
|
||||||
|
-- When a stream goes away if the list with which we track the streams above
|
||||||
|
-- is empty, then we revert back to the old profile.
|
||||||
|
|
||||||
|
-- settings file: bluetooth.conf
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
|
||||||
|
state = nil
|
||||||
|
headset_profiles = nil
|
||||||
|
device_loopback_sources = {}
|
||||||
|
|
||||||
|
local profile_restore_timeout_msec = 2000
|
||||||
|
|
||||||
|
local INVALID = -1
|
||||||
|
local timeout_source = {}
|
||||||
|
local restore_timeout_source = {}
|
||||||
|
|
||||||
|
local last_profiles = {}
|
||||||
|
|
||||||
|
local active_streams = {}
|
||||||
|
local previous_streams = {}
|
||||||
|
|
||||||
|
function handlePersistentSetting (enable)
|
||||||
|
if enable and state == nil then
|
||||||
|
-- the state storage
|
||||||
|
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
|
||||||
|
and State ("bluetooth-autoswitch") or nil
|
||||||
|
headset_profiles = state and state:load () or {}
|
||||||
|
else
|
||||||
|
state = nil
|
||||||
|
headset_profiles = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||||
|
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
||||||
|
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
devices_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "device",
|
||||||
|
Constraint { "device.api", "=", "bluez5" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streams_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "node",
|
||||||
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
|
Constraint { "node.link-group", "-", type = "pw" },
|
||||||
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_nodes_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "node",
|
||||||
|
Constraint { "node.link-group", "+", type = "pw" },
|
||||||
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loopback_nodes_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "node",
|
||||||
|
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
||||||
|
Constraint { "node.link-group", "+", type = "pw" },
|
||||||
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
|
Constraint { "bluez5.loopback", "=", "true", type = "pw" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local function saveHeadsetProfile (device, profile_name)
|
||||||
|
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||||
|
headset_profiles [key] = profile_name
|
||||||
|
state:save_after_timeout (headset_profiles)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getSavedHeadsetProfile (device)
|
||||||
|
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||||
|
return headset_profiles [key]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function saveLastProfile (device, profile_name)
|
||||||
|
last_profiles [device.properties ["device.name"]] = profile_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getSavedLastProfile (device)
|
||||||
|
return last_profiles [device.properties ["device.name"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isSwitchedToHeadsetProfile (device)
|
||||||
|
return getSavedLastProfile (device) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findProfile (device, index, name)
|
||||||
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
if not profile then
|
||||||
|
goto skip_enum_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.debug ("Profile name: " .. profile.name .. ", priority: "
|
||||||
|
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
|
||||||
|
if (index ~= nil and profile.index == index) or
|
||||||
|
(name ~= nil and profile.name == name) then
|
||||||
|
return profile.priority, profile.index, profile.name
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_enum_profile::
|
||||||
|
end
|
||||||
|
|
||||||
|
return INVALID, INVALID, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getCurrentProfile (device)
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
local profile = cutils.parseParam (p, "Profile")
|
||||||
|
if profile then
|
||||||
|
return profile.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function highestPrioProfileWithInputRoute (device)
|
||||||
|
local profile_priority = INVALID
|
||||||
|
local profile_index = INVALID
|
||||||
|
local profile_name = nil
|
||||||
|
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
-- Parse pod
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
if route.direction ~= "Input" then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.debug ("Route with index: " .. tostring (route.index) .. ", direction: "
|
||||||
|
.. route.direction .. ", name: " .. route.name .. ", description: "
|
||||||
|
.. route.description .. ", priority: " .. route.priority)
|
||||||
|
if route.profiles then
|
||||||
|
for _, v in pairs (route.profiles) do
|
||||||
|
local priority, index, name = findProfile (device, v)
|
||||||
|
if priority ~= INVALID then
|
||||||
|
if profile_priority < priority then
|
||||||
|
profile_priority = priority
|
||||||
|
profile_index = index
|
||||||
|
profile_name = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_enum_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
return profile_priority, profile_index, profile_name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasProfileInputRoute (device, profile_index)
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
if route and route.direction == "Input" and route.profiles then
|
||||||
|
for _, v in pairs (route.profiles) do
|
||||||
|
if v == profile_index then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function switchDeviceToHeadsetProfile (dev_id)
|
||||||
|
-- Find the actual device
|
||||||
|
local device = devices_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", dev_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if device == nil then
|
||||||
|
Log.info ("Device with id " .. tostring(dev_id).. " not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear restore callback, if any
|
||||||
|
if restore_timeout_source[dev_id] ~= nil then
|
||||||
|
restore_timeout_source[dev_id]:destroy ()
|
||||||
|
restore_timeout_source[dev_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
|
local priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||||
|
if hasProfileInputRoute (device, index) then
|
||||||
|
Log.info ("Current profile has input route, not switching")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if isSwitchedToHeadsetProfile (device) then
|
||||||
|
Log.info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||||
|
|
||||||
|
index = INVALID
|
||||||
|
if saved_headset_profile then
|
||||||
|
priority, index, name = findProfile (device, nil, saved_headset_profile)
|
||||||
|
if index ~= INVALID and not hasProfileInputRoute (device, index) then
|
||||||
|
index = INVALID
|
||||||
|
saveHeadsetProfile (device, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if index == INVALID then
|
||||||
|
priority, index, name = highestPrioProfileWithInputRoute (device)
|
||||||
|
end
|
||||||
|
|
||||||
|
if index ~= INVALID then
|
||||||
|
local pod = Pod.Object {
|
||||||
|
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||||
|
index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
-- store the current profile (needed when restoring)
|
||||||
|
saveLastProfile (device, cur_profile_name)
|
||||||
|
|
||||||
|
-- switch to headset profile
|
||||||
|
Log.info ("Setting profile of '"
|
||||||
|
.. device.properties ["device.description"]
|
||||||
|
.. "' from: " .. cur_profile_name
|
||||||
|
.. " to: " .. name)
|
||||||
|
device:set_params ("Profile", pod)
|
||||||
|
else
|
||||||
|
Log.warning ("Got invalid index when switching profile")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function restoreProfile (dev_id)
|
||||||
|
-- Find the actual device
|
||||||
|
local device = devices_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", dev_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if device == nil then
|
||||||
|
Log.info ("Device with id " .. tostring(dev_id).. " not found")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isSwitchedToHeadsetProfile (device) then
|
||||||
|
Log.info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile_name = getSavedLastProfile (device)
|
||||||
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
|
local priority, index, name
|
||||||
|
|
||||||
|
if cur_profile_name then
|
||||||
|
priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||||
|
|
||||||
|
if index ~= INVALID and hasProfileInputRoute (device, index) then
|
||||||
|
Log.info ("Setting saved headset profile to: " .. cur_profile_name)
|
||||||
|
saveHeadsetProfile (device, cur_profile_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if profile_name then
|
||||||
|
priority, index, name = findProfile (device, nil, profile_name)
|
||||||
|
|
||||||
|
if index ~= INVALID then
|
||||||
|
local pod = Pod.Object {
|
||||||
|
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||||
|
index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
-- clear last profile as we will restore it now
|
||||||
|
saveLastProfile (device, nil)
|
||||||
|
|
||||||
|
-- restore previous profile
|
||||||
|
Log.info ("Restoring profile of '"
|
||||||
|
.. device.properties ["device.description"]
|
||||||
|
.. "' from: " .. cur_profile_name
|
||||||
|
.. " to: " .. name)
|
||||||
|
device:set_params ("Profile", pod)
|
||||||
|
else
|
||||||
|
Log.warning ("Failed to restore profile")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function triggerRestoreProfile (dev_id)
|
||||||
|
-- we never restore the device profiles if there are active streams
|
||||||
|
for _, v in pairs (active_streams) do
|
||||||
|
if v == dev_id then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear restore callback, if any
|
||||||
|
if restore_timeout_source[dev_id] ~= nil then
|
||||||
|
restore_timeout_source[dev_id]:destroy ()
|
||||||
|
restore_timeout_source[dev_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create new restore callback
|
||||||
|
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||||
|
restore_timeout_source[dev_id] = nil
|
||||||
|
restoreProfile (dev_id)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||||
|
-- source filter
|
||||||
|
local function checkStreamStatus (stream)
|
||||||
|
-- check if the stream is linked to a bluetooth loopback source
|
||||||
|
local stream_id = tonumber(stream["bound-id"])
|
||||||
|
local peer_id = lutils.getNodePeerId (stream_id)
|
||||||
|
if peer_id ~= nil then
|
||||||
|
local bt_node = loopback_nodes_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", peer_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if bt_node ~= nil then
|
||||||
|
local dev_id = bt_node.properties["device.id"]
|
||||||
|
if dev_id ~= nil then
|
||||||
|
-- If a stream we previously saw stops running, we consider it
|
||||||
|
-- inactive, because some applications (Teams) just cork input
|
||||||
|
-- streams, but don't close them.
|
||||||
|
if previous_streams [stream.id] == dev_id and
|
||||||
|
stream.state ~= "running" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return dev_id
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Check if it is linked to a filter main node, and recursively advance if so
|
||||||
|
local filter_main_node = filter_nodes_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", peer_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if filter_main_node ~= nil then
|
||||||
|
-- Now check the all stream nodes for this filter
|
||||||
|
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||||
|
for filter_stream_node in filter_nodes_om:iterate {
|
||||||
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
|
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||||
|
} do
|
||||||
|
local dev_id = checkStreamStatus (filter_stream_node)
|
||||||
|
if dev_id ~= nil then
|
||||||
|
return dev_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handleStream (stream)
|
||||||
|
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local dev_id = checkStreamStatus (stream)
|
||||||
|
if dev_id ~= nil then
|
||||||
|
active_streams [stream.id] = dev_id
|
||||||
|
previous_streams [stream.id] = dev_id
|
||||||
|
switchDeviceToHeadsetProfile (dev_id)
|
||||||
|
else
|
||||||
|
dev_id = active_streams [stream.id]
|
||||||
|
active_streams [stream.id] = nil
|
||||||
|
if dev_id ~= nil then
|
||||||
|
triggerRestoreProfile (dev_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handleAllStreams ()
|
||||||
|
for stream in streams_om:iterate() do
|
||||||
|
handleStream (stream)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "node-removed@autoswitch-bluetooth-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local stream = event:get_subject ()
|
||||||
|
local dev_id = active_streams[stream.id]
|
||||||
|
active_streams[stream.id] = nil
|
||||||
|
previous_streams[stream.id] = nil
|
||||||
|
if dev_id ~= nil then
|
||||||
|
triggerRestoreProfile (dev_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "link-added@autoswitch-bluetooth-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "link-added" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local link = event:get_subject ()
|
||||||
|
local p = link.properties
|
||||||
|
for stream in streams_om:iterate () do
|
||||||
|
local in_id = tonumber(p["link.input.node"])
|
||||||
|
local stream_id = tonumber(stream["bound-id"])
|
||||||
|
if in_id == stream_id then
|
||||||
|
handleStream (stream)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "bluez-device-added@autoswitch-bluetooth-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-added" },
|
||||||
|
Constraint { "device.api", "=", "bluez5" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
-- Devices are unswitched initially
|
||||||
|
saveLastProfile (device, nil)
|
||||||
|
handleAllStreams ()
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
devices_om:activate ()
|
||||||
|
streams_om:activate ()
|
||||||
|
filter_nodes_om:activate ()
|
||||||
|
loopback_nodes_om:activate()
|
||||||
|
|
||||||
75
.config/wireplumber/scripts/device/find-best-profile.lua
Normal file
75
.config/wireplumber/scripts/device/find-best-profile.lua
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Find the best profile for a device based on profile priorities and
|
||||||
|
-- availability
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "device/find-best-profile",
|
||||||
|
after = "device/find-preferred-profile",
|
||||||
|
before = "device/apply-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-profile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local selected_profile = event:get_data ("selected-profile")
|
||||||
|
|
||||||
|
-- skip hook if profile is already selected
|
||||||
|
if selected_profile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local dev_name = device.properties["device.name"] or ""
|
||||||
|
local off_profile = nil
|
||||||
|
local best_profile = nil
|
||||||
|
local unk_profile = nil
|
||||||
|
-- Takes absolute priority if available or unknown
|
||||||
|
local profile_prop = device.properties["device.profile"]
|
||||||
|
|
||||||
|
|
||||||
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
|
profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
if profile and profile.name == profile_prop and profile.available ~= "no" then
|
||||||
|
selected_profile = profile
|
||||||
|
goto profile_set
|
||||||
|
elseif profile and profile.name ~= "pro-audio" then
|
||||||
|
if profile.name == "off" then
|
||||||
|
off_profile = profile
|
||||||
|
elseif profile.available == "yes" then
|
||||||
|
if best_profile == nil or profile.priority > best_profile.priority then
|
||||||
|
best_profile = profile
|
||||||
|
end
|
||||||
|
elseif profile.available ~= "no" then
|
||||||
|
if unk_profile == nil or profile.priority > unk_profile.priority then
|
||||||
|
unk_profile = profile
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if best_profile ~= nil then
|
||||||
|
selected_profile = best_profile
|
||||||
|
elseif unk_profile ~= nil then
|
||||||
|
selected_profile = unk_profile
|
||||||
|
elseif off_profile ~= nil then
|
||||||
|
selected_profile = off_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
::profile_set::
|
||||||
|
if selected_profile then
|
||||||
|
log:info (device, string.format (
|
||||||
|
"Found best profile '%s' (%d) for device '%s'",
|
||||||
|
selected_profile.name, selected_profile.index, dev_name))
|
||||||
|
event:set_data ("selected-profile", selected_profile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
77
.config/wireplumber/scripts/device/find-best-routes.lua
Normal file
77
.config/wireplumber/scripts/device/find-best-routes.lua
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021-2022 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- Based on default-routes.c from pipewire-media-session
|
||||||
|
-- Copyright © 2020 Wim Taymans
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- find the best route for a given device_id, based on availability and priority
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
devinfo = require ("device-info-cache")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "device/find-best-routes",
|
||||||
|
after = "device/find-stored-routes",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-routes" },
|
||||||
|
Constraint { "profile.active-device-ids", "is-present" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local event_properties = event:get_properties ()
|
||||||
|
local active_ids = event_properties ["profile.active-device-ids"]
|
||||||
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
assert (dev_info)
|
||||||
|
|
||||||
|
-- active IDs are exchanged in JSON format
|
||||||
|
active_ids = Json.Raw (active_ids):parse ()
|
||||||
|
|
||||||
|
for _, device_id in ipairs (active_ids) do
|
||||||
|
-- if a previous hook already selected a route for this device_id, skip it
|
||||||
|
if selected_routes [tostring (device_id)] then
|
||||||
|
goto next_device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
local best_avail = nil
|
||||||
|
local best_unk = nil
|
||||||
|
for _, ri in pairs (dev_info.route_infos) do
|
||||||
|
if cutils.arrayContains (ri.devices, device_id) and
|
||||||
|
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) then
|
||||||
|
if ri.available == "yes" or ri.available == "unknown" then
|
||||||
|
if ri.direction == "Output" and ri.available ~= ri.prev_available then
|
||||||
|
best_avail = ri
|
||||||
|
ri.save = true
|
||||||
|
break
|
||||||
|
elseif ri.available == "yes" then
|
||||||
|
if (best_avail == nil or ri.priority > best_avail.priority) then
|
||||||
|
best_avail = ri
|
||||||
|
end
|
||||||
|
elseif best_unk == nil or ri.priority > best_unk.priority then
|
||||||
|
best_unk = ri
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local route = best_avail or best_unk
|
||||||
|
if route then
|
||||||
|
selected_routes [tostring (device_id)] =
|
||||||
|
Json.Object { index = route.index }:to_string ()
|
||||||
|
end
|
||||||
|
|
||||||
|
::next_device_id::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save the selected routes for the apply-routes hook
|
||||||
|
event:set_data ("selected-routes", selected_routes)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2024 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Finds the user preferred profile for a device, based on the priorities
|
||||||
|
-- defined in the "device.profile.priority.rules" section of the configuration.
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("device.profile.priority.rules", Json.Array {})
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "device/find-preferred-profile",
|
||||||
|
after = "device/find-stored-profile",
|
||||||
|
before = "device/find-best-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-profile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local selected_profile = event:get_data ("selected-profile")
|
||||||
|
|
||||||
|
-- skip hook if the profile is already selected for this device.
|
||||||
|
if selected_profile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local props = JsonUtils.match_rules_update_properties (
|
||||||
|
config.rules, device.properties)
|
||||||
|
local p_array = props["priorities"]
|
||||||
|
|
||||||
|
-- skip hook if the profile priorities are NOT defined for this device.
|
||||||
|
if not p_array then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local p_json = Json.Raw(p_array)
|
||||||
|
local priorities = p_json:parse()
|
||||||
|
|
||||||
|
for _, priority_profile in ipairs(priorities) do
|
||||||
|
for p in device:iterate_params("EnumProfile") do
|
||||||
|
local device_profile = cutils.parseParam(p, "EnumProfile")
|
||||||
|
if device_profile and device_profile.name == priority_profile then
|
||||||
|
selected_profile = device_profile
|
||||||
|
goto profile_set
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
::profile_set::
|
||||||
|
if selected_profile then
|
||||||
|
log:info (device, string.format (
|
||||||
|
"Found preferred profile '%s' (%d) for device '%s'",
|
||||||
|
selected_profile.name, selected_profile.index, device_name))
|
||||||
|
event:set_data ("selected-profile", selected_profile)
|
||||||
|
else
|
||||||
|
log:info (device, "Profiles listed in 'device.profile.priority.rules'"
|
||||||
|
.. " do not match the available ones of device: " .. device_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
28
.config/wireplumber/scripts/device/select-profile.lua
Normal file
28
.config/wireplumber/scripts/device/select-profile.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- look for new devices and raise select-profile event.
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "device/select-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-added" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "EnumProfile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local device = event:get_subject ()
|
||||||
|
source:call ("push-event", "select-profile", device, nil)
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
160
.config/wireplumber/scripts/device/select-routes.lua
Normal file
160
.config/wireplumber/scripts/device/select-routes.lua
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021-2022 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- Based on default-routes.c from pipewire-media-session
|
||||||
|
-- Copyright © 2020 Wim Taymans
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Update the device info cache with the latest information from EnumRoute(all
|
||||||
|
-- the device routes) and trigger a "select-routes" event to select new routes
|
||||||
|
-- for the given device configuration, if it has changed
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
devinfo = require ("device-info-cache")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "device/select-route",
|
||||||
|
after = "device/select-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-added" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "c", "EnumRoute" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local device = event:get_subject ()
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
if not dev_info then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_route_infos = {}
|
||||||
|
local avail_routes_changed = false
|
||||||
|
local profile = nil
|
||||||
|
|
||||||
|
-- get current profile
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
profile = cutils.parseParam (p, "Profile")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- look at all the routes and update/reset cached information
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
-- parse pod
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find cached route information
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, true)
|
||||||
|
|
||||||
|
-- update properties
|
||||||
|
route_info.prev_available = route_info.available
|
||||||
|
if route_info.available ~= route.available then
|
||||||
|
log:info (device, "route " .. route.name .. " available changed " ..
|
||||||
|
route_info.available .. " -> " .. route.available)
|
||||||
|
route_info.available = route.available
|
||||||
|
if profile and cutils.arrayContains (route.profiles, profile.index) then
|
||||||
|
avail_routes_changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- store
|
||||||
|
new_route_infos [route.index] = route_info
|
||||||
|
|
||||||
|
::skip_enum_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- replace old route_infos to lose old routes
|
||||||
|
-- that no longer exist on the device
|
||||||
|
dev_info.route_infos = new_route_infos
|
||||||
|
new_route_infos = nil
|
||||||
|
|
||||||
|
-- restore routes for profile
|
||||||
|
if profile then
|
||||||
|
local profile_changed = (dev_info.active_profile ~= profile.index)
|
||||||
|
dev_info.active_profile = profile.index
|
||||||
|
|
||||||
|
-- if the profile changed, restore routes for that profile
|
||||||
|
-- if any of the routes of the current profile changed in availability,
|
||||||
|
-- then try to select a new "best" route for each device and ignore
|
||||||
|
-- what was stored
|
||||||
|
if profile_changed or avail_routes_changed then
|
||||||
|
log:info (device,
|
||||||
|
string.format ("restore routes for profile(%s) of device(%s)",
|
||||||
|
profile.name, dev_info.name))
|
||||||
|
|
||||||
|
-- find the active device IDs for which to select routes
|
||||||
|
local active_ids = findActiveDeviceIDs (profile)
|
||||||
|
active_ids = Json.Array (active_ids):to_string ()
|
||||||
|
|
||||||
|
-- push select-routes event and let the hooks select the appropriate routes
|
||||||
|
local props = {
|
||||||
|
["profile.changed"] = profile_changed,
|
||||||
|
["profile.name"] = profile.name,
|
||||||
|
["profile.active-device-ids"] = active_ids,
|
||||||
|
}
|
||||||
|
source:call ("push-event", "select-routes", device, props)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
|
|
||||||
|
-- These device ids are like routes(speaker, mic, headset etc) or sub-devices or
|
||||||
|
-- paths with in the pipewire devices/soundcards.
|
||||||
|
function findActiveDeviceIDs (profile)
|
||||||
|
-- parses the classes from the profile and returns the device IDs
|
||||||
|
----- sample structure, should return { 0, 8 } -----
|
||||||
|
-- classes:
|
||||||
|
-- 1: 2
|
||||||
|
-- 2:
|
||||||
|
-- 1: Audio/Source
|
||||||
|
-- 2: 1
|
||||||
|
-- 3: card.profile.devices
|
||||||
|
-- 4:
|
||||||
|
-- 1: 0
|
||||||
|
-- pod_type: Array
|
||||||
|
-- value_type: Spa:Int
|
||||||
|
-- pod_type: Struct
|
||||||
|
-- 3:
|
||||||
|
-- 1: Audio/Sink
|
||||||
|
-- 2: 1
|
||||||
|
-- 3: card.profile.devices
|
||||||
|
-- 4:
|
||||||
|
-- 1: 8
|
||||||
|
-- pod_type: Array
|
||||||
|
-- value_type: Spa:Int
|
||||||
|
-- pod_type: Struct
|
||||||
|
-- pod_type: Struct
|
||||||
|
local active_ids = {}
|
||||||
|
if type (profile.classes) == "table" and profile.classes.pod_type == "Struct" then
|
||||||
|
for _, p in ipairs (profile.classes) do
|
||||||
|
if type (p) == "table" and p.pod_type == "Struct" then
|
||||||
|
local i = 1
|
||||||
|
while true do
|
||||||
|
local k, v = p [i], p [i+1]
|
||||||
|
i = i + 2
|
||||||
|
if not k or not v then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if k == "card.profile.devices" and
|
||||||
|
type (v) == "table" and v.pod_type == "Array" then
|
||||||
|
for _, dev_id in ipairs (v) do
|
||||||
|
table.insert (active_ids, dev_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return active_ids
|
||||||
|
end
|
||||||
145
.config/wireplumber/scripts/device/state-profile.lua
Normal file
145
.config/wireplumber/scripts/device/state-profile.lua
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- This file contains all the logic related to saving device profiles
|
||||||
|
-- to a state file and restoring them later on.
|
||||||
|
|
||||||
|
-- A devices profile needs to be selected for any new device. the script selects
|
||||||
|
-- the device profile from the user preferences, as well as store the user
|
||||||
|
-- selected device profile to state file
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
-- the state storage
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
|
||||||
|
find_stored_profile_hook = SimpleEventHook {
|
||||||
|
name = "device/find-stored-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-profile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local selected_profile = event:get_data ("selected-profile")
|
||||||
|
|
||||||
|
-- skip hook if profile is already selected
|
||||||
|
if selected_profile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local dev_name = device.properties["device.name"]
|
||||||
|
if not dev_name then
|
||||||
|
log:critical (device, "invalid device.name")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile_name = state_table[dev_name]
|
||||||
|
|
||||||
|
if profile_name then
|
||||||
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
if profile.name == profile_name and profile.available ~= "no" then
|
||||||
|
selected_profile = profile
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if selected_profile then
|
||||||
|
log:info (device, string.format (
|
||||||
|
"Found stored profile '%s' (%d) for device '%s'",
|
||||||
|
selected_profile.name, selected_profile.index, dev_name))
|
||||||
|
event:set_data ("selected-profile", selected_profile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
store_user_selected_profile_hook = SimpleEventHook {
|
||||||
|
name = "device/store-user-selected-profile",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "Profile" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
local profile = cutils.parseParam (p, "Profile")
|
||||||
|
if profile.save then
|
||||||
|
-- store only if this was a user-generated action (save == true)
|
||||||
|
updateStoredProfile (device, profile)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStoredProfile (device, profile)
|
||||||
|
local dev_name = device.properties["device.name"]
|
||||||
|
local index = nil
|
||||||
|
|
||||||
|
if not dev_name then
|
||||||
|
log:critical (device, "invalid device.name")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log:debug (device, string.format (
|
||||||
|
"update stored profile to '%s' (%d) for device '%s'",
|
||||||
|
profile.name, profile.index, dev_name))
|
||||||
|
|
||||||
|
-- check if the new profile is the same as the current one
|
||||||
|
if state_table[dev_name] == profile.name then
|
||||||
|
log:debug (device, " ... profile is already stored")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find the full profile from EnumProfile, making also sure that the
|
||||||
|
-- user / client application has actually set an existing profile
|
||||||
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
|
local enum_profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
if enum_profile.name == profile.name then
|
||||||
|
index = enum_profile.index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not index then
|
||||||
|
log:info (device, string.format (
|
||||||
|
"profile '%s' (%d) is not valid on device '%s'",
|
||||||
|
profile.name, profile.index, dev_name))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
state_table[dev_name] = profile.name
|
||||||
|
state:save_after_timeout (state_table)
|
||||||
|
|
||||||
|
log:info (device, string.format (
|
||||||
|
"stored profile '%s' (%d) for device '%s'",
|
||||||
|
profile.name, index, dev_name))
|
||||||
|
end
|
||||||
|
|
||||||
|
function toggleState (enable)
|
||||||
|
if enable and not state then
|
||||||
|
state = State ("default-profile")
|
||||||
|
state_table = state:load ()
|
||||||
|
find_stored_profile_hook:register ()
|
||||||
|
store_user_selected_profile_hook:register ()
|
||||||
|
elseif not enable and state then
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
find_stored_profile_hook:remove ()
|
||||||
|
store_user_selected_profile_hook:remove ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Settings.subscribe ("device.restore-profile", function ()
|
||||||
|
toggleState (Settings.get_boolean ("device.restore-profile"))
|
||||||
|
end)
|
||||||
|
toggleState (Settings.get_boolean ("device.restore-profile"))
|
||||||
345
.config/wireplumber/scripts/device/state-routes.lua
Normal file
345
.config/wireplumber/scripts/device/state-routes.lua
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021-2022 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- Based on default-routes.c from pipewire-media-session
|
||||||
|
-- Copyright © 2020 Wim Taymans
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- This file contains all the logic related to saving device routes and their
|
||||||
|
-- properties to a state file and restoring both the routes selection and
|
||||||
|
-- the properties of routes later on.
|
||||||
|
--
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
devinfo = require ("device-info-cache")
|
||||||
|
log = Log.open_topic ("s-device")
|
||||||
|
|
||||||
|
-- the state storage
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
|
||||||
|
-- hook to restore routes selection for a newly selected profile
|
||||||
|
find_stored_routes_hook = SimpleEventHook {
|
||||||
|
name = "device/find-stored-routes",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-routes" },
|
||||||
|
Constraint { "profile.changed", "=", "true" },
|
||||||
|
Constraint { "profile.active-device-ids", "is-present" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local event_properties = event:get_properties ()
|
||||||
|
local profile_name = event_properties ["profile.name"]
|
||||||
|
local active_ids = event_properties ["profile.active-device-ids"]
|
||||||
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
assert (dev_info)
|
||||||
|
|
||||||
|
-- get the stored routes for this profile
|
||||||
|
-- skip the hook if there are no stored routes, there is no point
|
||||||
|
local spr = getStoredProfileRoutes (dev_info, profile_name)
|
||||||
|
if #spr == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- active IDs are exchanged in JSON format
|
||||||
|
active_ids = Json.Raw (active_ids):parse ()
|
||||||
|
|
||||||
|
for _, device_id in ipairs (active_ids) do
|
||||||
|
-- if a previous hook already selected a route for this device_id, skip it
|
||||||
|
if selected_routes [tostring (device_id)] then
|
||||||
|
goto next_device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (device, "restoring route for device ID " .. tostring (device_id));
|
||||||
|
|
||||||
|
local route_info = nil
|
||||||
|
|
||||||
|
-- find a route that was previously stored for a device_id
|
||||||
|
for _, ri in pairs (dev_info.route_infos) do
|
||||||
|
if cutils.arrayContains (ri.devices, tonumber (device_id)) and
|
||||||
|
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) and
|
||||||
|
cutils.arrayContains (spr, ri.name) then
|
||||||
|
route_info = ri
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if route_info then
|
||||||
|
-- we found a stored route
|
||||||
|
if route_info.available == "no" then
|
||||||
|
log:info (device, "stored route '" .. route_info.name .. "' not available")
|
||||||
|
-- not available, try to find next best
|
||||||
|
route_info = nil
|
||||||
|
else
|
||||||
|
log:info (device, "found stored route: " .. route_info.name)
|
||||||
|
-- make sure we save it again
|
||||||
|
route_info.save = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if route_info then
|
||||||
|
selected_routes [tostring (device_id)] =
|
||||||
|
Json.Object { index = route_info.index }:to_string ()
|
||||||
|
end
|
||||||
|
|
||||||
|
::next_device_id::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save the selected routes for the apply-routes hook
|
||||||
|
event:set_data ("selected-routes", selected_routes)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- extract the "selected-routes" event data and augment it to include
|
||||||
|
-- the route properties, as they were stored in the state file;
|
||||||
|
-- this is the last step before applying the routes
|
||||||
|
apply_route_props_hook = SimpleEventHook {
|
||||||
|
name = "device/apply-route-props",
|
||||||
|
after = { "device/find-stored-routes", "device/find-best-routes" },
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-routes" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local selected_routes = event:get_data ("selected-routes") or {}
|
||||||
|
local new_selected_routes = {}
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
assert (dev_info)
|
||||||
|
|
||||||
|
if next (selected_routes) == nil then
|
||||||
|
log:info (device, "No routes selected to set on " .. dev_info.name)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for device_id, route in pairs (selected_routes) do
|
||||||
|
-- JSON to lua table
|
||||||
|
route = Json.Raw (route):parse ()
|
||||||
|
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, false)
|
||||||
|
local props = getStoredRouteProps (dev_info, route_info)
|
||||||
|
|
||||||
|
-- convert arrays to Json
|
||||||
|
if props.channelVolumes then
|
||||||
|
props.channelVolumes = Json.Array (props.channelVolumes)
|
||||||
|
end
|
||||||
|
if props.channelMap then
|
||||||
|
props.channelMap = Json.Array (props.channelMap)
|
||||||
|
end
|
||||||
|
if props.iec958Codecs then
|
||||||
|
props.iec958Codecs = Json.Array (props.iec958Codecs)
|
||||||
|
end
|
||||||
|
|
||||||
|
local json = Json.Object {
|
||||||
|
index = route_info.index,
|
||||||
|
props = Json.Object (props),
|
||||||
|
}
|
||||||
|
new_selected_routes [device_id] = json:to_string ()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save the selected routes for the apply-routes hook
|
||||||
|
event:set_data ("selected-routes", new_selected_routes)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
store_or_restore_routes_hook = SimpleEventHook {
|
||||||
|
name = "device/store-or-restore-routes",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "Route" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
|
local source = event:get_source ()
|
||||||
|
local selected_routes = {}
|
||||||
|
local push_select_routes = false
|
||||||
|
|
||||||
|
local dev_info = devinfo:get_device_info (device)
|
||||||
|
if not dev_info then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local new_route_infos = {}
|
||||||
|
|
||||||
|
-- look at all the routes and update/reset cached information
|
||||||
|
for p in device:iterate_params ("EnumRoute") do
|
||||||
|
-- parse pod
|
||||||
|
local route = cutils.parseParam (p, "EnumRoute")
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find cached route information
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, true)
|
||||||
|
if not route_info then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update properties
|
||||||
|
route_info.prev_active = route_info.active
|
||||||
|
route_info.active = false
|
||||||
|
route_info.save = false
|
||||||
|
|
||||||
|
-- store
|
||||||
|
new_route_infos [route.index] = route_info
|
||||||
|
|
||||||
|
::skip_enum_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update route_infos with new prev_active, active and save changes
|
||||||
|
dev_info.route_infos = new_route_infos
|
||||||
|
new_route_infos = nil
|
||||||
|
|
||||||
|
-- check for changes in the active routes
|
||||||
|
for p in device:iterate_params ("Route") do
|
||||||
|
local route = cutils.parseParam (p, "Route")
|
||||||
|
if not route then
|
||||||
|
goto skip_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get cached route info and at the same time
|
||||||
|
-- ensure that the route is also in EnumRoute
|
||||||
|
local route_info = devinfo.find_route_info (dev_info, route, false)
|
||||||
|
if not route_info then
|
||||||
|
goto skip_route
|
||||||
|
end
|
||||||
|
|
||||||
|
-- update route_info state
|
||||||
|
route_info.active = true
|
||||||
|
route_info.save = route.save
|
||||||
|
|
||||||
|
if not route_info.prev_active then
|
||||||
|
-- a new route is now active, restore the volume and
|
||||||
|
-- make sure we save this as a preferred route
|
||||||
|
log:info (device,
|
||||||
|
string.format ("new active route(%s) found of device(%s)",
|
||||||
|
route.name, dev_info.name))
|
||||||
|
route_info.prev_active = true
|
||||||
|
route_info.active = true
|
||||||
|
|
||||||
|
selected_routes [tostring (route.device)] =
|
||||||
|
Json.Object { index = route_info.index }:to_string ()
|
||||||
|
push_select_routes = true
|
||||||
|
|
||||||
|
elseif route.save and route.props then
|
||||||
|
-- just save route properties
|
||||||
|
log:info (device,
|
||||||
|
string.format ("storing route(%s) props of device(%s)",
|
||||||
|
route.name, dev_info.name))
|
||||||
|
|
||||||
|
saveRouteProps (dev_info, route)
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_route::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- save selected routes for the active profile
|
||||||
|
for p in device:iterate_params ("Profile") do
|
||||||
|
local profile = cutils.parseParam (p, "Profile")
|
||||||
|
saveProfileRoutes (dev_info, profile.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- push a select-routes event to re-apply the routes with new properties
|
||||||
|
if push_select_routes then
|
||||||
|
local e = source:call ("create-event", "select-routes", device, nil)
|
||||||
|
e:set_data ("selected-routes", selected_routes)
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRouteProps (dev_info, route)
|
||||||
|
local props = route.props.properties
|
||||||
|
local key = dev_info.name .. ":" ..
|
||||||
|
route.direction:lower () .. ":" ..
|
||||||
|
route.name
|
||||||
|
|
||||||
|
state_table [key] = Json.Object {
|
||||||
|
volume = props.volume,
|
||||||
|
mute = props.mute,
|
||||||
|
channelVolumes = props.channelVolumes and Json.Array (props.channelVolumes),
|
||||||
|
channelMap = props.channelMap and Json.Array (props.channelMap),
|
||||||
|
latencyOffsetNsec = props.latencyOffsetNsec,
|
||||||
|
iec958Codecs = props.iec958Codecs and Json.Array (props.iec958Codecs),
|
||||||
|
}:to_string ()
|
||||||
|
|
||||||
|
state:save_after_timeout (state_table)
|
||||||
|
end
|
||||||
|
|
||||||
|
function getStoredRouteProps (dev_info, route)
|
||||||
|
local key = dev_info.name .. ":" ..
|
||||||
|
route.direction:lower () .. ":" ..
|
||||||
|
route.name
|
||||||
|
local value = state_table [key]
|
||||||
|
if value then
|
||||||
|
local json = Json.Raw (value)
|
||||||
|
if json and json:is_object () then
|
||||||
|
return json:parse ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- stores an array with the route names that are selected
|
||||||
|
-- for the given device and profile
|
||||||
|
function saveProfileRoutes (dev_info, profile_name)
|
||||||
|
-- select only routes with save == true
|
||||||
|
local routes = {}
|
||||||
|
for idx, ri in pairs (dev_info.route_infos) do
|
||||||
|
if ri.save then
|
||||||
|
table.insert (routes, ri.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #routes > 0 then
|
||||||
|
local key = dev_info.name .. ":profile:" .. profile_name
|
||||||
|
state_table [key] = Json.Array (routes):to_string()
|
||||||
|
state:save_after_timeout (state_table)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns an array of the route names that were previously selected
|
||||||
|
-- for the given device and profile
|
||||||
|
function getStoredProfileRoutes (dev_info, profile_name)
|
||||||
|
local key = dev_info.name .. ":profile:" .. profile_name
|
||||||
|
local value = state_table [key]
|
||||||
|
if value then
|
||||||
|
local json = Json.Raw (value)
|
||||||
|
if json and json:is_array () then
|
||||||
|
return json:parse ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function toggleState (enable)
|
||||||
|
if enable and not state then
|
||||||
|
state = State ("default-routes")
|
||||||
|
state_table = state:load ()
|
||||||
|
find_stored_routes_hook:register ()
|
||||||
|
apply_route_props_hook:register ()
|
||||||
|
store_or_restore_routes_hook:register ()
|
||||||
|
elseif not enable and state then
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
find_stored_routes_hook:remove ()
|
||||||
|
apply_route_props_hook:remove ()
|
||||||
|
store_or_restore_routes_hook:remove ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Settings.subscribe ("device.restore-routes", function ()
|
||||||
|
toggleState (Settings.get_boolean ("device.restore-routes"))
|
||||||
|
end)
|
||||||
|
toggleState (Settings.get_boolean ("device.restore-routes"))
|
||||||
@@ -5,15 +5,15 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
local sink_ids = {}
|
sink_ids = {}
|
||||||
local fallback_node = nil
|
fallback_node = nil
|
||||||
|
|
||||||
node_om = ObjectManager {
|
node_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
type = "node",
|
type = "node",
|
||||||
Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
|
Constraint { "media.class", "matches", "Audio/Sink", type = "pw-global" },
|
||||||
-- Do not consider endpoints created by WirePlumber
|
-- Do not consider virtual items created by WirePlumber
|
||||||
Constraint { "wireplumber.is-endpoint", "!", true, type = "pw" },
|
Constraint { "wireplumber.is-virtual", "!", true, type = "pw" },
|
||||||
-- or the fallback sink itself
|
-- or the fallback sink itself
|
||||||
Constraint { "wireplumber.is-fallback", "!", true, type = "pw" },
|
Constraint { "wireplumber.is-fallback", "!", true, type = "pw" },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Asymptotic
|
|
||||||
-- @author Arun Raghavan <arun@asymptotic.io>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
--
|
|
||||||
-- Route streams of a given role (media.role property) to devices that are
|
|
||||||
-- intended for that role (device.intended-roles property)
|
|
||||||
|
|
||||||
metadata_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "metadata",
|
|
||||||
Constraint { "metadata.name", "=", "default" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
|
|
||||||
Constraint { "device.intended-roles", "is-present", type = "pw" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
streams_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Stream/*/Audio", type = "pw-global" },
|
|
||||||
Constraint { "media.role", "is-present", type = "pw-global" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local function routeUsingIntendedRole(stream, dev)
|
|
||||||
local stream_role = stream.properties["media.role"]
|
|
||||||
local is_input = stream.properties["media.class"]:find("Input") ~= nil
|
|
||||||
|
|
||||||
local is_source = dev.properties["media.class"]:find("Source") ~= nil
|
|
||||||
local dev_roles = dev.properties["device.intended-roles"]
|
|
||||||
|
|
||||||
-- Make sure the stream and device direction match
|
|
||||||
if is_input ~= is_source then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for role in dev_roles:gmatch("(%a+)") do
|
|
||||||
if role == stream_role then
|
|
||||||
Log.info(stream,
|
|
||||||
string.format("Routing stream '%s' (%d) with role '%s' to '%s' (%d)",
|
|
||||||
stream.properties["node.name"], stream["bound-id"], stream_role,
|
|
||||||
dev.properties["node.name"], dev["bound-id"])
|
|
||||||
)
|
|
||||||
|
|
||||||
local metadata = metadata_om:lookup()
|
|
||||||
metadata:set(stream["bound-id"], "target.node", "Spa:Id", dev["bound-id"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
streams_om:connect("object-added", function (streams_om, stream)
|
|
||||||
for dev in devices_om:iterate() do
|
|
||||||
routeUsingIntendedRole(stream, dev)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
devices_om:connect("object-added", function (devices_om, dev)
|
|
||||||
for stream in streams_om:iterate() do
|
|
||||||
routeUsingIntendedRole(stream, dev)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
metadata_om:activate()
|
|
||||||
devices_om:activate()
|
|
||||||
streams_om:activate()
|
|
||||||
94
.config/wireplumber/scripts/lib/common-utils.lua
Normal file
94
.config/wireplumber/scripts/lib/common-utils.lua
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- Script is a Lua Module of common Lua utility functions
|
||||||
|
|
||||||
|
local cutils = {}
|
||||||
|
|
||||||
|
function cutils.parseBool (var)
|
||||||
|
return var and (var:lower () == "true" or var == "1")
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.parseParam (param, id)
|
||||||
|
local props = param:parse ()
|
||||||
|
if props.pod_type == "Object" and props.object_id == id then
|
||||||
|
return props.properties
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.mediaClassToDirection (media_class)
|
||||||
|
if media_class:find ("Sink") or
|
||||||
|
media_class:find ("Input") or
|
||||||
|
media_class:find ("Duplex") then
|
||||||
|
return "input"
|
||||||
|
elseif media_class:find ("Source") or media_class:find ("Output") then
|
||||||
|
return "output"
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.getTargetDirection (properties)
|
||||||
|
local target_direction = nil
|
||||||
|
|
||||||
|
if properties ["item.node.direction"] == "output" or
|
||||||
|
(properties ["item.node.direction"] == "input" and
|
||||||
|
cutils.parseBool (properties ["stream.capture.sink"])) then
|
||||||
|
target_direction = "input"
|
||||||
|
else
|
||||||
|
target_direction = "output"
|
||||||
|
end
|
||||||
|
return target_direction
|
||||||
|
end
|
||||||
|
|
||||||
|
local default_nodes = Plugin.find ("default-nodes-api")
|
||||||
|
|
||||||
|
function cutils.getDefaultNode (properties, target_direction)
|
||||||
|
local target_media_class =
|
||||||
|
properties ["media.type"] ..
|
||||||
|
(target_direction == "input" and "/Sink" or "/Source")
|
||||||
|
|
||||||
|
if not default_nodes then
|
||||||
|
default_nodes = Plugin.find ("default-nodes-api")
|
||||||
|
end
|
||||||
|
|
||||||
|
return default_nodes:call ("get-default-node", target_media_class)
|
||||||
|
end
|
||||||
|
|
||||||
|
cutils.source_plugin = nil
|
||||||
|
cutils.object_managers = {}
|
||||||
|
|
||||||
|
function cutils.get_object_manager (name)
|
||||||
|
cutils.source_plugin = cutils.source_plugin or
|
||||||
|
Plugin.find ("standard-event-source")
|
||||||
|
cutils.object_managers [name] = cutils.object_managers [name] or
|
||||||
|
cutils.source_plugin:call ("get-object-manager", name)
|
||||||
|
return cutils.object_managers [name]
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.get_default_metadata_object ()
|
||||||
|
return cutils.get_object_manager ("metadata"):lookup {
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.arrayContains (a, value)
|
||||||
|
for _, v in ipairs (a) do
|
||||||
|
if v == value then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function cutils.get_application_name ()
|
||||||
|
return Core.get_properties()["application.name"] or "WirePlumber"
|
||||||
|
end
|
||||||
|
|
||||||
|
return cutils
|
||||||
74
.config/wireplumber/scripts/lib/device-info-cache.lua
Normal file
74
.config/wireplumber/scripts/lib/device-info-cache.lua
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
local module = {
|
||||||
|
-- table of device info
|
||||||
|
dev_infos = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "lib/device-info-cache/cleanup",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-removed" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local device_id = props ["object.serial"]
|
||||||
|
Log.trace ("cleaning up dev_info for object.serial = " .. device_id)
|
||||||
|
module.dev_infos [device_id] = nil
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
|
|
||||||
|
function module.get_device_info (self, device)
|
||||||
|
local device_properties = device.properties
|
||||||
|
local device_id = device_properties ["object.serial"]
|
||||||
|
local dev_info = self.dev_infos [device_id]
|
||||||
|
|
||||||
|
-- new device
|
||||||
|
if not dev_info then
|
||||||
|
local device_name = device_properties ["device.name"]
|
||||||
|
if not device_name then
|
||||||
|
Log.critical (device, "invalid device.name")
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.trace (device, string.format (
|
||||||
|
"create dev_info for '%s', object.serial = %s", device_name, device_id))
|
||||||
|
|
||||||
|
dev_info = {
|
||||||
|
name = device_name,
|
||||||
|
active_profile = -1,
|
||||||
|
route_infos = {},
|
||||||
|
}
|
||||||
|
self.dev_infos [device_id] = dev_info
|
||||||
|
end
|
||||||
|
|
||||||
|
return dev_info
|
||||||
|
end
|
||||||
|
|
||||||
|
function module.find_route_info (dev_info, route, return_new)
|
||||||
|
local ri = dev_info.route_infos [route.index]
|
||||||
|
if not ri and return_new then
|
||||||
|
ri = {
|
||||||
|
index = route.index,
|
||||||
|
name = route.name,
|
||||||
|
direction = route.direction,
|
||||||
|
devices = route.devices or {},
|
||||||
|
profiles = route.profiles,
|
||||||
|
priority = route.priority or 0,
|
||||||
|
available = route.available or "unknown",
|
||||||
|
prev_available = route.available or "unknown",
|
||||||
|
active = false,
|
||||||
|
prev_active = false,
|
||||||
|
save = false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return ri
|
||||||
|
end
|
||||||
|
|
||||||
|
return module
|
||||||
501
.config/wireplumber/scripts/lib/filter-utils.lua
Normal file
501
.config/wireplumber/scripts/lib/filter-utils.lua
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||||
|
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- Script is a Lua Module of filter Lua utility functions
|
||||||
|
|
||||||
|
local cutils = require ("common-utils")
|
||||||
|
|
||||||
|
local module = {
|
||||||
|
metadata = nil,
|
||||||
|
filters = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function getFilterSmart (metadata, node)
|
||||||
|
-- Check metadata
|
||||||
|
if metadata ~= nil then
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = metadata:find (id, "filter.smart")
|
||||||
|
if value_str ~= nil then
|
||||||
|
local json = Json.Raw (value_str)
|
||||||
|
if json:is_boolean() then
|
||||||
|
return json:parse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check node properties
|
||||||
|
local prop_str = node.properties ["filter.smart"]
|
||||||
|
if prop_str ~= nil then
|
||||||
|
return cutils.parseBool (prop_str)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise consider the filter not smart by default
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartName (metadata, node)
|
||||||
|
-- Check metadata
|
||||||
|
if metadata ~= nil then
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = metadata:find (id, "filter.smart.name")
|
||||||
|
if value_str ~= nil then
|
||||||
|
local json = Json.Raw (value_str)
|
||||||
|
if json:is_string() then
|
||||||
|
return json:parse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check node properties
|
||||||
|
local prop_str = node.properties ["filter.smart.name"]
|
||||||
|
if prop_str ~= nil then
|
||||||
|
return prop_str
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise use link group as name
|
||||||
|
return node.properties ["node.link-group"]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartDisabled (metadata, node)
|
||||||
|
-- Check metadata
|
||||||
|
if metadata ~= nil then
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = metadata:find (id, "filter.smart.disabled")
|
||||||
|
if value_str ~= nil then
|
||||||
|
local json = Json.Raw (value_str)
|
||||||
|
if json:is_boolean() then
|
||||||
|
return json:parse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check node properties
|
||||||
|
local prop_str = node.properties ["filter.smart.disabled"]
|
||||||
|
if prop_str ~= nil then
|
||||||
|
return cutils.parseBool (prop_str)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise consider the filter not disabled by default
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartTargetable (metadata, node)
|
||||||
|
-- Check metadata
|
||||||
|
if metadata ~= nil then
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = metadata:find (id, "filter.smart.targetable")
|
||||||
|
if value_str ~= nil then
|
||||||
|
local json = Json.Raw (value_str)
|
||||||
|
if json:is_boolean() then
|
||||||
|
return json:parse()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check node properties
|
||||||
|
local prop_str = node.properties ["filter.smart.targetable"]
|
||||||
|
if prop_str ~= nil then
|
||||||
|
return cutils.parseBool (prop_str)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise consider the filter not targetable by default
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartTarget (metadata, node, om)
|
||||||
|
-- Check metadata and fallback to properties
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = nil
|
||||||
|
if metadata ~= nil then
|
||||||
|
value_str = metadata:find (id, "filter.smart.target")
|
||||||
|
end
|
||||||
|
if value_str == nil then
|
||||||
|
value_str = node.properties ["filter.smart.target"]
|
||||||
|
if value_str == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse match rules
|
||||||
|
local match_rules_json = Json.Raw (value_str)
|
||||||
|
if not match_rules_json:is_object () then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local match_rules = match_rules_json:parse ()
|
||||||
|
|
||||||
|
-- Find target
|
||||||
|
local target = nil
|
||||||
|
for si_target in om:iterate { type = "SiLinkable" } do
|
||||||
|
local n_target = si_target:get_associated_proxy ("node")
|
||||||
|
if n_target == nil then
|
||||||
|
goto skip_target
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Target nodes cannot be smart filters
|
||||||
|
if n_target.properties ["node.link-group"] ~= nil and
|
||||||
|
getFilterSmart (metadata, n_target) then
|
||||||
|
goto skip_target
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make sure the target node properties match all rules
|
||||||
|
for key, val in pairs(match_rules) do
|
||||||
|
if n_target.properties[key] ~= tostring (val) then
|
||||||
|
goto skip_target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Target found
|
||||||
|
target = si_target
|
||||||
|
break;
|
||||||
|
|
||||||
|
::skip_target::
|
||||||
|
end
|
||||||
|
|
||||||
|
return target
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartTargetless (metadata, node)
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = nil
|
||||||
|
if metadata ~= nil then
|
||||||
|
value_str = metadata:find (id, "filter.smart.target")
|
||||||
|
end
|
||||||
|
if value_str == nil then
|
||||||
|
value_str = node.properties ["filter.smart.target"]
|
||||||
|
end
|
||||||
|
|
||||||
|
return value_str == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartBefore (metadata, node)
|
||||||
|
-- Check metadata and fallback to properties
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = nil
|
||||||
|
if metadata ~= nil then
|
||||||
|
value_str = metadata:find (id, "filter.smart.before")
|
||||||
|
end
|
||||||
|
if value_str == nil then
|
||||||
|
value_str = node.properties ["filter.smart.before"]
|
||||||
|
if value_str == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse
|
||||||
|
local before_json = Json.Raw (value_str)
|
||||||
|
if not before_json:is_array() then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return before_json:parse ()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFilterSmartAfter (metadata, node)
|
||||||
|
-- Check metadata and fallback to properties
|
||||||
|
local id = node["bound-id"]
|
||||||
|
local value_str = nil
|
||||||
|
if metadata ~= nil then
|
||||||
|
value_str = metadata:find (id, "filter.smart.after")
|
||||||
|
end
|
||||||
|
if value_str == nil then
|
||||||
|
value_str = node.properties ["filter.smart.after"]
|
||||||
|
if value_str == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse
|
||||||
|
local after_json = Json.Raw (value_str)
|
||||||
|
if not after_json:is_array() then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return after_json:parse ()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function insertFilterSorted (curr_filters, filter)
|
||||||
|
local before_filters = {}
|
||||||
|
local after_filters = {}
|
||||||
|
local new_filters = {}
|
||||||
|
|
||||||
|
-- Check if the current filters need to be inserted before or after
|
||||||
|
for i, v in ipairs(curr_filters) do
|
||||||
|
local insert_before = true
|
||||||
|
local insert_after = false
|
||||||
|
|
||||||
|
if v.before ~= nil then
|
||||||
|
for j, b in ipairs(v.before) do
|
||||||
|
if filter.name == b then
|
||||||
|
insert_after = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if v.after ~= nil then
|
||||||
|
for j, b in ipairs(v.after) do
|
||||||
|
if filter.name == b then
|
||||||
|
insert_before = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if filter.before ~= nil then
|
||||||
|
for j, b in ipairs(filter.before) do
|
||||||
|
if v.name == b then
|
||||||
|
insert_after = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if filter.after ~= nil then
|
||||||
|
for j, b in ipairs(filter.after) do
|
||||||
|
if v.name == b then
|
||||||
|
insert_before = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if insert_before then
|
||||||
|
if insert_after then
|
||||||
|
Log.warning ("cyclic before/after found in filters " .. v.name .. " and " .. filter.name)
|
||||||
|
end
|
||||||
|
table.insert (before_filters, v)
|
||||||
|
else
|
||||||
|
table.insert (after_filters, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add the filters to the new table stored
|
||||||
|
for i, v in ipairs(before_filters) do
|
||||||
|
table.insert (new_filters, v)
|
||||||
|
end
|
||||||
|
table.insert (new_filters, filter)
|
||||||
|
for i, v in ipairs(after_filters) do
|
||||||
|
table.insert (new_filters, v)
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_filters
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rescanFilters (om, metadata_om)
|
||||||
|
local metadata =
|
||||||
|
metadata_om:lookup { Constraint { "metadata.name", "=", "filters" } }
|
||||||
|
|
||||||
|
-- Always clear all filters data on rescan
|
||||||
|
module.filters = {}
|
||||||
|
|
||||||
|
Log.info ("rescanning filters...")
|
||||||
|
|
||||||
|
for si in om:iterate { type = "SiLinkable" } do
|
||||||
|
local filter = {}
|
||||||
|
|
||||||
|
local n = si:get_associated_proxy ("node")
|
||||||
|
if n == nil then
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only handle nodes with link group (filters)
|
||||||
|
filter.link_group = n.properties ["node.link-group"]
|
||||||
|
if filter.link_group == nil then
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only handle the main filter nodes
|
||||||
|
filter.media_class = n.properties ["media.class"]
|
||||||
|
if string.find (filter.media_class, "Stream") then
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Filter direction
|
||||||
|
if string.find (filter.media_class, "Audio/Sink") or
|
||||||
|
string.find (filter.media_class, "Video/Sink") then
|
||||||
|
filter.direction = "input"
|
||||||
|
else
|
||||||
|
filter.direction = "output"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Filter media type
|
||||||
|
filter.media_type = si.properties["media.type"]
|
||||||
|
|
||||||
|
-- Get filter properties
|
||||||
|
filter.smart = getFilterSmart (metadata, n)
|
||||||
|
filter.name = getFilterSmartName (metadata, n)
|
||||||
|
filter.disabled = getFilterSmartDisabled (metadata, n)
|
||||||
|
filter.targetable = getFilterSmartTargetable (metadata, n)
|
||||||
|
filter.target = getFilterSmartTarget (metadata, n, om)
|
||||||
|
filter.targetless = getFilterSmartTargetless (metadata, n)
|
||||||
|
filter.before = getFilterSmartBefore (metadata, n)
|
||||||
|
filter.after = getFilterSmartAfter (metadata, n)
|
||||||
|
|
||||||
|
-- Add the main and stream session items
|
||||||
|
filter.main_si = si
|
||||||
|
filter.stream_si = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "node.link-group", "=", filter.link_group },
|
||||||
|
Constraint { "media.class", "#", "Stream/*", type = "pw-global" }
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Add the filter to the list sorted by before and after
|
||||||
|
module.filters = insertFilterSorted (module.filters, filter)
|
||||||
|
|
||||||
|
::skip_linkable::
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "lib/filter-utils/rescan",
|
||||||
|
before = "linking/rescan",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "rescan-for-linking" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||||
|
|
||||||
|
rescanFilters (om, metadata_om)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
function module.is_filter_smart (direction, link_group)
|
||||||
|
-- Make sure direction and link_group is valid
|
||||||
|
if direction == nil or link_group == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and v.link_group == link_group then
|
||||||
|
return v.smart
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function module.is_filter_disabled (direction, link_group)
|
||||||
|
-- Make sure direction and link_group is valid
|
||||||
|
if direction == nil or link_group == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and v.link_group == link_group then
|
||||||
|
return v.disabled
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function module.is_filter_targetable (direction, link_group)
|
||||||
|
-- Make sure direction and link_group is valid
|
||||||
|
if direction == nil or link_group == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and v.link_group == link_group then
|
||||||
|
return v.targetable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function module.get_filter_target (direction, link_group)
|
||||||
|
-- Make sure direction and link_group are valid
|
||||||
|
if direction == nil or link_group == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the current filter
|
||||||
|
local filter = nil
|
||||||
|
local index = nil
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and
|
||||||
|
v.link_group == link_group and
|
||||||
|
not v.disabled and
|
||||||
|
v.smart then
|
||||||
|
filter = v
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if filter == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the next filter with matching target
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and
|
||||||
|
v.media_type == filter.media_type and
|
||||||
|
v.name ~= filter.name and
|
||||||
|
v.link_group ~= link_group and
|
||||||
|
not v.disabled and
|
||||||
|
v.smart and
|
||||||
|
((v.target == nil and filter.target == nil) or
|
||||||
|
(v.target ~= nil and filter.target ~= nil and v.target.id == filter.target.id)) and
|
||||||
|
i > index then
|
||||||
|
return v.main_si
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Otherwise return the filter destination target
|
||||||
|
return filter.target
|
||||||
|
end
|
||||||
|
|
||||||
|
function module.get_filter_from_target (direction, media_type, si_target)
|
||||||
|
local target = si_target
|
||||||
|
|
||||||
|
-- Make sure direction and media_type are valid
|
||||||
|
if direction == nil or media_type == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If si_target is a filter, find it and use its target
|
||||||
|
if si_target then
|
||||||
|
local target_node = si_target:get_associated_proxy ("node")
|
||||||
|
local target_link_group = target_node.properties ["node.link-group"]
|
||||||
|
if target_link_group ~= nil then
|
||||||
|
local filter = nil
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and
|
||||||
|
v.media_type == media_type and
|
||||||
|
v.link_group == target_link_group and
|
||||||
|
not v.disabled and
|
||||||
|
v.smart then
|
||||||
|
filter = v
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if filter == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
target = filter.target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the first filter matching target
|
||||||
|
for i, v in ipairs(module.filters) do
|
||||||
|
if v.direction == direction and
|
||||||
|
v.media_type == media_type and
|
||||||
|
not v.disabled and
|
||||||
|
v.smart and
|
||||||
|
((v.target ~= nil and target ~= nil and v.target.id == target.id) or
|
||||||
|
(target == nil and v.targetless)) then
|
||||||
|
return v.main_si
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return module
|
||||||
431
.config/wireplumber/scripts/lib/linking-utils.lua
Normal file
431
.config/wireplumber/scripts/lib/linking-utils.lua
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- Script is a Lua Module of linking Lua utility functions
|
||||||
|
|
||||||
|
local cutils = require ("common-utils")
|
||||||
|
|
||||||
|
local lutils = {
|
||||||
|
si_flags = {},
|
||||||
|
priority_media_role_link = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
function lutils.get_flags (self, si_id)
|
||||||
|
if not self.si_flags [si_id] then
|
||||||
|
self.si_flags [si_id] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.si_flags [si_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.clear_flags (self, si_id)
|
||||||
|
self.si_flags [si_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function getprio (link)
|
||||||
|
return tonumber (link.properties ["policy.role-based.priority"]) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function getplugged (link)
|
||||||
|
return tonumber (link.properties ["item.plugged.usec"]) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.getAction (pmrl, link)
|
||||||
|
local props = pmrl.properties
|
||||||
|
|
||||||
|
if getprio (pmrl) == getprio (link) then
|
||||||
|
return props ["policy.role-based.action.same-priority"] or "mix"
|
||||||
|
else
|
||||||
|
return props ["policy.role-based.action.lower-priority"] or "mix"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if the link happens to be priority one, clear it and find the next
|
||||||
|
-- priority.
|
||||||
|
function lutils.clearPriorityMediaRoleLink (link)
|
||||||
|
local lprops = link.properties
|
||||||
|
local lmc = lprops ["target.media.class"]
|
||||||
|
|
||||||
|
pmrl = lutils.getPriorityMediaRoleLink (lmc)
|
||||||
|
|
||||||
|
-- only proceed if the link happens to be priority one.
|
||||||
|
if pmrl ~= link then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local prio_link = nil
|
||||||
|
local prio = 0
|
||||||
|
local plugged = 0
|
||||||
|
for l in cutils.get_object_manager ("session-item"):iterate {
|
||||||
|
type = "SiLink",
|
||||||
|
Constraint { "item.factory.name", "=", "si-standard-link", type = "pw-global" },
|
||||||
|
Constraint { "is.role.policy.link", "=", true },
|
||||||
|
Constraint { "target.media.class", "=", lmc },
|
||||||
|
} do
|
||||||
|
local props = l.properties
|
||||||
|
|
||||||
|
-- dont consider this link as it is about to be removed.
|
||||||
|
if pmrl == l then
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if getprio (link) > prio or
|
||||||
|
(getprio (link) == prio and getplugged (link) > plugged) then
|
||||||
|
prio = getprio (l)
|
||||||
|
plugged = getplugged (l)
|
||||||
|
prio_link = l
|
||||||
|
end
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
|
||||||
|
if prio_link then
|
||||||
|
setPriorityMediaRoleLink (lmc, prio_link)
|
||||||
|
else
|
||||||
|
setPriorityMediaRoleLink (lmc, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- record priority media role link
|
||||||
|
function lutils.updatePriorityMediaRoleLink (link)
|
||||||
|
local lprops = link.properties
|
||||||
|
local mc = lprops ["target.media.class"]
|
||||||
|
|
||||||
|
if not lutils.priority_media_role_link [mc] then
|
||||||
|
setPriorityMediaRoleLink (mc, link)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
pmrl = lutils.getPriorityMediaRoleLink (mc)
|
||||||
|
|
||||||
|
if getprio (link) > getprio (pmrl) or
|
||||||
|
(getprio (link) == getprio (pmrl) and getplugged (link) >= getplugged (pmrl)) then
|
||||||
|
setPriorityMediaRoleLink (mc, link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.getPriorityMediaRoleLink (lmc)
|
||||||
|
return lutils.priority_media_role_link [lmc]
|
||||||
|
end
|
||||||
|
|
||||||
|
function setPriorityMediaRoleLink (lmc, link)
|
||||||
|
lutils.priority_media_role_link [lmc] = link
|
||||||
|
if link then
|
||||||
|
Log.debug (
|
||||||
|
string.format ("update priority link(%d) media role(\"%s\") priority(%d)",
|
||||||
|
link.id, link.properties ["media.role"], getprio (link)))
|
||||||
|
else
|
||||||
|
Log.debug ("clear priority media role")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.is_role_policy_target (si_props, target_props)
|
||||||
|
-- role-based policy links are those that link to targets with
|
||||||
|
-- policy.role-based.target = true, unless the stream is a monitor
|
||||||
|
-- (usually pavucontrol) or the stream is linking to the monitor ports
|
||||||
|
-- of a sink (both are "input")
|
||||||
|
return Core.test_feature ("hooks.linking.role-based.rescan")
|
||||||
|
and cutils.parseBool (target_props["policy.role-based.target"])
|
||||||
|
and not cutils.parseBool (si_props ["stream.monitor"])
|
||||||
|
and si_props["item.node.direction"] ~= target_props["item.node.direction"]
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.unwrap_select_target_event (self, event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local si = event:get_subject ()
|
||||||
|
local target = event:get_data ("target")
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
local si_id = si.id
|
||||||
|
|
||||||
|
return source, om, si, si.properties, self:get_flags (si_id), target
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.canPassthrough (si, si_target)
|
||||||
|
local props = si.properties
|
||||||
|
local tprops = si_target.properties
|
||||||
|
-- both nodes must support encoded formats
|
||||||
|
if not cutils.parseBool (props ["item.node.supports-encoded-fmts"])
|
||||||
|
or not cutils.parseBool (tprops ["item.node.supports-encoded-fmts"]) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- make sure that the nodes have at least one common non-raw format
|
||||||
|
local n1 = si:get_associated_proxy ("node")
|
||||||
|
local n2 = si_target:get_associated_proxy ("node")
|
||||||
|
for p1 in n1:iterate_params ("EnumFormat") do
|
||||||
|
local p1p = p1:parse ()
|
||||||
|
if p1p.properties.mediaSubtype ~= "raw" then
|
||||||
|
for p2 in n2:iterate_params ("EnumFormat") do
|
||||||
|
if p1:filter (p2) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.checkFollowDefault (si, si_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.
|
||||||
|
local si_props = si.properties
|
||||||
|
local target_props = si_target.properties
|
||||||
|
local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
|
||||||
|
local is_filter = (si_props ["node.link-group"] ~= nil)
|
||||||
|
|
||||||
|
if reconnect and not is_filter then
|
||||||
|
local def_id = cutils.getDefaultNode (si_props,
|
||||||
|
cutils.getTargetDirection (si_props))
|
||||||
|
|
||||||
|
if target_props ["node.id"] == tostring (def_id) then
|
||||||
|
local metadata = cutils.get_default_metadata_object ()
|
||||||
|
-- Set target.node, for backward compatibility
|
||||||
|
metadata:set (tonumber
|
||||||
|
(si_props ["node.id"]), "target.node", "Spa:Id", "-1")
|
||||||
|
Log.info (si, "... set metadata to follow default")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.lookupLink (si_id, si_target_id)
|
||||||
|
local link = cutils.get_object_manager ("session-item"):lookup {
|
||||||
|
type = "SiLink",
|
||||||
|
Constraint { "out.item.id", "=", si_id },
|
||||||
|
Constraint { "in.item.id", "=", si_target_id }
|
||||||
|
}
|
||||||
|
if not link then
|
||||||
|
link = cutils.get_object_manager ("session-item"):lookup {
|
||||||
|
type = "SiLink",
|
||||||
|
Constraint { "in.item.id", "=", si_id },
|
||||||
|
Constraint { "out.item.id", "=", si_target_id }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return link
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.isLinked (si_target)
|
||||||
|
local target_id = si_target.id
|
||||||
|
local linked = false
|
||||||
|
local exclusive = false
|
||||||
|
|
||||||
|
for l in cutils.get_object_manager ("session-item"):iterate {
|
||||||
|
type = "SiLink",
|
||||||
|
} 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 = cutils.parseBool (p ["exclusive"]) or cutils.parseBool (p ["passthrough"])
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return linked, exclusive
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.getNodePeerId (node_id)
|
||||||
|
for l in cutils.get_object_manager ("link"):iterate() do
|
||||||
|
local p = l.properties
|
||||||
|
local in_id = tonumber(p["link.input.node"])
|
||||||
|
local out_id = tonumber(p["link.output.node"])
|
||||||
|
if in_id == node_id then
|
||||||
|
return out_id
|
||||||
|
elseif out_id == node_id then
|
||||||
|
return in_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.canLink (properties, si_target)
|
||||||
|
local target_props = si_target.properties
|
||||||
|
|
||||||
|
-- nodes must have the same media type
|
||||||
|
if properties ["media.type"] ~= target_props ["media.type"] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isMonitor(properties)
|
||||||
|
return properties ["item.node.direction"] == "input" and
|
||||||
|
cutils.parseBool (properties ["item.features.monitor"]) and
|
||||||
|
not cutils.parseBool (properties ["item.features.no-dsp"]) and
|
||||||
|
properties ["item.factory.name"] == "si-audio-adapter"
|
||||||
|
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
|
||||||
|
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 cutils.get_object_manager ("session-item"):iterate {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
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 cutils.get_object_manager ("session-item"):iterate {
|
||||||
|
type = "SiLink",
|
||||||
|
} 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 = cutils.get_object_manager ("session-item"):lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
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 lutils.findDefaultLinkable (si)
|
||||||
|
local si_props = si.properties
|
||||||
|
local target_direction = cutils.getTargetDirection (si_props)
|
||||||
|
local def_node_id = cutils.getDefaultNode (si_props, target_direction)
|
||||||
|
return cutils.get_object_manager ("session-item"):lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
Constraint { "node.id", "=", tostring (def_node_id) }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function lutils.checkPassthroughCompatibility (si, si_target)
|
||||||
|
local si_must_passthrough =
|
||||||
|
cutils.parseBool (si.properties ["item.node.encoded-only"])
|
||||||
|
local si_target_must_passthrough =
|
||||||
|
cutils.parseBool (si_target.properties ["item.node.encoded-only"])
|
||||||
|
local can_passthrough = lutils.canPassthrough (si, si_target)
|
||||||
|
if (si_must_passthrough or si_target_must_passthrough)
|
||||||
|
and not can_passthrough then
|
||||||
|
return false, can_passthrough
|
||||||
|
end
|
||||||
|
return true, can_passthrough
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Does the target device have any active/available paths/routes to
|
||||||
|
-- the physical device(spkr/mic/cam)?
|
||||||
|
function lutils.haveAvailableRoutes (si_props)
|
||||||
|
local card_profile_device = si_props ["card.profile.device"]
|
||||||
|
local device_id = si_props ["device.id"]
|
||||||
|
local device = device_id and cutils.get_object_manager ("device"):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 = cutils.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 = cutils.parseParam (p, "EnumRoute")
|
||||||
|
if not route then
|
||||||
|
goto skip_enum_route
|
||||||
|
end
|
||||||
|
|
||||||
|
if not cutils.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 lutils.sendClientError (event, node, code, message)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local client_id = node.properties ["client.id"]
|
||||||
|
if client_id then
|
||||||
|
local clients_om = source:call ("get-object-manager", "client")
|
||||||
|
local client = clients_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", client_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if client then
|
||||||
|
client:send_error (node ["bound-id"], code, message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return lutils
|
||||||
197
.config/wireplumber/scripts/lib/monitor-utils.lua
Normal file
197
.config/wireplumber/scripts/lib/monitor-utils.lua
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- Script is a Lua Module of monitor Lua utility functions
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-utils")
|
||||||
|
|
||||||
|
local mutils = {
|
||||||
|
cam_data = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- finds out if any of the managed objects(nodes of a device or devices of
|
||||||
|
-- device enumerator) has duplicate values
|
||||||
|
function mutils.find_duplicate (parent, id, property, value)
|
||||||
|
for i = 0, id - 1, 1 do
|
||||||
|
local obj = parent:get_managed_object (i)
|
||||||
|
if obj and obj.properties[property] == value then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_cam_data(self, dev_id)
|
||||||
|
if not self.cam_data[dev_id] then
|
||||||
|
self.cam_data[dev_id] = {}
|
||||||
|
self.cam_data[dev_id]["libcamera"] = {}
|
||||||
|
self.cam_data[dev_id]["v4l2"] = {}
|
||||||
|
end
|
||||||
|
return self.cam_data[dev_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
function parse_devids_get_cam_data(self, devids)
|
||||||
|
local dev_ids_json = Json.Raw(devids)
|
||||||
|
local dev_ids_table = {}
|
||||||
|
|
||||||
|
if dev_ids_json:is_array() then
|
||||||
|
dev_ids_table = dev_ids_json:parse()
|
||||||
|
else
|
||||||
|
-- to maintain the backward compatibility with earlier pipewire versions.
|
||||||
|
for dev_id_str in devids:gmatch("%S+") do
|
||||||
|
local dev_id = tonumber(dev_id_str)
|
||||||
|
if dev_id then
|
||||||
|
table.insert(dev_ids_table, dev_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local dev_num = nil
|
||||||
|
-- `device.devids` is a json array of device numbers
|
||||||
|
for _, dev_id_str in ipairs(dev_ids_table) do
|
||||||
|
local dev_id = tonumber(dev_id_str)
|
||||||
|
if not dev_id then
|
||||||
|
log:notice ("invalid device number")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log:debug ("Working on device " .. dev_id)
|
||||||
|
local dev_cam_data = get_cam_data (self, dev_id)
|
||||||
|
if not dev_num then
|
||||||
|
dev_num = dev_id
|
||||||
|
if #dev_ids_table > 1 then
|
||||||
|
-- libcam node can some times use more tha one V4L2 devices, in this
|
||||||
|
-- case, return the first device id and mark rest of the them as peers
|
||||||
|
-- to the first one.
|
||||||
|
log:debug ("Device " .. dev_id .. " uses multi V4L2 devices")
|
||||||
|
dev_cam_data.uses_multi_v4l2_devices = true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log:debug ("Device " .. dev_id .. " is peer to " .. dev_num)
|
||||||
|
dev_cam_data.peer_id = dev_num
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if dev_num then
|
||||||
|
return self.cam_data[dev_num], dev_num
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function mutils.clear_cam_data (self, dev_num)
|
||||||
|
local dev_cam_data = self.cam_data[dev_num]
|
||||||
|
if not dev_cam_data then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if dev_cam_data.uses_multi_v4l2_devices then
|
||||||
|
for dev_id, cam_data_ in pairs(self.cam_data) do
|
||||||
|
if cam_data_.peer_id == dev_num then
|
||||||
|
log:debug("clear " .. dev_id .. " it is peer to " .. dev_num)
|
||||||
|
self.cam_data[dev_id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.cam_data[dev_num] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function mutils.create_cam_node(self, dev_num)
|
||||||
|
local api = nil
|
||||||
|
local cam_data = get_cam_data (self, dev_num)
|
||||||
|
|
||||||
|
if cam_data["v4l2"].enum_status and cam_data["libcamera"].enum_status then
|
||||||
|
if cam_data.uses_multi_v4l2_devices then
|
||||||
|
api = "libcamera"
|
||||||
|
elseif cam_data.peer_id ~= nil then
|
||||||
|
-- no need to create node for peer
|
||||||
|
log:notice ("timer expired for peer device " .. dev_num)
|
||||||
|
return
|
||||||
|
elseif cam_data.is_device_uvc then
|
||||||
|
api = "v4l2"
|
||||||
|
else
|
||||||
|
api = "libcamera"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
api = cam_data["v4l2"].enum_status and "v4l2" or "libcamera"
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (string.format ("create \"%s\" node for device:%s", api,
|
||||||
|
cam_data.dev_path))
|
||||||
|
|
||||||
|
source = source or Plugin.find ("standard-event-source")
|
||||||
|
local e = source:call ("create-event", "create-" .. api .. "-device-node",
|
||||||
|
cam_data[api].parent, nil)
|
||||||
|
e:set_data ("factory", cam_data[api].factory)
|
||||||
|
e:set_data ("node-properties", cam_data[api].properties)
|
||||||
|
e:set_data ("node-sub-id", cam_data[api].id)
|
||||||
|
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
|
||||||
|
self:clear_cam_data (dev_num)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- arbitrates between v4l2 and libcamera on who gets to create the device node
|
||||||
|
-- for a device, logic is based on the device number of the device given by both
|
||||||
|
-- the parties.
|
||||||
|
function mutils.register_cam_node (self, parent, id, factory, properties)
|
||||||
|
local api = properties["device.api"]
|
||||||
|
local dev_ids = properties["device.devids"]
|
||||||
|
log:debug(api .. " reported " .. dev_ids)
|
||||||
|
|
||||||
|
local cam_data, dev_num = parse_devids_get_cam_data(self, dev_ids)
|
||||||
|
|
||||||
|
if not cam_data then
|
||||||
|
log:notice (string.format ("device numbers invalid for %s device:%s",
|
||||||
|
api, properties["device.name"]))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- only v4l2 can give this info
|
||||||
|
if properties["api.v4l2.cap.driver"] == "uvcvideo" then
|
||||||
|
log:debug ("Device " .. dev_num .. " is a UVC device")
|
||||||
|
cam_data.is_device_uvc = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- only v4l2 can give this info
|
||||||
|
if properties["api.v4l2.path"] then
|
||||||
|
cam_data.dev_path = properties["api.v4l2.path"]
|
||||||
|
end
|
||||||
|
|
||||||
|
local cam_api_data = cam_data[api]
|
||||||
|
cam_api_data.enum_status = true
|
||||||
|
|
||||||
|
-- cache info, it comes handy when creating node
|
||||||
|
cam_api_data.parent = parent
|
||||||
|
cam_api_data.id = id
|
||||||
|
cam_api_data.name = properties["device.name"]
|
||||||
|
cam_api_data.factory = factory
|
||||||
|
cam_api_data.properties = properties
|
||||||
|
|
||||||
|
local other_api = api == "v4l2" and "libcamera" or "v4l2"
|
||||||
|
if cam_api_data.enum_status and not cam_data[other_api].enum_status then
|
||||||
|
log:trace (string.format ("\"%s\" armed a timer for %d", api, dev_num))
|
||||||
|
cam_data.source = Core.timeout_add (
|
||||||
|
Settings.get_int ("monitor.camera-discovery-timeout"), function()
|
||||||
|
log:trace (string.format ("\"%s\" armed timer expired for %d", api, dev_num))
|
||||||
|
self:create_cam_node (dev_num)
|
||||||
|
cam_data.source = nil
|
||||||
|
end)
|
||||||
|
elseif cam_data.source then
|
||||||
|
log:trace (string.format ("\"%s\" disarmed timer for %d", api, dev_num))
|
||||||
|
cam_data.source:destroy ()
|
||||||
|
cam_data.source = nil
|
||||||
|
self:create_cam_node (dev_num)
|
||||||
|
else
|
||||||
|
log:notice (string.format ("\"%s\" calling after timer expiry for %d:%s%s",
|
||||||
|
api, dev_num, cam_data.dev_path,
|
||||||
|
(cam_data.is_device_uvc and "(uvc)" or "")))
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return mutils
|
||||||
18
.config/wireplumber/scripts/lib/node-utils.lua
Normal file
18
.config/wireplumber/scripts/lib/node-utils.lua
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2024 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
local module = {}
|
||||||
|
|
||||||
|
function module.get_session_priority (node_props)
|
||||||
|
local priority = node_props ["priority.session"]
|
||||||
|
-- fallback to driver priority if session priority is not set
|
||||||
|
if not priority then
|
||||||
|
priority = node_props ["priority.driver"]
|
||||||
|
end
|
||||||
|
return math.tointeger (priority) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return module
|
||||||
115
.config/wireplumber/scripts/linking/find-best-target.lua
Normal file
115
.config/wireplumber/scripts/linking/find-best-target.lua
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Traverse through all the possible targets to pick up target node.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
futils = require ("filter-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-best-target",
|
||||||
|
after = { "linking/find-defined-target",
|
||||||
|
"linking/find-filter-target",
|
||||||
|
"linking/find-media-role-target",
|
||||||
|
"linking/find-default-target" },
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target_direction = cutils.getTargetDirection (si_props)
|
||||||
|
local target_picked = nil
|
||||||
|
local target_can_passthrough = false
|
||||||
|
local target_priority = 0
|
||||||
|
local target_plugged = 0
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item: %s (%s)",
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
for target in om:iterate {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.node.type", "=", "device" },
|
||||||
|
Constraint { "item.node.direction", "=", target_direction },
|
||||||
|
Constraint { "media.type", "=", si_props ["media.type"] },
|
||||||
|
} do
|
||||||
|
local target_props = target.properties
|
||||||
|
local target_node_id = target_props ["node.id"]
|
||||||
|
local si_target_node = target:get_associated_proxy ("node")
|
||||||
|
local si_target_link_group = si_target_node.properties ["node.link-group"]
|
||||||
|
local priority = tonumber (target_props ["priority.session"]) or 0
|
||||||
|
|
||||||
|
log:debug (string.format ("Looking at: %s (%s)",
|
||||||
|
tostring (target_props ["node.name"]),
|
||||||
|
tostring (target_node_id)))
|
||||||
|
|
||||||
|
-- Skip smart filters as best target
|
||||||
|
if si_target_link_group ~= nil and
|
||||||
|
futils.is_filter_smart (target_direction, si_target_link_group) then
|
||||||
|
Log.debug ("... ignoring smart filter as best target")
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
if not lutils.canLink (si_props, target) then
|
||||||
|
log:debug ("... cannot link, skip linkable")
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
if not lutils.haveAvailableRoutes (target_props) then
|
||||||
|
log:debug ("... does not have routes, skip linkable")
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
local passthrough_compatible, can_passthrough =
|
||||||
|
lutils.checkPassthroughCompatibility (si, target)
|
||||||
|
if not passthrough_compatible then
|
||||||
|
log:debug ("... passthrough is not compatible, skip linkable")
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
local plugged = tonumber (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 = target
|
||||||
|
target_can_passthrough = can_passthrough
|
||||||
|
target_priority = priority
|
||||||
|
target_plugged = plugged
|
||||||
|
end
|
||||||
|
::skip_linkable::
|
||||||
|
end
|
||||||
|
|
||||||
|
if target_picked then
|
||||||
|
log:info (si,
|
||||||
|
string.format ("... best target picked: %s (%s), can_passthrough:%s",
|
||||||
|
tostring (target_picked.properties ["node.name"]),
|
||||||
|
tostring (target_picked.properties ["node.id"]),
|
||||||
|
tostring (target_can_passthrough)))
|
||||||
|
si_flags.can_passthrough = target_can_passthrough
|
||||||
|
event:set_data ("target", target_picked)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
58
.config/wireplumber/scripts/linking/find-default-target.lua
Normal file
58
.config/wireplumber/scripts/linking/find-default-target.lua
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Check if default nodes can be picked up as target node.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-default-target",
|
||||||
|
after = { "linking/find-defined-target",
|
||||||
|
"linking/find-filter-target",
|
||||||
|
"linking/find-media-role-target" },
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target_picked = false
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item: %s (%s)",
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
target = lutils.findDefaultLinkable (si)
|
||||||
|
|
||||||
|
local can_passthrough, passthrough_compatible
|
||||||
|
if target then
|
||||||
|
passthrough_compatible, can_passthrough =
|
||||||
|
lutils.checkPassthroughCompatibility (si, target)
|
||||||
|
if lutils.canLink (si_props, target) and passthrough_compatible then
|
||||||
|
target_picked = true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if target_picked then
|
||||||
|
log:info (si,
|
||||||
|
string.format ("... default target picked: %s (%s), can_passthrough:%s",
|
||||||
|
tostring (target.properties ["node.name"]),
|
||||||
|
tostring (target.properties ["node.id"]),
|
||||||
|
tostring (can_passthrough)))
|
||||||
|
si_flags.can_passthrough = can_passthrough
|
||||||
|
event:set_data ("target", target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
131
.config/wireplumber/scripts/linking/find-defined-target.lua
Normal file
131
.config/wireplumber/scripts/linking/find-defined-target.lua
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Check if the target node is defined explicitly.
|
||||||
|
-- This defination can be done in two ways.
|
||||||
|
-- 1. "node.target"/"target.object" in the node properties
|
||||||
|
-- 2. "target.node"/"target.object" in the default metadata
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-defined-target",
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target 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"])))
|
||||||
|
|
||||||
|
local metadata = Settings.get_boolean ("linking.allow-moving-streams") and
|
||||||
|
cutils.get_default_metadata_object ()
|
||||||
|
local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
|
||||||
|
local dont_move = cutils.parseBool (si_props ["node.dont-move"])
|
||||||
|
local target_key
|
||||||
|
local target_value = nil
|
||||||
|
local node_defined = false
|
||||||
|
local target_picked = nil
|
||||||
|
|
||||||
|
if si_props ["target.object"] ~= nil then
|
||||||
|
target_value = si_props ["target.object"]
|
||||||
|
target_key = "object.serial"
|
||||||
|
node_defined = true
|
||||||
|
elseif si_props ["node.target"] ~= nil then
|
||||||
|
target_value = si_props ["node.target"]
|
||||||
|
target_key = "node.id"
|
||||||
|
node_defined = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if metadata and not dont_move then
|
||||||
|
local id = metadata:find (si_props ["node.id"], "target.object")
|
||||||
|
if id ~= nil then
|
||||||
|
target_value = id
|
||||||
|
target_key = "object.serial"
|
||||||
|
node_defined = false
|
||||||
|
else
|
||||||
|
id = metadata:find (si_props ["node.id"], "target.node")
|
||||||
|
if id ~= nil then
|
||||||
|
target_value = id
|
||||||
|
target_key = "node.id"
|
||||||
|
node_defined = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if target_value == "-1" then
|
||||||
|
target_picked = false
|
||||||
|
target = nil
|
||||||
|
elseif target_value and tonumber (target_value) then
|
||||||
|
target = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { target_key, "=", target_value },
|
||||||
|
}
|
||||||
|
if target and lutils.canLink (si_props, target) then
|
||||||
|
target_picked = true
|
||||||
|
end
|
||||||
|
elseif target_value then
|
||||||
|
for lnkbl in om:iterate { type = "SiLinkable" } do
|
||||||
|
local target_props = lnkbl.properties
|
||||||
|
if (target_props ["node.name"] == target_value or
|
||||||
|
target_props ["object.path"] == target_value) and
|
||||||
|
target_props ["item.node.direction"] == cutils.getTargetDirection (si_props) and
|
||||||
|
lutils.canLink (si_props, lnkbl) then
|
||||||
|
target_picked = true
|
||||||
|
target = lnkbl
|
||||||
|
break
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
si_flags.has_defined_target = false
|
||||||
|
if target_picked and target then
|
||||||
|
log:info (si,
|
||||||
|
string.format ("... defined target picked: %s (%s), can_passthrough:%s",
|
||||||
|
tostring (target.properties ["node.name"]),
|
||||||
|
tostring (target.properties ["node.id"]),
|
||||||
|
tostring (can_passthrough)))
|
||||||
|
si_flags.has_node_defined_target = node_defined
|
||||||
|
si_flags.can_passthrough = can_passthrough
|
||||||
|
si_flags.has_defined_target = true
|
||||||
|
event:set_data ("target", target)
|
||||||
|
elseif target_value and dont_fallback then
|
||||||
|
-- send error to client and destroy node if linger is not set
|
||||||
|
local linger = cutils.parseBool (si_props ["node.linger"])
|
||||||
|
if not linger then
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
lutils.sendClientError (event, node, -2, "defined target not found")
|
||||||
|
node:request_destroy ()
|
||||||
|
log:info(si, "... destroyed node as defined target was not found")
|
||||||
|
else
|
||||||
|
log:info(si, "... waiting for defined target as dont-fallback is set")
|
||||||
|
end
|
||||||
|
event:stop_processing ()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
92
.config/wireplumber/scripts/linking/find-filter-target.lua
Normal file
92
.config/wireplumber/scripts/linking/find-filter-target.lua
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Check if the target node is a filter target.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
futils = require ("filter-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
function findFilterTarget (si, om)
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
local link_group = node.properties ["node.link-group"]
|
||||||
|
local target_id = -1
|
||||||
|
|
||||||
|
-- return nil if session item is not a filter node
|
||||||
|
if link_group == nil then
|
||||||
|
return nil, false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return nil if filter is not smart
|
||||||
|
local direction = cutils.getTargetDirection (si.properties)
|
||||||
|
if not futils.is_filter_smart (direction, link_group) then
|
||||||
|
return nil, false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- get the filter target
|
||||||
|
return futils.get_filter_target (direction, link_group), true
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-filter-target",
|
||||||
|
after = "linking/find-defined-target",
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local dont_fallback = cutils.parseBool (si_props ["node.dont-fallback"])
|
||||||
|
local target_picked = false
|
||||||
|
local allow_fallback
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
target, is_smart_filter = findFilterTarget (si, om)
|
||||||
|
|
||||||
|
local can_passthrough, passthrough_compatible
|
||||||
|
if target then
|
||||||
|
passthrough_compatible, can_passthrough =
|
||||||
|
lutils.checkPassthroughCompatibility (si, target)
|
||||||
|
if lutils.canLink (si_props, target) and passthrough_compatible then
|
||||||
|
target_picked = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if target_picked and target then
|
||||||
|
log:info (si,
|
||||||
|
string.format ("... filter target picked: %s (%s), can_passthrough:%s",
|
||||||
|
tostring (target.properties ["node.name"]),
|
||||||
|
tostring (target.properties ["node.id"]),
|
||||||
|
tostring (can_passthrough)))
|
||||||
|
si_flags.can_passthrough = can_passthrough
|
||||||
|
event:set_data ("target", target)
|
||||||
|
elseif is_smart_filter and dont_fallback then
|
||||||
|
-- send error to client and destroy node if linger is not set
|
||||||
|
local linger = cutils.parseBool (si_props ["node.linger"])
|
||||||
|
if not linger then
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
lutils.sendClientError (event, node, -2, "smart filter defined target not found")
|
||||||
|
node:request_destroy ()
|
||||||
|
log:info(si, "... destroyed node as smart filter defined target was not found")
|
||||||
|
else
|
||||||
|
log:info(si, "... waiting for smart filter defined target as dont-fallback is set")
|
||||||
|
end
|
||||||
|
event:stop_processing ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2024 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Select the media role target
|
||||||
|
|
||||||
|
cutils = require("common-utils")
|
||||||
|
lutils = require("linking-utils")
|
||||||
|
log = Log.open_topic("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/find-media-role-target",
|
||||||
|
after = { "linking/find-defined-target",
|
||||||
|
"linking/find-filter-target" },
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-target" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local _, om, si, si_props, _, target =
|
||||||
|
lutils:unwrap_select_target_event (event)
|
||||||
|
|
||||||
|
local target_direction = cutils.getTargetDirection (si_props)
|
||||||
|
local media_role = si_props["media.role"]
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up or if the role is not
|
||||||
|
-- defined
|
||||||
|
if target or media_role == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item %d: %s (%s) role (%s)", si.id,
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"]), media_role))
|
||||||
|
|
||||||
|
for si_target in om:iterate {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.node.direction", "=", target_direction },
|
||||||
|
Constraint { "device.intended-roles", "+" },
|
||||||
|
Constraint { "media.type", "=", si_props["media.type"] },
|
||||||
|
} do
|
||||||
|
|
||||||
|
local roles_json = si_target.properties["device.intended-roles"]
|
||||||
|
local roles_table = Json.Raw(roles_json):parse()
|
||||||
|
|
||||||
|
for _, target_role in ipairs(roles_table) do
|
||||||
|
if target_role == media_role then
|
||||||
|
target = si_target
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if target then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set target
|
||||||
|
if target ~= nil then
|
||||||
|
log:info(si,
|
||||||
|
string.format("... media role target picked: %s (%s)",
|
||||||
|
tostring(target.properties["node.name"]),
|
||||||
|
tostring(target.properties["node.id"])))
|
||||||
|
event:set_data("target", target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- example of a user injectible hook to link a node to a custom target
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/sample-find-user-target",
|
||||||
|
before = "linking/find-defined-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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target is already picked up
|
||||||
|
if target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (si, "in find-user-target")
|
||||||
|
|
||||||
|
-- implement logic here to find a suitable target
|
||||||
|
|
||||||
|
-- store the found target on the event,
|
||||||
|
-- the next hooks will take care of linking
|
||||||
|
event:set_data ("target", target)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Check if the target node is a filter target.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
futils = require ("filter-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/get-filter-from-target",
|
||||||
|
after = { "linking/find-defined-target",
|
||||||
|
"linking/find-filter-target",
|
||||||
|
"linking/find-media-role-target",
|
||||||
|
"linking/find-default-target",
|
||||||
|
"linking/find-best-target" },
|
||||||
|
before = "linking/prepare-link",
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- bypass the hook if the target was not found or if it is a role-based policy target
|
||||||
|
if target == nil or lutils.is_role_policy_target (si_props, target.properties) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- bypass the hook if the session item is a smart filter
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
local node_props = node.properties
|
||||||
|
local link_group = node_props ["node.link-group"]
|
||||||
|
local target_direction = cutils.getTargetDirection (si.properties)
|
||||||
|
if link_group ~= nil and
|
||||||
|
futils.is_filter_smart (target_direction, link_group) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- bypass the hook if target is defined, is a filter and is targetable
|
||||||
|
local target_node = target:get_associated_proxy ("node")
|
||||||
|
local target_node_props = target_node.properties
|
||||||
|
local target_link_group = target_node_props ["node.link-group"]
|
||||||
|
if target_link_group ~= nil and si_flags.has_defined_target then
|
||||||
|
if futils.is_filter_smart (target_direction, target_link_group) and
|
||||||
|
not futils.is_filter_disabled (target_direction, target_link_group) and
|
||||||
|
futils.is_filter_targetable (target_direction, target_link_group) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the filter from the given target if it exists, otherwise get the
|
||||||
|
-- default filter, but only if target was not defined
|
||||||
|
local media_type = si_props["media.type"]
|
||||||
|
local filter_target = futils.get_filter_from_target (target_direction, media_type, target)
|
||||||
|
if filter_target ~= nil then
|
||||||
|
target = filter_target
|
||||||
|
log:info (si, "... got filter for given target")
|
||||||
|
elseif filter_target == nil and not si_flags.has_defined_target then
|
||||||
|
filter_target = futils.get_filter_from_target (target_direction, media_type, nil)
|
||||||
|
if filter_target ~= nil then
|
||||||
|
target = filter_target
|
||||||
|
log:info (si, "... got default filter for given target")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local can_passthrough, passthrough_compatible
|
||||||
|
if target ~= nil then
|
||||||
|
passthrough_compatible, can_passthrough =
|
||||||
|
lutils.checkPassthroughCompatibility (si, target)
|
||||||
|
if lutils.canLink (si_props, target) and passthrough_compatible then
|
||||||
|
target_picked = true;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if target_picked then
|
||||||
|
log:info (si,
|
||||||
|
string.format ("... target picked: %s (%s), can_passthrough:%s",
|
||||||
|
tostring (target.properties ["node.name"]),
|
||||||
|
tostring (target.properties ["node.id"]),
|
||||||
|
tostring (can_passthrough)))
|
||||||
|
si_flags.can_passthrough = can_passthrough
|
||||||
|
event:set_data ("target", target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
157
.config/wireplumber/scripts/linking/link-target.lua
Normal file
157
.config/wireplumber/scripts/linking/link-target.lua
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Links a session item to the target that has been previously selected.
|
||||||
|
-- This is meant to be the last hook in the select-target chain.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
AsyncEventHook {
|
||||||
|
name = "linking/link-target",
|
||||||
|
after = "linking/prepare-link",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-target" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps = {
|
||||||
|
start = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local source, om, si, si_props, si_flags, target =
|
||||||
|
lutils:unwrap_select_target_event (event)
|
||||||
|
|
||||||
|
if not target then
|
||||||
|
-- bypass the hook, nothing to link to.
|
||||||
|
transition:advance ()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target_props = target.properties
|
||||||
|
local out_item = nil
|
||||||
|
local in_item = nil
|
||||||
|
local si_link = nil
|
||||||
|
local passthrough = si_flags.can_passthrough
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item %d: %s (%s)", si.id,
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
local exclusive = cutils.parseBool (si_props ["node.exclusive"])
|
||||||
|
|
||||||
|
-- break rescan if tried more than 5 times with same target
|
||||||
|
if si_flags.failed_peer_id ~= nil and
|
||||||
|
si_flags.failed_peer_id == target.id and
|
||||||
|
si_flags.failed_count ~= nil and
|
||||||
|
si_flags.failed_count > 5 then
|
||||||
|
transition:return_error ("tried to link on last rescan, not retrying "
|
||||||
|
.. tostring (si_link))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if si_props["item.node.direction"] == "output" then
|
||||||
|
-- playback
|
||||||
|
out_item = si
|
||||||
|
in_item = target
|
||||||
|
else
|
||||||
|
-- capture
|
||||||
|
in_item = si
|
||||||
|
out_item = target
|
||||||
|
end
|
||||||
|
|
||||||
|
local is_role_policy_link = lutils.is_role_policy_target (si_props, target_props)
|
||||||
|
|
||||||
|
log:info (si,
|
||||||
|
string.format ("link %s <-> %s passthrough:%s, exclusive:%s, media role link:%s",
|
||||||
|
tostring (si_props ["node.name"]),
|
||||||
|
tostring (target_props ["node.name"]),
|
||||||
|
tostring (passthrough),
|
||||||
|
tostring (exclusive),
|
||||||
|
tostring (is_role_policy_link)))
|
||||||
|
|
||||||
|
-- create and configure link
|
||||||
|
si_link = SessionItem ("si-standard-link")
|
||||||
|
if not si_link:configure {
|
||||||
|
["out.item"] = out_item,
|
||||||
|
["in.item"] = in_item,
|
||||||
|
["passthrough"] = passthrough,
|
||||||
|
["exclusive"] = exclusive,
|
||||||
|
["out.item.port.context"] = "output",
|
||||||
|
["in.item.port.context"] = "input",
|
||||||
|
["media.role"] = si_props["media.role"],
|
||||||
|
["target.media.class"] = target_props["media.class"],
|
||||||
|
["policy.role-based.priority"] = target_props["policy.role-based.priority"],
|
||||||
|
["policy.role-based.action.same-priority"] = target_props["policy.role-based.action.same-priority"],
|
||||||
|
["policy.role-based.action.lower-priority"] = target_props["policy.role-based.action.lower-priority"],
|
||||||
|
["is.role.policy.link"] = is_role_policy_link,
|
||||||
|
["main.item.id"] = si.id,
|
||||||
|
["target.item.id"] = target.id,
|
||||||
|
} then
|
||||||
|
transition:return_error ("failed to configure si-standard-link "
|
||||||
|
.. tostring (si_link))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ids = {si.id, target.id}
|
||||||
|
si_link:connect("link-error", function (_, error_msg)
|
||||||
|
for _, id in ipairs (ids) do
|
||||||
|
local si = om:lookup {
|
||||||
|
Constraint { "id", "=", id, type = "gobject" },
|
||||||
|
}
|
||||||
|
if si then
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
lutils.sendClientError(event, node, -32, error_msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- register
|
||||||
|
si_flags.was_handled = true
|
||||||
|
si_flags.peer_id = target.id
|
||||||
|
si_flags.failed_peer_id = target.id
|
||||||
|
if si_flags.failed_count ~= nil then
|
||||||
|
si_flags.failed_count = si_flags.failed_count + 1
|
||||||
|
else
|
||||||
|
si_flags.failed_count = 1
|
||||||
|
end
|
||||||
|
si_link:register ()
|
||||||
|
|
||||||
|
log:debug (si_link, "registered link between "
|
||||||
|
.. tostring (si) .. " and " .. tostring (target))
|
||||||
|
|
||||||
|
-- only activate non role-based policy links because their activation is
|
||||||
|
-- handled by rescan-media-role-links.lua
|
||||||
|
if not is_role_policy_link then
|
||||||
|
si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
|
||||||
|
if e then
|
||||||
|
transition:return_error (tostring (l) .. " link failed: "
|
||||||
|
.. tostring (e))
|
||||||
|
if si_flags ~= nil then
|
||||||
|
si_flags.peer_id = nil
|
||||||
|
end
|
||||||
|
l:remove ()
|
||||||
|
else
|
||||||
|
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:debug (l, "activated link between "
|
||||||
|
.. tostring (si) .. " and " .. tostring (target))
|
||||||
|
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
lutils.updatePriorityMediaRoleLink(si_link)
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:register ()
|
||||||
125
.config/wireplumber/scripts/linking/prepare-link.lua
Normal file
125
.config/wireplumber/scripts/linking/prepare-link.lua
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- remove the existing link if needed, check the properties of target, which
|
||||||
|
-- indicate it is not available for linking. If no target is available, send
|
||||||
|
-- down an error to the corresponding client.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/prepare-link",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "select-target" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source, _, si, si_props, si_flags, target =
|
||||||
|
lutils:unwrap_select_target_event (event)
|
||||||
|
|
||||||
|
local si_id = si.id
|
||||||
|
local reconnect = not cutils.parseBool (si_props ["node.dont-reconnect"])
|
||||||
|
local exclusive = cutils.parseBool (si_props ["node.exclusive"])
|
||||||
|
local si_must_passthrough = cutils.parseBool (si_props ["item.node.encoded-only"])
|
||||||
|
|
||||||
|
log:info (si, string.format ("handling item %d: %s (%s)", si_id,
|
||||||
|
tostring (si_props ["node.name"]), tostring (si_props ["node.id"])))
|
||||||
|
|
||||||
|
-- Check if item is linked to proper target, otherwise re-link
|
||||||
|
if si_flags.peer_id then
|
||||||
|
if target and si_flags.peer_id == target.id then
|
||||||
|
log:info (si, "... already linked to proper target")
|
||||||
|
|
||||||
|
-- Check this also here, in case in default targets changed
|
||||||
|
if Settings.get_boolean ("linking.follow-default-target") and
|
||||||
|
si_flags.has_node_defined_target then
|
||||||
|
lutils.checkFollowDefault (si, target)
|
||||||
|
end
|
||||||
|
|
||||||
|
target = nil
|
||||||
|
goto done
|
||||||
|
end
|
||||||
|
|
||||||
|
local link = lutils.lookupLink (si_id, si_flags.peer_id)
|
||||||
|
if reconnect then
|
||||||
|
if link ~= nil then
|
||||||
|
-- remove old link
|
||||||
|
if ((link:get_active_features () & Feature.SessionItem.ACTIVE) == 0)
|
||||||
|
then
|
||||||
|
-- remove also not yet activated links: they might never become
|
||||||
|
-- active, and we need not wait for it to become active
|
||||||
|
log:warning (link, "Link was not activated before removing")
|
||||||
|
end
|
||||||
|
si_flags.peer_id = nil
|
||||||
|
link:remove ()
|
||||||
|
log:info (si, "... moving to new target")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if link ~= nil then
|
||||||
|
log:info (si, "... dont-reconnect, not moving")
|
||||||
|
goto done
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if the stream has dont-reconnect and was already linked before,
|
||||||
|
-- don't link it to a new target
|
||||||
|
if not reconnect and si_flags.was_handled then
|
||||||
|
target = nil
|
||||||
|
goto done
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check target's availability
|
||||||
|
if target then
|
||||||
|
local target_is_linked, target_is_exclusive = lutils.isLinked (target)
|
||||||
|
if target_is_exclusive then
|
||||||
|
log:info (si, "... target is linked exclusively")
|
||||||
|
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")
|
||||||
|
target = nil
|
||||||
|
else
|
||||||
|
-- disable passthrough, we can live without it
|
||||||
|
si_flags.can_passthrough = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not target then
|
||||||
|
log:info (si, "... target not found, reconnect:" .. tostring (reconnect))
|
||||||
|
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
if reconnect and si_flags.was_handled then
|
||||||
|
log:info (si, "... waiting reconnect")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local linger = cutils.parseBool (si_props ["node.linger"])
|
||||||
|
|
||||||
|
if linger then
|
||||||
|
log:info (si, "... node linger")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
lutils.sendClientError (event, node, -2,
|
||||||
|
reconnect and "no target node available" or "target not found")
|
||||||
|
|
||||||
|
if not reconnect then
|
||||||
|
log:info (si, "... destroy node")
|
||||||
|
node:request_destroy ()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
::done::
|
||||||
|
event:set_data ("target", target)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
203
.config/wireplumber/scripts/linking/rescan-media-role-links.lua
Normal file
203
.config/wireplumber/scripts/linking/rescan-media-role-links.lua
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2024 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
lutils = require("linking-utils")
|
||||||
|
cutils = require("common-utils")
|
||||||
|
log = Log.open_topic("s-linking")
|
||||||
|
|
||||||
|
function restoreVolume (om, link)
|
||||||
|
setVolume(om, link, 1.0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function duckVolume (om, link)
|
||||||
|
setVolume(om, link, Settings.get_float("linking.role-based.duck-level"))
|
||||||
|
end
|
||||||
|
|
||||||
|
function setVolume (om, link, level)
|
||||||
|
local lprops = link.properties
|
||||||
|
local media_role_si_id = nil
|
||||||
|
local dir = lprops ["item.node.direction"]
|
||||||
|
|
||||||
|
if dir == "output" then
|
||||||
|
media_role_si_id = lprops ["out.item.id"]
|
||||||
|
else
|
||||||
|
media_role_si_id = lprops ["in.item.id"]
|
||||||
|
end
|
||||||
|
|
||||||
|
local media_role_lnkbl = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
Constraint { "id", "=", media_role_si_id, type = "gobject" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- apply volume control on the stream node of the loopback module, instead of
|
||||||
|
-- the sink/source node as it simplyfies the volume ducking and
|
||||||
|
-- restoration.
|
||||||
|
local media_role_other_lnkbl = om:lookup {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
Constraint { "node.link-group", "=", media_role_lnkbl.properties ["node.link-group"] },
|
||||||
|
Constraint { "id", "!", media_role_lnkbl.id, type = "gobject" },
|
||||||
|
}
|
||||||
|
|
||||||
|
if media_role_other_lnkbl then
|
||||||
|
local n = media_role_other_lnkbl:get_associated_proxy("node")
|
||||||
|
if n then
|
||||||
|
log:info(string.format(".. %s volume of media role node \"%s(%d)\" to %f",
|
||||||
|
level < 1.0 and "duck" or "restore", n.properties ["node.name"],
|
||||||
|
n ["bound-id"], level))
|
||||||
|
|
||||||
|
local props = {
|
||||||
|
"Spa:Pod:Object:Param:Props",
|
||||||
|
"Props",
|
||||||
|
volume = level,
|
||||||
|
}
|
||||||
|
|
||||||
|
local param = Pod.Object(props)
|
||||||
|
n:set_param("Props", param)
|
||||||
|
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-media-role-links",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
-- on media client link added and removed
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "link" },
|
||||||
|
Constraint { "is.role.policy.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, om, _, si_props, _, _ =
|
||||||
|
lutils:unwrap_select_target_event(event)
|
||||||
|
|
||||||
|
local metadata_om = source:call("get-object-manager", "metadata")
|
||||||
|
local suspend = getSuspendPlaybackFromMetadata(metadata_om)
|
||||||
|
local pending_activations = 0
|
||||||
|
local mc = si_props ["target.media.class"]
|
||||||
|
local pmrl_active = nil
|
||||||
|
pmrl = lutils.getPriorityMediaRoleLink(mc)
|
||||||
|
|
||||||
|
log:debug("Rescanning media role links...")
|
||||||
|
|
||||||
|
local function onMediaRoleLinkActivated (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 media role link: " .. e)
|
||||||
|
if si_flags ~= nil then
|
||||||
|
si_flags.peer_id = nil
|
||||||
|
end
|
||||||
|
l:remove()
|
||||||
|
else
|
||||||
|
log:info(l, "media role 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 media role links activated")
|
||||||
|
transition:advance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for link in om:iterate {
|
||||||
|
type = "SiLink",
|
||||||
|
Constraint { "is.role.policy.link", "=", true },
|
||||||
|
Constraint { "target.media.class", "=", mc },
|
||||||
|
} do
|
||||||
|
-- deactivate all links if suspend playback metadata is present
|
||||||
|
if suspend then
|
||||||
|
link:deactivate(Feature.SessionItem.ACTIVE)
|
||||||
|
end
|
||||||
|
|
||||||
|
local active = ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
|
||||||
|
|
||||||
|
log:debug(string.format(" .. looking at link(%d) active %s pmrl %s", link.id, tostring(active),
|
||||||
|
tostring(link == pmrl)))
|
||||||
|
|
||||||
|
if link == pmrl then
|
||||||
|
pmrl_active = active
|
||||||
|
restoreVolume(om, pmrl)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local action = lutils.getAction(pmrl, link)
|
||||||
|
|
||||||
|
log:debug(string.format(" .. apply action(%s) on link(%d)", action, link.id, tostring(active)))
|
||||||
|
|
||||||
|
if action == "cork" then
|
||||||
|
if active then
|
||||||
|
link:deactivate(Feature.SessionItem.ACTIVE)
|
||||||
|
end
|
||||||
|
elseif action == "mix" then
|
||||||
|
if not active and not suspend then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
link:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
|
||||||
|
end
|
||||||
|
restoreVolume(om, link)
|
||||||
|
elseif action == "duck" then
|
||||||
|
if not active and not suspend then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
link:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
|
||||||
|
end
|
||||||
|
duckVolume(om, link)
|
||||||
|
else
|
||||||
|
log:warning("Unknown action: " .. action)
|
||||||
|
end
|
||||||
|
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
|
||||||
|
if pmrl and not pmrl_active then
|
||||||
|
pending_activations = pending_activations + 1
|
||||||
|
pmrl:activate(Feature.SessionItem.ACTIVE, onMediaRoleLinkActivated)
|
||||||
|
restoreVolume(om, pmrl)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- just advance transition if no pending activations are needed
|
||||||
|
if pending_activations <= 0 then
|
||||||
|
log:debug("All media role links rescanned")
|
||||||
|
transition:advance()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:register()
|
||||||
235
.config/wireplumber/scripts/linking/rescan.lua
Normal file
235
.config/wireplumber/scripts/linking/rescan.lua
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2020-2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Handle new linkables and trigger rescanning of the graph.
|
||||||
|
-- Rescan the graph by pushing new select-target events for
|
||||||
|
-- all linkables that need to be linked
|
||||||
|
-- Cleanup links when the linkables they are associated with are removed.
|
||||||
|
-- Also, cleanup flags attached to linkables.
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
futils = require ("filter-utils")
|
||||||
|
log = Log.open_topic ("s-linking")
|
||||||
|
handles = {}
|
||||||
|
|
||||||
|
function checkFilter (si, om, handle_nonstreams)
|
||||||
|
-- always handle filters if handle_nonstreams is true, even if it is disabled
|
||||||
|
if handle_nonstreams then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- always return true if this is not a filter
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
local link_group = node.properties["node.link-group"]
|
||||||
|
if link_group == nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local direction = cutils.getTargetDirection (si.properties)
|
||||||
|
|
||||||
|
-- always handle filters that are not smart
|
||||||
|
if not futils.is_filter_smart (direction, link_group) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- dont handle smart filters that are disabled
|
||||||
|
return not futils.is_filter_disabled (direction, link_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
function checkLinkable (si, om, handle_nonstreams)
|
||||||
|
local si_props = si.properties
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- check filters
|
||||||
|
if not checkFilter (si, om, handle_nonstreams) then
|
||||||
|
return false, si_props
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, si_props
|
||||||
|
end
|
||||||
|
|
||||||
|
function unhandleLinkable (si, om)
|
||||||
|
local si_id = si.id
|
||||||
|
local valid, si_props = checkLinkable (si, om, true)
|
||||||
|
if not valid then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (si, string.format ("unhandling item %d", si_id))
|
||||||
|
|
||||||
|
-- iterate over all the links in the graph and
|
||||||
|
-- remove any links associated with this item
|
||||||
|
for silink in om:iterate { type = "SiLink" } 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
|
||||||
|
local in_flags = lutils:get_flags (in_id)
|
||||||
|
local out_flags = lutils:get_flags (out_id)
|
||||||
|
|
||||||
|
if out_id == si_id and in_flags.peer_id == out_id then
|
||||||
|
in_flags.peer_id = nil
|
||||||
|
elseif in_id == si_id and out_flags.peer_id == in_id then
|
||||||
|
out_flags.peer_id = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if cutils.parseBool (silink.properties["is.role.policy.link"]) then
|
||||||
|
lutils.clearPriorityMediaRoleLink(silink)
|
||||||
|
end
|
||||||
|
|
||||||
|
silink:remove ()
|
||||||
|
log:info (silink, "... link removed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lutils:clear_flags (si_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/linkable-removed",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local si = event:get_subject ()
|
||||||
|
local source = event:get_source ()
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
|
||||||
|
unhandleLinkable (si, om)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
function handleLinkables (source)
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
|
||||||
|
for si in om:iterate { type = "SiLinkable" } do
|
||||||
|
local valid, si_props = checkLinkable (si, om)
|
||||||
|
if not valid then
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if we need to link this node at all
|
||||||
|
local autoconnect = cutils.parseBool (si_props ["node.autoconnect"])
|
||||||
|
if not autoconnect then
|
||||||
|
log:debug (si, tostring (si_props ["node.name"]) .. " does not need to be autoconnected")
|
||||||
|
goto skip_linkable
|
||||||
|
end
|
||||||
|
|
||||||
|
-- push event to find target and link
|
||||||
|
source:call ("push-event", "select-target", si, nil)
|
||||||
|
|
||||||
|
::skip_linkable::
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/rescan",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "rescan-for-linking" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local om = source:call ("get-object-manager", "session-item")
|
||||||
|
|
||||||
|
log:info ("rescanning...")
|
||||||
|
|
||||||
|
-- always unlink all filters that are smart and disabled
|
||||||
|
for si in om:iterate {
|
||||||
|
type = "SiLinkable",
|
||||||
|
Constraint { "node.link-group", "+" },
|
||||||
|
} do
|
||||||
|
local node = si:get_associated_proxy ("node")
|
||||||
|
local link_group = node.properties["node.link-group"]
|
||||||
|
local direction = cutils.getTargetDirection (si.properties)
|
||||||
|
if futils.is_filter_smart (direction, link_group) and
|
||||||
|
futils.is_filter_disabled (direction, link_group) then
|
||||||
|
unhandleLinkable (si, om)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handleLinkables (source)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/rescan-trigger",
|
||||||
|
interests = {
|
||||||
|
-- on linkable added or removed, where linkable is adapter or plain node
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
},
|
||||||
|
-- on device Routes changed
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "device-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "c", "Route", "EnumRoute" },
|
||||||
|
},
|
||||||
|
-- on any "default" target changed
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "c", "default.audio.source",
|
||||||
|
"default.audio.sink", "default.video.source" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
source:call ("schedule-rescan", "linking")
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "linking/rescan-trigger-on-filters-metadata-changed",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "filters" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
source:call ("schedule-rescan", "linking")
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
function handleMoveSetting (enable)
|
||||||
|
if (not handles.move_hook) and (enable == true) then
|
||||||
|
handles.move_hook = SimpleEventHook {
|
||||||
|
name = "linking/rescan-trigger-on-target-metadata-changed",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "c", "target.object", "target.node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
source:call ("schedule-rescan", "linking")
|
||||||
|
end
|
||||||
|
}
|
||||||
|
handles.move_hook:register()
|
||||||
|
elseif (handles.move_hook) and (enable == false) then
|
||||||
|
handles.move_hook:remove ()
|
||||||
|
handles.move_hook = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Settings.subscribe ("linking.allow-moving-streams", function ()
|
||||||
|
handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))
|
||||||
|
end)
|
||||||
|
handleMoveSetting (Settings.get_boolean ("linking.allow-moving-streams"))
|
||||||
28
.config/wireplumber/scripts/metadata.lua
Normal file
28
.config/wireplumber/scripts/metadata.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Provides the default metadata object
|
||||||
|
|
||||||
|
Script.async_activation = true
|
||||||
|
|
||||||
|
-- note that args is a WpSpaJson
|
||||||
|
local args = ...
|
||||||
|
args = args:parse(1)
|
||||||
|
|
||||||
|
local metadata_name = args["metadata.name"]
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-metadata")
|
||||||
|
log:info ("creating metadata object: " .. metadata_name)
|
||||||
|
|
||||||
|
impl_metadata = ImplMetadata (metadata_name)
|
||||||
|
impl_metadata:activate (Features.ALL, function (m, e)
|
||||||
|
if e then
|
||||||
|
Script:finish_activation_with_error (
|
||||||
|
"failed to activate the ".. metadata_name .." metadata: " .. tostring (e))
|
||||||
|
else
|
||||||
|
Script:finish_activation ()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -5,11 +5,25 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
-- Receive script arguments from config.lua
|
cutils = require ("common-utils")
|
||||||
local config = ... or {}
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
||||||
-- ensure config.properties is not nil
|
defaults = {}
|
||||||
config.properties = config.properties or {}
|
defaults.node_properties = { -- Midi bridge node properties
|
||||||
|
["factory.name"] = "api.alsa.seq.bridge",
|
||||||
|
|
||||||
|
-- Name set for the node with ALSA MIDI ports
|
||||||
|
["node.name"] = "Midi-Bridge",
|
||||||
|
|
||||||
|
-- Set priorities so that it can be used as a fallback driver (see pipewire#3562)
|
||||||
|
["priority.session"] = "100",
|
||||||
|
["priority.driver"] = "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.monitoring = Core.test_feature ("monitor.alsa-midi.monitoring")
|
||||||
|
config.node_properties = Conf.get_section_as_properties (
|
||||||
|
"monitor.alsa-midi.properties", defaults.node_properties)
|
||||||
|
|
||||||
SND_PATH = "/dev/snd"
|
SND_PATH = "/dev/snd"
|
||||||
SEQ_NAME = "seq"
|
SEQ_NAME = "seq"
|
||||||
@@ -19,18 +33,10 @@ midi_node = nil
|
|||||||
fm_plugin = nil
|
fm_plugin = nil
|
||||||
|
|
||||||
function CreateMidiNode ()
|
function CreateMidiNode ()
|
||||||
-- Midi properties
|
|
||||||
local props = {}
|
|
||||||
if type(config.properties["alsa.midi.node-properties"]) == "table" then
|
|
||||||
props = config.properties["alsa.midi.node-properties"]
|
|
||||||
end
|
|
||||||
props["factory.name"] = "api.alsa.seq.bridge"
|
|
||||||
props["node.name"] = props["node.name"] or "Midi-Bridge"
|
|
||||||
|
|
||||||
-- create the midi node
|
-- create the midi node
|
||||||
local node = Node("spa-node-factory", props)
|
local node = Node("spa-node-factory", config.node_properties)
|
||||||
node:activate(Feature.Proxy.BOUND, function (n)
|
node:activate(Feature.Proxy.BOUND, function (n)
|
||||||
Log.info ("activated Midi bridge")
|
log:info ("activated Midi bridge")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
@@ -38,7 +44,7 @@ end
|
|||||||
|
|
||||||
if GLib.access (SND_SEQ_PATH, "rw") then
|
if GLib.access (SND_SEQ_PATH, "rw") then
|
||||||
midi_node = CreateMidiNode ()
|
midi_node = CreateMidiNode ()
|
||||||
elseif config.properties["alsa.midi.monitoring"] then
|
elseif config.monitoring then
|
||||||
fm_plugin = Plugin.find("file-monitor-api")
|
fm_plugin = Plugin.find("file-monitor-api")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,50 +5,28 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
-- Receive script arguments from config.lua
|
cutils = require ("common-utils")
|
||||||
local config = ... or {}
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
||||||
-- ensure config.properties is not nil
|
config = {}
|
||||||
config.properties = config.properties or {}
|
config.reserve_device = Core.test_feature ("monitor.alsa.reserve-device")
|
||||||
|
config.properties = Conf.get_section_as_properties ("monitor.alsa.properties")
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.alsa.rules", Json.Array {})
|
||||||
|
|
||||||
-- unique device/node name tables
|
-- unique device/node name tables
|
||||||
device_names_table = nil
|
device_names_table = nil
|
||||||
node_names_table = nil
|
node_names_table = nil
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function nonempty(str)
|
function nonempty(str)
|
||||||
return str ~= "" and str or nil
|
return str ~= "" and str or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function applyDefaultDeviceProperties (properties)
|
||||||
|
properties["api.alsa.use-acp"] = true
|
||||||
|
properties["api.acp.auto-port"] = false
|
||||||
|
properties["api.dbus.ReserveDevice1.Priority"] = -20
|
||||||
|
end
|
||||||
|
|
||||||
function createNode(parent, id, obj_type, factory, properties)
|
function createNode(parent, id, obj_type, factory, properties)
|
||||||
local dev_props = parent.properties
|
local dev_props = parent.properties
|
||||||
|
|
||||||
@@ -119,6 +97,8 @@ function createNode(parent, id, obj_type, factory, properties)
|
|||||||
|
|
||||||
properties["node.name"] = name
|
properties["node.name"] = name
|
||||||
|
|
||||||
|
log:info ("Creating node " .. name)
|
||||||
|
|
||||||
-- deduplicate nodes with the same name
|
-- deduplicate nodes with the same name
|
||||||
for counter = 2, 99, 1 do
|
for counter = 2, 99, 1 do
|
||||||
if node_names_table[properties["node.name"]] ~= true then
|
if node_names_table[properties["node.name"]] ~= true then
|
||||||
@@ -126,6 +106,7 @@ function createNode(parent, id, obj_type, factory, properties)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
properties["node.name"] = name .. "." .. counter
|
properties["node.name"] = name .. "." .. counter
|
||||||
|
log:info ("deduplicating node name -> " .. properties["node.name"])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -166,17 +147,17 @@ function createNode(parent, id, obj_type, factory, properties)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- apply VM overrides
|
-- add cpu.vm.name for rule matching purposes
|
||||||
local vm_overrides = config.properties["vm.node.defaults"]
|
local vm_type = Core.get_vm_type()
|
||||||
if nonempty(Core.get_vm_type()) and type(vm_overrides) == "table" then
|
if nonempty(vm_type) then
|
||||||
for k, v in pairs(vm_overrides) do
|
properties["cpu.vm.name"] = vm_type
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- apply properties from config.rules
|
-- apply properties from rules defined in JSON .conf file
|
||||||
rulesApplyProperties(properties)
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
if properties["node.disabled"] then
|
|
||||||
|
if cutils.parseBool (properties ["node.disabled"]) then
|
||||||
|
log:notice ("ALSA node " .. properties["node.name"] .. " disabled")
|
||||||
node_names_table [properties ["node.name"]] = nil
|
node_names_table [properties ["node.name"]] = nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -197,12 +178,14 @@ function createDevice(parent, id, factory, properties)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
node_names_table[node.properties["node.name"]] = nil
|
local node_name = node.properties["node.name"]
|
||||||
|
log:info ("Removing node " .. node_name)
|
||||||
|
node_names_table[node_name] = nil
|
||||||
end)
|
end)
|
||||||
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
||||||
parent:store_managed_object(id, device)
|
parent:store_managed_object(id, device)
|
||||||
else
|
else
|
||||||
Log.warning ("Failed to create '" .. factory .. "' device")
|
log:warning ("Failed to create '" .. factory .. "' device")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -276,16 +259,19 @@ function prepareDevice(parent, id, obj_type, factory, properties)
|
|||||||
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
|
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- apply properties from config.rules
|
-- apply properties from rules defined in JSON .conf file
|
||||||
rulesApplyProperties(properties)
|
applyDefaultDeviceProperties (properties)
|
||||||
if properties["device.disabled"] then
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
|
if cutils.parseBool (properties ["device.disabled"]) then
|
||||||
|
log:notice ("ALSA card/device " .. properties ["device.name"] .. " disabled")
|
||||||
device_names_table [properties ["device.name"]] = nil
|
device_names_table [properties ["device.name"]] = nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- override the device factory to use ACP
|
-- override the device factory to use ACP
|
||||||
if properties["api.alsa.use-acp"] then
|
if cutils.parseBool (properties ["api.alsa.use-acp"]) then
|
||||||
Log.info("Enabling the use of ACP on " .. properties["device.name"])
|
log:info("Enabling the use of ACP on " .. properties["device.name"])
|
||||||
factory = "api.alsa.acp.device"
|
factory = "api.alsa.acp.device"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -294,9 +280,9 @@ function prepareDevice(parent, id, obj_type, factory, properties)
|
|||||||
local rd_name = "Audio" .. properties["api.alsa.card"]
|
local rd_name = "Audio" .. properties["api.alsa.card"]
|
||||||
local rd = rd_plugin:call("create-reservation",
|
local rd = rd_plugin:call("create-reservation",
|
||||||
rd_name,
|
rd_name,
|
||||||
config.properties["alsa.reserve.application-name"] or "WirePlumber",
|
cutils.get_application_name (),
|
||||||
properties["device.name"],
|
properties["device.name"],
|
||||||
config.properties["alsa.reserve.priority"] or -20);
|
properties["api.dbus.ReserveDevice1.Priority"]);
|
||||||
|
|
||||||
properties["api.dbus.ReserveDevice1"] = rd_name
|
properties["api.dbus.ReserveDevice1"] = rd_name
|
||||||
|
|
||||||
@@ -320,22 +306,11 @@ function prepareDevice(parent, id, obj_type, factory, properties)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
rd:connect("release-requested", function (rd)
|
rd:connect("release-requested", function (rd)
|
||||||
Log.info("release requested")
|
log:info("release requested")
|
||||||
parent:store_managed_object(id, nil)
|
parent:store_managed_object(id, nil)
|
||||||
rd:call("release")
|
rd:call("release")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if jack_device then
|
|
||||||
rd:connect("notify::owner-name-changed", function (rd, pspec)
|
|
||||||
if rd["state"] == "busy" and
|
|
||||||
rd["owner-application-name"] == "Jack audio server" then
|
|
||||||
-- TODO enable the jack device
|
|
||||||
else
|
|
||||||
-- TODO disable the jack device
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
rd:call("acquire")
|
rd:call("acquire")
|
||||||
else
|
else
|
||||||
-- create the device
|
-- create the device
|
||||||
@@ -346,8 +321,8 @@ end
|
|||||||
function createMonitor ()
|
function createMonitor ()
|
||||||
local m = SpaDevice("api.alsa.enum.udev", config.properties)
|
local m = SpaDevice("api.alsa.enum.udev", config.properties)
|
||||||
if m == nil then
|
if m == nil then
|
||||||
Log.message("PipeWire's SPA ALSA udev plugin(\"api.alsa.enum.udev\")"
|
log:notice("PipeWire's ALSA SPA plugin is missing or broken. " ..
|
||||||
.. "missing or broken. Sound Cards cannot be enumerated")
|
"Sound cards will not be supported")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -378,30 +353,19 @@ function createMonitor ()
|
|||||||
node_names_table = {}
|
node_names_table = {}
|
||||||
|
|
||||||
-- activate monitor
|
-- activate monitor
|
||||||
Log.info("Activating ALSA monitor")
|
log:info("Activating ALSA monitor")
|
||||||
m:activate(Feature.SpaDevice.ENABLED)
|
m:activate(Feature.SpaDevice.ENABLED)
|
||||||
return m
|
return m
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create the JACK device (for PipeWire to act as client to a JACK server)
|
|
||||||
if config.properties["alsa.jack-device"] then
|
|
||||||
jack_device = Device("spa-device-factory", {
|
|
||||||
["factory.name"] = "api.jack.device",
|
|
||||||
["node.name"] = "JACK-Device",
|
|
||||||
})
|
|
||||||
jack_device:activate(Feature.Proxy.BOUND)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- enable device reservation if requested
|
|
||||||
if config.properties["alsa.reserve"] then
|
|
||||||
rd_plugin = Plugin.find("reserve-device")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- if the reserve-device plugin is enabled, at the point of script execution
|
-- if the reserve-device plugin is enabled, at the point of script execution
|
||||||
-- it is expected to be connected. if it is not, assume the d-bus connection
|
-- it is expected to be connected. if it is not, assume the d-bus connection
|
||||||
-- has failed and continue without it
|
-- has failed and continue without it
|
||||||
|
if config.reserve_device then
|
||||||
|
rd_plugin = Plugin.find("reserve-device")
|
||||||
|
end
|
||||||
if rd_plugin and rd_plugin:call("get-dbus")["state"] ~= "connected" then
|
if rd_plugin and rd_plugin:call("get-dbus")["state"] ~= "connected" then
|
||||||
Log.message("reserve-device plugin is not connected to D-Bus, "
|
log:notice("reserve-device plugin is not connected to D-Bus, "
|
||||||
.. "disabling device reservation")
|
.. "disabling device reservation")
|
||||||
rd_plugin = nil
|
rd_plugin = nil
|
||||||
end
|
end
|
||||||
@@ -412,12 +376,12 @@ if rd_plugin then
|
|||||||
local dbus = rd_plugin:call("get-dbus")
|
local dbus = rd_plugin:call("get-dbus")
|
||||||
dbus:connect("notify::state", function (b, pspec)
|
dbus:connect("notify::state", function (b, pspec)
|
||||||
local state = b["state"]
|
local state = b["state"]
|
||||||
Log.info ("rd-plugin state changed to " .. state)
|
log:info ("rd-plugin state changed to " .. state)
|
||||||
if state == "connected" then
|
if state == "connected" then
|
||||||
Log.info ("Creating ALSA monitor")
|
log:info ("Creating ALSA monitor")
|
||||||
monitor = createMonitor()
|
monitor = createMonitor()
|
||||||
elseif state == "closed" then
|
elseif state == "closed" then
|
||||||
Log.info ("Destroying ALSA monitor")
|
log:info ("Destroying ALSA monitor")
|
||||||
monitor = nil
|
monitor = nil
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -5,42 +5,22 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
local config = ... or {}
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
||||||
|
defaults = {}
|
||||||
|
defaults.servers = { "bluez_midi.server" }
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.seat_monitoring = Core.test_feature ("monitor.bluez.seat-monitoring")
|
||||||
|
config.properties = Conf.get_section_as_properties ("monitor.bluez-midi.properties")
|
||||||
|
config.servers = Conf.get_section_as_array ("monitor.bluez-midi.servers", defaults.servers)
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.bluez-midi.rules", Json.Array {})
|
||||||
|
|
||||||
-- unique device/node name tables
|
-- unique device/node name tables
|
||||||
node_names_table = nil
|
node_names_table = nil
|
||||||
id_to_name_table = nil
|
id_to_name_table = nil
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function setLatencyOffset(node, offset_msec)
|
function setLatencyOffset(node, offset_msec)
|
||||||
if not offset_msec then
|
if not offset_msec then
|
||||||
return
|
return
|
||||||
@@ -50,7 +30,7 @@ function setLatencyOffset(node, offset_msec)
|
|||||||
props.latencyOffsetNsec = tonumber(offset_msec) * 1000000
|
props.latencyOffsetNsec = tonumber(offset_msec) * 1000000
|
||||||
|
|
||||||
local param = Pod.Object(props)
|
local param = Pod.Object(props)
|
||||||
Log.debug(param, "setting latency offset on " .. tostring(node))
|
log:debug(param, "setting latency offset on " .. tostring(node))
|
||||||
node:set_param("Props", param)
|
node:set_param("Props", param)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,8 +59,8 @@ function createNode(parent, id, type, factory, properties)
|
|||||||
|
|
||||||
properties["api.glib.mainloop"] = "true"
|
properties["api.glib.mainloop"] = "true"
|
||||||
|
|
||||||
-- apply properties from config.rules
|
-- apply properties from the rules in the configuration file
|
||||||
rulesApplyProperties(properties)
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
local latency_offset = properties["node.latency-offset-msec"]
|
local latency_offset = properties["node.latency-offset-msec"]
|
||||||
properties["node.latency-offset-msec"] = nil
|
properties["node.latency-offset-msec"] = nil
|
||||||
@@ -101,7 +81,6 @@ function createMonitor()
|
|||||||
for k, v in pairs(config.properties or {}) do
|
for k, v in pairs(config.properties or {}) do
|
||||||
monitor_props[k] = v
|
monitor_props[k] = v
|
||||||
end
|
end
|
||||||
monitor_props["server"] = nil
|
|
||||||
|
|
||||||
monitor_props["api.glib.mainloop"] = "true"
|
monitor_props["api.glib.mainloop"] = "true"
|
||||||
|
|
||||||
@@ -113,7 +92,7 @@ function createMonitor()
|
|||||||
id_to_name_table[id] = nil
|
id_to_name_table[id] = nil
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
Log.message("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
|
log:notice("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -126,16 +105,10 @@ function createMonitor()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function createServers()
|
function createServers()
|
||||||
local props = config.properties or {}
|
|
||||||
|
|
||||||
if not props["servers"] then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local servers = {}
|
local servers = {}
|
||||||
local i = 1
|
local i = 1
|
||||||
|
|
||||||
for k, v in pairs(props["servers"]) do
|
for k, v in pairs(config.servers) do
|
||||||
local node_props = {
|
local node_props = {
|
||||||
["node.name"] = v,
|
["node.name"] = v,
|
||||||
["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
|
["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
|
||||||
@@ -143,7 +116,7 @@ function createServers()
|
|||||||
["factory.name"] = "api.bluez5.midi.node",
|
["factory.name"] = "api.bluez5.midi.node",
|
||||||
["api.glib.mainloop"] = "true",
|
["api.glib.mainloop"] = "true",
|
||||||
}
|
}
|
||||||
rulesApplyProperties(node_props)
|
node_props = JsonUtils.match_rules_update_properties (config.rules, node_props)
|
||||||
|
|
||||||
local latency_offset = node_props["node.latency-offset-msec"]
|
local latency_offset = node_props["node.latency-offset-msec"]
|
||||||
node_props["node.latency-offset-msec"] = nil
|
node_props["node.latency-offset-msec"] = nil
|
||||||
@@ -154,7 +127,7 @@ function createServers()
|
|||||||
table.insert(servers, node)
|
table.insert(servers, node)
|
||||||
setLatencyOffset(node, latency_offset)
|
setLatencyOffset(node, latency_offset)
|
||||||
else
|
else
|
||||||
Log.message("Failed to create BLE MIDI server.")
|
log:notice("Failed to create BLE MIDI server.")
|
||||||
end
|
end
|
||||||
i = i + 1
|
i = i + 1
|
||||||
end
|
end
|
||||||
@@ -162,12 +135,14 @@ function createServers()
|
|||||||
return servers
|
return servers
|
||||||
end
|
end
|
||||||
|
|
||||||
logind_plugin = Plugin.find("logind")
|
if config.seat_monitoring then
|
||||||
|
logind_plugin = Plugin.find("logind")
|
||||||
|
end
|
||||||
if logind_plugin then
|
if logind_plugin then
|
||||||
-- if logind support is enabled, activate
|
-- if logind support is enabled, activate
|
||||||
-- the monitor only when the seat is active
|
-- the monitor only when the seat is active
|
||||||
function startStopMonitor(seat_state)
|
function startStopMonitor(seat_state)
|
||||||
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
|
log:info(logind_plugin, "Seat state changed: " .. seat_state)
|
||||||
|
|
||||||
if seat_state == "active" then
|
if seat_state == "active" then
|
||||||
monitor = createMonitor()
|
monitor = createMonitor()
|
||||||
|
|||||||
@@ -5,7 +5,20 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
local config = ... or {}
|
COMBINE_OFFSET = 64
|
||||||
|
LOOPBACK_SOURCE_ID = 128
|
||||||
|
DEVICE_SOURCE_ID = 0
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.seat_monitoring = Core.test_feature ("monitor.bluez.seat-monitoring")
|
||||||
|
config.properties = Conf.get_section_as_properties ("monitor.bluez.properties")
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.bluez.rules", Json.Array {})
|
||||||
|
|
||||||
|
-- This is not a setting, it must always be enabled
|
||||||
|
config.properties["api.bluez5.connection-info"] = true
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
devices_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
@@ -21,36 +34,6 @@ nodes_om = ObjectManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function setOffloadActive(device, value)
|
function setOffloadActive(device, value)
|
||||||
local pod = Pod.Object {
|
local pod = Pod.Object {
|
||||||
"Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value
|
"Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value
|
||||||
@@ -126,7 +109,7 @@ function createOffloadScoNode(parent, id, type, factory, properties)
|
|||||||
}
|
}
|
||||||
args["playback.props"] = Json.Object(playback_args)
|
args["playback.props"] = Json.Object(playback_args)
|
||||||
else
|
else
|
||||||
Log.warning(parent, "Unsupported factory: " .. factory)
|
log:warning(parent, "Unsupported factory: " .. factory)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -142,16 +125,120 @@ function createOffloadScoNode(parent, id, type, factory, properties)
|
|||||||
parent:store_managed_object(id, loopback)
|
parent:store_managed_object(id, loopback)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
device_set_nodes_om = ObjectManager {
|
||||||
|
Interest {
|
||||||
|
type = "node",
|
||||||
|
Constraint { "api.bluez5.set.leader", "+", type = "pw" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device_set_nodes_om:connect ("object-added", function(_, node)
|
||||||
|
-- Connect ObjectConfig events to the right node
|
||||||
|
if not monitor then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local interest = Interest {
|
||||||
|
type = "device",
|
||||||
|
Constraint { "object.id", "=", node.properties["device.id"] }
|
||||||
|
}
|
||||||
|
log:info("Device set node found: " .. tostring (node["bound-id"]))
|
||||||
|
for device in devices_om:iterate (interest) do
|
||||||
|
local device_id = device.properties["api.bluez5.id"]
|
||||||
|
if not device_id then
|
||||||
|
goto next_device
|
||||||
|
end
|
||||||
|
|
||||||
|
local spa_device = monitor:get_managed_object (tonumber (device_id))
|
||||||
|
if not spa_device then
|
||||||
|
goto next_device
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = node.properties["card.profile.device"]
|
||||||
|
if id ~= nil then
|
||||||
|
log:info(".. assign to device: " .. tostring (device["bound-id"]) .. " node " .. tostring (id))
|
||||||
|
spa_device:store_managed_object (id, node)
|
||||||
|
|
||||||
|
-- set routes again to update volumes etc.
|
||||||
|
for route in device:iterate_params ("Route") do
|
||||||
|
device:set_param ("Route", route)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
::next_device::
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
function createSetNode(parent, id, type, factory, properties)
|
||||||
|
local args = {}
|
||||||
|
local target_class
|
||||||
|
local stream_class
|
||||||
|
local rules = {}
|
||||||
|
local members_json = Json.Raw (properties["api.bluez5.set.members"])
|
||||||
|
local channels_json = Json.Raw (properties["api.bluez5.set.channels"])
|
||||||
|
local members = members_json:parse ()
|
||||||
|
local channels = channels_json:parse ()
|
||||||
|
|
||||||
|
if properties["media.class"] == "Audio/Sink" then
|
||||||
|
args["combine.mode"] = "sink"
|
||||||
|
target_class = "Audio/Sink/Internal"
|
||||||
|
stream_class = "Stream/Output/Audio/Internal"
|
||||||
|
else
|
||||||
|
args["combine.mode"] = "source"
|
||||||
|
target_class = "Audio/Source/Internal"
|
||||||
|
stream_class = "Stream/Input/Audio/Internal"
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info("Device set: " .. properties["node.name"])
|
||||||
|
|
||||||
|
for _, member in pairs(members) do
|
||||||
|
log:info("Device set member:" .. member["object.path"])
|
||||||
|
table.insert(rules,
|
||||||
|
Json.Object {
|
||||||
|
["matches"] = Json.Array {
|
||||||
|
Json.Object {
|
||||||
|
["object.path"] = member["object.path"],
|
||||||
|
["media.class"] = target_class,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
["actions"] = Json.Object {
|
||||||
|
["create-stream"] = Json.Object {
|
||||||
|
["media.class"] = stream_class,
|
||||||
|
["audio.position"] = Json.Array (member["channels"]),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
properties["node.virtual"] = false
|
||||||
|
properties["device.api"] = "bluez5"
|
||||||
|
properties["api.bluez5.set.members"] = nil
|
||||||
|
properties["api.bluez5.set.channels"] = nil
|
||||||
|
properties["api.bluez5.set.leader"] = true
|
||||||
|
properties["audio.position"] = Json.Array (channels)
|
||||||
|
args["combine.props"] = Json.Object (properties)
|
||||||
|
args["stream.props"] = Json.Object {}
|
||||||
|
args["stream.rules"] = Json.Array (rules)
|
||||||
|
|
||||||
|
local args_json = Json.Object(args)
|
||||||
|
local args_string = args_json:get_data()
|
||||||
|
local combine_properties = {}
|
||||||
|
log:info("Device set node: " .. args_string)
|
||||||
|
return LocalModule("libpipewire-module-combine-stream", args_string, combine_properties)
|
||||||
|
end
|
||||||
|
|
||||||
function createNode(parent, id, type, factory, properties)
|
function createNode(parent, id, type, factory, properties)
|
||||||
local dev_props = parent.properties
|
local dev_props = parent.properties
|
||||||
|
local parent_id = parent["bound-id"]
|
||||||
|
|
||||||
if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then
|
if cutils.parseBool (config.properties ["bluez5.hw-offload-sco"]) and factory:find("sco") then
|
||||||
createOffloadScoNode(parent, id, type, factory, properties)
|
createOffloadScoNode(parent, id, type, factory, properties)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set the device id and spa factory name; REQUIRED, do not change
|
-- set the device id and spa factory name; REQUIRED, do not change
|
||||||
properties["device.id"] = parent["bound-id"]
|
properties["device.id"] = parent_id
|
||||||
properties["factory.name"] = factory
|
properties["factory.name"] = factory
|
||||||
|
|
||||||
-- set the default pause-on-idle setting
|
-- set the default pause-on-idle setting
|
||||||
@@ -167,10 +254,21 @@ function createNode(parent, id, type, factory, properties)
|
|||||||
-- sanitize description, replace ':' with ' '
|
-- sanitize description, replace ':' with ' '
|
||||||
properties["node.description"] = desc:gsub("(:)", " ")
|
properties["node.description"] = desc:gsub("(:)", " ")
|
||||||
|
|
||||||
|
local name_prefix = ((factory:find("sink") and "bluez_output") or
|
||||||
|
(factory:find("source") and "bluez_input" or factory))
|
||||||
|
|
||||||
|
-- hide the source node because we use the loopback source instead
|
||||||
|
if parent:get_managed_object (LOOPBACK_SOURCE_ID) ~= nil and
|
||||||
|
(factory == "api.bluez5.sco.source" or
|
||||||
|
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"]))) then
|
||||||
|
properties["bluez5.loopback-target"] = true
|
||||||
|
properties["api.bluez5.internal"] = true
|
||||||
|
-- add 'internal' to name prefix to not be confused with loopback node
|
||||||
|
name_prefix = name_prefix .. "_internal"
|
||||||
|
end
|
||||||
|
|
||||||
-- set the node name
|
-- set the node name
|
||||||
local name =
|
local name = name_prefix .. "." ..
|
||||||
((factory:find("sink") and "bluez_output") or
|
|
||||||
(factory:find("source") and "bluez_input" or factory)) .. "." ..
|
|
||||||
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
|
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
|
||||||
tostring(id)
|
tostring(id)
|
||||||
-- sanitize name
|
-- sanitize name
|
||||||
@@ -190,14 +288,26 @@ function createNode(parent, id, type, factory, properties)
|
|||||||
properties["node.autoconnect"] = true
|
properties["node.autoconnect"] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- apply properties from config.rules
|
-- apply properties from the rules in the configuration file
|
||||||
rulesApplyProperties(properties)
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
-- create the node; bluez requires "local" nodes, i.e. ones that run in
|
-- create the node; bluez requires "local" nodes, i.e. ones that run in
|
||||||
-- the same process as the spa device, for several reasons
|
-- the same process as the spa device, for several reasons
|
||||||
local node = LocalNode("adapter", properties)
|
|
||||||
node:activate(Feature.Proxy.BOUND)
|
if properties["api.bluez5.set.leader"] then
|
||||||
parent:store_managed_object(id, node)
|
local combine = createSetNode(parent, id, type, factory, properties)
|
||||||
|
parent:store_managed_object(id + COMBINE_OFFSET, combine)
|
||||||
|
else
|
||||||
|
properties["bluez5.loopback"] = false
|
||||||
|
local node = LocalNode("adapter", properties)
|
||||||
|
node:activate(Feature.Proxy.BOUND)
|
||||||
|
parent:store_managed_object(id, node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function removeNode(parent, id)
|
||||||
|
-- Clear also the device set module, if any
|
||||||
|
parent:store_managed_object(id + COMBINE_OFFSET, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
function createDevice(parent, id, type, factory, properties)
|
function createDevice(parent, id, type, factory, properties)
|
||||||
@@ -239,22 +349,24 @@ function createDevice(parent, id, type, factory, properties)
|
|||||||
|
|
||||||
-- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
|
-- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
|
||||||
properties["bluez5.profile"] = "off"
|
properties["bluez5.profile"] = "off"
|
||||||
|
properties["api.bluez5.id"] = id
|
||||||
|
|
||||||
-- apply properties from config.rules
|
-- apply properties from the rules in the configuration file
|
||||||
rulesApplyProperties(properties)
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
-- create the device
|
-- create the device
|
||||||
device = SpaDevice(factory, properties)
|
device = SpaDevice(factory, properties)
|
||||||
if device then
|
if device then
|
||||||
device:connect("create-object", createNode)
|
device:connect("create-object", createNode)
|
||||||
|
device:connect("object-removed", removeNode)
|
||||||
parent:store_managed_object(id, device)
|
parent:store_managed_object(id, device)
|
||||||
else
|
else
|
||||||
Log.warning ("Failed to create '" .. factory .. "' device")
|
log:warning ("Failed to create '" .. factory .. "' device")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Log.info(parent, string.format("%d, %s (%s): %s",
|
log:info(parent, string.format("%d, %s (%s): %s",
|
||||||
id, properties["device.description"],
|
id, properties["device.description"],
|
||||||
properties["api.bluez5.address"], properties["api.bluez5.connection"]))
|
properties["api.bluez5.address"], properties["api.bluez5.connection"]))
|
||||||
|
|
||||||
@@ -267,14 +379,12 @@ function createDevice(parent, id, type, factory, properties)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function createMonitor()
|
function createMonitor()
|
||||||
local monitor_props = config.properties or {}
|
local monitor = SpaDevice("api.bluez5.enum.dbus", config.properties)
|
||||||
monitor_props["api.bluez5.connection-info"] = true
|
|
||||||
|
|
||||||
local monitor = SpaDevice("api.bluez5.enum.dbus", monitor_props)
|
|
||||||
if monitor then
|
if monitor then
|
||||||
monitor:connect("create-object", createDevice)
|
monitor:connect("create-object", createDevice)
|
||||||
else
|
else
|
||||||
Log.message("PipeWire's BlueZ SPA missing or broken. Bluetooth not supported.")
|
log:notice("PipeWire's BlueZ SPA plugin is missing or broken. " ..
|
||||||
|
"Bluetooth devices will not be supported.")
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
monitor:activate(Feature.SpaDevice.ENABLED)
|
monitor:activate(Feature.SpaDevice.ENABLED)
|
||||||
@@ -282,12 +392,111 @@ function createMonitor()
|
|||||||
return monitor
|
return monitor
|
||||||
end
|
end
|
||||||
|
|
||||||
logind_plugin = Plugin.find("logind")
|
function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
||||||
|
local args = Json.Object {
|
||||||
|
["capture.props"] = Json.Object {
|
||||||
|
["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
|
||||||
|
["media.class"] = "Stream/Input/Audio/Internal",
|
||||||
|
["node.description"] =
|
||||||
|
string.format ("Bluetooth internal capture stream for %s", dec_desc),
|
||||||
|
["audio.channels"] = 1,
|
||||||
|
["audio.position"] = "[MONO]",
|
||||||
|
["bluez5.loopback"] = true,
|
||||||
|
["stream.dont-remix"] = true,
|
||||||
|
["node.passive"] = true,
|
||||||
|
["node.dont-fallback"] = true,
|
||||||
|
["node.linger"] = true
|
||||||
|
},
|
||||||
|
["playback.props"] = Json.Object {
|
||||||
|
["node.name"] = string.format ("bluez_input.%s", dev_name),
|
||||||
|
["node.description"] = string.format ("%s", dec_desc),
|
||||||
|
["audio.position"] = "[MONO]",
|
||||||
|
["media.class"] = "Audio/Source",
|
||||||
|
["device.id"] = dev_id,
|
||||||
|
["card.profile.device"] = DEVICE_SOURCE_ID,
|
||||||
|
["priority.driver"] = 2010,
|
||||||
|
["priority.session"] = 2010,
|
||||||
|
["bluez5.loopback"] = true,
|
||||||
|
["filter.smart"] = true,
|
||||||
|
["filter.smart.target"] = Json.Object {
|
||||||
|
["bluez5.loopback-target"] = true,
|
||||||
|
["bluez5.loopback"] = false,
|
||||||
|
["device.id"] = dev_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||||
|
end
|
||||||
|
|
||||||
|
function checkProfiles (dev)
|
||||||
|
local device_id = dev["bound-id"]
|
||||||
|
local props = dev.properties
|
||||||
|
|
||||||
|
-- Don't create loopback source device if autoswitch is disabled
|
||||||
|
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the associated BT SpaDevice
|
||||||
|
local internal_id = tostring (props["api.bluez5.id"])
|
||||||
|
local spa_device = monitor:get_managed_object (internal_id)
|
||||||
|
if spa_device == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ignore devices that don't support both A2DP sink and HSP/HFP profiles
|
||||||
|
local has_a2dpsink_profile = false
|
||||||
|
local has_headset_profile = false
|
||||||
|
for p in dev:iterate_params("EnumProfile") do
|
||||||
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
if profile.name:find ("a2dp") and profile.name:find ("sink") then
|
||||||
|
has_a2dpsink_profile = true
|
||||||
|
elseif profile.name:find ("headset") then
|
||||||
|
has_headset_profile = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not has_a2dpsink_profile or not has_headset_profile then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create the loopback device if never created before
|
||||||
|
local loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||||
|
if loopback == nil then
|
||||||
|
local dev_name = props["api.bluez5.address"] or props["device.name"]
|
||||||
|
local dec_desc = props["device.description"] or props["device.name"]
|
||||||
|
or props["device.nick"] or props["device.alias"] or "bluetooth-device"
|
||||||
|
-- sanitize description, replace ':' with ' '
|
||||||
|
dec_desc = dec_desc:gsub("(:)", " ")
|
||||||
|
loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
|
||||||
|
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, loopback)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function onDeviceParamsChanged (dev, param_name)
|
||||||
|
if param_name == "EnumProfile" then
|
||||||
|
checkProfiles (dev)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
devices_om:connect("object-added", function(_, dev)
|
||||||
|
-- Ignore all devices that are not BT devices
|
||||||
|
if dev.properties["device.api"] ~= "bluez5" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check available profiles
|
||||||
|
dev:connect ("params-changed", onDeviceParamsChanged)
|
||||||
|
checkProfiles (dev)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if config.seat_monitoring then
|
||||||
|
logind_plugin = Plugin.find("logind")
|
||||||
|
end
|
||||||
if logind_plugin then
|
if logind_plugin then
|
||||||
-- if logind support is enabled, activate
|
-- if logind support is enabled, activate
|
||||||
-- the monitor only when the seat is active
|
-- the monitor only when the seat is active
|
||||||
function startStopMonitor(seat_state)
|
function startStopMonitor(seat_state)
|
||||||
Log.info(logind_plugin, "Seat state changed: " .. seat_state)
|
log:info(logind_plugin, "Seat state changed: " .. seat_state)
|
||||||
|
|
||||||
if seat_state == "active" then
|
if seat_state == "active" then
|
||||||
monitor = createMonitor()
|
monitor = createMonitor()
|
||||||
@@ -305,3 +514,4 @@ end
|
|||||||
|
|
||||||
nodes_om:activate()
|
nodes_om:activate()
|
||||||
devices_om:activate()
|
devices_om:activate()
|
||||||
|
device_set_nodes_om:activate()
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local config = ... or {}
|
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function findDuplicate(parent, id, property, value)
|
|
||||||
for i = 0, id - 1, 1 do
|
|
||||||
local obj = parent:get_managed_object(i)
|
|
||||||
if obj and obj.properties[property] == value then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function createNode(parent, id, type, factory, properties)
|
|
||||||
local dev_props = parent.properties
|
|
||||||
local location = properties["api.libcamera.location"]
|
|
||||||
|
|
||||||
-- set the device id and spa factory name; REQUIRED, do not change
|
|
||||||
properties["device.id"] = parent["bound-id"]
|
|
||||||
properties["factory.name"] = factory
|
|
||||||
|
|
||||||
-- set the default pause-on-idle setting
|
|
||||||
properties["node.pause-on-idle"] = false
|
|
||||||
|
|
||||||
-- set the node name
|
|
||||||
local name =
|
|
||||||
(factory:find("sink") and "libcamera_output") or
|
|
||||||
(factory:find("source") and "libcamera_input" or factory)
|
|
||||||
.. "." ..
|
|
||||||
(dev_props["device.name"]:gsub("^libcamera_device%.(.+)", "%1") or
|
|
||||||
dev_props["device.name"] or
|
|
||||||
dev_props["device.nick"] or
|
|
||||||
dev_props["device.alias"] or
|
|
||||||
"libcamera-device")
|
|
||||||
-- sanitize name
|
|
||||||
name = name:gsub("([^%w_%-%.])", "_")
|
|
||||||
|
|
||||||
properties["node.name"] = name
|
|
||||||
|
|
||||||
-- deduplicate nodes with the same name
|
|
||||||
for counter = 2, 99, 1 do
|
|
||||||
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
|
|
||||||
properties["node.name"] = name .. "." .. counter
|
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- set the node description
|
|
||||||
local desc = dev_props["device.description"] or "libcamera-device"
|
|
||||||
if location == "front" then
|
|
||||||
desc = I18n.gettext("Built-in Front Camera")
|
|
||||||
elseif location == "back" then
|
|
||||||
desc = I18n.gettext("Built-in Back Camera")
|
|
||||||
end
|
|
||||||
-- sanitize description, replace ':' with ' '
|
|
||||||
properties["node.description"] = desc:gsub("(:)", " ")
|
|
||||||
|
|
||||||
-- set the node nick
|
|
||||||
local nick = properties["node.nick"] or
|
|
||||||
dev_props["device.product.name"] or
|
|
||||||
dev_props["device.description"] or
|
|
||||||
dev_props["device.nick"]
|
|
||||||
properties["node.nick"] = nick:gsub("(:)", " ")
|
|
||||||
|
|
||||||
-- set priority
|
|
||||||
if not properties["priority.session"] then
|
|
||||||
local priority = 700
|
|
||||||
if location == "external" then
|
|
||||||
priority = priority + 150
|
|
||||||
elseif location == "front" then
|
|
||||||
priority = priority + 100
|
|
||||||
elseif location == "back" then
|
|
||||||
priority = priority + 50
|
|
||||||
end
|
|
||||||
properties["priority.session"] = priority
|
|
||||||
end
|
|
||||||
|
|
||||||
-- apply properties from config.rules
|
|
||||||
rulesApplyProperties(properties)
|
|
||||||
if properties ["node.disabled"] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create the node
|
|
||||||
local node = Node("spa-node-factory", properties)
|
|
||||||
node:activate(Feature.Proxy.BOUND)
|
|
||||||
parent:store_managed_object(id, node)
|
|
||||||
end
|
|
||||||
|
|
||||||
function createDevice(parent, id, type, factory, properties)
|
|
||||||
-- ensure the device has an appropriate name
|
|
||||||
local name = "libcamera_device." ..
|
|
||||||
(properties["device.name"] or
|
|
||||||
properties["device.bus-id"] or
|
|
||||||
properties["device.bus-path"] or
|
|
||||||
tostring(id)):gsub("([^%w_%-%.])", "_")
|
|
||||||
|
|
||||||
properties["device.name"] = name
|
|
||||||
|
|
||||||
-- deduplicate devices with the same name
|
|
||||||
for counter = 2, 99, 1 do
|
|
||||||
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
|
|
||||||
properties["device.name"] = name .. "." .. counter
|
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ensure the device has a description
|
|
||||||
properties["device.description"] =
|
|
||||||
properties["device.description"]
|
|
||||||
or properties["device.product.name"]
|
|
||||||
or "Unknown device"
|
|
||||||
|
|
||||||
-- apply properties from config.rules
|
|
||||||
rulesApplyProperties(properties)
|
|
||||||
if properties ["device.disabled"] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- create the device
|
|
||||||
local device = SpaDevice(factory, properties)
|
|
||||||
if device then
|
|
||||||
device:connect("create-object", createNode)
|
|
||||||
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
|
||||||
parent:store_managed_object(id, device)
|
|
||||||
else
|
|
||||||
Log.warning ("Failed to create '" .. factory .. "' device")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
monitor = SpaDevice("api.libcamera.enum.manager", config.properties or {})
|
|
||||||
if monitor then
|
|
||||||
monitor:connect("create-object", createDevice)
|
|
||||||
monitor:activate(Feature.SpaDevice.ENABLED)
|
|
||||||
else
|
|
||||||
Log.message("PipeWire's libcamera SPA missing or broken. libcamera not supported.")
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-libcamera")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.libcamera.rules", Json.Array {})
|
||||||
|
|
||||||
|
function createLibcamNode (parent, id, type, factory, properties)
|
||||||
|
local registered = mutils:register_cam_node (parent, id, factory, properties)
|
||||||
|
if not registered then
|
||||||
|
source = source or Plugin.find ("standard-event-source")
|
||||||
|
local e = source:call ("create-event", "create-libcamera-device-node",
|
||||||
|
parent, nil)
|
||||||
|
e:set_data ("factory", factory)
|
||||||
|
e:set_data ("node-properties", properties)
|
||||||
|
e:set_data ("node-sub-id", id)
|
||||||
|
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/libcamera/create-device",
|
||||||
|
after = "monitor/libcamera/name-device",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-libcamera-device" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("device-properties")
|
||||||
|
local factory = event:get_data ("factory")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local id = event:get_data ("device-sub-id")
|
||||||
|
|
||||||
|
-- apply properties from rules defined in JSON .conf file
|
||||||
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
|
if cutils.parseBool (properties ["device.disabled"]) then
|
||||||
|
log:notice ("libcam device " .. properties["device.name"] .. " disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local device = SpaDevice (factory, properties)
|
||||||
|
|
||||||
|
if device then
|
||||||
|
device:connect ("create-object", createLibcamNode)
|
||||||
|
device:activate (Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
||||||
|
parent:store_managed_object (id, device)
|
||||||
|
else
|
||||||
|
log:warning ("Failed to create '" .. factory .. "' device")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-libcamera")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.libcamera.rules", Json.Array {})
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/libcamera/create-node",
|
||||||
|
after = "monitor/libcamera/name-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-libcamera-device-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("node-properties")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local id = event:get_data ("node-sub-id")
|
||||||
|
|
||||||
|
-- apply properties from rules defined in JSON .conf file
|
||||||
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
|
if cutils.parseBool (properties["node.disabled"]) then
|
||||||
|
log:notice ("libcam node" .. properties ["node.name"] .. " disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- create the node
|
||||||
|
local node = Node ("spa-node-factory", properties)
|
||||||
|
node:activate (Feature.Proxy.BOUND)
|
||||||
|
parent:store_managed_object (id, node)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-monitors-libcamera")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.properties = Conf.get_section_as_properties ("monitor.libcamera.properties")
|
||||||
|
|
||||||
|
function createCamDevice (parent, id, type, factory, properties)
|
||||||
|
source = source or Plugin.find ("standard-event-source")
|
||||||
|
|
||||||
|
local e = source:call ("create-event", "create-libcamera-device", parent, nil)
|
||||||
|
e:set_data ("device-properties", properties)
|
||||||
|
e:set_data ("factory", factory)
|
||||||
|
e:set_data ("device-sub-id", id)
|
||||||
|
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
|
||||||
|
monitor = SpaDevice ("api.libcamera.enum.manager", config.properties)
|
||||||
|
if monitor then
|
||||||
|
monitor:connect ("create-object", createCamDevice)
|
||||||
|
monitor:activate (Feature.SpaDevice.ENABLED)
|
||||||
|
else
|
||||||
|
log:notice ("PipeWire's libcamera SPA plugin is missing or broken. " ..
|
||||||
|
"Some camera types may not be supported.")
|
||||||
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-libcamera")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/libcamera/name-device",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-libcamera-device" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local properties = event:get_data ("device-properties")
|
||||||
|
local id = event:get_data ("device-sub-id")
|
||||||
|
|
||||||
|
local name = "libcamera_device." ..
|
||||||
|
(properties["device.name"] or
|
||||||
|
properties["device.bus-id"] or
|
||||||
|
properties["device.bus-path"] or
|
||||||
|
tostring (id)):gsub ("([^%w_%-%.])", "_")
|
||||||
|
|
||||||
|
properties["device.name"] = name
|
||||||
|
|
||||||
|
-- deduplicate devices with the same name
|
||||||
|
for counter = 2, 99, 1 do
|
||||||
|
if mutils.find_duplicate (parent, id, "device.name", properties["node.name"]) then
|
||||||
|
properties["device.name"] = name .. "." .. counter
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ensure the device has a description
|
||||||
|
properties["device.description"] =
|
||||||
|
properties["device.description"]
|
||||||
|
or properties["device.product.name"]
|
||||||
|
or "Unknown device"
|
||||||
|
|
||||||
|
event:set_data ("device-properties", properties)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
90
.config/wireplumber/scripts/monitors/libcamera/name-node.lua
Normal file
90
.config/wireplumber/scripts/monitors/libcamera/name-node.lua
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-libcamera")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/libcamera/name-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-libcamera-device-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("node-properties")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local dev_props = parent.properties
|
||||||
|
local factory = event:get_data ("factory")
|
||||||
|
local id = event:get_data ("node-sub-id")
|
||||||
|
local location = properties ["api.libcamera.location"]
|
||||||
|
|
||||||
|
-- set the device id and spa factory name; REQUIRED, do not change
|
||||||
|
properties ["device.id"] = parent ["bound-id"]
|
||||||
|
properties ["factory.name"] = factory
|
||||||
|
|
||||||
|
-- set the default pause-on-idle setting
|
||||||
|
properties ["node.pause-on-idle"] = false
|
||||||
|
|
||||||
|
-- set the node name
|
||||||
|
local name =
|
||||||
|
(factory:find ("sink") and "libcamera_output") or
|
||||||
|
(factory:find ("source") and "libcamera_input" or factory)
|
||||||
|
.. "." ..
|
||||||
|
(dev_props ["device.name"]:gsub ("^libcamera_device%.(.+)", "%1") or
|
||||||
|
dev_props ["device.name"] or
|
||||||
|
dev_props ["device.nick"] or
|
||||||
|
dev_props ["device.alias"] or
|
||||||
|
"libcamera-device")
|
||||||
|
-- sanitize name
|
||||||
|
name = name:gsub ("([^%w_%-%.])", "_")
|
||||||
|
|
||||||
|
properties ["node.name"] = name
|
||||||
|
|
||||||
|
-- deduplicate nodes with the same name
|
||||||
|
for counter = 2, 99, 1 do
|
||||||
|
if mutils.find_duplicate (parent, id, "node.name", properties ["node.name"]) then
|
||||||
|
properties ["node.name"] = name .. "." .. counter
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the node description
|
||||||
|
local desc = dev_props ["device.description"] or "libcamera-device"
|
||||||
|
if location == "front" then
|
||||||
|
desc = I18n.gettext ("Built-in Front Camera")
|
||||||
|
elseif location == "back" then
|
||||||
|
desc = I18n.gettext ("Built-in Back Camera")
|
||||||
|
end
|
||||||
|
-- sanitize description, replace ':' with ' '
|
||||||
|
properties ["node.description"] = desc:gsub ("(:)", " ")
|
||||||
|
|
||||||
|
-- set the node nick
|
||||||
|
local nick = properties ["node.nick"] or
|
||||||
|
dev_props ["device.product.name"] or
|
||||||
|
dev_props ["device.description"] or
|
||||||
|
dev_props ["device.nick"]
|
||||||
|
properties ["node.nick"] = nick:gsub ("(:)", " ")
|
||||||
|
|
||||||
|
-- set priority
|
||||||
|
if not properties ["priority.session"] then
|
||||||
|
local priority = 700
|
||||||
|
if location == "external" then
|
||||||
|
priority = priority + 150
|
||||||
|
elseif location == "front" then
|
||||||
|
priority = priority + 100
|
||||||
|
elseif location == "back" then
|
||||||
|
priority = priority + 50
|
||||||
|
end
|
||||||
|
properties ["priority.session"] = priority
|
||||||
|
end
|
||||||
|
|
||||||
|
event:set_data ("node-properties", properties)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local config = ... or {}
|
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function findDuplicate(parent, id, property, value)
|
|
||||||
for i = 0, id - 1, 1 do
|
|
||||||
local obj = parent:get_managed_object(i)
|
|
||||||
if obj and obj.properties[property] == value then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function createNode(parent, id, type, factory, properties)
|
|
||||||
local dev_props = parent.properties
|
|
||||||
|
|
||||||
-- set the device id and spa factory name; REQUIRED, do not change
|
|
||||||
properties["device.id"] = parent["bound-id"]
|
|
||||||
properties["factory.name"] = factory
|
|
||||||
|
|
||||||
-- set the default pause-on-idle setting
|
|
||||||
properties["node.pause-on-idle"] = false
|
|
||||||
|
|
||||||
-- set the node name
|
|
||||||
local name =
|
|
||||||
(factory:find("sink") and "v4l2_output") or
|
|
||||||
(factory:find("source") and "v4l2_input" or factory)
|
|
||||||
.. "." ..
|
|
||||||
(dev_props["device.name"]:gsub("^v4l2_device%.(.+)", "%1") or
|
|
||||||
dev_props["device.name"] or
|
|
||||||
dev_props["device.nick"] or
|
|
||||||
dev_props["device.alias"] or
|
|
||||||
"v4l2-device")
|
|
||||||
-- sanitize name
|
|
||||||
name = name:gsub("([^%w_%-%.])", "_")
|
|
||||||
|
|
||||||
properties["node.name"] = name
|
|
||||||
|
|
||||||
-- deduplicate nodes with the same name
|
|
||||||
for counter = 2, 99, 1 do
|
|
||||||
if findDuplicate(parent, id, "node.name", properties["node.name"]) then
|
|
||||||
properties["node.name"] = name .. "." .. counter
|
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- set the node description
|
|
||||||
local desc = dev_props["device.description"] or "v4l2-device"
|
|
||||||
desc = desc .. " (V4L2)"
|
|
||||||
-- sanitize description, replace ':' with ' '
|
|
||||||
properties["node.description"] = desc:gsub("(:)", " ")
|
|
||||||
|
|
||||||
-- set the node nick
|
|
||||||
local nick = properties["node.nick"] or
|
|
||||||
dev_props["device.product.name"] or
|
|
||||||
dev_props["api.v4l2.cap.card"] or
|
|
||||||
dev_props["device.description"] or
|
|
||||||
dev_props["device.nick"]
|
|
||||||
properties["node.nick"] = nick:gsub("(:)", " ")
|
|
||||||
|
|
||||||
-- set priority
|
|
||||||
if not properties["priority.session"] then
|
|
||||||
local path = properties["api.v4l2.path"] or "/dev/video100"
|
|
||||||
local dev = path:gsub("/dev/video(%d+)", "%1")
|
|
||||||
properties["priority.session"] = 1000 - (tonumber(dev) * 10)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- apply properties from config.rules
|
|
||||||
rulesApplyProperties(properties)
|
|
||||||
if properties["node.disabled"] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create the node
|
|
||||||
local node = Node("spa-node-factory", properties)
|
|
||||||
node:activate(Feature.Proxy.BOUND)
|
|
||||||
parent:store_managed_object(id, node)
|
|
||||||
end
|
|
||||||
|
|
||||||
function createDevice(parent, id, type, factory, properties)
|
|
||||||
-- ensure the device has an appropriate name
|
|
||||||
local name = "v4l2_device." ..
|
|
||||||
(properties["device.name"] or
|
|
||||||
properties["device.bus-id"] or
|
|
||||||
properties["device.bus-path"] or
|
|
||||||
tostring(id)):gsub("([^%w_%-%.])", "_")
|
|
||||||
|
|
||||||
properties["device.name"] = name
|
|
||||||
|
|
||||||
-- deduplicate devices with the same name
|
|
||||||
for counter = 2, 99, 1 do
|
|
||||||
if findDuplicate(parent, id, "device.name", properties["device.name"]) then
|
|
||||||
properties["device.name"] = name .. "." .. counter
|
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ensure the device has a description
|
|
||||||
properties["device.description"] =
|
|
||||||
properties["device.description"]
|
|
||||||
or properties["device.product.name"]
|
|
||||||
or "Unknown device"
|
|
||||||
|
|
||||||
-- apply properties from config.rules
|
|
||||||
rulesApplyProperties(properties)
|
|
||||||
if properties["device.disabled"] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- create the device
|
|
||||||
local device = SpaDevice(factory, properties)
|
|
||||||
if device then
|
|
||||||
device:connect("create-object", createNode)
|
|
||||||
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
|
||||||
parent:store_managed_object(id, device)
|
|
||||||
else
|
|
||||||
Log.warning ("Failed to create '" .. factory .. "' device")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
monitor = SpaDevice("api.v4l2.enum.udev", config.properties or {})
|
|
||||||
if monitor then
|
|
||||||
monitor:connect("create-object", createDevice)
|
|
||||||
monitor:activate(Feature.SpaDevice.ENABLED)
|
|
||||||
else
|
|
||||||
Log.message("PipeWire's V4L SPA missing or broken. Video4Linux not supported.")
|
|
||||||
end
|
|
||||||
61
.config/wireplumber/scripts/monitors/v4l2/create-device.lua
Normal file
61
.config/wireplumber/scripts/monitors/v4l2/create-device.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-v4l2")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.v4l2.rules", Json.Array {})
|
||||||
|
|
||||||
|
function createV4l2camNode (parent, id, type, factory, properties)
|
||||||
|
local registered = mutils:register_cam_node (parent, id, factory, properties)
|
||||||
|
if not registered then
|
||||||
|
source = source or Plugin.find ("standard-event-source")
|
||||||
|
local e = source:call ("create-event", "create-v4l2-device-node",
|
||||||
|
parent, nil)
|
||||||
|
e:set_data ("factory", factory)
|
||||||
|
e:set_data ("node-properties", properties)
|
||||||
|
e:set_data ("node-sub-id", id)
|
||||||
|
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/v4l2/create-device",
|
||||||
|
after = "monitor/v4l2/name-device",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-v4l2-device" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("device-properties")
|
||||||
|
local factory = event:get_data ("factory")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local id = event:get_data ("device-sub-id")
|
||||||
|
|
||||||
|
-- apply properties from rules defined in JSON .conf file
|
||||||
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
|
if cutils.parseBool (properties ["device.disabled"]) then
|
||||||
|
log:notice ("V4L2 device " .. properties["device.name"] .. " disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local device = SpaDevice (factory, properties)
|
||||||
|
|
||||||
|
if device then
|
||||||
|
device:connect ("create-object", createV4l2camNode)
|
||||||
|
device:activate (Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
||||||
|
parent:store_managed_object (id, device)
|
||||||
|
else
|
||||||
|
log:warning ("Failed to create '" .. factory .. "' device")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
42
.config/wireplumber/scripts/monitors/v4l2/create-node.lua
Normal file
42
.config/wireplumber/scripts/monitors/v4l2/create-node.lua
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-v4l2")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("monitor.v4l2.rules", Json.Array {})
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/v4l2/create-node",
|
||||||
|
after = "monitor/v4l2/name-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-v4l2-device-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("node-properties")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local id = event:get_data ("node-sub-id")
|
||||||
|
local factory = event:get_data ("factory")
|
||||||
|
|
||||||
|
-- apply properties from rules defined in JSON .conf file
|
||||||
|
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||||
|
|
||||||
|
if cutils.parseBool (properties ["node.disabled"]) then
|
||||||
|
log:notice ("V4L2 node" .. properties ["node.name"] .. " disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- create the node
|
||||||
|
local node = Node ("spa-node-factory", properties)
|
||||||
|
node:activate (Feature.Proxy.BOUND)
|
||||||
|
parent:store_managed_object (id, node)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-monitors-v4l2")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.properties = Conf.get_section_as_properties ("monitor.v4l2.properties")
|
||||||
|
|
||||||
|
function createCamDevice (parent, id, type, factory, properties)
|
||||||
|
source = source or Plugin.find ("standard-event-source")
|
||||||
|
|
||||||
|
local e = source:call ("create-event", "create-v4l2-device", parent, nil)
|
||||||
|
e:set_data ("device-properties", properties)
|
||||||
|
e:set_data ("factory", factory)
|
||||||
|
e:set_data ("device-sub-id", id)
|
||||||
|
|
||||||
|
EventDispatcher.push_event (e)
|
||||||
|
end
|
||||||
|
|
||||||
|
monitor = SpaDevice ("api.v4l2.enum.udev", config.properties)
|
||||||
|
if monitor then
|
||||||
|
monitor:connect ("create-object", createCamDevice)
|
||||||
|
monitor:activate (Feature.SpaDevice.ENABLED)
|
||||||
|
else
|
||||||
|
log:notice ("PipeWire's V4L2 SPA plugin is missing or broken. " ..
|
||||||
|
"Some camera types may not be supported.")
|
||||||
|
end
|
||||||
49
.config/wireplumber/scripts/monitors/v4l2/name-device.lua
Normal file
49
.config/wireplumber/scripts/monitors/v4l2/name-device.lua
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-v4l2")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/v4l2/name-device",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-v4l2-device" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("device-properties")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local id = event:get_data ("device-sub-id")
|
||||||
|
|
||||||
|
local name = "v4l2_device." ..
|
||||||
|
(properties["device.name"] or
|
||||||
|
properties["device.bus-id"] or
|
||||||
|
properties["device.bus-path"] or
|
||||||
|
tostring (id)):gsub ("([^%w_%-%.])", "_")
|
||||||
|
|
||||||
|
properties["device.name"] = name
|
||||||
|
|
||||||
|
-- deduplicate devices with the same name
|
||||||
|
for counter = 2, 99, 1 do
|
||||||
|
if mutils.find_duplicate (parent, id, "device.name", properties["node.name"]) then
|
||||||
|
properties["device.name"] = name .. "." .. counter
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ensure the device has a description
|
||||||
|
properties["device.description"] =
|
||||||
|
properties["device.description"]
|
||||||
|
or properties["device.product.name"]
|
||||||
|
or "Unknown device"
|
||||||
|
|
||||||
|
event:set_data ("device-properties", properties)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
80
.config/wireplumber/scripts/monitors/v4l2/name-node.lua
Normal file
80
.config/wireplumber/scripts/monitors/v4l2/name-node.lua
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author Ashok Sidipotu <ashok.sidipotu@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mutils = require ("monitor-utils")
|
||||||
|
|
||||||
|
log = Log.open_topic ("s-monitors-v4l2")
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "monitor/v4l2/name-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "create-v4l2-device-node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local properties = event:get_data ("node-properties")
|
||||||
|
local parent = event:get_subject ()
|
||||||
|
local dev_props = parent.properties
|
||||||
|
local factory = event:get_data ("factory")
|
||||||
|
local id = event:get_data ("node-sub-id")
|
||||||
|
|
||||||
|
-- set the device id and spa factory name; REQUIRED, do not change
|
||||||
|
properties["device.id"] = parent["bound-id"]
|
||||||
|
properties["factory.name"] = factory
|
||||||
|
|
||||||
|
-- set the default pause-on-idle setting
|
||||||
|
properties["node.pause-on-idle"] = false
|
||||||
|
|
||||||
|
-- set the node name
|
||||||
|
local name =
|
||||||
|
(factory:find ("sink") and "v4l2_output") or
|
||||||
|
(factory:find ("source") and "v4l2_input" or factory)
|
||||||
|
.. "." ..
|
||||||
|
(dev_props["device.name"]:gsub ("^v4l2_device%.(.+)", "%1") or
|
||||||
|
dev_props["device.name"] or
|
||||||
|
dev_props["device.nick"] or
|
||||||
|
dev_props["device.alias"] or
|
||||||
|
"v4l2-device")
|
||||||
|
-- sanitize name
|
||||||
|
name = name:gsub ("([^%w_%-%.])", "_")
|
||||||
|
|
||||||
|
properties["node.name"] = name
|
||||||
|
|
||||||
|
-- deduplicate nodes with the same name
|
||||||
|
for counter = 2, 99, 1 do
|
||||||
|
if mutils.find_duplicate (parent, id, "node.name", properties["node.name"]) then
|
||||||
|
properties["node.name"] = name .. "." .. counter
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the node description
|
||||||
|
local desc = dev_props["device.description"] or "v4l2-device"
|
||||||
|
desc = desc .. " (V4L2)"
|
||||||
|
-- sanitize description, replace ':' with ' '
|
||||||
|
properties["node.description"] = desc:gsub ("(:)", " ")
|
||||||
|
|
||||||
|
-- set the node nick
|
||||||
|
local nick = properties["node.nick"] or
|
||||||
|
dev_props["device.product.name"] or
|
||||||
|
dev_props["api.v4l2.cap.card"] or
|
||||||
|
dev_props["device.description"] or
|
||||||
|
dev_props["device.nick"]
|
||||||
|
properties["node.nick"] = nick:gsub ("(:)", " ")
|
||||||
|
|
||||||
|
-- set priority
|
||||||
|
if not properties["priority.session"] then
|
||||||
|
local path = properties["api.v4l2.path"] or "/dev/video100"
|
||||||
|
local dev = path:gsub ("/dev/video(%d+)", "%1")
|
||||||
|
properties["priority.session"] = 1000 - (tonumber (dev) * 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
event:set_data ("node-properties", properties)
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
154
.config/wireplumber/scripts/node/create-item.lua
Normal file
154
.config/wireplumber/scripts/node/create-item.lua
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021 Collabora Ltd.
|
||||||
|
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
-- create-item.lua script takes pipewire nodes and creates session items (a.k.a
|
||||||
|
-- linkable) objects out of them.
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-node")
|
||||||
|
|
||||||
|
items = {}
|
||||||
|
|
||||||
|
function configProperties (node)
|
||||||
|
local properties = node.properties
|
||||||
|
local media_class = properties ["media.class"] or ""
|
||||||
|
|
||||||
|
-- ensure a media.type is set
|
||||||
|
if not properties ["media.type"] then
|
||||||
|
for _, i in ipairs ({ "Audio", "Video", "Midi" }) do
|
||||||
|
if media_class:find (i) then
|
||||||
|
properties ["media.type"] = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
properties ["item.node"] = node
|
||||||
|
properties ["item.node.direction"] =
|
||||||
|
cutils.mediaClassToDirection (media_class)
|
||||||
|
properties ["item.node.type"] =
|
||||||
|
media_class:find ("^Stream/") and "stream" or "device"
|
||||||
|
properties ["item.plugged.usec"] = GLib.get_monotonic_time ()
|
||||||
|
properties ["item.features.no-dsp"] =
|
||||||
|
Settings.get_boolean ("node.features.audio.no-dsp")
|
||||||
|
properties ["item.features.monitor"] =
|
||||||
|
Settings.get_boolean ("node.features.audio.monitor-ports")
|
||||||
|
properties ["item.features.control-port"] =
|
||||||
|
Settings.get_boolean ("node.features.audio.control-port")
|
||||||
|
properties ["node.id"] = node ["bound-id"]
|
||||||
|
|
||||||
|
-- set the default media.role, if configured
|
||||||
|
-- avoid Settings.get_string(), as it will parse the default "null" value
|
||||||
|
-- as a string instead of returning nil
|
||||||
|
local default_role = Settings.get ("node.stream.default-media-role")
|
||||||
|
if default_role then
|
||||||
|
default_role = default_role:parse()
|
||||||
|
properties ["media.role"] = properties ["media.role"] or default_role
|
||||||
|
end
|
||||||
|
|
||||||
|
return properties
|
||||||
|
end
|
||||||
|
|
||||||
|
AsyncEventHook {
|
||||||
|
name = "node/create-item",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
||||||
|
Constraint { "wireplumber.is-virtual", "-", type = "pw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps = {
|
||||||
|
start = {
|
||||||
|
next = "register",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local id = node.id
|
||||||
|
local item
|
||||||
|
local item_type
|
||||||
|
|
||||||
|
local media_class = node.properties ['media.class']
|
||||||
|
if string.find (media_class, "Audio") then
|
||||||
|
item_type = "si-audio-adapter"
|
||||||
|
else
|
||||||
|
item_type = "si-node"
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (node, "creating item for node -> " .. item_type)
|
||||||
|
|
||||||
|
-- create item
|
||||||
|
item = SessionItem (item_type)
|
||||||
|
items [id] = item
|
||||||
|
|
||||||
|
-- configure item
|
||||||
|
if not item:configure (configProperties (node)) then
|
||||||
|
transition:return_error ("failed to configure item for node "
|
||||||
|
.. tostring (id))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- activate item
|
||||||
|
item:activate (Features.ALL, function (_, e)
|
||||||
|
if e then
|
||||||
|
transition:return_error ("failed to activate item: "
|
||||||
|
.. tostring (e));
|
||||||
|
else
|
||||||
|
transition:advance ()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
register = {
|
||||||
|
next = "none",
|
||||||
|
execute = function (event, transition)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local bound_id = node ["bound-id"]
|
||||||
|
local item = items [node.id]
|
||||||
|
|
||||||
|
log:info (item, "activated item for node " .. tostring (bound_id))
|
||||||
|
item:register ()
|
||||||
|
transition:advance ()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}:register ()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "node/destroy-item",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
|
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
|
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
|
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
||||||
|
Constraint { "wireplumber.is-virtual", "-", type = "pw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local id = node.id
|
||||||
|
if items [id] then
|
||||||
|
items [id]:remove ()
|
||||||
|
items [id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
111
.config/wireplumber/scripts/node/filter-forward-format.lua
Normal file
111
.config/wireplumber/scripts/node/filter-forward-format.lua
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022 Collabora Ltd.
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- Logic to "forward" the format set on special filter nodes to their
|
||||||
|
-- virtual device peer node. This is for things like the "loopback" module,
|
||||||
|
-- which always comes in pairs of 2 nodes, one stream and one virtual device.
|
||||||
|
--
|
||||||
|
-- FIXME: this script can be further improved
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
|
log = Log.open_topic ("s-node")
|
||||||
|
|
||||||
|
function findAssociatedLinkGroupNode (si)
|
||||||
|
local si_props = si.properties
|
||||||
|
local link_group = si_props ["node.link-group"]
|
||||||
|
if link_group == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local std_event_source = Plugin.find ("standard-event-source")
|
||||||
|
local om = std_event_source:call ("get-object-manager", "session-item")
|
||||||
|
|
||||||
|
-- get the associated media class
|
||||||
|
local assoc_direction = cutils.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 om:iterate { type = "SiLinkable" } do
|
||||||
|
local assoc_props = assoc_si.properties
|
||||||
|
local assoc_link_group = assoc_props ["node.link-group"]
|
||||||
|
if assoc_link_group == link_group and
|
||||||
|
assoc_media_class == assoc_props ["media.class"] then
|
||||||
|
return assoc_si
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function onLinkGroupPortsStateChanged (si, old_state, new_state)
|
||||||
|
local si_props = si.properties
|
||||||
|
|
||||||
|
-- only handle items with configured ports state
|
||||||
|
if new_state ~= "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
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "node/filter-forward-format",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "session-item-added" },
|
||||||
|
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||||
|
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||||
|
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local si = event:get_subject ()
|
||||||
|
|
||||||
|
-- Forward filters ports format to associated virtual devices if enabled
|
||||||
|
if Settings.get_boolean ("node.filter.forward-format") then
|
||||||
|
local si_props = si.properties
|
||||||
|
local link_group = si_props ["node.link-group"]
|
||||||
|
local si_flags = lutils:get_flags (si.id)
|
||||||
|
|
||||||
|
-- only listen for ports state changed on audio filter streams
|
||||||
|
if si_flags.ports_state_signal ~= true and
|
||||||
|
si_props ["item.factory.name"] == "si-audio-adapter" and
|
||||||
|
si_props ["item.node.type"] == "stream" and
|
||||||
|
link_group ~= nil then
|
||||||
|
si:connect ("adapter-ports-state-changed", onLinkGroupPortsStateChanged)
|
||||||
|
si_flags.ports_state_signal = true
|
||||||
|
log:info (si, "listening ports state changed on " .. si_props ["node.name"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register ()
|
||||||
92
.config/wireplumber/scripts/node/software-dsp.lua
Normal file
92
.config/wireplumber/scripts/node/software-dsp.lua
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2022-2023 The WirePlumber project contributors
|
||||||
|
-- @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
log = Log.open_topic("s-node")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json("node.software-dsp.rules", Json.Array{})
|
||||||
|
|
||||||
|
-- TODO: port from Obj Manager to Hooks
|
||||||
|
clients_om = ObjectManager {
|
||||||
|
Interest { type = "client" }
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_nodes = {}
|
||||||
|
hidden_nodes = {}
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "node/dsp/create-dsp-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local node = event:get_subject()
|
||||||
|
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
|
||||||
|
if action == "create-filter" then
|
||||||
|
local props = value:parse (1)
|
||||||
|
log:debug("DSP rule found for " .. node.properties["node.name"])
|
||||||
|
|
||||||
|
if props["filter-graph"] then
|
||||||
|
log:debug("Loading filter graph for " .. node.properties["node.name"])
|
||||||
|
filter_nodes[node.id] = LocalModule("libpipewire-module-filter-chain", props["filter-graph"], {})
|
||||||
|
elseif props["filter-path"] then
|
||||||
|
log:debug("Loading filter graph for " .. node.properties["node.name"] .. " from disk")
|
||||||
|
local conf = Conf(props["filter-path"], {
|
||||||
|
["as-section"] = "node.software-dsp.graph",
|
||||||
|
["no-fragments"] = true
|
||||||
|
})
|
||||||
|
local err = conf:open()
|
||||||
|
if not err then
|
||||||
|
local args = conf:get_section_as_json("node.software-dsp.graph"):to_string()
|
||||||
|
filter_nodes[node.id] = LocalModule("libpipewire-module-filter-chain", args, {})
|
||||||
|
else
|
||||||
|
log:warning("Unable to load filter graph for " .. node.properties["node.name"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if props["hide-parent"] then
|
||||||
|
log:debug("Setting permissions to '-' on " .. node.properties["node.name"] .. " for open clients")
|
||||||
|
for client in clients_om:iterate{ type = "client" } do
|
||||||
|
if not client["properties"]["wireplumber.daemon"] then
|
||||||
|
client:update_permissions{ [node["bound-id"]] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
hidden_nodes[node["bound-id"]] = node.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
|
|
||||||
|
SimpleEventHook {
|
||||||
|
name = "node/dsp/free-dsp-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function(event)
|
||||||
|
local node = event:get_subject()
|
||||||
|
if filter_nodes[node.id] then
|
||||||
|
log:debug("Freeing filter on node " .. node.id)
|
||||||
|
filter_nodes[node.id] = nil
|
||||||
|
hidden_nodes[node["bound-id"]] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}:register()
|
||||||
|
|
||||||
|
clients_om:connect("object-added", function (om, client)
|
||||||
|
for id, _ in pairs(hidden_nodes) do
|
||||||
|
if not client["properties"]["wireplumber.daemon"] then
|
||||||
|
client:update_permissions { [id] = "-" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
clients_om:activate()
|
||||||
452
.config/wireplumber/scripts/node/state-stream.lua
Normal file
452
.config/wireplumber/scripts/node/state-stream.lua
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2021-2022 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- Based on restore-stream.c from pipewire-media-session
|
||||||
|
-- Copyright © 2020 Wim Taymans
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cutils = require ("common-utils")
|
||||||
|
log = Log.open_topic ("s-node")
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
config.rules = Conf.get_section_as_json ("stream.rules", Json.Array {})
|
||||||
|
|
||||||
|
-- the state storage
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
|
||||||
|
-- Support for the "System Sounds" volume control in pavucontrol
|
||||||
|
rs_metadata = nil
|
||||||
|
|
||||||
|
-- hook to restore stream properties & target
|
||||||
|
restore_stream_hook = SimpleEventHook {
|
||||||
|
name = "node/restore-stream",
|
||||||
|
interests = {
|
||||||
|
-- match stream nodes
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "matches", "Stream/*" },
|
||||||
|
},
|
||||||
|
-- and device nodes that are not associated with any routes
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "matches", "Audio/*" },
|
||||||
|
Constraint { "device.routes", "is-absent" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-added" },
|
||||||
|
Constraint { "media.class", "matches", "Audio/*" },
|
||||||
|
Constraint { "device.routes", "equals", "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local stream_props = node.properties
|
||||||
|
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||||
|
|
||||||
|
local key = formKey (stream_props)
|
||||||
|
if not key then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local stored_values = getStoredStreamProps (key) or {}
|
||||||
|
|
||||||
|
-- restore node Props (volumes, channelMap, etc...)
|
||||||
|
if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
|
||||||
|
then
|
||||||
|
local props = {
|
||||||
|
"Spa:Pod:Object:Param:Props", "Props",
|
||||||
|
volume = stored_values.volume,
|
||||||
|
mute = stored_values.mute,
|
||||||
|
channelVolumes = stored_values.channelVolumes ~= nil and
|
||||||
|
stored_values.channelVolumes or buildDefaultChannelVolumes (node),
|
||||||
|
channelMap = stored_values.channelMap,
|
||||||
|
}
|
||||||
|
-- convert arrays to Spa Pod
|
||||||
|
if props.channelVolumes then
|
||||||
|
table.insert (props.channelVolumes, 1, "Spa:Float")
|
||||||
|
props.channelVolumes = Pod.Array (props.channelVolumes)
|
||||||
|
end
|
||||||
|
if props.channelMap then
|
||||||
|
table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
|
||||||
|
props.channelMap = Pod.Array (props.channelMap)
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.volume or (props.mute ~= nil) or props.channelVolumes or props.channelMap
|
||||||
|
then
|
||||||
|
log:info (node, "restore values from " .. key)
|
||||||
|
|
||||||
|
local param = Pod.Object (props)
|
||||||
|
log:debug (param, "setting props on " .. tostring (stream_props ["node.name"]))
|
||||||
|
node:set_param ("Props", param)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- restore the node's link target on metadata
|
||||||
|
if Settings.get_boolean ("node.stream.restore-target") and stream_props ["state.restore-target"] ~= "false"
|
||||||
|
then
|
||||||
|
if stored_values.target then
|
||||||
|
-- check first if there is a defined target in the node's properties
|
||||||
|
-- and skip restoring if this is the case (#335)
|
||||||
|
local target_in_props =
|
||||||
|
stream_props ["target.object"] or stream_props ["node.target"]
|
||||||
|
|
||||||
|
if not target_in_props then
|
||||||
|
local source = event:get_source ()
|
||||||
|
local nodes_om = source:call ("get-object-manager", "node")
|
||||||
|
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||||
|
|
||||||
|
local target_node = nodes_om:lookup {
|
||||||
|
Constraint { "node.name", "=", stored_values.target, type = "pw" }
|
||||||
|
}
|
||||||
|
local metadata = metadata_om:lookup {
|
||||||
|
Constraint { "metadata.name", "=", "default" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if target_node and metadata then
|
||||||
|
metadata:set (node ["bound-id"], "target.object", "Spa:Id",
|
||||||
|
target_node.properties ["object.serial"])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log:debug (node,
|
||||||
|
"Not restoring the target for " ..
|
||||||
|
tostring (stream_props ["node.name"]) ..
|
||||||
|
" because it is already set to " .. target_in_props)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- store stream properties on the state file
|
||||||
|
store_stream_props_hook = SimpleEventHook {
|
||||||
|
name = "node/store-stream-props",
|
||||||
|
interests = {
|
||||||
|
-- match stream nodes
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "Props" },
|
||||||
|
Constraint { "media.class", "matches", "Stream/*" },
|
||||||
|
},
|
||||||
|
-- and device nodes that are not associated with any routes
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "Props" },
|
||||||
|
Constraint { "media.class", "matches", "Audio/*" },
|
||||||
|
Constraint { "device.routes", "is-absent" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-params-changed" },
|
||||||
|
Constraint { "event.subject.param-id", "=", "Props" },
|
||||||
|
Constraint { "media.class", "matches", "Audio/*" },
|
||||||
|
Constraint { "device.routes", "equals", "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local stream_props = node.properties
|
||||||
|
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||||
|
|
||||||
|
if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
|
||||||
|
then
|
||||||
|
local key = formKey (stream_props)
|
||||||
|
if not key then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local stored_values = getStoredStreamProps (key) or {}
|
||||||
|
local hasChanges = false
|
||||||
|
|
||||||
|
log:info (node, "saving stream props for " ..
|
||||||
|
tostring (stream_props ["node.name"]))
|
||||||
|
|
||||||
|
for p in node:iterate_params ("Props") do
|
||||||
|
local props = cutils.parseParam (p, "Props")
|
||||||
|
if not props then
|
||||||
|
goto skip_prop
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.volume ~= stored_values.volume then
|
||||||
|
stored_values.volume = props.volume
|
||||||
|
hasChanges = true
|
||||||
|
end
|
||||||
|
if props.mute ~= stored_values.mute then
|
||||||
|
stored_values.mute = props.mute
|
||||||
|
hasChanges = true
|
||||||
|
end
|
||||||
|
if props.channelVolumes then
|
||||||
|
stored_values.channelVolumes = props.channelVolumes
|
||||||
|
hasChanges = true
|
||||||
|
end
|
||||||
|
if props.channelMap then
|
||||||
|
stored_values.channelMap = props.channelMap
|
||||||
|
hasChanges = true
|
||||||
|
end
|
||||||
|
|
||||||
|
::skip_prop::
|
||||||
|
end
|
||||||
|
|
||||||
|
if hasChanges then
|
||||||
|
saveStreamProps (key, stored_values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- save "target.node"/"target.object" on metadata changes
|
||||||
|
store_stream_target_hook = SimpleEventHook {
|
||||||
|
name = "node/store-stream-target-metadata-changed",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "default" },
|
||||||
|
Constraint { "event.subject.key", "c", "target.object", "target.node" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local source = event:get_source ()
|
||||||
|
local nodes_om = source:call ("get-object-manager", "node")
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local subject_id = props ["event.subject.id"]
|
||||||
|
local target_key = props ["event.subject.key"]
|
||||||
|
local target_value = props ["event.subject.value"]
|
||||||
|
|
||||||
|
local node = nodes_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", subject_id, type = "gobject" }
|
||||||
|
}
|
||||||
|
if not node then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local stream_props = node.properties
|
||||||
|
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||||
|
|
||||||
|
if stream_props ["state.restore-target"] == "false" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local key = formKey (stream_props)
|
||||||
|
if not key then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target_name = nil
|
||||||
|
|
||||||
|
if target_value and target_value ~= "-1" then
|
||||||
|
local target_node
|
||||||
|
if target_key == "target.object" then
|
||||||
|
target_node = nodes_om:lookup {
|
||||||
|
Constraint { "object.serial", "=", target_value, type = "pw-global" }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
target_node = nodes_om:lookup {
|
||||||
|
Constraint { "bound-id", "=", target_value, type = "gobject" }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if target_node then
|
||||||
|
target_name = target_node.properties ["node.name"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (node, "saving stream target for " ..
|
||||||
|
tostring (stream_props ["node.name"]) .. " -> " .. tostring (target_name))
|
||||||
|
|
||||||
|
local stored_values = getStoredStreamProps (key) or {}
|
||||||
|
stored_values.target = target_name
|
||||||
|
saveStreamProps (key, stored_values)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- populate route-settings metadata
|
||||||
|
function populateMetadata (metadata)
|
||||||
|
-- copy state into the metadata
|
||||||
|
local key = "Output/Audio:media.role:Notification"
|
||||||
|
local p = getStoredStreamProps (key)
|
||||||
|
if p then
|
||||||
|
p.channels = p.channelMap and Json.Array (p.channelMap)
|
||||||
|
p.volumes = p.channelVolumes and Json.Array (p.channelVolumes)
|
||||||
|
p.channelMap = nil
|
||||||
|
p.channelVolumes = nil
|
||||||
|
p.target = nil
|
||||||
|
|
||||||
|
-- pipewire-pulse expects the key to be
|
||||||
|
-- "restore.stream.Output/Audio.media.role:Notification"
|
||||||
|
key = string.gsub (key, ":", ".", 1);
|
||||||
|
metadata:set (0, "restore.stream." .. key, "Spa:String:JSON",
|
||||||
|
Json.Object (p):to_string ())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- track route-settings metadata changes
|
||||||
|
route_settings_metadata_changed_hook = SimpleEventHook {
|
||||||
|
name = "node/route-settings-metadata-changed",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "metadata-changed" },
|
||||||
|
Constraint { "metadata.name", "=", "route-settings" },
|
||||||
|
Constraint { "event.subject.key", "=",
|
||||||
|
"restore.stream.Output/Audio.media.role:Notification" },
|
||||||
|
Constraint { "event.subject.spa_type", "=", "Spa:String:JSON" },
|
||||||
|
Constraint { "event.subject.value", "is-present" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local props = event:get_properties ()
|
||||||
|
local subject_id = props ["event.subject.id"]
|
||||||
|
local key = props ["event.subject.key"]
|
||||||
|
local value = props ["event.subject.value"]
|
||||||
|
|
||||||
|
local json = Json.Raw (value)
|
||||||
|
if json == nil or not json:is_object () then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local vparsed = json:parse ()
|
||||||
|
|
||||||
|
-- we store the key as "Output/Audio:media.role:Notification"
|
||||||
|
local key = string.sub (key, string.len ("restore.stream.") + 1)
|
||||||
|
key = string.gsub (key, "%.", ":", 1);
|
||||||
|
|
||||||
|
local stored_values = getStoredStreamProps (key) or {}
|
||||||
|
|
||||||
|
if vparsed.volume ~= nil then
|
||||||
|
stored_values.volume = vparsed.volume
|
||||||
|
end
|
||||||
|
if vparsed.mute ~= nil then
|
||||||
|
stored_values.mute = vparsed.mute
|
||||||
|
end
|
||||||
|
if vparsed.channels ~= nil then
|
||||||
|
stored_values.channelMap = vparsed.channels
|
||||||
|
end
|
||||||
|
if vparsed.volumes ~= nil then
|
||||||
|
stored_values.channelVolumes = vparsed.volumes
|
||||||
|
end
|
||||||
|
saveStreamProps (key, stored_values)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultChannelVolumes (node)
|
||||||
|
local node_props = node.properties
|
||||||
|
local direction = cutils.mediaClassToDirection (node_props ["media.class"] or "")
|
||||||
|
local def_vol = 1.0
|
||||||
|
local channels = 2
|
||||||
|
local res = {}
|
||||||
|
|
||||||
|
local str = node.properties["state.default-volume"]
|
||||||
|
if str ~= nil then
|
||||||
|
def_vol = tonumber (str)
|
||||||
|
elseif direction == "input" then
|
||||||
|
def_vol = Settings.get_float ("node.stream.default-capture-volume")
|
||||||
|
elseif direction == "output" then
|
||||||
|
def_vol = Settings.get_float ("node.stream.default-playback-volume")
|
||||||
|
end
|
||||||
|
|
||||||
|
for pod in node:iterate_params("Format") do
|
||||||
|
local pod_parsed = pod:parse()
|
||||||
|
if pod_parsed ~= nil then
|
||||||
|
channels = pod_parsed.properties.channels
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
log:info (node, "using default volume: " .. tostring(def_vol) ..
|
||||||
|
", channels: " .. tostring(channels))
|
||||||
|
|
||||||
|
while (#res < channels) do
|
||||||
|
table.insert(res, def_vol)
|
||||||
|
end
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
function getStoredStreamProps (key)
|
||||||
|
local value = state_table [key]
|
||||||
|
if not value then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local json = Json.Raw (value)
|
||||||
|
if not json or not json:is_object () then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return json:parse ()
|
||||||
|
end
|
||||||
|
|
||||||
|
function saveStreamProps (key, p)
|
||||||
|
assert (type (p) == "table")
|
||||||
|
|
||||||
|
p.channelMap = p.channelMap and Json.Array (p.channelMap)
|
||||||
|
p.channelVolumes = p.channelVolumes and Json.Array (p.channelVolumes)
|
||||||
|
|
||||||
|
state_table [key] = Json.Object (p):to_string ()
|
||||||
|
state:save_after_timeout (state_table)
|
||||||
|
end
|
||||||
|
|
||||||
|
function formKey (properties)
|
||||||
|
local keys = {
|
||||||
|
"media.role",
|
||||||
|
"application.id",
|
||||||
|
"application.name",
|
||||||
|
"media.name",
|
||||||
|
"node.name",
|
||||||
|
}
|
||||||
|
local key_base = nil
|
||||||
|
|
||||||
|
for _, k in ipairs (keys) do
|
||||||
|
local p = properties [k]
|
||||||
|
if p then
|
||||||
|
key_base = string.format ("%s:%s:%s",
|
||||||
|
properties ["media.class"]:gsub ("^Stream/", ""), k, p)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return key_base
|
||||||
|
end
|
||||||
|
|
||||||
|
function toggleState (enable)
|
||||||
|
if enable and not state then
|
||||||
|
state = State ("stream-properties")
|
||||||
|
state_table = state:load ()
|
||||||
|
|
||||||
|
restore_stream_hook:register ()
|
||||||
|
store_stream_props_hook:register ()
|
||||||
|
store_stream_target_hook:register ()
|
||||||
|
route_settings_metadata_changed_hook:register ()
|
||||||
|
|
||||||
|
rs_metadata = ImplMetadata ("route-settings")
|
||||||
|
rs_metadata:activate (Features.ALL, function (m, e)
|
||||||
|
if e then
|
||||||
|
log:warning ("failed to activate route-settings metadata: " .. tostring (e))
|
||||||
|
else
|
||||||
|
populateMetadata (m)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
elseif not enable and state then
|
||||||
|
state = nil
|
||||||
|
state_table = nil
|
||||||
|
restore_stream_hook:remove ()
|
||||||
|
store_stream_props_hook:remove ()
|
||||||
|
store_stream_target_hook:remove ()
|
||||||
|
route_settings_metadata_changed_hook:remove ()
|
||||||
|
rs_metadata = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Settings.subscribe ("node.stream.restore-props", function ()
|
||||||
|
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||||
|
Settings.get_boolean ("node.stream.restore-target"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
Settings.subscribe ("node.stream.restore-target", function ()
|
||||||
|
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||||
|
Settings.get_boolean ("node.stream.restore-target"))
|
||||||
|
end)
|
||||||
|
|
||||||
|
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||||
|
Settings.get_boolean ("node.stream.restore-target"))
|
||||||
@@ -5,19 +5,28 @@
|
|||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
om = ObjectManager {
|
log = Log.open_topic ("s-node")
|
||||||
Interest { type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Audio/*" }
|
|
||||||
},
|
|
||||||
Interest { type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Video/*" }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
sources = {}
|
sources = {}
|
||||||
|
|
||||||
om:connect("object-added", function (om, node)
|
SimpleEventHook {
|
||||||
node:connect("state-changed", function (node, old_state, cur_state)
|
name = "node/suspend-node",
|
||||||
|
interests = {
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-state-changed" },
|
||||||
|
Constraint { "media.class", "matches", "Audio/*" },
|
||||||
|
},
|
||||||
|
EventInterest {
|
||||||
|
Constraint { "event.type", "=", "node-state-changed" },
|
||||||
|
Constraint { "media.class", "matches", "Video/*" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute = function (event)
|
||||||
|
local node = event:get_subject ()
|
||||||
|
local new_state = event:get_properties ()["event.subject.new-state"]
|
||||||
|
|
||||||
|
log:debug (node, "changed state to " .. new_state)
|
||||||
|
|
||||||
-- Always clear the current source if any
|
-- Always clear the current source if any
|
||||||
local id = node["bound-id"]
|
local id = node["bound-id"]
|
||||||
if sources[id] then
|
if sources[id] then
|
||||||
@@ -26,7 +35,7 @@ om:connect("object-added", function (om, node)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Add a timeout source if idle for at least 5 seconds
|
-- Add a timeout source if idle for at least 5 seconds
|
||||||
if cur_state == "idle" or cur_state == "error" then
|
if new_state == "idle" or new_state == "error" then
|
||||||
-- honor "session.suspend-timeout-seconds" if specified
|
-- honor "session.suspend-timeout-seconds" if specified
|
||||||
local timeout =
|
local timeout =
|
||||||
tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
|
tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
|
||||||
@@ -38,8 +47,11 @@ om:connect("object-added", function (om, node)
|
|||||||
-- add idle timeout; multiply by 1000, timeout_add() expects ms
|
-- add idle timeout; multiply by 1000, timeout_add() expects ms
|
||||||
sources[id] = Core.timeout_add(timeout * 1000, function()
|
sources[id] = Core.timeout_add(timeout * 1000, function()
|
||||||
-- Suspend the node
|
-- Suspend the node
|
||||||
Log.info(node, "was idle for a while; suspending ...")
|
-- but check first if the node still exists
|
||||||
node:send_command("Suspend")
|
if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
|
||||||
|
log:info(node, "was idle for a while; suspending ...")
|
||||||
|
node:send_command("Suspend")
|
||||||
|
end
|
||||||
|
|
||||||
-- Unref the source
|
-- Unref the source
|
||||||
sources[id] = nil
|
sources[id] = nil
|
||||||
@@ -49,8 +61,5 @@ om:connect("object-added", function (om, node)
|
|||||||
return false
|
return false
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end)
|
}:register ()
|
||||||
end)
|
|
||||||
|
|
||||||
om:activate()
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Asymptotic Inc.
|
|
||||||
-- @author Sanchayan Maity <sanchayan@asymptotic.io>
|
|
||||||
--
|
|
||||||
-- Based on bt-profile-switch.lua in tests/examples
|
|
||||||
-- Copyright © 2021 George Kiagiadakis
|
|
||||||
--
|
|
||||||
-- Based on bluez-autoswitch in media-session
|
|
||||||
-- Copyright © 2021 Pauli Virtanen
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
--
|
|
||||||
-- Checks for the existence of media.role and if present switches the bluetooth
|
|
||||||
-- profile accordingly. Also see bluez-autoswitch in media-session.
|
|
||||||
-- The intended logic of the script is as follows.
|
|
||||||
--
|
|
||||||
-- When a stream comes in, if it has a Communication or phone role in PulseAudio
|
|
||||||
-- speak in props, we switch to the highest priority profile that has an Input
|
|
||||||
-- route available. The reason for this is that we may have microphone enabled
|
|
||||||
-- non-HFP codecs eg. Faststream.
|
|
||||||
-- We track the incoming streams with Communication role or the applications
|
|
||||||
-- specified which do not set the media.role correctly perhaps.
|
|
||||||
-- When a stream goes away if the list with which we track the streams above
|
|
||||||
-- is empty, then we revert back to the old profile.
|
|
||||||
|
|
||||||
local config = ...
|
|
||||||
local use_persistent_storage = config["use-persistent-storage"] or false
|
|
||||||
local applications = {}
|
|
||||||
local use_headset_profile = config["media-role.use-headset-profile"] or false
|
|
||||||
local profile_restore_timeout_msec = 2000
|
|
||||||
|
|
||||||
local INVALID = -1
|
|
||||||
local timeout_source = nil
|
|
||||||
local restore_timeout_source = nil
|
|
||||||
|
|
||||||
local state = use_persistent_storage and State("policy-bluetooth") or nil
|
|
||||||
local headset_profiles = state and state:load() or {}
|
|
||||||
local last_profiles = {}
|
|
||||||
|
|
||||||
local active_streams = {}
|
|
||||||
local previous_streams = {}
|
|
||||||
|
|
||||||
for _, value in ipairs(config["media-role.applications"] or {}) do
|
|
||||||
applications[value] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "metadata",
|
|
||||||
Constraint { "metadata.name", "=", "default" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "device",
|
|
||||||
Constraint { "device.api", "=", "bluez5" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
streams_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
||||||
-- Do not consider monitor streams
|
|
||||||
Constraint { "stream.monitor", "!", "true" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local function parseParam(param_to_parse, id)
|
|
||||||
local param = param_to_parse:parse()
|
|
||||||
if param.pod_type == "Object" and param.object_id == id then
|
|
||||||
return param.properties
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function storeAfterTimeout()
|
|
||||||
if not use_persistent_storage then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if timeout_source then
|
|
||||||
timeout_source:destroy()
|
|
||||||
end
|
|
||||||
timeout_source = Core.timeout_add(1000, function ()
|
|
||||||
local saved, err = state:save(headset_profiles)
|
|
||||||
if not saved then
|
|
||||||
Log.warning(err)
|
|
||||||
end
|
|
||||||
timeout_source = nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function saveHeadsetProfile(device, profile_name)
|
|
||||||
local key = "saved-headset-profile:" .. device.properties["device.name"]
|
|
||||||
headset_profiles[key] = profile_name
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getSavedHeadsetProfile(device)
|
|
||||||
local key = "saved-headset-profile:" .. device.properties["device.name"]
|
|
||||||
return headset_profiles[key]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function saveLastProfile(device, profile_name)
|
|
||||||
last_profiles[device.properties["device.name"]] = profile_name
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getSavedLastProfile(device)
|
|
||||||
return last_profiles[device.properties["device.name"]]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isSwitched(device)
|
|
||||||
return getSavedLastProfile(device) ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isBluez5AudioSink(sink_name)
|
|
||||||
if sink_name and string.find(sink_name, "bluez_output.") ~= nil then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isBluez5DefaultAudioSink()
|
|
||||||
local metadata = metadata_om:lookup()
|
|
||||||
local default_audio_sink = metadata:find(0, "default.audio.sink")
|
|
||||||
return isBluez5AudioSink(default_audio_sink)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function findProfile(device, index, name)
|
|
||||||
for p in device:iterate_params("EnumProfile") do
|
|
||||||
local profile = parseParam(p, "EnumProfile")
|
|
||||||
if not profile then
|
|
||||||
goto skip_enum_profile
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.debug("Profile name: " .. profile.name .. ", priority: "
|
|
||||||
.. tostring(profile.priority) .. ", index: " .. tostring(profile.index))
|
|
||||||
if (index ~= nil and profile.index == index) or
|
|
||||||
(name ~= nil and profile.name == name) then
|
|
||||||
return profile.priority, profile.index, profile.name
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_enum_profile::
|
|
||||||
end
|
|
||||||
|
|
||||||
return INVALID, INVALID, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getCurrentProfile(device)
|
|
||||||
for p in device:iterate_params("Profile") do
|
|
||||||
local profile = parseParam(p, "Profile")
|
|
||||||
if profile then
|
|
||||||
return profile.name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function highestPrioProfileWithInputRoute(device)
|
|
||||||
local profile_priority = INVALID
|
|
||||||
local profile_index = INVALID
|
|
||||||
local profile_name = nil
|
|
||||||
|
|
||||||
for p in device:iterate_params("EnumRoute") do
|
|
||||||
local route = parseParam(p, "EnumRoute")
|
|
||||||
-- Parse pod
|
|
||||||
if not route then
|
|
||||||
goto skip_enum_route
|
|
||||||
end
|
|
||||||
|
|
||||||
if route.direction ~= "Input" then
|
|
||||||
goto skip_enum_route
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.debug("Route with index: " .. tostring(route.index) .. ", direction: "
|
|
||||||
.. route.direction .. ", name: " .. route.name .. ", description: "
|
|
||||||
.. route.description .. ", priority: " .. route.priority)
|
|
||||||
if route.profiles then
|
|
||||||
for _, v in pairs(route.profiles) do
|
|
||||||
local priority, index, name = findProfile(device, v)
|
|
||||||
if priority ~= INVALID then
|
|
||||||
if profile_priority < priority then
|
|
||||||
profile_priority = priority
|
|
||||||
profile_index = index
|
|
||||||
profile_name = name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_enum_route::
|
|
||||||
end
|
|
||||||
|
|
||||||
return profile_priority, profile_index, profile_name
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hasProfileInputRoute(device, profile_index)
|
|
||||||
for p in device:iterate_params("EnumRoute") do
|
|
||||||
local route = parseParam(p, "EnumRoute")
|
|
||||||
if route and route.direction == "Input" and route.profiles then
|
|
||||||
for _, v in pairs(route.profiles) do
|
|
||||||
if v == profile_index then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function switchProfile()
|
|
||||||
local index
|
|
||||||
local name
|
|
||||||
|
|
||||||
if restore_timeout_source then
|
|
||||||
restore_timeout_source:destroy()
|
|
||||||
restore_timeout_source = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
for device in devices_om:iterate() do
|
|
||||||
if isSwitched(device) then
|
|
||||||
goto skip_device
|
|
||||||
end
|
|
||||||
|
|
||||||
local cur_profile_name = getCurrentProfile(device)
|
|
||||||
saveLastProfile(device, cur_profile_name)
|
|
||||||
|
|
||||||
_, index, name = findProfile(device, nil, cur_profile_name)
|
|
||||||
if hasProfileInputRoute(device, index) then
|
|
||||||
Log.info("Current profile has input route, not switching")
|
|
||||||
goto skip_device
|
|
||||||
end
|
|
||||||
|
|
||||||
local saved_headset_profile = getSavedHeadsetProfile(device)
|
|
||||||
index = INVALID
|
|
||||||
if saved_headset_profile then
|
|
||||||
_, index, name = findProfile(device, nil, saved_headset_profile)
|
|
||||||
end
|
|
||||||
if index == INVALID then
|
|
||||||
_, index, name = highestPrioProfileWithInputRoute(device)
|
|
||||||
end
|
|
||||||
|
|
||||||
if index ~= INVALID then
|
|
||||||
local pod = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
|
||||||
index = index
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Setting profile of '"
|
|
||||||
.. device.properties["device.description"]
|
|
||||||
.. "' from: " .. cur_profile_name
|
|
||||||
.. " to: " .. name)
|
|
||||||
device:set_params("Profile", pod)
|
|
||||||
else
|
|
||||||
Log.warning("Got invalid index when switching profile")
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_device::
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function restoreProfile()
|
|
||||||
for device in devices_om:iterate() do
|
|
||||||
if isSwitched(device) then
|
|
||||||
local profile_name = getSavedLastProfile(device)
|
|
||||||
local cur_profile_name = getCurrentProfile(device)
|
|
||||||
|
|
||||||
saveLastProfile(device, nil)
|
|
||||||
|
|
||||||
if cur_profile_name then
|
|
||||||
Log.info("Setting saved headset profile to: " .. cur_profile_name)
|
|
||||||
saveHeadsetProfile(device, cur_profile_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
if profile_name then
|
|
||||||
local _, index, name = findProfile(device, nil, profile_name)
|
|
||||||
|
|
||||||
if index ~= INVALID then
|
|
||||||
local pod = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
|
||||||
index = index
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Restoring profile of '"
|
|
||||||
.. device.properties["device.description"]
|
|
||||||
.. "' from: " .. cur_profile_name
|
|
||||||
.. " to: " .. name)
|
|
||||||
device:set_params("Profile", pod)
|
|
||||||
else
|
|
||||||
Log.warning("Failed to restore profile")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function triggerRestoreProfile()
|
|
||||||
if restore_timeout_source then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if next(active_streams) ~= nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
restore_timeout_source = Core.timeout_add(profile_restore_timeout_msec, function ()
|
|
||||||
restore_timeout_source = nil
|
|
||||||
restoreProfile()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- We consider a Stream of interest to have role Communication if it has
|
|
||||||
-- media.role set to Communication in props or it is in our list of
|
|
||||||
-- applications as these applications do not set media.role correctly or at
|
|
||||||
-- all.
|
|
||||||
local function checkStreamStatus(stream)
|
|
||||||
local app_name = stream.properties["application.name"]
|
|
||||||
local stream_role = stream.properties["media.role"]
|
|
||||||
|
|
||||||
if not (stream_role == "Communication" or applications[app_name]) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
if not isBluez5DefaultAudioSink() then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If a stream we previously saw stops running, we consider it
|
|
||||||
-- inactive, because some applications (Teams) just cork input
|
|
||||||
-- streams, but don't close them.
|
|
||||||
if previous_streams[stream["bound-id"]] and stream.state ~= "running" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handleStream(stream)
|
|
||||||
if not use_headset_profile then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if checkStreamStatus(stream) then
|
|
||||||
active_streams[stream["bound-id"]] = true
|
|
||||||
previous_streams[stream["bound-id"]] = true
|
|
||||||
switchProfile()
|
|
||||||
else
|
|
||||||
active_streams[stream["bound-id"]] = nil
|
|
||||||
triggerRestoreProfile()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handleAllStreams()
|
|
||||||
for stream in streams_om:iterate {
|
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
||||||
Constraint { "stream.monitor", "!", "true" }
|
|
||||||
} do
|
|
||||||
handleStream(stream)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
streams_om:connect("object-added", function (_, stream)
|
|
||||||
stream:connect("state-changed", function (stream, old_state, cur_state)
|
|
||||||
handleStream(stream)
|
|
||||||
end)
|
|
||||||
stream:connect("params-changed", handleStream)
|
|
||||||
handleStream(stream)
|
|
||||||
end)
|
|
||||||
|
|
||||||
streams_om:connect("object-removed", function (_, stream)
|
|
||||||
active_streams[stream["bound-id"]] = nil
|
|
||||||
previous_streams[stream["bound-id"]] = nil
|
|
||||||
triggerRestoreProfile()
|
|
||||||
end)
|
|
||||||
|
|
||||||
devices_om:connect("object-added", function (_, device)
|
|
||||||
-- Devices are unswitched initially
|
|
||||||
if isSwitched(device) then
|
|
||||||
saveLastProfile(device, nil)
|
|
||||||
end
|
|
||||||
handleAllStreams()
|
|
||||||
end)
|
|
||||||
|
|
||||||
metadata_om:connect("object-added", function (_, metadata)
|
|
||||||
metadata:connect("changed", function (m, subject, key, t, value)
|
|
||||||
if (use_headset_profile and subject == 0 and key == "default.audio.sink"
|
|
||||||
and isBluez5AudioSink(value)) then
|
|
||||||
-- If bluez sink is set as default, rescan for active input streams
|
|
||||||
handleAllStreams()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
metadata_om:activate()
|
|
||||||
devices_om:activate()
|
|
||||||
streams_om:activate()
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2022 Collabora Ltd.
|
|
||||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local self = {}
|
|
||||||
self.config = ... or {}
|
|
||||||
self.config.persistent = self.config.persistent or {}
|
|
||||||
self.active_profiles = {}
|
|
||||||
self.default_profile_plugin = Plugin.find("default-profile")
|
|
||||||
|
|
||||||
-- Preprocess persisten profiles and create Interest objects
|
|
||||||
for _, p in ipairs(self.config.persistent or {}) do
|
|
||||||
p.interests = {}
|
|
||||||
for _, i in ipairs(p.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(p.interests, interest)
|
|
||||||
end
|
|
||||||
p.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Checks whether a device profile is persistent or not
|
|
||||||
function isProfilePersistent(device_props, profile_name)
|
|
||||||
for _, p in ipairs(self.config.persistent or {}) do
|
|
||||||
if p.profile_names then
|
|
||||||
for _, interest in ipairs(p.interests) do
|
|
||||||
if interest:matches(device_props) then
|
|
||||||
for _, pn in ipairs(p.profile_names) do
|
|
||||||
if pn == profile_name then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function parseParam(param, id)
|
|
||||||
local parsed = param:parse()
|
|
||||||
if parsed.pod_type == "Object" and parsed.object_id == id then
|
|
||||||
return parsed.properties
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function setDeviceProfile (device, dev_id, dev_name, profile)
|
|
||||||
if self.active_profiles[dev_id] and
|
|
||||||
self.active_profiles[dev_id].index == profile.index then
|
|
||||||
Log.info ("Profile " .. profile.name .. " is already set in " .. dev_name)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local param = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
|
||||||
index = profile.index,
|
|
||||||
}
|
|
||||||
Log.info ("Setting profile " .. profile.name .. " on " .. dev_name)
|
|
||||||
device:set_param("Profile", param)
|
|
||||||
end
|
|
||||||
|
|
||||||
function findDefaultProfile (device)
|
|
||||||
local def_name = nil
|
|
||||||
|
|
||||||
if self.default_profile_plugin ~= nil then
|
|
||||||
def_name = self.default_profile_plugin:call ("get-profile", device)
|
|
||||||
end
|
|
||||||
if def_name == nil then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
for p in device:iterate_params("EnumProfile") do
|
|
||||||
local profile = parseParam(p, "EnumProfile")
|
|
||||||
if profile.name == def_name then
|
|
||||||
return profile
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function findBestProfile (device)
|
|
||||||
local off_profile = nil
|
|
||||||
local best_profile = nil
|
|
||||||
local unk_profile = nil
|
|
||||||
|
|
||||||
for p in device:iterate_params("EnumProfile") do
|
|
||||||
profile = parseParam(p, "EnumProfile")
|
|
||||||
if profile and profile.name ~= "pro-audio" then
|
|
||||||
if profile.name == "off" then
|
|
||||||
off_profile = profile
|
|
||||||
elseif profile.available == "yes" then
|
|
||||||
if best_profile == nil or profile.priority > best_profile.priority then
|
|
||||||
best_profile = profile
|
|
||||||
end
|
|
||||||
elseif profile.available ~= "no" then
|
|
||||||
if unk_profile == nil or profile.priority > unk_profile.priority then
|
|
||||||
unk_profile = profile
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if best_profile ~= nil then
|
|
||||||
return best_profile
|
|
||||||
elseif unk_profile ~= nil then
|
|
||||||
return unk_profile
|
|
||||||
elseif off_profile ~= nil then
|
|
||||||
return off_profile
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleProfiles (device, new_device)
|
|
||||||
local dev_id = device["bound-id"]
|
|
||||||
local dev_name = device.properties["device.name"]
|
|
||||||
|
|
||||||
local def_profile = findDefaultProfile (device)
|
|
||||||
|
|
||||||
-- Do not do anything if active profile is both persistent and default
|
|
||||||
if not new_device and
|
|
||||||
self.active_profiles[dev_id] ~= nil and
|
|
||||||
isProfilePersistent (device.properties, self.active_profiles[dev_id].name) and
|
|
||||||
def_profile ~= nil and
|
|
||||||
self.active_profiles[dev_id].name == def_profile.name
|
|
||||||
then
|
|
||||||
local active_profile = self.active_profiles[dev_id].name
|
|
||||||
Log.info ("Device profile " .. active_profile .. " is persistent for " .. dev_name)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if def_profile ~= nil then
|
|
||||||
if def_profile.available == "no" then
|
|
||||||
Log.info ("Default profile " .. def_profile.name .. " unavailable for " .. dev_name)
|
|
||||||
else
|
|
||||||
Log.info ("Found default profile " .. def_profile.name .. " for " .. dev_name)
|
|
||||||
setDeviceProfile (device, dev_id, dev_name, def_profile)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
else
|
|
||||||
Log.info ("Default profile not found for " .. dev_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
local best_profile = findBestProfile (device)
|
|
||||||
if best_profile ~= nil then
|
|
||||||
Log.info ("Found best profile " .. best_profile.name .. " for " .. dev_name)
|
|
||||||
setDeviceProfile (device, dev_id, dev_name, best_profile)
|
|
||||||
else
|
|
||||||
Log.info ("Best profile not found on " .. dev_name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function onDeviceParamsChanged (device, param_name)
|
|
||||||
if param_name == "EnumProfile" then
|
|
||||||
handleProfiles (device, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "device",
|
|
||||||
Constraint { "device.name", "is-present", type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.om:connect("object-added", function (_, device)
|
|
||||||
device:connect ("params-changed", onDeviceParamsChanged)
|
|
||||||
handleProfiles (device, true)
|
|
||||||
end)
|
|
||||||
|
|
||||||
self.om:connect("object-removed", function (_, device)
|
|
||||||
local dev_id = device["bound-id"]
|
|
||||||
self.active_profiles[dev_id] = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
self.om:activate()
|
|
||||||
@@ -1,487 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- Based on default-routes.c from pipewire-media-session
|
|
||||||
-- Copyright © 2020 Wim Taymans
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local config = ... or {}
|
|
||||||
|
|
||||||
-- whether to store state on the file system
|
|
||||||
use_persistent_storage = config["use-persistent-storage"] or false
|
|
||||||
|
|
||||||
-- the default volume to apply
|
|
||||||
default_volume = tonumber(config["default-volume"] or 0.4^3)
|
|
||||||
default_input_volume = tonumber(config["default-input-volume"] or 1.0)
|
|
||||||
|
|
||||||
-- table of device info
|
|
||||||
dev_infos = {}
|
|
||||||
|
|
||||||
-- the state storage
|
|
||||||
state = use_persistent_storage and State("default-routes") or nil
|
|
||||||
state_table = state and state:load() or {}
|
|
||||||
|
|
||||||
-- simple serializer {"foo", "bar"} -> "foo;bar;"
|
|
||||||
function serializeArray(a)
|
|
||||||
local str = ""
|
|
||||||
for _, v in ipairs(a) do
|
|
||||||
str = str .. tostring(v):gsub(";", "\\;") .. ";"
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
|
|
||||||
function parseArray(str, convert_value)
|
|
||||||
local array = {}
|
|
||||||
local val = ""
|
|
||||||
local escaped = false
|
|
||||||
for i = 1, #str do
|
|
||||||
local c = str:sub(i,i)
|
|
||||||
if c == '\\' then
|
|
||||||
escaped = true
|
|
||||||
elseif c == ';' and not escaped then
|
|
||||||
val = convert_value and convert_value(val) or val
|
|
||||||
table.insert(array, val)
|
|
||||||
val = ""
|
|
||||||
else
|
|
||||||
val = val .. tostring(c)
|
|
||||||
escaped = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return array
|
|
||||||
end
|
|
||||||
|
|
||||||
function arrayContains(a, value)
|
|
||||||
for _, v in ipairs(a) do
|
|
||||||
if v == value then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
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 storeAfterTimeout()
|
|
||||||
if timeout_source then
|
|
||||||
timeout_source:destroy()
|
|
||||||
end
|
|
||||||
timeout_source = Core.timeout_add(1000, function ()
|
|
||||||
local saved, err = state:save(state_table)
|
|
||||||
if not saved then
|
|
||||||
Log.warning(err)
|
|
||||||
end
|
|
||||||
timeout_source = nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function saveProfile(dev_info, profile_name)
|
|
||||||
if not use_persistent_storage then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local routes = {}
|
|
||||||
for idx, ri in pairs(dev_info.route_infos) do
|
|
||||||
if ri.save then
|
|
||||||
table.insert(routes, ri.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #routes > 0 then
|
|
||||||
local key = dev_info.name .. ":profile:" .. profile_name
|
|
||||||
state_table[key] = serializeArray(routes)
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function saveRouteProps(dev_info, route)
|
|
||||||
if not use_persistent_storage or not route.props then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local props = route.props.properties
|
|
||||||
local key_base = dev_info.name .. ":" ..
|
|
||||||
route.direction:lower() .. ":" ..
|
|
||||||
route.name .. ":"
|
|
||||||
|
|
||||||
state_table[key_base .. "volume"] =
|
|
||||||
props.volume and tostring(props.volume) or nil
|
|
||||||
state_table[key_base .. "mute"] =
|
|
||||||
props.mute and tostring(props.mute) or nil
|
|
||||||
state_table[key_base .. "channelVolumes"] =
|
|
||||||
props.channelVolumes and serializeArray(props.channelVolumes) or nil
|
|
||||||
state_table[key_base .. "channelMap"] =
|
|
||||||
props.channelMap and serializeArray(props.channelMap) or nil
|
|
||||||
state_table[key_base .. "latencyOffsetNsec"] =
|
|
||||||
props.latencyOffsetNsec and tostring(props.latencyOffsetNsec) or nil
|
|
||||||
state_table[key_base .. "iec958Codecs"] =
|
|
||||||
props.iec958Codecs and serializeArray(props.iec958Codecs) or nil
|
|
||||||
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
|
|
||||||
function restoreRoute(device, dev_info, device_id, route)
|
|
||||||
-- default props
|
|
||||||
local props = {
|
|
||||||
"Spa:Pod:Object:Param:Props", "Route",
|
|
||||||
mute = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.direction == "Input" then
|
|
||||||
props.channelVolumes = { default_input_volume }
|
|
||||||
else
|
|
||||||
props.channelVolumes = { default_volume }
|
|
||||||
end
|
|
||||||
|
|
||||||
-- restore props from persistent storage
|
|
||||||
if use_persistent_storage then
|
|
||||||
local key_base = dev_info.name .. ":" ..
|
|
||||||
route.direction:lower() .. ":" ..
|
|
||||||
route.name .. ":"
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "volume"]
|
|
||||||
props.volume = str and tonumber(str) or props.volume
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "mute"]
|
|
||||||
props.mute = str and (str == "true") or false
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "channelVolumes"]
|
|
||||||
props.channelVolumes = str and parseArray(str, tonumber) or props.channelVolumes
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "channelMap"]
|
|
||||||
props.channelMap = str and parseArray(str) or props.channelMap
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "latencyOffsetNsec"]
|
|
||||||
props.latencyOffsetNsec = str and math.tointeger(str) or props.latencyOffsetNsec
|
|
||||||
|
|
||||||
local str = state_table[key_base .. "iec958Codecs"]
|
|
||||||
props.iec958Codecs = str and parseArray(str) or props.iec958Codecs
|
|
||||||
end
|
|
||||||
|
|
||||||
-- convert arrays to Spa Pod
|
|
||||||
if props.channelVolumes then
|
|
||||||
table.insert(props.channelVolumes, 1, "Spa:Float")
|
|
||||||
props.channelVolumes = Pod.Array(props.channelVolumes)
|
|
||||||
end
|
|
||||||
if props.channelMap then
|
|
||||||
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
|
|
||||||
props.channelMap = Pod.Array(props.channelMap)
|
|
||||||
end
|
|
||||||
if props.iec958Codecs then
|
|
||||||
table.insert(props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
|
|
||||||
props.iec958Codecs = Pod.Array(props.iec958Codecs)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- construct Route param
|
|
||||||
local param = Pod.Object {
|
|
||||||
"Spa:Pod:Object:Param:Route", "Route",
|
|
||||||
index = route.index,
|
|
||||||
device = device_id,
|
|
||||||
props = Pod.Object(props),
|
|
||||||
save = route.save,
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.debug(param, "setting route on " .. tostring(device))
|
|
||||||
device:set_param("Route", param)
|
|
||||||
|
|
||||||
route.prev_active = true
|
|
||||||
route.active = true
|
|
||||||
end
|
|
||||||
|
|
||||||
function findActiveDeviceIDs(profile)
|
|
||||||
-- parses the classes from the profile and returns the device IDs
|
|
||||||
----- sample structure, should return { 0, 8 } -----
|
|
||||||
-- classes:
|
|
||||||
-- 1: 2
|
|
||||||
-- 2:
|
|
||||||
-- 1: Audio/Source
|
|
||||||
-- 2: 1
|
|
||||||
-- 3: card.profile.devices
|
|
||||||
-- 4:
|
|
||||||
-- 1: 0
|
|
||||||
-- pod_type: Array
|
|
||||||
-- value_type: Spa:Int
|
|
||||||
-- pod_type: Struct
|
|
||||||
-- 3:
|
|
||||||
-- 1: Audio/Sink
|
|
||||||
-- 2: 1
|
|
||||||
-- 3: card.profile.devices
|
|
||||||
-- 4:
|
|
||||||
-- 1: 8
|
|
||||||
-- pod_type: Array
|
|
||||||
-- value_type: Spa:Int
|
|
||||||
-- pod_type: Struct
|
|
||||||
-- pod_type: Struct
|
|
||||||
local active_ids = {}
|
|
||||||
if type(profile.classes) == "table" and profile.classes.pod_type == "Struct" then
|
|
||||||
for _, p in ipairs(profile.classes) do
|
|
||||||
if type(p) == "table" and p.pod_type == "Struct" then
|
|
||||||
local i = 1
|
|
||||||
while true do
|
|
||||||
local k, v = p[i], p[i+1]
|
|
||||||
i = i + 2
|
|
||||||
if not k or not v then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
if k == "card.profile.devices" and
|
|
||||||
type(v) == "table" and v.pod_type == "Array" then
|
|
||||||
for _, dev_id in ipairs(v) do
|
|
||||||
table.insert(active_ids, dev_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return active_ids
|
|
||||||
end
|
|
||||||
|
|
||||||
-- returns an array of the route names that were previously selected
|
|
||||||
-- for the given device and profile
|
|
||||||
function getStoredProfileRoutes(dev_name, profile_name)
|
|
||||||
local key = dev_name .. ":profile:" .. profile_name
|
|
||||||
local str = state_table[key]
|
|
||||||
return str and parseArray(str) or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- find a route that was previously stored for a device_id
|
|
||||||
-- spr needs to be the array returned from getStoredProfileRoutes()
|
|
||||||
function findSavedRoute(dev_info, device_id, spr)
|
|
||||||
for idx, ri in pairs(dev_info.route_infos) do
|
|
||||||
if arrayContains(ri.devices, device_id) and
|
|
||||||
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) and
|
|
||||||
arrayContains(spr, ri.name) then
|
|
||||||
return ri
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- find the best route for a given device_id, based on availability and priority
|
|
||||||
function findBestRoute(dev_info, device_id)
|
|
||||||
local best_avail = nil
|
|
||||||
local best_unk = nil
|
|
||||||
for idx, ri in pairs(dev_info.route_infos) do
|
|
||||||
if arrayContains(ri.devices, device_id) and
|
|
||||||
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) then
|
|
||||||
if ri.available == "yes" or ri.available == "unknown" then
|
|
||||||
if ri.direction == "Output" and ri.available ~= ri.prev_available then
|
|
||||||
best_avail = ri
|
|
||||||
ri.save = true
|
|
||||||
break
|
|
||||||
elseif ri.available == "yes" then
|
|
||||||
if (best_avail == nil or ri.priority > best_avail.priority) then
|
|
||||||
best_avail = ri
|
|
||||||
end
|
|
||||||
elseif best_unk == nil or ri.priority > best_unk.priority then
|
|
||||||
best_unk = ri
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return best_avail or best_unk
|
|
||||||
end
|
|
||||||
|
|
||||||
function restoreProfileRoutes(device, dev_info, profile, profile_changed)
|
|
||||||
Log.info(device, "restore routes for profile " .. profile.name)
|
|
||||||
|
|
||||||
local active_ids = findActiveDeviceIDs(profile)
|
|
||||||
local spr = getStoredProfileRoutes(dev_info.name, profile.name)
|
|
||||||
|
|
||||||
for _, device_id in ipairs(active_ids) do
|
|
||||||
Log.info(device, "restoring device " .. device_id);
|
|
||||||
|
|
||||||
local route = nil
|
|
||||||
|
|
||||||
-- restore routes selection for the newly selected profile
|
|
||||||
-- don't bother if spr is empty, there is no point
|
|
||||||
if profile_changed and #spr > 0 then
|
|
||||||
route = findSavedRoute(dev_info, device_id, spr)
|
|
||||||
if route then
|
|
||||||
-- we found a saved route
|
|
||||||
if route.available == "no" then
|
|
||||||
Log.info(device, "saved route '" .. route.name .. "' not available")
|
|
||||||
-- not available, try to find next best
|
|
||||||
route = nil
|
|
||||||
else
|
|
||||||
Log.info(device, "found saved route: " .. route.name)
|
|
||||||
-- make sure we save it again
|
|
||||||
route.save = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- we could not find a saved route, try to find a new best
|
|
||||||
if not route then
|
|
||||||
route = findBestRoute(dev_info, device_id)
|
|
||||||
if not route then
|
|
||||||
Log.info(device, "can't find best route")
|
|
||||||
else
|
|
||||||
Log.info(device, "found best route: " .. route.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- restore route
|
|
||||||
if route then
|
|
||||||
restoreRoute(device, dev_info, device_id, route)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function findRouteInfo(dev_info, route, return_new)
|
|
||||||
local ri = dev_info.route_infos[route.index]
|
|
||||||
if not ri and return_new then
|
|
||||||
ri = {
|
|
||||||
index = route.index,
|
|
||||||
name = route.name,
|
|
||||||
direction = route.direction,
|
|
||||||
devices = route.devices or {},
|
|
||||||
profiles = route.profiles,
|
|
||||||
priority = route.priority or 0,
|
|
||||||
available = route.available or "unknown",
|
|
||||||
prev_available = route.available or "unknown",
|
|
||||||
active = false,
|
|
||||||
prev_active = false,
|
|
||||||
save = false,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return ri
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleDevice(device)
|
|
||||||
local dev_info = dev_infos[device["bound-id"]]
|
|
||||||
local new_route_infos = {}
|
|
||||||
local avail_routes_changed = false
|
|
||||||
local profile = nil
|
|
||||||
|
|
||||||
-- get current profile
|
|
||||||
for p in device:iterate_params("Profile") do
|
|
||||||
profile = parseParam(p, "Profile")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- look at all the routes and update/reset cached information
|
|
||||||
for p in device:iterate_params("EnumRoute") do
|
|
||||||
-- parse pod
|
|
||||||
local route = parseParam(p, "EnumRoute")
|
|
||||||
if not route then
|
|
||||||
goto skip_enum_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- find cached route information
|
|
||||||
local route_info = findRouteInfo(dev_info, route, true)
|
|
||||||
|
|
||||||
-- update properties
|
|
||||||
route_info.prev_available = route_info.available
|
|
||||||
if route_info.available ~= route.available then
|
|
||||||
Log.info(device, "route " .. route.name .. " available changed " ..
|
|
||||||
route_info.available .. " -> " .. route.available)
|
|
||||||
route_info.available = route.available
|
|
||||||
if profile and arrayContains(route.profiles, profile.index) then
|
|
||||||
avail_routes_changed = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
route_info.prev_active = route_info.active
|
|
||||||
route_info.active = false
|
|
||||||
route_info.save = false
|
|
||||||
|
|
||||||
-- store
|
|
||||||
new_route_infos[route.index] = route_info
|
|
||||||
|
|
||||||
::skip_enum_route::
|
|
||||||
end
|
|
||||||
|
|
||||||
-- replace old route_infos to lose old routes
|
|
||||||
-- that no longer exist on the device
|
|
||||||
dev_info.route_infos = new_route_infos
|
|
||||||
new_route_infos = nil
|
|
||||||
|
|
||||||
-- check for changes in the active routes
|
|
||||||
for p in device:iterate_params("Route") do
|
|
||||||
local route = parseParam(p, "Route")
|
|
||||||
if not route then
|
|
||||||
goto skip_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get cached route info and at the same time
|
|
||||||
-- ensure that the route is also in EnumRoute
|
|
||||||
local route_info = findRouteInfo(dev_info, route, false)
|
|
||||||
if not route_info then
|
|
||||||
goto skip_route
|
|
||||||
end
|
|
||||||
|
|
||||||
-- update state
|
|
||||||
route_info.active = true
|
|
||||||
route_info.save = route.save
|
|
||||||
|
|
||||||
if not route_info.prev_active then
|
|
||||||
-- a new route is now active, restore the volume and
|
|
||||||
-- make sure we save this as a preferred route
|
|
||||||
Log.info(device, "new active route found " .. route.name)
|
|
||||||
restoreRoute(device, dev_info, route.device, route_info)
|
|
||||||
elseif route.save then
|
|
||||||
-- just save route properties
|
|
||||||
Log.info(device, "storing route props for " .. route.name)
|
|
||||||
saveRouteProps(dev_info, route)
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_route::
|
|
||||||
end
|
|
||||||
|
|
||||||
-- restore routes for profile
|
|
||||||
if profile then
|
|
||||||
local profile_changed = (dev_info.active_profile ~= profile.index)
|
|
||||||
|
|
||||||
-- if the profile changed, restore routes for that profile
|
|
||||||
-- if any of the routes of the current profile changed in availability,
|
|
||||||
-- then try to select a new "best" route for each device and ignore
|
|
||||||
-- what was stored
|
|
||||||
if profile_changed or avail_routes_changed then
|
|
||||||
dev_info.active_profile = profile.index
|
|
||||||
restoreProfileRoutes(device, dev_info, profile, profile_changed)
|
|
||||||
end
|
|
||||||
|
|
||||||
saveProfile(dev_info, profile.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "device",
|
|
||||||
Constraint { "device.name", "is-present", type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
om:connect("objects-changed", function (om)
|
|
||||||
local new_dev_infos = {}
|
|
||||||
for device in om:iterate() do
|
|
||||||
local dev_info = dev_infos[device["bound-id"]]
|
|
||||||
-- new device appeared
|
|
||||||
if not dev_info then
|
|
||||||
dev_info = {
|
|
||||||
name = device.properties["device.name"],
|
|
||||||
active_profile = -1,
|
|
||||||
route_infos = {},
|
|
||||||
}
|
|
||||||
dev_infos[device["bound-id"]] = dev_info
|
|
||||||
|
|
||||||
device:connect("params-changed", handleDevice)
|
|
||||||
handleDevice(device)
|
|
||||||
end
|
|
||||||
|
|
||||||
new_dev_infos[device["bound-id"]] = dev_info
|
|
||||||
end
|
|
||||||
-- replace list to get rid of dev_info for devices that no longer exist
|
|
||||||
dev_infos = new_dev_infos
|
|
||||||
end)
|
|
||||||
|
|
||||||
om:activate()
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
local config = ... or {}
|
|
||||||
config.roles = config.roles or {}
|
|
||||||
config["duck.level"] = config["duck.level"] or 0.3
|
|
||||||
|
|
||||||
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 ep = endpoints_om:lookup {
|
|
||||||
Constraint { "media.role", "=", role, type = "pw" },
|
|
||||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep and ep.properties["node.id"] then
|
|
||||||
Log.debug(ep, "restore role " .. role)
|
|
||||||
mixer_api:call("set-volume", ep.properties["node.id"], {
|
|
||||||
monitorVolume = 1.0,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function duck(role, media_class)
|
|
||||||
if not mixer_api then return end
|
|
||||||
|
|
||||||
local ep = endpoints_om:lookup {
|
|
||||||
Constraint { "media.role", "=", role, type = "pw" },
|
|
||||||
Constraint { "media.class", "=", media_class, type = "pw" },
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep and ep.properties["node.id"] then
|
|
||||||
Log.debug(ep, "duck role " .. role)
|
|
||||||
mixer_api:call("set-volume", ep.properties["node.id"], {
|
|
||||||
monitorVolume = config["duck.level"],
|
|
||||||
})
|
|
||||||
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 endpoint 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.endpoint.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
|
|
||||||
endpoints_om = ObjectManager {
|
|
||||||
Interest { type = "endpoint" },
|
|
||||||
}
|
|
||||||
endpoints_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,259 +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 config = ... or {}
|
|
||||||
config.roles = config.roles or {}
|
|
||||||
|
|
||||||
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 findTargetEndpoint (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 endpoint by role
|
|
||||||
media_role = findRole(role, target_media_class)
|
|
||||||
for si_target_ep in endpoints_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.endpoint.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 endpoints_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 endpoint
|
|
||||||
local si_target_ep = findTargetEndpoint (node, media_class, media_role)
|
|
||||||
if not si_target_ep then
|
|
||||||
Log.info (si, "... target endpoint 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 endpoints_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 endpoint")
|
|
||||||
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
|
|
||||||
|
|
||||||
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
|
|
||||||
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.endpoint.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)
|
|
||||||
|
|
||||||
endpoints_om:activate()
|
|
||||||
linkables_om:activate()
|
|
||||||
links_om:activate()
|
|
||||||
@@ -1,234 +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 config = ... or {}
|
|
||||||
|
|
||||||
-- ensure config.move and config.follow are not nil
|
|
||||||
config.move = config.move or false
|
|
||||||
config.follow = config.follow or false
|
|
||||||
|
|
||||||
local self = {}
|
|
||||||
self.scanning = false
|
|
||||||
self.pending_rescan = false
|
|
||||||
|
|
||||||
function rescan ()
|
|
||||||
-- check endpoints and register new links
|
|
||||||
for si_ep in endpoints_om:iterate() do
|
|
||||||
handleEndpoint (si_ep)
|
|
||||||
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.endpoint.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 handleEndpoint (si_ep)
|
|
||||||
Log.info (si_ep, "handling endpoint " .. 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")
|
|
||||||
endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
|
|
||||||
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.endpoint.device.link", "=", true, type = "pw-global" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-- listen for default node changes if config.follow is enabled
|
|
||||||
if config.follow then
|
|
||||||
default_nodes:connect("changed", function (p)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
linkables_om:connect("objects-changed", function (om)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
|
|
||||||
endpoints_om:connect("object-added", function (om)
|
|
||||||
scheduleRescan ()
|
|
||||||
end)
|
|
||||||
|
|
||||||
linkables_om:connect("object-removed", function (om, si)
|
|
||||||
unhandleLinkable (si)
|
|
||||||
end)
|
|
||||||
|
|
||||||
endpoints_om:activate()
|
|
||||||
linkables_om:activate()
|
|
||||||
links_om:activate()
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,499 +0,0 @@
|
|||||||
-- WirePlumber
|
|
||||||
--
|
|
||||||
-- Copyright © 2021 Collabora Ltd.
|
|
||||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
||||||
--
|
|
||||||
-- Based on restore-stream.c from pipewire-media-session
|
|
||||||
-- Copyright © 2020 Wim Taymans
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
-- Receive script arguments from config.lua
|
|
||||||
local config = ... or {}
|
|
||||||
config.properties = config.properties or {}
|
|
||||||
config_restore_props = config.properties["restore-props"] or false
|
|
||||||
config_restore_target = config.properties["restore-target"] or false
|
|
||||||
config_default_channel_volume = config.properties["default-channel-volume"] or 1.0
|
|
||||||
|
|
||||||
-- preprocess rules and create Interest objects
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
r.interests = {}
|
|
||||||
for _, i in ipairs(r.matches) do
|
|
||||||
local interest_desc = { type = "properties" }
|
|
||||||
for _, c in ipairs(i) do
|
|
||||||
c.type = "pw"
|
|
||||||
table.insert(interest_desc, Constraint(c))
|
|
||||||
end
|
|
||||||
local interest = Interest(interest_desc)
|
|
||||||
table.insert(r.interests, interest)
|
|
||||||
end
|
|
||||||
r.matches = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- applies properties from config.rules when asked to
|
|
||||||
function rulesApplyProperties(properties)
|
|
||||||
for _, r in ipairs(config.rules or {}) do
|
|
||||||
if r.apply_properties then
|
|
||||||
for _, interest in ipairs(r.interests) do
|
|
||||||
if interest:matches(properties) then
|
|
||||||
for k, v in pairs(r.apply_properties) do
|
|
||||||
properties[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- the state storage
|
|
||||||
state = State("restore-stream")
|
|
||||||
state_table = state:load()
|
|
||||||
|
|
||||||
-- simple serializer {"foo", "bar"} -> "foo;bar;"
|
|
||||||
function serializeArray(a)
|
|
||||||
local str = ""
|
|
||||||
for _, v in ipairs(a) do
|
|
||||||
str = str .. tostring(v):gsub(";", "\\;") .. ";"
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
|
|
||||||
function parseArray(str, convert_value, with_type)
|
|
||||||
local array = {}
|
|
||||||
local val = ""
|
|
||||||
local escaped = false
|
|
||||||
for i = 1, #str do
|
|
||||||
local c = str:sub(i,i)
|
|
||||||
if c == '\\' then
|
|
||||||
escaped = true
|
|
||||||
elseif c == ';' and not escaped then
|
|
||||||
val = convert_value and convert_value(val) or val
|
|
||||||
table.insert(array, val)
|
|
||||||
val = ""
|
|
||||||
else
|
|
||||||
val = val .. tostring(c)
|
|
||||||
escaped = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if with_type then
|
|
||||||
array["pod_type"] = "Array"
|
|
||||||
end
|
|
||||||
return array
|
|
||||||
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 storeAfterTimeout()
|
|
||||||
if timeout_source then
|
|
||||||
timeout_source:destroy()
|
|
||||||
end
|
|
||||||
timeout_source = Core.timeout_add(1000, function ()
|
|
||||||
local saved, err = state:save(state_table)
|
|
||||||
if not saved then
|
|
||||||
Log.warning(err)
|
|
||||||
end
|
|
||||||
timeout_source = nil
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function findSuitableKey(properties)
|
|
||||||
local keys = {
|
|
||||||
"media.role",
|
|
||||||
"application.id",
|
|
||||||
"application.name",
|
|
||||||
"media.name",
|
|
||||||
"node.name",
|
|
||||||
}
|
|
||||||
local key = nil
|
|
||||||
|
|
||||||
for _, k in ipairs(keys) do
|
|
||||||
local p = properties[k]
|
|
||||||
if p then
|
|
||||||
key = string.format("%s:%s:%s",
|
|
||||||
properties["media.class"]:gsub("^Stream/", ""), k, p)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return key
|
|
||||||
end
|
|
||||||
|
|
||||||
function saveTarget(subject, target_key, type, value)
|
|
||||||
if target_key ~= "target.node" and target_key ~= "target.object" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local node = streams_om:lookup {
|
|
||||||
Constraint { "bound-id", "=", subject, type = "gobject" }
|
|
||||||
}
|
|
||||||
if not node then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local stream_props = node.properties
|
|
||||||
rulesApplyProperties(stream_props)
|
|
||||||
|
|
||||||
if stream_props["state.restore-target"] == false then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local key_base = findSuitableKey(stream_props)
|
|
||||||
if not key_base then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local target_value = value
|
|
||||||
local target_name = nil
|
|
||||||
|
|
||||||
if not target_value then
|
|
||||||
local metadata = metadata_om:lookup()
|
|
||||||
if metadata then
|
|
||||||
target_value = metadata:find(node["bound-id"], target_key)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if target_value and target_value ~= "-1" then
|
|
||||||
local target_node
|
|
||||||
if target_key == "target.object" then
|
|
||||||
target_node = allnodes_om:lookup {
|
|
||||||
Constraint { "object.serial", "=", target_value, type = "pw-global" }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
target_node = allnodes_om:lookup {
|
|
||||||
Constraint { "bound-id", "=", target_value, type = "gobject" }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
if target_node then
|
|
||||||
target_name = target_node.properties["node.name"]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
state_table[key_base .. ":target"] = target_name
|
|
||||||
|
|
||||||
Log.info(node, "saving stream target for " ..
|
|
||||||
tostring(stream_props["node.name"]) ..
|
|
||||||
" -> " .. tostring(target_name))
|
|
||||||
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
|
|
||||||
function restoreTarget(node, target_name)
|
|
||||||
|
|
||||||
local stream_props = node.properties
|
|
||||||
local target_in_props = nil
|
|
||||||
|
|
||||||
if stream_props ["target.object"] ~= nil or
|
|
||||||
stream_props ["node.target"] ~= nil then
|
|
||||||
target_in_props = stream_props ["target.object"] or
|
|
||||||
stream_props ["node.target"]
|
|
||||||
|
|
||||||
Log.debug (string.format ("%s%s%s%s",
|
|
||||||
"Not restoring the target for ",
|
|
||||||
stream_props ["node.name"],
|
|
||||||
" because it is already set to ",
|
|
||||||
target_in_props))
|
|
||||||
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local target_node = allnodes_om:lookup {
|
|
||||||
Constraint { "node.name", "=", target_name, type = "pw" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if target_node then
|
|
||||||
local metadata = metadata_om:lookup()
|
|
||||||
if metadata then
|
|
||||||
metadata:set(node["bound-id"], "target.node", "Spa:Id",
|
|
||||||
target_node["bound-id"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function jsonTable(val, name)
|
|
||||||
local tmp = ""
|
|
||||||
local count = 0
|
|
||||||
|
|
||||||
if name then tmp = tmp .. string.format("%q", name) .. ": " end
|
|
||||||
|
|
||||||
if type(val) == "table" then
|
|
||||||
if val["pod_type"] == "Array" then
|
|
||||||
tmp = tmp .. "["
|
|
||||||
for _, v in ipairs(val) do
|
|
||||||
if count > 0 then tmp = tmp .. "," end
|
|
||||||
tmp = tmp .. jsonTable(v)
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
tmp = tmp .. "]"
|
|
||||||
else
|
|
||||||
tmp = tmp .. "{"
|
|
||||||
for k, v in pairs(val) do
|
|
||||||
if count > 0 then tmp = tmp .. "," end
|
|
||||||
tmp = tmp .. jsonTable(v, k)
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
tmp = tmp .. "}"
|
|
||||||
end
|
|
||||||
elseif type(val) == "number" then
|
|
||||||
tmp = tmp .. tostring(val)
|
|
||||||
elseif type(val) == "string" then
|
|
||||||
tmp = tmp .. string.format("%q", val)
|
|
||||||
elseif type(val) == "boolean" then
|
|
||||||
tmp = tmp .. (val and "true" or "false")
|
|
||||||
else
|
|
||||||
tmp = tmp .. "\"[type:" .. type(val) .. "]\""
|
|
||||||
end
|
|
||||||
return tmp
|
|
||||||
end
|
|
||||||
|
|
||||||
function moveToMetadata(key_base, metadata)
|
|
||||||
local route_table = { }
|
|
||||||
local count = 0
|
|
||||||
|
|
||||||
key = "restore.stream." .. key_base
|
|
||||||
key = string.gsub(key, ":", ".", 1);
|
|
||||||
|
|
||||||
local str = state_table[key_base .. ":volume"]
|
|
||||||
if str then
|
|
||||||
route_table["volume"] = tonumber(str)
|
|
||||||
count = count + 1;
|
|
||||||
end
|
|
||||||
local str = state_table[key_base .. ":mute"]
|
|
||||||
if str then
|
|
||||||
route_table["mute"] = str == "true"
|
|
||||||
count = count + 1;
|
|
||||||
end
|
|
||||||
local str = state_table[key_base .. ":channelVolumes"]
|
|
||||||
if str then
|
|
||||||
route_table["volumes"] = parseArray(str, tonumber, true)
|
|
||||||
count = count + 1;
|
|
||||||
end
|
|
||||||
local str = state_table[key_base .. ":channelMap"]
|
|
||||||
if str then
|
|
||||||
route_table["channels"] = parseArray(str, nil, true)
|
|
||||||
count = count + 1;
|
|
||||||
end
|
|
||||||
|
|
||||||
if count > 0 then
|
|
||||||
metadata:set(0, key, "Spa:String:JSON", jsonTable(route_table));
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function saveStream(node)
|
|
||||||
local stream_props = node.properties
|
|
||||||
rulesApplyProperties(stream_props)
|
|
||||||
|
|
||||||
if config_restore_props and stream_props["state.restore-props"] ~= false then
|
|
||||||
local key_base = findSuitableKey(stream_props)
|
|
||||||
if not key_base then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.info(node, "saving stream props for " ..
|
|
||||||
tostring(stream_props["node.name"]))
|
|
||||||
|
|
||||||
for p in node:iterate_params("Props") do
|
|
||||||
local props = parseParam(p, "Props")
|
|
||||||
if not props then
|
|
||||||
goto skip_prop
|
|
||||||
end
|
|
||||||
|
|
||||||
if props.volume then
|
|
||||||
state_table[key_base .. ":volume"] = tostring(props.volume)
|
|
||||||
end
|
|
||||||
if props.mute ~= nil then
|
|
||||||
state_table[key_base .. ":mute"] = tostring(props.mute)
|
|
||||||
end
|
|
||||||
if props.channelVolumes then
|
|
||||||
state_table[key_base .. ":channelVolumes"] = serializeArray(props.channelVolumes)
|
|
||||||
end
|
|
||||||
if props.channelMap then
|
|
||||||
state_table[key_base .. ":channelMap"] = serializeArray(props.channelMap)
|
|
||||||
end
|
|
||||||
|
|
||||||
::skip_prop::
|
|
||||||
end
|
|
||||||
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function build_default_channel_volumes (node)
|
|
||||||
local def_vol = config_default_channel_volume
|
|
||||||
local channels = 2
|
|
||||||
local res = {}
|
|
||||||
|
|
||||||
local str = node.properties["state.default-channel-volume"]
|
|
||||||
if str ~= nil then
|
|
||||||
def_vol = tonumber (str)
|
|
||||||
end
|
|
||||||
|
|
||||||
for pod in node:iterate_params("Format") do
|
|
||||||
local pod_parsed = pod:parse()
|
|
||||||
if pod_parsed ~= nil then
|
|
||||||
channels = pod_parsed.properties.channels
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
while (#res < channels) do
|
|
||||||
table.insert(res, def_vol)
|
|
||||||
end
|
|
||||||
|
|
||||||
return res;
|
|
||||||
end
|
|
||||||
|
|
||||||
function restoreStream(node)
|
|
||||||
local stream_props = node.properties
|
|
||||||
rulesApplyProperties(stream_props)
|
|
||||||
|
|
||||||
local key_base = findSuitableKey(stream_props)
|
|
||||||
if not key_base then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if config_restore_props and stream_props["state.restore-props"] ~= false then
|
|
||||||
local props = { "Spa:Pod:Object:Param:Props", "Props" }
|
|
||||||
|
|
||||||
local str = state_table[key_base .. ":volume"]
|
|
||||||
props.volume = str and tonumber(str) or nil
|
|
||||||
|
|
||||||
local str = state_table[key_base .. ":mute"]
|
|
||||||
props.mute = str and (str == "true") or nil
|
|
||||||
|
|
||||||
local str = state_table[key_base .. ":channelVolumes"]
|
|
||||||
props.channelVolumes = str and parseArray(str, tonumber) or
|
|
||||||
build_default_channel_volumes (node)
|
|
||||||
|
|
||||||
local str = state_table[key_base .. ":channelMap"]
|
|
||||||
props.channelMap = str and parseArray(str) or nil
|
|
||||||
|
|
||||||
-- convert arrays to Spa Pod
|
|
||||||
if props.channelVolumes then
|
|
||||||
table.insert(props.channelVolumes, 1, "Spa:Float")
|
|
||||||
props.channelVolumes = Pod.Array(props.channelVolumes)
|
|
||||||
end
|
|
||||||
if props.channelMap then
|
|
||||||
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
|
|
||||||
props.channelMap = Pod.Array(props.channelMap)
|
|
||||||
end
|
|
||||||
|
|
||||||
Log.info(node, "restore values from " .. key_base)
|
|
||||||
local param = Pod.Object(props)
|
|
||||||
Log.debug(param, "setting props on " .. tostring(node))
|
|
||||||
node:set_param("Props", param)
|
|
||||||
end
|
|
||||||
|
|
||||||
if config_restore_target and stream_props["state.restore-target"] ~= false then
|
|
||||||
local str = state_table[key_base .. ":target"]
|
|
||||||
if str then
|
|
||||||
restoreTarget(node, str)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if config_restore_target then
|
|
||||||
metadata_om = ObjectManager {
|
|
||||||
Interest {
|
|
||||||
type = "metadata",
|
|
||||||
Constraint { "metadata.name", "=", "default" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata_om:connect("object-added", function (om, metadata)
|
|
||||||
-- process existing metadata
|
|
||||||
for s, k, t, v in metadata:iterate(Id.ANY) do
|
|
||||||
saveTarget(s, k, t, v)
|
|
||||||
end
|
|
||||||
-- and watch for changes
|
|
||||||
metadata:connect("changed", function (m, subject, key, type, value)
|
|
||||||
saveTarget(subject, key, type, value)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
metadata_om:activate()
|
|
||||||
end
|
|
||||||
|
|
||||||
function handleRouteSettings(subject, key, type, value)
|
|
||||||
if type ~= "Spa:String:JSON" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if string.find(key, "^restore.stream.") == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if value == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local json = Json.Raw (value);
|
|
||||||
if json == nil or not json:is_object () then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local vparsed = json:parse()
|
|
||||||
local key_base = string.sub(key, string.len("restore.stream.") + 1)
|
|
||||||
local str;
|
|
||||||
|
|
||||||
key_base = string.gsub(key_base, "%.", ":", 1);
|
|
||||||
|
|
||||||
if vparsed.volume ~= nil then
|
|
||||||
state_table[key_base .. ":volume"] = tostring (vparsed.volume)
|
|
||||||
end
|
|
||||||
if vparsed.mute ~= nil then
|
|
||||||
state_table[key_base .. ":mute"] = tostring (vparsed.mute)
|
|
||||||
end
|
|
||||||
if vparsed.channels ~= nil then
|
|
||||||
state_table[key_base .. ":channelMap"] = serializeArray (vparsed.channels)
|
|
||||||
end
|
|
||||||
if vparsed.volumes ~= nil then
|
|
||||||
state_table[key_base .. ":channelVolumes"] = serializeArray (vparsed.volumes)
|
|
||||||
end
|
|
||||||
|
|
||||||
storeAfterTimeout()
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
rs_metadata = ImplMetadata("route-settings")
|
|
||||||
rs_metadata:activate(Features.ALL, function (m, e)
|
|
||||||
if e then
|
|
||||||
Log.warning("failed to activate route-settings metadata: " .. tostring(e))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- copy state into the metadata
|
|
||||||
moveToMetadata("Output/Audio:media.role:Notification", m)
|
|
||||||
-- watch for changes
|
|
||||||
m:connect("changed", function (m, subject, key, type, value)
|
|
||||||
handleRouteSettings(subject, key, type, value)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
allnodes_om = ObjectManager { Interest { type = "node" } }
|
|
||||||
allnodes_om:activate()
|
|
||||||
|
|
||||||
streams_om = ObjectManager {
|
|
||||||
-- match stream nodes
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Stream/*", type = "pw-global" },
|
|
||||||
},
|
|
||||||
-- and device nodes that are not associated with any routes
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
|
|
||||||
Constraint { "device.routes", "is-absent", type = "pw" },
|
|
||||||
},
|
|
||||||
Interest {
|
|
||||||
type = "node",
|
|
||||||
Constraint { "media.class", "matches", "Audio/*", type = "pw-global" },
|
|
||||||
Constraint { "device.routes", "equals", "0", type = "pw" },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
streams_om:connect("object-added", function (streams_om, node)
|
|
||||||
node:connect("params-changed", saveStream)
|
|
||||||
restoreStream(node)
|
|
||||||
end)
|
|
||||||
streams_om:activate()
|
|
||||||
103
.config/wireplumber/scripts/sm-objects.lua
Normal file
103
.config/wireplumber/scripts/sm-objects.lua
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
-- WirePlumber
|
||||||
|
--
|
||||||
|
-- Copyright © 2023 Collabora Ltd.
|
||||||
|
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: MIT
|
||||||
|
--
|
||||||
|
-- The script exposes a metadata object named "sm-objects" that clients can
|
||||||
|
-- use to load objects into the WirePlumber daemon process. The objects are
|
||||||
|
-- loaded as soon as the metadata is set and are destroyed when the metadata
|
||||||
|
-- is cleared.
|
||||||
|
--
|
||||||
|
-- To load an object, a client needs to set a metadata entry with:
|
||||||
|
--
|
||||||
|
-- * subject:
|
||||||
|
-- The ID of the owner of the object; you can use 0 here, but the
|
||||||
|
-- idea is to be able to restrict which clients can change and/or
|
||||||
|
-- delete these objects by using IDs of other objects appropriately
|
||||||
|
--
|
||||||
|
-- * key: "<UNIQUE-OBJECT-NAME>"
|
||||||
|
-- This is the name that will be used to identify the object.
|
||||||
|
-- If an object with the same name already exists, it will be destroyed.
|
||||||
|
-- Note that the keys are unique per subject, so you can have multiple
|
||||||
|
-- objects with the same name as long as they are owned by different subjects.
|
||||||
|
--
|
||||||
|
-- * type: "Spa:String:JSON"
|
||||||
|
--
|
||||||
|
-- * value: "{ type = <object-type>,
|
||||||
|
-- name = <object-name>,
|
||||||
|
-- args = { ...object arguments... } }"
|
||||||
|
-- The object type can be one of the following:
|
||||||
|
-- - "pw-module": loads a pipewire module: `name` and `args` are interpreted
|
||||||
|
-- just like a module entry in pipewire.conf
|
||||||
|
-- - "metadata": loads a metadata object with `metadata.name` = `name`
|
||||||
|
-- and any additional properties provided in `args`
|
||||||
|
--
|
||||||
|
|
||||||
|
on_demand_objects = {}
|
||||||
|
|
||||||
|
object_constructors = {
|
||||||
|
["pw-module"] = LocalModule,
|
||||||
|
["metadata"] = function (name, args)
|
||||||
|
local m = ImplMetadata (name, args)
|
||||||
|
m:activate (Features.ALL, function (m, e)
|
||||||
|
if e then
|
||||||
|
Log.warning ("failed to activate on-demand metadata `" .. name .. "`: " .. tostring (e))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return m
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_metadata_changed (m, subject, key, type, value)
|
||||||
|
-- destroy all objects when metadata is cleared
|
||||||
|
if not key then
|
||||||
|
on_demand_objects = {}
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local object_id = key .. "@" .. tostring(subject)
|
||||||
|
|
||||||
|
-- destroy existing object instance, if needed
|
||||||
|
if on_demand_objects[object_id] then
|
||||||
|
Log.debug("destroy on-demand object: " .. object_id)
|
||||||
|
on_demand_objects[object_id] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if value then
|
||||||
|
local json = Json.Raw(value)
|
||||||
|
if not json:is_object() then
|
||||||
|
Log.warning("loading '".. object_id .. "' failed: expected JSON object, got: '" .. value .. "'")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local obj = json:parse(1)
|
||||||
|
if not obj.type then
|
||||||
|
Log.warning("loading '".. object_id .. "' failed: no object type specified")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not obj.name then
|
||||||
|
Log.warning("loading '".. object_id .. "' failed: no object name specified")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local constructor = object_constructors[obj.type]
|
||||||
|
if not constructor then
|
||||||
|
Log.warning("loading '".. object_id .. "' failed: unknown object type: " .. obj.type)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Log.info("load on-demand object: " .. object_id .. " -> " .. obj.name)
|
||||||
|
on_demand_objects[object_id] = constructor(obj.name, obj.args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
objects_metadata = ImplMetadata ("sm-objects")
|
||||||
|
objects_metadata:activate (Features.ALL, function (m, e)
|
||||||
|
if e then
|
||||||
|
Log.warning ("failed to activate the sm-objects metadata: " .. tostring (e))
|
||||||
|
else
|
||||||
|
m:connect("changed", handle_metadata_changed)
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -1,36 +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 endpoints_config = ...
|
|
||||||
|
|
||||||
function createEndpoint (factory_name, properties)
|
|
||||||
-- create endpoint
|
|
||||||
local ep = SessionItem ( factory_name )
|
|
||||||
if not ep then
|
|
||||||
Log.warning (ep, "could not create endpoint of type " .. factory_name)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- configure endpoint
|
|
||||||
if not ep:configure(properties) then
|
|
||||||
Log.warning(ep, "failed to configure endpoint " .. properties.name)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- activate and register endpoint
|
|
||||||
ep:activate (Features.ALL, function (item)
|
|
||||||
item:register ()
|
|
||||||
Log.info(item, "registered endpoint " .. properties.name)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
for name, properties in pairs(endpoints_config) do
|
|
||||||
properties["name"] = name
|
|
||||||
createEndpoint ("si-audio-endpoint", properties)
|
|
||||||
end
|
|
||||||
806
.config/wireplumber/wireplumber.conf
Normal file
806
.config/wireplumber/wireplumber.conf
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
## The WirePlumber configuration
|
||||||
|
|
||||||
|
context.spa-libs = {
|
||||||
|
## SPA factory name to library mappings
|
||||||
|
## Used to find SPA factory names. It maps a SPA factory name regular
|
||||||
|
## expression to a library name that should contain that factory.
|
||||||
|
##
|
||||||
|
## Syntax:
|
||||||
|
## <factory-name regex> = <library-name>
|
||||||
|
|
||||||
|
api.alsa.* = alsa/libspa-alsa
|
||||||
|
api.bluez5.* = bluez5/libspa-bluez5
|
||||||
|
api.v4l2.* = v4l2/libspa-v4l2
|
||||||
|
api.libcamera.* = libcamera/libspa-libcamera
|
||||||
|
audio.convert.* = audioconvert/libspa-audioconvert
|
||||||
|
support.* = support/libspa-support
|
||||||
|
}
|
||||||
|
|
||||||
|
context.modules = [
|
||||||
|
## PipeWire modules to load.
|
||||||
|
## These modules are loaded before a connection to pipewire is attempted.
|
||||||
|
## This section should be kept minimal and load only the modules that are
|
||||||
|
## necessary for the protocol to work.
|
||||||
|
##
|
||||||
|
## If ifexists is given, the module is ignored when it is not found.
|
||||||
|
## If nofail is given, module initialization failures are ignored.
|
||||||
|
##
|
||||||
|
## Syntax:
|
||||||
|
## {
|
||||||
|
## name = <module-name>
|
||||||
|
## [ args = { <key> = <value> ... } ]
|
||||||
|
## [ flags = [ ifexists | nofail ] ]
|
||||||
|
## }
|
||||||
|
|
||||||
|
# Uses RTKit to boost the data thread priority. Also allows clamping
|
||||||
|
# of utilisation when using the Completely Fair Scheduler on Linux.
|
||||||
|
{
|
||||||
|
name = libpipewire-module-rt
|
||||||
|
args = {
|
||||||
|
nice.level = -11
|
||||||
|
# rt.prio = 88
|
||||||
|
# rt.time.soft = -1
|
||||||
|
# rt.time.hard = -1
|
||||||
|
# uclamp.min = 0
|
||||||
|
# uclamp.max = 1024
|
||||||
|
}
|
||||||
|
flags = [ ifexists, nofail ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## The native communication protocol.
|
||||||
|
{ name = libpipewire-module-protocol-native }
|
||||||
|
|
||||||
|
## Support for metadata objects
|
||||||
|
{ name = libpipewire-module-metadata }
|
||||||
|
]
|
||||||
|
|
||||||
|
wireplumber.profiles = {
|
||||||
|
## Syntax:
|
||||||
|
## <profile> = {
|
||||||
|
## # optional is the default
|
||||||
|
## <feature name> = [ required | optional | disabled ]
|
||||||
|
## ...
|
||||||
|
## }
|
||||||
|
|
||||||
|
# The default profile
|
||||||
|
main = {
|
||||||
|
check.no-media-session = required
|
||||||
|
metadata.sm-settings = required
|
||||||
|
support.settings = required
|
||||||
|
support.log-settings = required
|
||||||
|
metadata.sm-objects = required
|
||||||
|
hardware.audio = required
|
||||||
|
hardware.bluetooth = required
|
||||||
|
hardware.video-capture = required
|
||||||
|
policy.standard = required
|
||||||
|
}
|
||||||
|
|
||||||
|
# Profile for video-only use cases (camera & screen sharing)
|
||||||
|
video-only = {
|
||||||
|
check.no-media-session = required
|
||||||
|
metadata.sm-settings = required
|
||||||
|
support.settings = required
|
||||||
|
support.log-settings = required
|
||||||
|
metadata.sm-objects = required
|
||||||
|
hardware.audio = disabled
|
||||||
|
hardware.bluetooth = disabled
|
||||||
|
hardware.video-capture = required
|
||||||
|
policy.standard = required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wireplumber.components = [
|
||||||
|
## WirePlumber components to load.
|
||||||
|
## These components are loaded after a connection to pipewire is established.
|
||||||
|
## type is mandatory; rest of the tags are optional
|
||||||
|
##
|
||||||
|
## Syntax:
|
||||||
|
## {
|
||||||
|
## name = <component-name>
|
||||||
|
## type = <component-type>
|
||||||
|
## arguments = { <json object> }
|
||||||
|
##
|
||||||
|
## # Feature that this component provides
|
||||||
|
## provides = <feature>
|
||||||
|
##
|
||||||
|
## # List of features that must be provided before this component is loaded
|
||||||
|
## requires = [ <features> ]
|
||||||
|
##
|
||||||
|
## # List of features that would offer additional functionality if provided
|
||||||
|
## # but are not strictly required
|
||||||
|
## wants = [ <features> ]
|
||||||
|
## }
|
||||||
|
|
||||||
|
## Check to avoid loading together with media-session
|
||||||
|
{
|
||||||
|
name = ensure-no-media-session, type = built-in
|
||||||
|
provides = check.no-media-session
|
||||||
|
}
|
||||||
|
|
||||||
|
## Makes a secondary connection to PipeWire for exporting objects
|
||||||
|
{
|
||||||
|
name = export-core, type = built-in
|
||||||
|
provides = support.export-core
|
||||||
|
}
|
||||||
|
|
||||||
|
## Enables creating local nodes that are exported to pipewire
|
||||||
|
## This is needed for LocalNode() / WpImplNode
|
||||||
|
## This should be used with the export-core to avoid protocol deadlocks,
|
||||||
|
## unless you know what you are doing
|
||||||
|
{
|
||||||
|
name = libpipewire-module-client-node, type = pw-module
|
||||||
|
provides = pw.client-node
|
||||||
|
wants = [ support.export-core ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Enables creating local devices that are exported to pipewire
|
||||||
|
## This is needed for SpaDevice() / WpSpaDevice
|
||||||
|
## This should be used with the export-core to avoid protocol deadlocks,
|
||||||
|
## unless you know what you are doing
|
||||||
|
{
|
||||||
|
name = libpipewire-module-client-device, type = pw-module
|
||||||
|
provides = pw.client-device
|
||||||
|
wants = [ support.export-core ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Provides a node factory to create SPA nodes
|
||||||
|
# You need this to use LocalNode("spa-node-factory", ...)
|
||||||
|
{
|
||||||
|
name = libpipewire-module-spa-node-factory, type = pw-module
|
||||||
|
provides = pw.node-factory.spa
|
||||||
|
requires = [ pw.client-node ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Provides a node factory to create SPA nodes wrapped in an adapter
|
||||||
|
## You need this to use LocalNode("adapter", ...)
|
||||||
|
{
|
||||||
|
name = libpipewire-module-adapter, type = pw-module
|
||||||
|
provides = pw.node-factory.adapter
|
||||||
|
requires = [ pw.client-node ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Provides the "sm-settings" metadata object
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-settings, type = module
|
||||||
|
arguments = { metadata.name = sm-settings }
|
||||||
|
provides = metadata.sm-settings
|
||||||
|
}
|
||||||
|
|
||||||
|
## Activates a global WpSettings instance, providing settings from
|
||||||
|
## the sm-settings metadata object
|
||||||
|
{
|
||||||
|
name = settings-instance, type = built-in
|
||||||
|
arguments = { metadata.name = sm-settings }
|
||||||
|
provides = support.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
## Log level settings
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-log-settings, type = module
|
||||||
|
provides = support.log-settings
|
||||||
|
}
|
||||||
|
|
||||||
|
## The lua scripting engine
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-lua-scripting, type = module
|
||||||
|
provides = support.lua-scripting
|
||||||
|
}
|
||||||
|
|
||||||
|
## Module listening for pipewire objects to push events
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-standard-event-source, type = module
|
||||||
|
provides = support.standard-event-source
|
||||||
|
}
|
||||||
|
|
||||||
|
## The shared D-Bus connection
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-dbus-connection, type = module
|
||||||
|
provides = support.dbus
|
||||||
|
}
|
||||||
|
|
||||||
|
## Module managing the portal permissions
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-portal-permissionstore, type = module
|
||||||
|
provides = support.portal-permissionstore
|
||||||
|
requires = [ support.dbus ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Needed for device reservation to work
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-reserve-device, type = module
|
||||||
|
provides = support.reserve-device
|
||||||
|
requires = [ support.dbus ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## logind integration to enable certain functionality only on the active seat
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-logind, type = module
|
||||||
|
provides = support.logind
|
||||||
|
}
|
||||||
|
|
||||||
|
## Session item factories
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-si-node, type = module
|
||||||
|
provides = si.node
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-si-audio-adapter, type = module
|
||||||
|
provides = si.audio-adapter
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-si-standard-link, type = module
|
||||||
|
provides = si.standard-link
|
||||||
|
}
|
||||||
|
|
||||||
|
## API to access default nodes from scripts
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-default-nodes-api, type = module
|
||||||
|
provides = api.default-nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
## API to access mixer controls
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-mixer-api, type = module
|
||||||
|
provides = api.mixer
|
||||||
|
}
|
||||||
|
|
||||||
|
## API to get notified about file changes
|
||||||
|
{
|
||||||
|
name = libwireplumber-module-file-monitor-api, type = module
|
||||||
|
provides = api.file-monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
## Provide the "default" pw_metadata
|
||||||
|
{
|
||||||
|
name = metadata.lua, type = script/lua
|
||||||
|
arguments = { metadata.name = default }
|
||||||
|
provides = metadata.default
|
||||||
|
}
|
||||||
|
|
||||||
|
## Provide the "filters" pw_metadata
|
||||||
|
{
|
||||||
|
name = metadata.lua, type = script/lua
|
||||||
|
arguments = { metadata.name = filters }
|
||||||
|
provides = metadata.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
## Provide the "sm-objects" pw_metadata, supporting dynamic loadable objects
|
||||||
|
{
|
||||||
|
name = sm-objects.lua, type = script/lua
|
||||||
|
provides = metadata.sm-objects
|
||||||
|
}
|
||||||
|
|
||||||
|
## Device monitors' optional features
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.alsa.reserve-device,
|
||||||
|
requires = [ support.reserve-device ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.alsa-midi.monitoring,
|
||||||
|
requires = [ api.file-monitor ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.bluez.seat-monitoring,
|
||||||
|
requires = [ support.logind ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Device monitors
|
||||||
|
{
|
||||||
|
name = monitors/alsa.lua, type = script/lua
|
||||||
|
provides = monitor.alsa
|
||||||
|
requires = [ support.export-core, pw.client-device ]
|
||||||
|
wants = [ monitor.alsa.reserve-device ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/bluez.lua, type = script/lua
|
||||||
|
provides = monitor.bluez
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
pw.client-node,
|
||||||
|
pw.node-factory.adapter ]
|
||||||
|
wants = [ monitor.bluez.seat-monitoring ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/bluez-midi.lua, type = script/lua
|
||||||
|
provides = monitor.bluez-midi
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
pw.client-node,
|
||||||
|
pw.node-factory.spa ]
|
||||||
|
wants = [ monitor.bluez.seat-monitoring ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/alsa-midi.lua, type = script/lua
|
||||||
|
provides = monitor.alsa-midi
|
||||||
|
wants = [ monitor.alsa-midi.monitoring ]
|
||||||
|
}
|
||||||
|
## v4l2 monitor hooks
|
||||||
|
{
|
||||||
|
name = monitors/v4l2/name-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.v4l2-name-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/v4l2/create-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.v4l2-create-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/v4l2/name-node.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.v4l2-name-node
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/v4l2/create-node.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.v4l2-create-node
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.v4l2.hooks
|
||||||
|
wants = [ hooks.monitor.v4l2-name-device,
|
||||||
|
hooks.monitor.v4l2-create-device,
|
||||||
|
hooks.monitor.v4l2-name-node,
|
||||||
|
hooks.monitor.v4l2-create-node ]
|
||||||
|
}
|
||||||
|
# enumerate-device.lua needs rest of the monitor hooks to be loaded first.
|
||||||
|
{
|
||||||
|
name = monitors/v4l2/enumerate-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.v4l2-enumerate-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
support.standard-event-source,
|
||||||
|
monitor.v4l2.hooks ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.v4l2
|
||||||
|
wants = [ hooks.monitor.v4l2-enumerate-device,
|
||||||
|
monitor.v4l2.hooks ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## libcamera monitor hooks
|
||||||
|
{
|
||||||
|
name = monitors/libcamera/name-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.libcamera-name-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/libcamera/create-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.libcamera-create-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/libcamera/name-node.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.libcamera-name-node
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = monitors/libcamera/create-node.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.libcamera-create-node
|
||||||
|
requires = [ support.export-core,
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.libcamera.hooks
|
||||||
|
wants = [ hooks.monitor.libcamera-name-device,
|
||||||
|
hooks.monitor.libcamera-create-device,
|
||||||
|
hooks.monitor.libcamera-name-node,
|
||||||
|
hooks.monitor.libcamera-create-node ]
|
||||||
|
}
|
||||||
|
# enumerate-device.lua needs rest of the monitor hooks to be loaded first.
|
||||||
|
{
|
||||||
|
name = monitors/libcamera/enumerate-device.lua, type = script/lua
|
||||||
|
provides = hooks.monitor.libcamera-enumerate-device
|
||||||
|
requires = [ support.export-core,
|
||||||
|
pw.client-device,
|
||||||
|
support.standard-event-source,
|
||||||
|
monitor.libcamera.hooks ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = monitor.libcamera
|
||||||
|
wants = [ hooks.monitor.libcamera-enumerate-device,
|
||||||
|
monitor.libcamera.hooks ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Client access configuration hooks
|
||||||
|
{
|
||||||
|
name = client/access-default.lua, type = script/lua
|
||||||
|
provides = script.client.access-default
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = client/access-portal.lua, type = script/lua
|
||||||
|
provides = script.client.access-portal
|
||||||
|
requires = [ support.portal-permissionstore ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = client/access-snap.lua, type = script/lua
|
||||||
|
provides = script.client.access-snap
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.client.access
|
||||||
|
wants = [ script.client.access-default,
|
||||||
|
script.client.access-portal,
|
||||||
|
script.client.access-snap ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Device profile selection hooks
|
||||||
|
{
|
||||||
|
name = device/select-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.select
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/find-preferred-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.find-preferred
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/find-best-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.find-best
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/state-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.state
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/apply-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.apply
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/autoswitch-bluetooth-profile.lua, type = script/lua
|
||||||
|
provides = hooks.device.profile.autoswitch-bluetooth
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.device.profile
|
||||||
|
requires = [ hooks.device.profile.select,
|
||||||
|
hooks.device.profile.autoswitch-bluetooth,
|
||||||
|
hooks.device.profile.apply ]
|
||||||
|
wants = [ hooks.device.profile.find-best, hooks.device.profile.find-preferred,
|
||||||
|
hooks.device.profile.state ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Device route selection hooks
|
||||||
|
{
|
||||||
|
name = device/select-routes.lua, type = script/lua
|
||||||
|
provides = hooks.device.routes.select
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/find-best-routes.lua, type = script/lua
|
||||||
|
provides = hooks.device.routes.find-best
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/state-routes.lua, type = script/lua
|
||||||
|
provides = hooks.device.routes.state
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = device/apply-routes.lua, type = script/lua
|
||||||
|
provides = hooks.device.routes.apply
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.device.routes
|
||||||
|
requires = [ hooks.device.routes.select,
|
||||||
|
hooks.device.routes.apply ]
|
||||||
|
wants = [ hooks.device.routes.find-best,
|
||||||
|
hooks.device.routes.state ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Default nodes selection hooks
|
||||||
|
{
|
||||||
|
name = default-nodes/rescan.lua, type = script/lua
|
||||||
|
provides = hooks.default-nodes.rescan
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = default-nodes/find-selected-default-node.lua, type = script/lua
|
||||||
|
provides = hooks.default-nodes.find-selected
|
||||||
|
requires = [ metadata.default ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = default-nodes/find-best-default-node.lua, type = script/lua
|
||||||
|
provides = hooks.default-nodes.find-best
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = default-nodes/state-default-nodes.lua, type = script/lua
|
||||||
|
provides = hooks.default-nodes.state
|
||||||
|
requires = [ metadata.default ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = default-nodes/apply-default-node.lua, type = script/lua,
|
||||||
|
provides = hooks.default-nodes.apply
|
||||||
|
requires = [ metadata.default ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.default-nodes
|
||||||
|
requires = [ hooks.default-nodes.rescan,
|
||||||
|
hooks.default-nodes.apply ]
|
||||||
|
wants = [ hooks.default-nodes.find-selected,
|
||||||
|
hooks.default-nodes.find-best,
|
||||||
|
hooks.default-nodes.state ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Node configuration hooks
|
||||||
|
{
|
||||||
|
name = node/create-item.lua, type = script/lua
|
||||||
|
provides = hooks.node.create-session-item
|
||||||
|
requires = [ si.audio-adapter, si.node ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = node/suspend-node.lua, type = script/lua
|
||||||
|
provides = hooks.node.suspend
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = node/state-stream.lua, type = script/lua
|
||||||
|
provides = hooks.stream.state
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = node/filter-forward-format.lua, type = script/lua
|
||||||
|
provides = hooks.filter.forward-format
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.node
|
||||||
|
requires = [ hooks.node.create-session-item ]
|
||||||
|
wants = [ hooks.node.suspend
|
||||||
|
hooks.stream.state
|
||||||
|
hooks.filter.forward-format ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = node/software-dsp.lua, type = script/lua
|
||||||
|
provides = node.software-dsp
|
||||||
|
}
|
||||||
|
|
||||||
|
## Linking hooks
|
||||||
|
{
|
||||||
|
name = linking/rescan.lua, type = script/lua
|
||||||
|
provides = hooks.linking.rescan
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/find-media-role-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.find-media-role
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/find-defined-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.find-defined
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/find-filter-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.find-filter
|
||||||
|
requires = [ metadata.filters ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/find-default-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.find-default
|
||||||
|
requires = [ api.default-nodes ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/find-best-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.find-best
|
||||||
|
requires = [ metadata.filters ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/get-filter-from-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.get-filter-from
|
||||||
|
requires = [ metadata.filters ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/prepare-link.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.prepare-link
|
||||||
|
requires = [ api.default-nodes ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = linking/link-target.lua, type = script/lua
|
||||||
|
provides = hooks.linking.target.link
|
||||||
|
requires = [ si.standard-link ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.linking.standard
|
||||||
|
requires = [ hooks.linking.rescan,
|
||||||
|
hooks.linking.target.prepare-link,
|
||||||
|
hooks.linking.target.link ]
|
||||||
|
wants = [ hooks.linking.target.find-media-role,
|
||||||
|
hooks.linking.target.find-defined,
|
||||||
|
hooks.linking.target.find-filter,
|
||||||
|
hooks.linking.target.find-default,
|
||||||
|
hooks.linking.target.find-best,
|
||||||
|
hooks.linking.target.get-filter-from ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Linking: Role-based priority system
|
||||||
|
{
|
||||||
|
name = linking/rescan-media-role-links.lua, type = script/lua
|
||||||
|
provides = hooks.linking.role-based.rescan
|
||||||
|
requires = [ api.mixer ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.linking.role-based
|
||||||
|
requires = [ policy.linking.standard,
|
||||||
|
hooks.linking.role-based.rescan ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Standard policy definition
|
||||||
|
{
|
||||||
|
type = virtual, provides = policy.standard
|
||||||
|
requires = [ policy.client.access
|
||||||
|
policy.device.profile
|
||||||
|
policy.device.routes
|
||||||
|
policy.default-nodes
|
||||||
|
policy.linking.standard
|
||||||
|
policy.linking.role-based
|
||||||
|
policy.node
|
||||||
|
support.standard-event-source ]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Load targets
|
||||||
|
{
|
||||||
|
type = virtual, provides = hardware.audio
|
||||||
|
wants = [ monitor.alsa, monitor.alsa-midi ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = hardware.bluetooth
|
||||||
|
wants = [ monitor.bluez, monitor.bluez-midi ]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = virtual, provides = hardware.video-capture
|
||||||
|
wants = [ monitor.v4l2, monitor.libcamera ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
wireplumber.components.rules = [
|
||||||
|
## Rules to apply on top of wireplumber.components
|
||||||
|
## Syntax:
|
||||||
|
## {
|
||||||
|
## matches = [
|
||||||
|
## {
|
||||||
|
## [ <key> = <value> ... ]
|
||||||
|
## }
|
||||||
|
## ...
|
||||||
|
## ]
|
||||||
|
## actions = {
|
||||||
|
## <override|merge> = {
|
||||||
|
## [ <key> = <value> ... ]
|
||||||
|
## }
|
||||||
|
## ...
|
||||||
|
## }
|
||||||
|
## }
|
||||||
|
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
type = "script/lua"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
merge = {
|
||||||
|
requires = [ support.lua-scripting ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
wireplumber.settings.schema = {
|
||||||
|
## Bluetooth
|
||||||
|
bluetooth.use-persistent-storage = {
|
||||||
|
description = "Whether to use persistent BT storage or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
bluetooth.autoswitch-to-headset-profile = {
|
||||||
|
description = "Whether to autoswitch to BT headset profile or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
## Device
|
||||||
|
device.restore-profile = {
|
||||||
|
description = "Whether to restore device profile or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
device.restore-routes = {
|
||||||
|
description = "Whether to restore device routes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
device.routes.default-sink-volume = {
|
||||||
|
description = "The default volume for sink devices"
|
||||||
|
type = "float"
|
||||||
|
default = 0.064
|
||||||
|
min = 0.0
|
||||||
|
max = 1.0
|
||||||
|
}
|
||||||
|
device.routes.default-source-volume = {
|
||||||
|
description = "The default volume for source devices"
|
||||||
|
type = "float"
|
||||||
|
default = 1.0
|
||||||
|
min = 0.0
|
||||||
|
max = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
## Linking
|
||||||
|
linking.role-based.duck-level = {
|
||||||
|
description = "The volume level to apply when ducking (= reducing volume for a higher priority stream to be audible) in the role-based linking policy"
|
||||||
|
type = "float"
|
||||||
|
default = 0.3
|
||||||
|
min = 0.0
|
||||||
|
max = 1.0
|
||||||
|
}
|
||||||
|
linking.allow-moving-streams = {
|
||||||
|
description = "Whether to allow metadata to move streams at runtime or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
linking.follow-default-target = {
|
||||||
|
description = "Whether to allow streams follow the default device or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
## Monitor
|
||||||
|
monitor.camera-discovery-timeout = {
|
||||||
|
description = "The camera discovery timeout in milliseconds"
|
||||||
|
type = "int"
|
||||||
|
default = 1000
|
||||||
|
min = 0
|
||||||
|
max = 60000
|
||||||
|
}
|
||||||
|
|
||||||
|
## Node
|
||||||
|
node.features.audio.no-dsp = {
|
||||||
|
description = "Whether to never convert audio to F32 format or not"
|
||||||
|
type = "bool"
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
node.features.audio.monitor-ports = {
|
||||||
|
description = "Whether to enable monitor ports on audio nodes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
node.features.audio.control-port = {
|
||||||
|
description = "Whether to enable control ports on audio nodes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
node.stream.restore-props = {
|
||||||
|
description = "Whether to restore properties on stream nodes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
node.stream.restore-target = {
|
||||||
|
description = "Whether to restore target on stream nodes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
node.stream.default-playback-volume = {
|
||||||
|
description = "The default volume for playback nodes"
|
||||||
|
type = "float"
|
||||||
|
default = 1.0
|
||||||
|
min = 0.0
|
||||||
|
max = 1.0
|
||||||
|
}
|
||||||
|
node.stream.default-capture-volume = {
|
||||||
|
description = "The default volume for capture nodes"
|
||||||
|
type = "float"
|
||||||
|
default = 1.0
|
||||||
|
min = 0.0
|
||||||
|
max = 1.0
|
||||||
|
}
|
||||||
|
node.stream.default-media-role = {
|
||||||
|
description = "A media.role to assign on streams that have none specified"
|
||||||
|
type = "string"
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
node.filter.forward-format = {
|
||||||
|
description = "Whether to forward format on filter nodes or not"
|
||||||
|
type = "bool"
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
node.restore-default-targets = {
|
||||||
|
description = "Whether to restore default targets or not"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
}
|
||||||
23
.config/wireplumber/wireplumber.conf.d/alsa-vm.conf
Normal file
23
.config/wireplumber/wireplumber.conf.d/alsa-vm.conf
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ALSA node property overrides for virtual machine hardware
|
||||||
|
|
||||||
|
monitor.alsa.rules = [
|
||||||
|
# Generic PCI cards on any VM type
|
||||||
|
{
|
||||||
|
matches = [
|
||||||
|
{
|
||||||
|
node.name = "~alsa_input.pci.*"
|
||||||
|
cpu.vm.name = "~.*"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
node.name = "~alsa_output.pci.*"
|
||||||
|
cpu.vm.name = "~.*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
actions = {
|
||||||
|
update-props = {
|
||||||
|
api.alsa.period-size = 1024
|
||||||
|
api.alsa.headroom = 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user