Moving files:
This commit is contained in:
74
wireplumber/scripts/monitors/alsa-midi.lua
Normal file
74
wireplumber/scripts/monitors/alsa-midi.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-monitors")
|
||||
|
||||
defaults = {}
|
||||
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"
|
||||
SEQ_NAME = "seq"
|
||||
SND_SEQ_PATH = SND_PATH .. "/" .. SEQ_NAME
|
||||
|
||||
midi_node = nil
|
||||
fm_plugin = nil
|
||||
|
||||
function CreateMidiNode ()
|
||||
-- create the midi node
|
||||
local node = Node("spa-node-factory", config.node_properties)
|
||||
node:activate(Feature.Proxy.BOUND, function (n)
|
||||
log:info ("activated Midi bridge")
|
||||
end)
|
||||
|
||||
return node;
|
||||
end
|
||||
|
||||
if GLib.access (SND_SEQ_PATH, "rw") then
|
||||
midi_node = CreateMidiNode ()
|
||||
elseif config.monitoring then
|
||||
fm_plugin = Plugin.find("file-monitor-api")
|
||||
end
|
||||
|
||||
-- Only monitor the MIDI device if file does not exist and plugin API is loaded
|
||||
if midi_node == nil and fm_plugin ~= nil then
|
||||
-- listen for changed events
|
||||
fm_plugin:connect ("changed", function (o, file, old, evtype)
|
||||
-- files attributes changed
|
||||
if evtype == "attribute-changed" then
|
||||
if file ~= SND_SEQ_PATH then
|
||||
return
|
||||
end
|
||||
if midi_node == nil and GLib.access (SND_SEQ_PATH, "rw") then
|
||||
midi_node = CreateMidiNode ()
|
||||
fm_plugin:call ("remove-watch", SND_PATH)
|
||||
end
|
||||
end
|
||||
|
||||
-- directory is going to be unmounted
|
||||
if evtype == "pre-unmount" then
|
||||
fm_plugin:call ("remove-watch", SND_PATH)
|
||||
end
|
||||
end)
|
||||
|
||||
-- add watch
|
||||
fm_plugin:call ("add-watch", SND_PATH, "m")
|
||||
end
|
||||
391
wireplumber/scripts/monitors/alsa.lua
Normal file
391
wireplumber/scripts/monitors/alsa.lua
Normal file
@@ -0,0 +1,391 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-monitors")
|
||||
|
||||
config = {}
|
||||
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
|
||||
device_names_table = nil
|
||||
node_names_table = nil
|
||||
|
||||
function nonempty(str)
|
||||
return str ~= "" and str or nil
|
||||
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)
|
||||
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
|
||||
|
||||
-- try to negotiate the max ammount of channels
|
||||
if dev_props["api.alsa.use-acp"] ~= "true" then
|
||||
properties["audio.channels"] = properties["audio.channels"] or "64"
|
||||
end
|
||||
|
||||
local dev = properties["api.alsa.pcm.device"]
|
||||
or properties["alsa.device"] or "0"
|
||||
local subdev = properties["api.alsa.pcm.subdevice"]
|
||||
or properties["alsa.subdevice"] or "0"
|
||||
local stream = properties["api.alsa.pcm.stream"] or "unknown"
|
||||
local profile = properties["device.profile.name"]
|
||||
or (stream .. "." .. dev .. "." .. subdev)
|
||||
local profile_desc = properties["device.profile.description"]
|
||||
|
||||
-- set priority
|
||||
if not properties["priority.driver"] then
|
||||
local priority = (dev == "0") and 1000 or 744
|
||||
if stream == "capture" then
|
||||
priority = priority + 1000
|
||||
end
|
||||
|
||||
priority = priority - (tonumber(dev) * 16) - tonumber(subdev)
|
||||
|
||||
if profile:find("^pro%-") then
|
||||
priority = priority + 500
|
||||
elseif profile:find("^analog%-") then
|
||||
priority = priority + 9
|
||||
elseif profile:find("^iec958%-") then
|
||||
priority = priority + 8
|
||||
end
|
||||
|
||||
properties["priority.driver"] = priority
|
||||
properties["priority.session"] = priority
|
||||
end
|
||||
|
||||
-- ensure the node has a media class
|
||||
if not properties["media.class"] then
|
||||
if stream == "capture" then
|
||||
properties["media.class"] = "Audio/Source"
|
||||
else
|
||||
properties["media.class"] = "Audio/Sink"
|
||||
end
|
||||
end
|
||||
|
||||
-- ensure the node has a name
|
||||
if not properties["node.name"] then
|
||||
local name =
|
||||
(stream == "capture" and "alsa_input" or "alsa_output")
|
||||
.. "." ..
|
||||
(dev_props["device.name"]:gsub("^alsa_card%.(.+)", "%1") or
|
||||
dev_props["device.name"] or
|
||||
"unnamed-device")
|
||||
.. "." ..
|
||||
profile
|
||||
|
||||
-- sanitize name
|
||||
name = name:gsub("([^%w_%-%.])", "_")
|
||||
|
||||
properties["node.name"] = name
|
||||
|
||||
log:info ("Creating node " .. name)
|
||||
|
||||
-- deduplicate nodes with the same name
|
||||
for counter = 2, 99, 1 do
|
||||
if node_names_table[properties["node.name"]] ~= true then
|
||||
node_names_table[properties["node.name"]] = true
|
||||
break
|
||||
end
|
||||
properties["node.name"] = name .. "." .. counter
|
||||
log:info ("deduplicating node name -> " .. properties["node.name"])
|
||||
end
|
||||
end
|
||||
|
||||
-- and a nick
|
||||
local nick = nonempty(properties["node.nick"])
|
||||
or nonempty(properties["api.alsa.pcm.name"])
|
||||
or nonempty(properties["alsa.name"])
|
||||
or nonempty(profile_desc)
|
||||
or dev_props["device.nick"]
|
||||
if nick == "USB Audio" then
|
||||
nick = dev_props["device.nick"]
|
||||
end
|
||||
-- also sanitize nick, replace ':' with ' '
|
||||
properties["node.nick"] = nick:gsub("(:)", " ")
|
||||
|
||||
-- ensure the node has a description
|
||||
if not properties["node.description"] then
|
||||
local desc = nonempty(dev_props["device.description"]) or "unknown"
|
||||
local name = nonempty(properties["api.alsa.pcm.name"]) or
|
||||
nonempty(properties["api.alsa.pcm.id"]) or dev
|
||||
|
||||
if profile_desc then
|
||||
desc = desc .. " " .. profile_desc
|
||||
elseif subdev ~= "0" then
|
||||
desc = desc .. " (" .. name .. " " .. subdev .. ")"
|
||||
elseif dev ~= "0" then
|
||||
desc = desc .. " (" .. name .. ")"
|
||||
end
|
||||
|
||||
-- also sanitize description, replace ':' with ' '
|
||||
properties["node.description"] = desc:gsub("(:)", " ")
|
||||
end
|
||||
|
||||
-- add api.alsa.card.* properties for rule matching purposes
|
||||
for k, v in pairs(dev_props) do
|
||||
if k:find("^api%.alsa%.card%..*") then
|
||||
properties[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- add cpu.vm.name for rule matching purposes
|
||||
local vm_type = Core.get_vm_type()
|
||||
if nonempty(vm_type) then
|
||||
properties["cpu.vm.name"] = vm_type
|
||||
end
|
||||
|
||||
-- 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 ("ALSA node " .. properties["node.name"] .. " disabled")
|
||||
node_names_table [properties ["node.name"]] = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- create the node
|
||||
local node = Node("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
parent:store_managed_object(id, node)
|
||||
end
|
||||
|
||||
function createDevice(parent, id, factory, properties)
|
||||
local device = SpaDevice(factory, properties)
|
||||
if device then
|
||||
device:connect("create-object", createNode)
|
||||
device:connect("object-removed", function (parent, id)
|
||||
local node = parent:get_managed_object(id)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
|
||||
local node_name = node.properties["node.name"]
|
||||
log:info ("Removing node " .. node_name)
|
||||
node_names_table[node_name] = nil
|
||||
end)
|
||||
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
||||
parent:store_managed_object(id, device)
|
||||
else
|
||||
log:warning ("Failed to create '" .. factory .. "' device")
|
||||
end
|
||||
end
|
||||
|
||||
function prepareDevice(parent, id, obj_type, factory, properties)
|
||||
-- ensure the device has an appropriate name
|
||||
local name = "alsa_card." ..
|
||||
(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 device_names_table[properties["device.name"]] ~= true then
|
||||
device_names_table[properties["device.name"]] = true
|
||||
break
|
||||
end
|
||||
properties["device.name"] = name .. "." .. counter
|
||||
end
|
||||
|
||||
-- ensure the device has a description
|
||||
if not properties["device.description"] then
|
||||
local d = nil
|
||||
local f = properties["device.form-factor"]
|
||||
local c = properties["device.class"]
|
||||
local n = properties["api.alsa.card.name"]
|
||||
|
||||
if n == "Loopback" then
|
||||
d = I18n.gettext("Loopback")
|
||||
elseif f == "internal" then
|
||||
d = I18n.gettext("Built-in Audio")
|
||||
elseif c == "modem" then
|
||||
d = I18n.gettext("Modem")
|
||||
end
|
||||
|
||||
d = d or properties["device.product.name"]
|
||||
or properties["api.alsa.card.name"]
|
||||
or properties["alsa.card_name"]
|
||||
or "Unknown device"
|
||||
properties["device.description"] = d
|
||||
end
|
||||
|
||||
-- ensure the device has a nick
|
||||
properties["device.nick"] =
|
||||
properties["device.nick"] or
|
||||
properties["api.alsa.card.name"] or
|
||||
properties["alsa.card_name"]
|
||||
|
||||
-- set the icon name
|
||||
if not properties["device.icon-name"] then
|
||||
local icon = nil
|
||||
local icon_map = {
|
||||
-- form factor -> icon
|
||||
["microphone"] = "audio-input-microphone",
|
||||
["webcam"] = "camera-web",
|
||||
["handset"] = "phone",
|
||||
["portable"] = "multimedia-player",
|
||||
["tv"] = "video-display",
|
||||
["headset"] = "audio-headset",
|
||||
["headphone"] = "audio-headphones",
|
||||
["speaker"] = "audio-speakers",
|
||||
["hands-free"] = "audio-handsfree",
|
||||
}
|
||||
local f = properties["device.form-factor"]
|
||||
local c = properties["device.class"]
|
||||
local b = properties["device.bus"]
|
||||
|
||||
icon = icon_map[f] or ((c == "modem") and "modem") or "audio-card"
|
||||
properties["device.icon-name"] = icon .. "-analog" .. (b and ("-" .. b) or "")
|
||||
end
|
||||
|
||||
-- apply properties from rules defined in JSON .conf file
|
||||
applyDefaultDeviceProperties (properties)
|
||||
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
|
||||
return
|
||||
end
|
||||
|
||||
-- override the device factory to use ACP
|
||||
if cutils.parseBool (properties ["api.alsa.use-acp"]) then
|
||||
log:info("Enabling the use of ACP on " .. properties["device.name"])
|
||||
factory = "api.alsa.acp.device"
|
||||
end
|
||||
|
||||
-- use device reservation, if available
|
||||
if rd_plugin and properties["api.alsa.card"] then
|
||||
local rd_name = "Audio" .. properties["api.alsa.card"]
|
||||
local rd = rd_plugin:call("create-reservation",
|
||||
rd_name,
|
||||
cutils.get_application_name (),
|
||||
properties["device.name"],
|
||||
properties["api.dbus.ReserveDevice1.Priority"]);
|
||||
|
||||
properties["api.dbus.ReserveDevice1"] = rd_name
|
||||
|
||||
-- unlike pipewire-media-session, this logic here keeps the device
|
||||
-- acquired at all times and destroys it if someone else acquires
|
||||
rd:connect("notify::state", function (rd, pspec)
|
||||
local state = rd["state"]
|
||||
|
||||
if state == "acquired" then
|
||||
-- create the device
|
||||
createDevice(parent, id, factory, properties)
|
||||
|
||||
elseif state == "available" then
|
||||
-- attempt to acquire again
|
||||
rd:call("acquire")
|
||||
|
||||
elseif state == "busy" then
|
||||
-- destroy the device
|
||||
parent:store_managed_object(id, nil)
|
||||
end
|
||||
end)
|
||||
|
||||
rd:connect("release-requested", function (rd)
|
||||
log:info("release requested")
|
||||
parent:store_managed_object(id, nil)
|
||||
rd:call("release")
|
||||
end)
|
||||
|
||||
rd:call("acquire")
|
||||
else
|
||||
-- create the device
|
||||
createDevice(parent, id, factory, properties)
|
||||
end
|
||||
end
|
||||
|
||||
function createMonitor ()
|
||||
local m = SpaDevice("api.alsa.enum.udev", config.properties)
|
||||
if m == nil then
|
||||
log:notice("PipeWire's ALSA SPA plugin is missing or broken. " ..
|
||||
"Sound cards will not be supported")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- handle create-object to prepare device
|
||||
m:connect("create-object", prepareDevice)
|
||||
|
||||
-- handle object-removed to destroy device reservations and recycle device name
|
||||
m:connect("object-removed", function (parent, id)
|
||||
local device = parent:get_managed_object(id)
|
||||
if not device then
|
||||
return
|
||||
end
|
||||
|
||||
if rd_plugin then
|
||||
local rd_name = device.properties["api.dbus.ReserveDevice1"]
|
||||
if rd_name then
|
||||
rd_plugin:call("destroy-reservation", rd_name)
|
||||
end
|
||||
end
|
||||
device_names_table[device.properties["device.name"]] = nil
|
||||
for managed_node in device:iterate_managed_objects() do
|
||||
node_names_table[managed_node.properties["node.name"]] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
-- reset the name tables to make sure names are recycled
|
||||
device_names_table = {}
|
||||
node_names_table = {}
|
||||
|
||||
-- activate monitor
|
||||
log:info("Activating ALSA monitor")
|
||||
m:activate(Feature.SpaDevice.ENABLED)
|
||||
return m
|
||||
end
|
||||
|
||||
-- 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
|
||||
-- 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
|
||||
log:notice("reserve-device plugin is not connected to D-Bus, "
|
||||
.. "disabling device reservation")
|
||||
rd_plugin = nil
|
||||
end
|
||||
|
||||
-- handle rd_plugin state changes to destroy and re-create the ALSA monitor in
|
||||
-- case D-Bus service is restarted
|
||||
if rd_plugin then
|
||||
local dbus = rd_plugin:call("get-dbus")
|
||||
dbus:connect("notify::state", function (b, pspec)
|
||||
local state = b["state"]
|
||||
log:info ("rd-plugin state changed to " .. state)
|
||||
if state == "connected" then
|
||||
log:info ("Creating ALSA monitor")
|
||||
monitor = createMonitor()
|
||||
elseif state == "closed" then
|
||||
log:info ("Destroying ALSA monitor")
|
||||
monitor = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- create the monitor
|
||||
monitor = createMonitor()
|
||||
162
wireplumber/scripts/monitors/bluez-midi.lua
Normal file
162
wireplumber/scripts/monitors/bluez-midi.lua
Normal file
@@ -0,0 +1,162 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Pauli Virtanen
|
||||
-- @author Pauli Virtanen
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
node_names_table = nil
|
||||
id_to_name_table = nil
|
||||
|
||||
function setLatencyOffset(node, offset_msec)
|
||||
if not offset_msec then
|
||||
return
|
||||
end
|
||||
|
||||
local props = { "Spa:Pod:Object:Param:Props", "Props" }
|
||||
props.latencyOffsetNsec = tonumber(offset_msec) * 1000000
|
||||
|
||||
local param = Pod.Object(props)
|
||||
log:debug(param, "setting latency offset on " .. tostring(node))
|
||||
node:set_param("Props", param)
|
||||
end
|
||||
|
||||
function createNode(parent, id, type, factory, properties)
|
||||
properties["factory.name"] = factory
|
||||
|
||||
-- set the node description
|
||||
local desc = properties["node.description"]
|
||||
-- sanitize description, replace ':' with ' '
|
||||
properties["node.description"] = desc:gsub("(:)", " ")
|
||||
|
||||
-- set the node name
|
||||
local name =
|
||||
"bluez_midi." .. properties["api.bluez5.address"]
|
||||
-- sanitize name
|
||||
name = name:gsub("([^%w_%-%.])", "_")
|
||||
-- deduplicate nodes with the same name
|
||||
properties["node.name"] = name
|
||||
for counter = 2, 99, 1 do
|
||||
if node_names_table[properties["node.name"]] ~= true then
|
||||
node_names_table[properties["node.name"]] = true
|
||||
break
|
||||
end
|
||||
properties["node.name"] = name .. "." .. counter
|
||||
end
|
||||
|
||||
properties["api.glib.mainloop"] = "true"
|
||||
|
||||
-- apply properties from the rules in the configuration file
|
||||
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||
|
||||
local latency_offset = properties["node.latency-offset-msec"]
|
||||
properties["node.latency-offset-msec"] = nil
|
||||
|
||||
-- create the node
|
||||
-- it doesn't necessarily need to be a local node,
|
||||
-- the other Bluetooth parts run in the local process,
|
||||
-- so it's consistent to have also this here
|
||||
local node = LocalNode("spa-node-factory", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
parent:store_managed_object(id, node)
|
||||
id_to_name_table[id] = properties["node.name"]
|
||||
setLatencyOffset(node, latency_offset)
|
||||
end
|
||||
|
||||
function createMonitor()
|
||||
local monitor_props = {}
|
||||
for k, v in pairs(config.properties or {}) do
|
||||
monitor_props[k] = v
|
||||
end
|
||||
|
||||
monitor_props["api.glib.mainloop"] = "true"
|
||||
|
||||
local monitor = SpaDevice("api.bluez5.midi.enum", monitor_props)
|
||||
if monitor then
|
||||
monitor:connect("create-object", createNode)
|
||||
monitor:connect("object-removed", function (parent, id)
|
||||
node_names_table[id_to_name_table[id]] = nil
|
||||
id_to_name_table[id] = nil
|
||||
end)
|
||||
else
|
||||
log:notice("PipeWire's BlueZ MIDI SPA missing or broken. Bluetooth not supported.")
|
||||
return nil
|
||||
end
|
||||
|
||||
-- reset the name tables to make sure names are recycled
|
||||
node_names_table = {}
|
||||
id_to_name_table = {}
|
||||
|
||||
monitor:activate(Feature.SpaDevice.ENABLED)
|
||||
return monitor
|
||||
end
|
||||
|
||||
function createServers()
|
||||
local servers = {}
|
||||
local i = 1
|
||||
|
||||
for k, v in pairs(config.servers) do
|
||||
local node_props = {
|
||||
["node.name"] = v,
|
||||
["node.description"] = string.format(I18n.gettext("BLE MIDI %d"), i),
|
||||
["api.bluez5.role"] = "server",
|
||||
["factory.name"] = "api.bluez5.midi.node",
|
||||
["api.glib.mainloop"] = "true",
|
||||
}
|
||||
node_props = JsonUtils.match_rules_update_properties (config.rules, node_props)
|
||||
|
||||
local latency_offset = node_props["node.latency-offset-msec"]
|
||||
node_props["node.latency-offset-msec"] = nil
|
||||
|
||||
local node = LocalNode("spa-node-factory", node_props)
|
||||
if node then
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
table.insert(servers, node)
|
||||
setLatencyOffset(node, latency_offset)
|
||||
else
|
||||
log:notice("Failed to create BLE MIDI server.")
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
return servers
|
||||
end
|
||||
|
||||
if config.seat_monitoring then
|
||||
logind_plugin = Plugin.find("logind")
|
||||
end
|
||||
if logind_plugin then
|
||||
-- if logind support is enabled, activate
|
||||
-- the monitor only when the seat is active
|
||||
function startStopMonitor(seat_state)
|
||||
log:info(logind_plugin, "Seat state changed: " .. seat_state)
|
||||
|
||||
if seat_state == "active" then
|
||||
monitor = createMonitor()
|
||||
servers = createServers()
|
||||
elseif monitor then
|
||||
monitor:deactivate(Feature.SpaDevice.ENABLED)
|
||||
monitor = nil
|
||||
servers = nil
|
||||
end
|
||||
end
|
||||
|
||||
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
|
||||
startStopMonitor(logind_plugin:call("get-state"))
|
||||
else
|
||||
monitor = createMonitor()
|
||||
servers = createServers()
|
||||
end
|
||||
517
wireplumber/scripts/monitors/bluez.lua
Normal file
517
wireplumber/scripts/monitors/bluez.lua
Normal file
@@ -0,0 +1,517 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
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 {
|
||||
Interest {
|
||||
type = "device",
|
||||
}
|
||||
}
|
||||
|
||||
nodes_om = ObjectManager {
|
||||
Interest {
|
||||
type = "node",
|
||||
Constraint { "node.name", "#", "*.bluez_*put*"},
|
||||
Constraint { "device.id", "+" },
|
||||
}
|
||||
}
|
||||
|
||||
function setOffloadActive(device, value)
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Props", "Props", bluetoothOffloadActive = value
|
||||
}
|
||||
device:set_params("Props", pod)
|
||||
end
|
||||
|
||||
nodes_om:connect("object-added", function(_, node)
|
||||
node:connect("state-changed", function(node, old_state, cur_state)
|
||||
local interest = Interest {
|
||||
type = "device",
|
||||
Constraint { "object.id", "=", node.properties["device.id"]}
|
||||
}
|
||||
for d in devices_om:iterate (interest) do
|
||||
if cur_state == "running" then
|
||||
setOffloadActive(d, true)
|
||||
else
|
||||
setOffloadActive(d, false)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
function createOffloadScoNode(parent, id, type, factory, properties)
|
||||
local dev_props = parent.properties
|
||||
|
||||
local args = {
|
||||
["audio.channels"] = 1,
|
||||
["audio.position"] = "[MONO]",
|
||||
}
|
||||
|
||||
local desc =
|
||||
dev_props["device.description"]
|
||||
or dev_props["device.name"]
|
||||
or dev_props["device.nick"]
|
||||
or dev_props["device.alias"]
|
||||
or "bluetooth-device"
|
||||
-- sanitize description, replace ':' with ' '
|
||||
args["node.description"] = desc:gsub("(:)", " ")
|
||||
|
||||
if factory:find("sink") then
|
||||
local capture_args = {
|
||||
["device.id"] = parent["bound-id"],
|
||||
["media.class"] = "Audio/Sink",
|
||||
["node.pause-on-idle"] = false,
|
||||
}
|
||||
for k, v in pairs(properties) do
|
||||
capture_args[k] = v
|
||||
end
|
||||
|
||||
local name = "bluez_output" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
|
||||
args["node.name"] = name:gsub("([^%w_%-%.])", "_")
|
||||
args["capture.props"] = Json.Object(capture_args)
|
||||
args["playback.props"] = Json.Object {
|
||||
["node.passive"] = true,
|
||||
["node.pause-on-idle"] = false,
|
||||
}
|
||||
elseif factory:find("source") then
|
||||
local playback_args = {
|
||||
["device.id"] = parent["bound-id"],
|
||||
["media.class"] = "Audio/Source",
|
||||
["node.pause-on-idle"] = false,
|
||||
}
|
||||
for k, v in pairs(properties) do
|
||||
playback_args[k] = v
|
||||
end
|
||||
|
||||
local name = "bluez_input" .. "." .. (properties["api.bluez5.address"] or dev_props["device.name"]) .. "." .. tostring(id)
|
||||
args["node.name"] = name:gsub("([^%w_%-%.])", "_")
|
||||
args["capture.props"] = Json.Object {
|
||||
["node.passive"] = true,
|
||||
["node.pause-on-idle"] = false,
|
||||
}
|
||||
args["playback.props"] = Json.Object(playback_args)
|
||||
else
|
||||
log:warning(parent, "Unsupported factory: " .. factory)
|
||||
return
|
||||
end
|
||||
|
||||
-- Transform 'args' to a json object here
|
||||
local args_json = Json.Object(args)
|
||||
|
||||
-- and get the final JSON as a string from the json object
|
||||
local args_string = args_json:get_data()
|
||||
|
||||
local loopback_properties = {}
|
||||
|
||||
local loopback = LocalModule("libpipewire-module-loopback", args_string, loopback_properties)
|
||||
parent:store_managed_object(id, loopback)
|
||||
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)
|
||||
local dev_props = parent.properties
|
||||
local parent_id = parent["bound-id"]
|
||||
|
||||
if cutils.parseBool (config.properties ["bluez5.hw-offload-sco"]) and factory:find("sco") then
|
||||
createOffloadScoNode(parent, id, type, factory, properties)
|
||||
return
|
||||
end
|
||||
|
||||
-- set the device id and spa factory name; REQUIRED, do not change
|
||||
properties["device.id"] = parent_id
|
||||
properties["factory.name"] = factory
|
||||
|
||||
-- set the default pause-on-idle setting
|
||||
properties["node.pause-on-idle"] = false
|
||||
|
||||
-- set the node description
|
||||
local desc =
|
||||
dev_props["device.description"]
|
||||
or dev_props["device.name"]
|
||||
or dev_props["device.nick"]
|
||||
or dev_props["device.alias"]
|
||||
or "bluetooth-device"
|
||||
-- sanitize description, replace ':' with ' '
|
||||
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
|
||||
local name = name_prefix .. "." ..
|
||||
(properties["api.bluez5.address"] or dev_props["device.name"]) .. "." ..
|
||||
tostring(id)
|
||||
-- sanitize name
|
||||
properties["node.name"] = name:gsub("([^%w_%-%.])", "_")
|
||||
|
||||
-- set priority
|
||||
if not properties["priority.driver"] then
|
||||
local priority = factory:find("source") and 2010 or 1010
|
||||
properties["priority.driver"] = priority
|
||||
properties["priority.session"] = priority
|
||||
end
|
||||
|
||||
-- autoconnect if it's a stream
|
||||
if properties["api.bluez5.profile"] == "headset-audio-gateway" or
|
||||
properties["api.bluez5.profile"] == "bap-sink" or
|
||||
factory:find("a2dp.source") or factory:find("media.source") then
|
||||
properties["node.autoconnect"] = true
|
||||
end
|
||||
|
||||
-- apply properties from the rules in the configuration file
|
||||
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||
|
||||
-- create the node; bluez requires "local" nodes, i.e. ones that run in
|
||||
-- the same process as the spa device, for several reasons
|
||||
|
||||
if properties["api.bluez5.set.leader"] then
|
||||
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
|
||||
|
||||
function createDevice(parent, id, type, factory, properties)
|
||||
local device = parent:get_managed_object(id)
|
||||
if not device then
|
||||
-- ensure a proper device name
|
||||
local name =
|
||||
(properties["device.name"] or
|
||||
properties["api.bluez5.address"] or
|
||||
properties["device.description"] or
|
||||
tostring(id)):gsub("([^%w_%-%.])", "_")
|
||||
|
||||
if not name:find("^bluez_card%.", 1) then
|
||||
name = "bluez_card." .. name
|
||||
end
|
||||
properties["device.name"] = name
|
||||
|
||||
-- set the icon name
|
||||
if not properties["device.icon-name"] then
|
||||
local icon = nil
|
||||
local icon_map = {
|
||||
-- form factor -> icon
|
||||
["microphone"] = "audio-input-microphone",
|
||||
["webcam"] = "camera-web",
|
||||
["handset"] = "phone",
|
||||
["portable"] = "multimedia-player",
|
||||
["tv"] = "video-display",
|
||||
["headset"] = "audio-headset",
|
||||
["headphone"] = "audio-headphones",
|
||||
["speaker"] = "audio-speakers",
|
||||
["hands-free"] = "audio-handsfree",
|
||||
}
|
||||
local f = properties["device.form-factor"]
|
||||
local b = properties["device.bus"]
|
||||
|
||||
icon = icon_map[f] or "audio-card"
|
||||
properties["device.icon-name"] = icon .. (b and ("-" .. b) or "")
|
||||
end
|
||||
|
||||
-- initial profile is to be set by policy-device-profile.lua, not spa-bluez5
|
||||
properties["bluez5.profile"] = "off"
|
||||
properties["api.bluez5.id"] = id
|
||||
|
||||
-- apply properties from the rules in the configuration file
|
||||
properties = JsonUtils.match_rules_update_properties (config.rules, properties)
|
||||
|
||||
-- create the device
|
||||
device = SpaDevice(factory, properties)
|
||||
if device then
|
||||
device:connect("create-object", createNode)
|
||||
device:connect("object-removed", removeNode)
|
||||
parent:store_managed_object(id, device)
|
||||
else
|
||||
log:warning ("Failed to create '" .. factory .. "' device")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
log:info(parent, string.format("%d, %s (%s): %s",
|
||||
id, properties["device.description"],
|
||||
properties["api.bluez5.address"], properties["api.bluez5.connection"]))
|
||||
|
||||
-- activate the device after the bluez profiles are connected
|
||||
if properties["api.bluez5.connection"] == "connected" then
|
||||
device:activate(Feature.SpaDevice.ENABLED | Feature.Proxy.BOUND)
|
||||
else
|
||||
device:deactivate(Features.ALL)
|
||||
end
|
||||
end
|
||||
|
||||
function createMonitor()
|
||||
local monitor = SpaDevice("api.bluez5.enum.dbus", config.properties)
|
||||
if monitor then
|
||||
monitor:connect("create-object", createDevice)
|
||||
else
|
||||
log:notice("PipeWire's BlueZ SPA plugin is missing or broken. " ..
|
||||
"Bluetooth devices will not be supported.")
|
||||
return nil
|
||||
end
|
||||
monitor:activate(Feature.SpaDevice.ENABLED)
|
||||
|
||||
return monitor
|
||||
end
|
||||
|
||||
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 support is enabled, activate
|
||||
-- the monitor only when the seat is active
|
||||
function startStopMonitor(seat_state)
|
||||
log:info(logind_plugin, "Seat state changed: " .. seat_state)
|
||||
|
||||
if seat_state == "active" then
|
||||
monitor = createMonitor()
|
||||
elseif monitor then
|
||||
monitor:deactivate(Feature.SpaDevice.ENABLED)
|
||||
monitor = nil
|
||||
end
|
||||
end
|
||||
|
||||
logind_plugin:connect("state-changed", function(p, s) startStopMonitor(s) end)
|
||||
startStopMonitor(logind_plugin:call("get-state"))
|
||||
else
|
||||
monitor = createMonitor()
|
||||
end
|
||||
|
||||
nodes_om:activate()
|
||||
devices_om:activate()
|
||||
device_set_nodes_om:activate()
|
||||
61
wireplumber/scripts/monitors/libcamera/create-device.lua
Normal file
61
wireplumber/scripts/monitors/libcamera/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-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 ()
|
||||
41
wireplumber/scripts/monitors/libcamera/create-node.lua
Normal file
41
wireplumber/scripts/monitors/libcamera/create-node.lua
Normal file
@@ -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 ()
|
||||
32
wireplumber/scripts/monitors/libcamera/enumerate-device.lua
Normal file
32
wireplumber/scripts/monitors/libcamera/enumerate-device.lua
Normal file
@@ -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
|
||||
49
wireplumber/scripts/monitors/libcamera/name-device.lua
Normal file
49
wireplumber/scripts/monitors/libcamera/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-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
wireplumber/scripts/monitors/libcamera/name-node.lua
Normal file
90
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 ()
|
||||
61
wireplumber/scripts/monitors/v4l2/create-device.lua
Normal file
61
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
wireplumber/scripts/monitors/v4l2/create-node.lua
Normal file
42
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 ()
|
||||
32
wireplumber/scripts/monitors/v4l2/enumerate-device.lua
Normal file
32
wireplumber/scripts/monitors/v4l2/enumerate-device.lua
Normal file
@@ -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
wireplumber/scripts/monitors/v4l2/name-device.lua
Normal file
49
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
wireplumber/scripts/monitors/v4l2/name-node.lua
Normal file
80
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 ()
|
||||
Reference in New Issue
Block a user