mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-20 04:10:03 +01:00
autoswitch-bluetooth-profile: remove applications array and use loopback filter
This patch improves the bluetooth profile autoswitch so that it works with any application that wants to capture from a bluetooth device. To do so, a loopback source filter is created per connected bluetooth device. If an application wants to capture audio from such loopback source filter, the profile in the associated bluetooth device is changed to HSP/HFP. If there isn't any application connected to the loopback source filter, the profile switches back to A2DP.
This commit is contained in:
parent
68c6fc2a38
commit
874a432c69
6 changed files with 284 additions and 196 deletions
|
|
@ -180,7 +180,6 @@ files and are placed under ``wireplumber.conf.d/``. More on this below.
|
|||
alsa_monitor.alsa.reserve = true
|
||||
alsa_monitor.alsa.midi = "true"
|
||||
default-policy-duck.level = 0.3
|
||||
bt-policy-media-role.applications = ["Firefox", "Chromium input"]
|
||||
}
|
||||
|
||||
Value can be string, int, float, boolean and can even be a JSON array.
|
||||
|
|
|
|||
|
|
@ -6,16 +6,6 @@ wireplumber.settings = {
|
|||
|
||||
## Whether to use headset profile in the presence of an input stream.
|
||||
# bluetooth.autoswitch-to-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.
|
||||
# bluetooth.autoswitch-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"
|
||||
# ]
|
||||
}
|
||||
|
||||
monitor.bluez.properties = {
|
||||
|
|
|
|||
|
|
@ -11,33 +11,33 @@
|
|||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Scriupt 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.
|
||||
-- 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.
|
||||
--
|
||||
-- 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.
|
||||
-- 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")
|
||||
settings = require ("settings-bluetooth")
|
||||
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
device_loopback_sources = {}
|
||||
|
||||
local applications = {}
|
||||
local profile_restore_timeout_msec = 2000
|
||||
|
||||
local INVALID = -1
|
||||
local timeout_source = nil
|
||||
local restore_timeout_source = nil
|
||||
local timeout_source = {}
|
||||
local restore_timeout_source = {}
|
||||
|
||||
local last_profiles = {}
|
||||
|
||||
|
|
@ -55,18 +55,9 @@ function handlePersistentSetting (enable)
|
|||
end
|
||||
end
|
||||
|
||||
function loadAppNames (appNames)
|
||||
applications = {}
|
||||
for i = 1, #appNames do
|
||||
applications [appNames [i]] = true
|
||||
end
|
||||
end
|
||||
|
||||
handlePersistentSetting (settings.use_persistent_storage)
|
||||
loadAppNames (settings.autoswitch_applications)
|
||||
|
||||
settings:subscribe ("use-persistent-storage", handlePersistentSetting)
|
||||
settings:subscribe ("autoswitch-applications", loadAppNames)
|
||||
|
||||
devices_om = ObjectManager {
|
||||
Interest {
|
||||
|
|
@ -79,8 +70,16 @@ 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" }
|
||||
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 { "bluez5.loopback", "=", "true", type = "pw" },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,19 +106,6 @@ local function isSwitchedToHeadsetProfile (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 = cutils.get_default_metadata_object ()
|
||||
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 = cutils.parseParam (p, "EnumProfile")
|
||||
|
|
@ -203,37 +189,91 @@ local function hasProfileInputRoute (device, profile_index)
|
|||
return false
|
||||
end
|
||||
|
||||
local function switchDevicesToHeadsetProfile ()
|
||||
local function switchDeviceToHeadsetProfile (dev_id)
|
||||
local index
|
||||
local name
|
||||
|
||||
-- clear restore callback, if any
|
||||
if restore_timeout_source then
|
||||
restore_timeout_source:destroy ()
|
||||
restore_timeout_source = nil
|
||||
-- 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
|
||||
|
||||
for device in devices_om:iterate () do
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
goto skip_device
|
||||
end
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
Log.info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||
return
|
||||
end
|
||||
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
_, index, name = findProfile (device, nil, cur_profile_name)
|
||||
if hasProfileInputRoute (device, index) then
|
||||
Log.info ("Current profile has input route, not switching")
|
||||
return
|
||||
end
|
||||
|
||||
_, 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
|
||||
-- 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 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
|
||||
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
|
||||
}
|
||||
|
||||
-- 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)
|
||||
|
||||
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 {
|
||||
|
|
@ -241,99 +281,63 @@ local function switchDevicesToHeadsetProfile ()
|
|||
index = index
|
||||
}
|
||||
|
||||
-- store the current profile (needed when restoring)
|
||||
saveLastProfile (device, cur_profile_name)
|
||||
-- clear last profile as we will restore it now
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- switch to headset profile
|
||||
Log.info ("Setting profile of '"
|
||||
-- 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 ("Got invalid index when switching profile")
|
||||
end
|
||||
|
||||
::skip_device::
|
||||
end
|
||||
end
|
||||
|
||||
local function restoreProfile ()
|
||||
for device in devices_om:iterate () do
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
local profile_name = getSavedLastProfile (device)
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
-- 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
|
||||
Log.warning ("Failed to restore profile")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function triggerRestoreProfile ()
|
||||
if restore_timeout_source then
|
||||
return
|
||||
end
|
||||
|
||||
local function triggerRestoreProfile (dev_id)
|
||||
-- we never restore the device profiles if there are active streams
|
||||
if next (active_streams) ~= nil then
|
||||
return
|
||||
for _, v in pairs (active_streams) do
|
||||
if v == dev_id then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
restore_timeout_source = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||
restore_timeout_source = nil
|
||||
restoreProfile ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
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 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.
|
||||
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||
-- source filter
|
||||
local function checkStreamStatus (stream)
|
||||
local app_name = stream.properties ["application.name"]
|
||||
local stream_role = stream.properties ["media.role"]
|
||||
-- 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
|
||||
|
||||
if not (stream_role == "Communication" or applications [app_name]) then
|
||||
return false
|
||||
end
|
||||
if not isBluez5DefaultAudioSink () then
|
||||
return false
|
||||
return dev_id
|
||||
end
|
||||
end
|
||||
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.id] and stream.state ~= "running" then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
return nil
|
||||
end
|
||||
|
||||
local function handleStream (stream)
|
||||
|
|
@ -341,59 +345,67 @@ local function handleStream (stream)
|
|||
return
|
||||
end
|
||||
|
||||
if checkStreamStatus (stream) then
|
||||
active_streams [stream.id] = true
|
||||
previous_streams [stream.id] = true
|
||||
switchDevicesToHeadsetProfile ()
|
||||
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
|
||||
triggerRestoreProfile ()
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id)
|
||||
end
|
||||
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
|
||||
for stream in streams_om:iterate() do
|
||||
handleStream (stream)
|
||||
end
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "input-stream-removed@autoswitch-bluetooth-profile",
|
||||
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)
|
||||
stream = event:get_subject ()
|
||||
local stream = event:get_subject ()
|
||||
local dev_id = active_streams[stream.id]
|
||||
active_streams[stream.id] = nil
|
||||
previous_streams[stream.id] = nil
|
||||
triggerRestoreProfile ()
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "input-stream-changed@autoswitch-bluetooth-profile",
|
||||
name = "link-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "#", "Stream/Input/Audio", type = "pw-global" },
|
||||
-- Do not consider monitor streams
|
||||
Constraint { "stream.monitor", "!", "true" }
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-params-changed" },
|
||||
Constraint { "media.class", "#", "Stream/Input/Audio", type = "pw-global" },
|
||||
-- Do not consider monitor streams
|
||||
Constraint { "stream.monitor", "!", "true" }
|
||||
Constraint { "event.type", "=", "link-added" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
handleStream (event:get_subject ())
|
||||
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 out_id = tonumber(p["link.output.node"])
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
local bt_node = loopback_nodes_om:lookup {
|
||||
Constraint { "bound-id", "=", out_id, type = "gobject" }
|
||||
}
|
||||
if in_id == stream_id and bt_node ~= nil then
|
||||
handleStream (stream)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
|
|
@ -406,32 +418,14 @@ SimpleEventHook {
|
|||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
-- Devices are unswitched initially
|
||||
device = event:get_subject ()
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
handleAllStreams ()
|
||||
end
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "metadata-changed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "=", "default.audio.sink" },
|
||||
Constraint { "event.subject.id", "=", "0" },
|
||||
Constraint { "event.subject.value", "#", "*bluez_output*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
if (settings.autoswitch_to_headset_profile) then
|
||||
-- If bluez sink is set as default, rescan for active input streams
|
||||
handleAllStreams ()
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
|
||||
devices_om:activate ()
|
||||
streams_om:activate ()
|
||||
loopback_nodes_om:activate()
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,20 @@ function lutils.isLinked (si_target)
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,7 @@ local settings_manager = require ("settings-manager")
|
|||
|
||||
local defaults = {
|
||||
["use-persistent-storage"] = true,
|
||||
["autoswitch-to-headset-profile"] = true,
|
||||
["autoswitch-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"
|
||||
}
|
||||
["autoswitch-to-headset-profile"] = true
|
||||
}
|
||||
|
||||
return settings_manager.new ("bluetooth.", defaults)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
-- 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")
|
||||
|
|
@ -230,6 +232,7 @@ end
|
|||
|
||||
function createNode(parent, id, type, factory, properties)
|
||||
local dev_props = parent.properties
|
||||
local parent_id = parent["bound-id"]
|
||||
|
||||
if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then
|
||||
createOffloadScoNode(parent, id, type, factory, properties)
|
||||
|
|
@ -237,7 +240,7 @@ function createNode(parent, id, type, factory, properties)
|
|||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- set the default pause-on-idle setting
|
||||
|
|
@ -286,6 +289,7 @@ function createNode(parent, id, type, factory, properties)
|
|||
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)
|
||||
|
|
@ -378,6 +382,99 @@ function createMonitor()
|
|||
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.%s", dev_name),
|
||||
["node.description"] =
|
||||
string.format ("Bluetooth capture for %s", dec_desc),
|
||||
["audio.channels"] = 1,
|
||||
["audio.position"] = "[MONO]",
|
||||
["bluez5.loopback"] = true,
|
||||
["stream.dont-remix"] = true,
|
||||
["node.passive"] = true,
|
||||
["target.dont-fallback"] = true,
|
||||
["target.linger"] = true
|
||||
},
|
||||
["playback.props"] = Json.Object {
|
||||
["node.name"] = string.format ("bluez_source.%s", dev_name),
|
||||
["node.description"] =
|
||||
string.format ("Bluetooth source for %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 {
|
||||
["media.class"] = "Audio/Source",
|
||||
["device.api"] = "bluez5",
|
||||
["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
|
||||
|
||||
-- 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue