mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-27 07:40:05 +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.reserve = true
|
||||||
alsa_monitor.alsa.midi = "true"
|
alsa_monitor.alsa.midi = "true"
|
||||||
default-policy-duck.level = 0.3
|
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.
|
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.
|
## Whether to use headset profile in the presence of an input stream.
|
||||||
# bluetooth.autoswitch-to-headset-profile = true
|
# 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 = {
|
monitor.bluez.properties = {
|
||||||
|
|
|
||||||
|
|
@ -11,33 +11,33 @@
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
--
|
--
|
||||||
-- Scriupt Checks for the existence of media.role and if present switches the
|
-- This script is charged to automatically change BT profiles on a device. If a
|
||||||
-- bluetooth profile accordingly. Also see bluez-autoswitch in media-session.
|
-- client is linked to the device's loopback source node, the associated BT
|
||||||
-- The intended logic of the script is as follows.
|
-- 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
|
-- We switch to the highest priority profile that has an Input route available.
|
||||||
-- speak in props, we switch to the highest priority profile that has an Input
|
-- The reason for this is that we may have microphone enabled with non-HFP
|
||||||
-- route available. The reason for this is that we may have microphone enabled
|
-- codecs eg. Faststream.
|
||||||
-- non-HFP codecs eg. Faststream.
|
|
||||||
-- We track the incoming streams with Communication role or the applications
|
|
||||||
-- specified which do not set the media.role correctly perhaps.
|
|
||||||
-- When a stream goes away if the list with which we track the streams above
|
-- 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.
|
-- is empty, then we revert back to the old profile.
|
||||||
|
|
||||||
-- settings file: bluetooth.conf
|
-- settings file: bluetooth.conf
|
||||||
|
|
||||||
|
lutils = require ("linking-utils")
|
||||||
cutils = require ("common-utils")
|
cutils = require ("common-utils")
|
||||||
settings = require ("settings-bluetooth")
|
settings = require ("settings-bluetooth")
|
||||||
|
|
||||||
state = nil
|
state = nil
|
||||||
headset_profiles = nil
|
headset_profiles = nil
|
||||||
|
device_loopback_sources = {}
|
||||||
|
|
||||||
local applications = {}
|
|
||||||
local profile_restore_timeout_msec = 2000
|
local profile_restore_timeout_msec = 2000
|
||||||
|
|
||||||
local INVALID = -1
|
local INVALID = -1
|
||||||
local timeout_source = nil
|
local timeout_source = {}
|
||||||
local restore_timeout_source = nil
|
local restore_timeout_source = {}
|
||||||
|
|
||||||
local last_profiles = {}
|
local last_profiles = {}
|
||||||
|
|
||||||
|
|
@ -55,18 +55,9 @@ function handlePersistentSetting (enable)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function loadAppNames (appNames)
|
|
||||||
applications = {}
|
|
||||||
for i = 1, #appNames do
|
|
||||||
applications [appNames [i]] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
handlePersistentSetting (settings.use_persistent_storage)
|
handlePersistentSetting (settings.use_persistent_storage)
|
||||||
loadAppNames (settings.autoswitch_applications)
|
|
||||||
|
|
||||||
settings:subscribe ("use-persistent-storage", handlePersistentSetting)
|
settings:subscribe ("use-persistent-storage", handlePersistentSetting)
|
||||||
settings:subscribe ("autoswitch-applications", loadAppNames)
|
|
||||||
|
|
||||||
devices_om = ObjectManager {
|
devices_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
|
|
@ -79,8 +70,16 @@ streams_om = ObjectManager {
|
||||||
Interest {
|
Interest {
|
||||||
type = "node",
|
type = "node",
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
-- Do not consider monitor streams
|
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||||
Constraint { "stream.monitor", "!", "true" }
|
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
|
return getSavedLastProfile (device) ~= nil
|
||||||
end
|
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)
|
local function findProfile (device, index, name)
|
||||||
for p in device:iterate_params ("EnumProfile") do
|
for p in device:iterate_params ("EnumProfile") do
|
||||||
local profile = cutils.parseParam (p, "EnumProfile")
|
local profile = cutils.parseParam (p, "EnumProfile")
|
||||||
|
|
@ -203,27 +189,35 @@ local function hasProfileInputRoute (device, profile_index)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function switchDevicesToHeadsetProfile ()
|
local function switchDeviceToHeadsetProfile (dev_id)
|
||||||
local index
|
local index
|
||||||
local name
|
local name
|
||||||
|
|
||||||
-- clear restore callback, if any
|
-- Find the actual device
|
||||||
if restore_timeout_source then
|
local device = devices_om:lookup {
|
||||||
restore_timeout_source:destroy ()
|
Constraint { "bound-id", "=", dev_id, type = "gobject" }
|
||||||
restore_timeout_source = nil
|
}
|
||||||
|
if device == nil then
|
||||||
|
Log.info ("Device with id " .. tostring(dev_id).. " not found")
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
for device in devices_om:iterate () do
|
|
||||||
if isSwitchedToHeadsetProfile (device) then
|
if isSwitchedToHeadsetProfile (device) then
|
||||||
goto skip_device
|
Log.info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local cur_profile_name = getCurrentProfile (device)
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
|
|
||||||
_, index, name = findProfile (device, nil, cur_profile_name)
|
_, index, name = findProfile (device, nil, cur_profile_name)
|
||||||
if hasProfileInputRoute (device, index) then
|
if hasProfileInputRoute (device, index) then
|
||||||
Log.info ("Current profile has input route, not switching")
|
Log.info ("Current profile has input route, not switching")
|
||||||
goto skip_device
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear restore callback, if any
|
||||||
|
if restore_timeout_source[dev_id] ~= nil then
|
||||||
|
restore_timeout_source[dev_id]:destroy ()
|
||||||
|
restore_timeout_source[dev_id] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local saved_headset_profile = getSavedHeadsetProfile (device)
|
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||||
|
|
@ -253,14 +247,23 @@ local function switchDevicesToHeadsetProfile ()
|
||||||
else
|
else
|
||||||
Log.warning ("Got invalid index when switching profile")
|
Log.warning ("Got invalid index when switching profile")
|
||||||
end
|
end
|
||||||
|
|
||||||
::skip_device::
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restoreProfile ()
|
local function restoreProfile (dev_id)
|
||||||
for device in devices_om:iterate () do
|
-- Find the actual device
|
||||||
if isSwitchedToHeadsetProfile (device) then
|
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 profile_name = getSavedLastProfile (device)
|
||||||
local cur_profile_name = getCurrentProfile (device)
|
local cur_profile_name = getCurrentProfile (device)
|
||||||
|
|
||||||
|
|
@ -292,48 +295,49 @@ local function restoreProfile ()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
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
|
-- we never restore the device profiles if there are active streams
|
||||||
if next (active_streams) ~= nil then
|
for _, v in pairs (active_streams) do
|
||||||
|
if v == dev_id then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
restore_timeout_source = Core.timeout_add (profile_restore_timeout_msec, function ()
|
restore_timeout_source[dev_id] = nil
|
||||||
restore_timeout_source = nil
|
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||||
restoreProfile ()
|
restore_timeout_source[dev_id] = nil
|
||||||
|
restoreProfile (dev_id)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- We consider a Stream of interest to have role Communication if it has
|
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||||
-- media.role set to Communication in props or it is in our list of
|
-- source filter
|
||||||
-- applications as these applications do not set media.role correctly or at
|
|
||||||
-- all.
|
|
||||||
local function checkStreamStatus (stream)
|
local function checkStreamStatus (stream)
|
||||||
local app_name = stream.properties ["application.name"]
|
-- check if the stream is linked to a bluetooth loopback source
|
||||||
local stream_role = stream.properties ["media.role"]
|
local stream_id = tonumber(stream["bound-id"])
|
||||||
|
local peer_id = lutils.getNodePeerId (stream_id)
|
||||||
if not (stream_role == "Communication" or applications [app_name]) then
|
if peer_id ~= nil then
|
||||||
return false
|
local bt_node = loopback_nodes_om:lookup {
|
||||||
end
|
Constraint { "bound-id", "=", peer_id, type = "gobject" }
|
||||||
if not isBluez5DefaultAudioSink () then
|
}
|
||||||
return false
|
if bt_node ~= nil then
|
||||||
end
|
local dev_id = bt_node.properties["device.id"]
|
||||||
|
if dev_id ~= nil then
|
||||||
-- If a stream we previously saw stops running, we consider it
|
-- If a stream we previously saw stops running, we consider it
|
||||||
-- inactive, because some applications (Teams) just cork input
|
-- inactive, because some applications (Teams) just cork input
|
||||||
-- streams, but don't close them.
|
-- streams, but don't close them.
|
||||||
if previous_streams [stream.id] and stream.state ~= "running" then
|
if previous_streams [stream.id] == dev_id and
|
||||||
return false
|
stream.state ~= "running" then
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return dev_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handleStream (stream)
|
local function handleStream (stream)
|
||||||
|
|
@ -341,59 +345,67 @@ local function handleStream (stream)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if checkStreamStatus (stream) then
|
local dev_id = checkStreamStatus (stream)
|
||||||
active_streams [stream.id] = true
|
if dev_id ~= nil then
|
||||||
previous_streams [stream.id] = true
|
active_streams [stream.id] = dev_id
|
||||||
switchDevicesToHeadsetProfile ()
|
previous_streams [stream.id] = dev_id
|
||||||
|
switchDeviceToHeadsetProfile (dev_id)
|
||||||
else
|
else
|
||||||
|
dev_id = active_streams [stream.id]
|
||||||
active_streams [stream.id] = nil
|
active_streams [stream.id] = nil
|
||||||
triggerRestoreProfile ()
|
if dev_id ~= nil then
|
||||||
|
triggerRestoreProfile (dev_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function handleAllStreams ()
|
local function handleAllStreams ()
|
||||||
for stream in streams_om:iterate {
|
for stream in streams_om:iterate() do
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
|
||||||
Constraint { "stream.monitor", "!", "true" }
|
|
||||||
} do
|
|
||||||
handleStream (stream)
|
handleStream (stream)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "input-stream-removed@autoswitch-bluetooth-profile",
|
name = "node-removed@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "node-removed" },
|
Constraint { "event.type", "=", "node-removed" },
|
||||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||||
|
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
stream = event:get_subject ()
|
local stream = event:get_subject ()
|
||||||
|
local dev_id = active_streams[stream.id]
|
||||||
active_streams[stream.id] = nil
|
active_streams[stream.id] = nil
|
||||||
previous_streams[stream.id] = nil
|
previous_streams[stream.id] = nil
|
||||||
triggerRestoreProfile ()
|
if dev_id ~= nil then
|
||||||
|
triggerRestoreProfile (dev_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
}:register ()
|
}:register ()
|
||||||
|
|
||||||
SimpleEventHook {
|
SimpleEventHook {
|
||||||
name = "input-stream-changed@autoswitch-bluetooth-profile",
|
name = "link-added@autoswitch-bluetooth-profile",
|
||||||
interests = {
|
interests = {
|
||||||
EventInterest {
|
EventInterest {
|
||||||
Constraint { "event.type", "=", "node-state-changed" },
|
Constraint { "event.type", "=", "link-added" },
|
||||||
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" }
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
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
|
end
|
||||||
}:register ()
|
}:register ()
|
||||||
|
|
||||||
|
|
@ -406,32 +418,14 @@ SimpleEventHook {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
execute = function (event)
|
execute = function (event)
|
||||||
|
local device = event:get_subject ()
|
||||||
-- Devices are unswitched initially
|
-- Devices are unswitched initially
|
||||||
device = event:get_subject ()
|
|
||||||
saveLastProfile (device, nil)
|
saveLastProfile (device, nil)
|
||||||
|
|
||||||
handleAllStreams ()
|
handleAllStreams ()
|
||||||
end
|
end
|
||||||
}:register ()
|
}: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 ()
|
devices_om:activate ()
|
||||||
streams_om:activate ()
|
streams_om:activate ()
|
||||||
|
loopback_nodes_om:activate()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,20 @@ function lutils.isLinked (si_target)
|
||||||
return linked, exclusive
|
return linked, exclusive
|
||||||
end
|
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)
|
function lutils.canLink (properties, si_target)
|
||||||
local target_props = si_target.properties
|
local target_props = si_target.properties
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,7 @@ local settings_manager = require ("settings-manager")
|
||||||
|
|
||||||
local defaults = {
|
local defaults = {
|
||||||
["use-persistent-storage"] = true,
|
["use-persistent-storage"] = true,
|
||||||
["autoswitch-to-headset-profile"] = 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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings_manager.new ("bluetooth.", defaults)
|
return settings_manager.new ("bluetooth.", defaults)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
COMBINE_OFFSET = 64
|
COMBINE_OFFSET = 64
|
||||||
|
LOOPBACK_SOURCE_ID = 128
|
||||||
|
DEVICE_SOURCE_ID = 0
|
||||||
|
|
||||||
cutils = require ("common-utils")
|
cutils = require ("common-utils")
|
||||||
log = Log.open_topic ("s-monitors")
|
log = Log.open_topic ("s-monitors")
|
||||||
|
|
@ -230,6 +232,7 @@ end
|
||||||
|
|
||||||
function createNode(parent, id, type, factory, properties)
|
function createNode(parent, id, type, factory, properties)
|
||||||
local dev_props = parent.properties
|
local dev_props = parent.properties
|
||||||
|
local parent_id = parent["bound-id"]
|
||||||
|
|
||||||
if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then
|
if config.properties["bluez5.hw-offload-sco"] and factory:find("sco") then
|
||||||
createOffloadScoNode(parent, id, type, factory, properties)
|
createOffloadScoNode(parent, id, type, factory, properties)
|
||||||
|
|
@ -237,7 +240,7 @@ function createNode(parent, id, type, factory, properties)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- set the device id and spa factory name; REQUIRED, do not change
|
-- set the device id and spa factory name; REQUIRED, do not change
|
||||||
properties["device.id"] = parent["bound-id"]
|
properties["device.id"] = parent_id
|
||||||
properties["factory.name"] = factory
|
properties["factory.name"] = factory
|
||||||
|
|
||||||
-- set the default pause-on-idle setting
|
-- set the default pause-on-idle setting
|
||||||
|
|
@ -286,6 +289,7 @@ function createNode(parent, id, type, factory, properties)
|
||||||
local combine = createSetNode(parent, id, type, factory, properties)
|
local combine = createSetNode(parent, id, type, factory, properties)
|
||||||
parent:store_managed_object(id + COMBINE_OFFSET, combine)
|
parent:store_managed_object(id + COMBINE_OFFSET, combine)
|
||||||
else
|
else
|
||||||
|
properties["bluez5.loopback"] = false
|
||||||
local node = LocalNode("adapter", properties)
|
local node = LocalNode("adapter", properties)
|
||||||
node:activate(Feature.Proxy.BOUND)
|
node:activate(Feature.Proxy.BOUND)
|
||||||
parent:store_managed_object(id, node)
|
parent:store_managed_object(id, node)
|
||||||
|
|
@ -378,6 +382,99 @@ function createMonitor()
|
||||||
return monitor
|
return monitor
|
||||||
end
|
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
|
if config.seat_monitoring then
|
||||||
logind_plugin = Plugin.find("logind")
|
logind_plugin = Plugin.find("logind")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue