mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-02-04 08:30:31 +01:00
monitors/bluez: Always create loopbacks if Device support A2DP and HSP/HFP profiles
This simplifies a lot the logic as we don't need to destroy and re-create the internal BT nodes right away if the loopback nodes were create after them. We also now listen for changes in the BT profile autoswitch setting. If the setting is disabled, the source loopback is destroyed. If it is enabled, the source loopack is created. This makes the setting to take effect immediately, without needing to disconnect and re-connect the BT device for the setting to take effect.
This commit is contained in:
parent
d81b170bbf
commit
4cebb63d76
1 changed files with 94 additions and 127 deletions
|
|
@ -22,15 +22,10 @@ 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
|
||||
|
||||
-- Properties used for previously creating a SCO source node. key: SPA device id
|
||||
sco_source_node_properties = {}
|
||||
|
||||
-- Properties used for previously creating a SCO or A2DP sink node. key: SPA device id
|
||||
sco_a2dp_sink_node_properties = {}
|
||||
|
||||
devices_om = ObjectManager {
|
||||
Interest {
|
||||
type = "device",
|
||||
Constraint { "device.api", "=", "bluez5" },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,30 +259,9 @@ function createNode(parent, id, type, factory, properties)
|
|||
-- sanitize description, replace ':' with ' '
|
||||
properties["node.description"] = desc:gsub("(:)", " ")
|
||||
|
||||
-- set the node name
|
||||
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
|
||||
|
||||
-- hide the sink node because we use the loopback sink instead
|
||||
if parent:get_managed_object (LOOPBACK_SINK_ID) ~= nil and
|
||||
(factory == "api.bluez5.sco.sink" or
|
||||
factory == "api.bluez5.a2dp.sink") then
|
||||
properties["bluez5.sink-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)
|
||||
|
|
@ -320,13 +294,19 @@ function createNode(parent, id, type, factory, properties)
|
|||
parent:set_managed_pending(id)
|
||||
else
|
||||
log:info("Create node: " .. properties["node.name"] .. ": " .. factory .. " " .. tostring (id))
|
||||
if factory == "api.bluez5.sco.source" then
|
||||
|
||||
-- Set sink/source specific properties
|
||||
if factory == "api.bluez5.sco.source" or
|
||||
(factory == "api.bluez5.a2dp.source" and cutils.parseBool (properties["api.bluez5.a2dp-duplex"])) then
|
||||
properties["bluez5.loopback"] = false
|
||||
sco_source_node_properties[parent_spa_id] = properties
|
||||
properties["bluez5.loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
elseif factory == "api.bluez5.sco.sink" or factory == "api.bluez5.a2dp.sink" then
|
||||
properties["bluez5.sink-loopback"] = false
|
||||
sco_a2dp_sink_node_properties[parent_spa_id] = properties
|
||||
properties["bluez5.sink-loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
end
|
||||
|
||||
local node = LocalNode("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
parent:store_managed_object(id, node)
|
||||
|
|
@ -336,21 +316,9 @@ end
|
|||
function removeNode(parent, id)
|
||||
local dev_props = parent.properties
|
||||
local parent_spa_id = tonumber(dev_props["spa.object.id"])
|
||||
local src_properties = sco_source_node_properties[parent_spa_id]
|
||||
local sink_properties = sco_a2dp_sink_node_properties[parent_spa_id]
|
||||
|
||||
log:debug("Remove node: " .. tostring (id))
|
||||
|
||||
if src_properties ~= nil and id == tonumber(src_properties["spa.object.id"]) then
|
||||
log:debug("Clear old SCO source properties")
|
||||
sco_source_node_properties[parent_spa_id] = nil
|
||||
end
|
||||
|
||||
if sink_properties ~= nil and id == tonumber(sink_properties["spa.object.id"]) then
|
||||
log:debug("Clear old SCO-A2DP sink properties")
|
||||
sco_a2dp_sink_node_properties[parent_spa_id] = nil
|
||||
end
|
||||
|
||||
-- Clear also the device set module, if any
|
||||
parent:store_managed_object(id + COMBINE_OFFSET, nil)
|
||||
end
|
||||
|
|
@ -424,8 +392,7 @@ function createDevice(parent, id, type, factory, properties)
|
|||
end
|
||||
|
||||
function removeDevice(parent, id)
|
||||
sco_source_node_properties[id] = nil
|
||||
sco_a2dp_sink_node_properties[id] = nil
|
||||
log:debug("Remove device: " .. tostring (id))
|
||||
end
|
||||
|
||||
function createMonitor()
|
||||
|
|
@ -443,7 +410,16 @@ function createMonitor()
|
|||
return monitor
|
||||
end
|
||||
|
||||
function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
||||
function CreateDeviceLoopbackSource (dev_props, dev_id)
|
||||
local dev_name = dev_props["api.bluez5.address"] or dev_props["device.name"]
|
||||
local dec_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 ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
|
||||
log:info("create SCO source loopback node: " .. dev_name)
|
||||
|
||||
local args = Json.Object {
|
||||
["capture.props"] = Json.Object {
|
||||
["node.name"] = string.format ("bluez_capture_internal.%s", dev_name),
|
||||
|
|
@ -481,7 +457,16 @@ function CreateDeviceLoopbackSource (dev_name, dec_desc, dev_id)
|
|||
return LocalModule("libpipewire-module-loopback", args:get_data(), {})
|
||||
end
|
||||
|
||||
function CreateDeviceLoopbackSink (dev_name, dec_desc, dev_id)
|
||||
function CreateDeviceLoopbackSink (dev_props, dev_id)
|
||||
local dev_name = dev_props["api.bluez5.address"] or dev_props["device.name"]
|
||||
local dec_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 ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
|
||||
log:info("create SCO-A2DP sink loopback node: " .. dev_name)
|
||||
|
||||
local args = Json.Object {
|
||||
["capture.props"] = Json.Object {
|
||||
["node.name"] = string.format ("bluez_output.%s", dev_name),
|
||||
|
|
@ -521,11 +506,6 @@ function checkProfiles (dev)
|
|||
local props = dev.properties
|
||||
local device_spa_id = tonumber(props["spa.object.id"])
|
||||
|
||||
-- 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["spa.object.id"])
|
||||
local spa_device = monitor:get_managed_object (internal_id)
|
||||
|
|
@ -533,7 +513,7 @@ function checkProfiles (dev)
|
|||
return
|
||||
end
|
||||
|
||||
-- Ignore devices that don't support both A2DP sink and HSP/HFP profiles
|
||||
-- Check if the device supports A2DP and HFP/HSP profiles
|
||||
local has_a2dpsink_profile = false
|
||||
local has_headset_profile = false
|
||||
for p in dev:iterate_params("EnumProfile") do
|
||||
|
|
@ -544,82 +524,61 @@ function checkProfiles (dev)
|
|||
has_headset_profile = true
|
||||
end
|
||||
end
|
||||
if not has_a2dpsink_profile or not has_headset_profile then
|
||||
return
|
||||
end
|
||||
|
||||
-- Setup Route/Port correctly for loopback nodes
|
||||
local param = Pod.Object ({
|
||||
"Spa:Pod:Object:Param:Props",
|
||||
"Props",
|
||||
params = Pod.Struct ({ "bluez5.autoswitch-routes", true })
|
||||
})
|
||||
dev:set_param("Props", param)
|
||||
if has_a2dpsink_profile or has_headset_profile then
|
||||
local param = Pod.Object ({
|
||||
"Spa:Pod:Object:Param:Props",
|
||||
"Props",
|
||||
params = Pod.Struct ({ "bluez5.autoswitch-routes", true })
|
||||
})
|
||||
dev:set_param("Props", param)
|
||||
end
|
||||
|
||||
-- Create the source loopback device if never created before
|
||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||
if source_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"
|
||||
if has_headset_profile then
|
||||
-- Always create the source loopback device if autoswitch is enabled.
|
||||
-- Otherwise, only create the source loopback device if the current profile
|
||||
-- is headset, and destroy the source loopback deivce if the current profile
|
||||
-- is A2DP.
|
||||
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||
-- Create source loopback
|
||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||
if source_loopback == nil and has_headset_profile then
|
||||
source_loopback = CreateDeviceLoopbackSource (props, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
||||
end
|
||||
else
|
||||
-- Check if current profile is headset
|
||||
local is_current_profile_headset = false
|
||||
for p in dev:iterate_params("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile.name:find ("headset") then
|
||||
is_current_profile_headset = true
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
log:info("create SCO source loopback node: " .. dev_name)
|
||||
|
||||
-- sanitize description, replace ':' with ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
source_loopback = CreateDeviceLoopbackSource (dev_name, dec_desc, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
||||
|
||||
-- recreate any sco source node
|
||||
local properties = sco_source_node_properties[device_spa_id]
|
||||
if properties ~= nil then
|
||||
local node_id = tonumber(properties["spa.object.id"])
|
||||
local node = spa_device:get_managed_object (node_id)
|
||||
if node ~= nil then
|
||||
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
|
||||
properties["factory.name"] .. " " .. tostring (node_id))
|
||||
|
||||
spa_device:store_managed_object(node_id, nil)
|
||||
|
||||
properties["bluez5.loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
node = LocalNode("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
spa_device:store_managed_object(node_id, node)
|
||||
if is_current_profile_headset then
|
||||
-- Create source loopback
|
||||
local source_loopback = spa_device:get_managed_object (LOOPBACK_SOURCE_ID)
|
||||
if source_loopback == nil and has_headset_profile then
|
||||
source_loopback = CreateDeviceLoopbackSource (props, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, source_loopback)
|
||||
end
|
||||
else
|
||||
-- Destroy source loopback
|
||||
spa_device:store_managed_object(LOOPBACK_SOURCE_ID, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local sink_loopback = spa_device:get_managed_object (LOOPBACK_SINK_ID)
|
||||
if sink_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"
|
||||
|
||||
log:info("create SCO-A2DP sink loopback node: " .. dev_name)
|
||||
|
||||
-- sanitize description, replace ':' with ' '
|
||||
dec_desc = dec_desc:gsub("(:)", " ")
|
||||
sink_loopback = CreateDeviceLoopbackSink (dev_name, dec_desc, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SINK_ID, sink_loopback)
|
||||
|
||||
-- recreate any sco-a2dp sink node
|
||||
local properties = sco_a2dp_sink_node_properties[device_spa_id]
|
||||
if properties ~= nil then
|
||||
local node_id = tonumber(properties["spa.object.id"])
|
||||
local node = spa_device:get_managed_object (node_id)
|
||||
if node ~= nil then
|
||||
log:info("Recreate node: " .. properties["node.name"] .. ": " ..
|
||||
properties["factory.name"] .. " " .. tostring (node_id))
|
||||
|
||||
spa_device:store_managed_object(node_id, nil)
|
||||
|
||||
properties["bluez5.sink-loopback-target"] = true
|
||||
properties["api.bluez5.internal"] = true
|
||||
node = LocalNode("adapter", properties)
|
||||
node:activate(Feature.Proxy.BOUND)
|
||||
spa_device:store_managed_object(node_id, node)
|
||||
end
|
||||
if has_a2dpsink_profile or has_headset_profile then
|
||||
-- Always create sink loopback regardless of the current profile or whether
|
||||
-- the autoswitch setting is enabled or not.
|
||||
local sink_loopback = spa_device:get_managed_object (LOOPBACK_SINK_ID)
|
||||
if sink_loopback == nil then
|
||||
sink_loopback = CreateDeviceLoopbackSink (props, device_id)
|
||||
spa_device:store_managed_object(LOOPBACK_SINK_ID, sink_loopback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -627,16 +586,12 @@ end
|
|||
function onDeviceParamsChanged (dev, param_name)
|
||||
if param_name == "EnumProfile" then
|
||||
checkProfiles (dev)
|
||||
elseif param_name == "Profile" 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)
|
||||
|
|
@ -667,3 +622,15 @@ end
|
|||
nodes_om:activate()
|
||||
devices_om:activate()
|
||||
device_set_nodes_om:activate()
|
||||
|
||||
function evaluateAutoswitch ()
|
||||
-- Evaluate loopbacks on all BT devices
|
||||
for dev in devices_om:iterate () do
|
||||
checkProfiles (dev)
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
|
||||
evaluateAutoswitch ()
|
||||
end)
|
||||
evaluateAutoswitch ()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue