mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-20 04:10:03 +01:00
autoswitch-bluetooth-profile.lua: Refactor and fix issues with saved profiles
This patch improves the BT profile autoswitch logic so that it is simpler and more robust. Instead of just relying on capture clients that are linked to (or unlinked from) the BT loopback source node to evaluate whether we have to switch to a headset profile or not, we now also evaluate the autoswitch every time the BT loopback source node state changes. This avoids problems with some capture clients that pause the input stream without closing them. Apart from this, the patch also fixes some issues with saved profiles if the user manually switched to a headset profile when no capture streams are present. The autoswitch logic should restore back the non-headset profile (A2DP) every time a capture client is disconnected or the BT loopback source node stops running. Finally, the 'bluetooth.autoswitch-to-headset-profile' setting will now register or remove the necessary hooks depending on whether the setting is enabled or disabled, improving WirePlumber performance if autoswitching is disabled.
This commit is contained in:
parent
f196d10e87
commit
e94caea3a4
1 changed files with 323 additions and 266 deletions
|
|
@ -28,43 +28,25 @@
|
|||
lutils = require ("linking-utils")
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-device")
|
||||
persistent_storage_hooks_registered = false
|
||||
autoswitch_hooks_registered = false
|
||||
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
local PROFILE_RESTORE_TIMEOUT_MSEC = 2000
|
||||
local PROFILE_SWITCH_TIMEOUT_MSEC = 500
|
||||
|
||||
local profile_restore_timeout_msec = 2000
|
||||
local profile_switch_timeout_msec = 500
|
||||
|
||||
local INVALID = -1
|
||||
local state = nil
|
||||
local headset_profiles = {}
|
||||
local non_headset_profiles = {}
|
||||
local capture_stream_links = {}
|
||||
local restore_timeout_source = {}
|
||||
local switch_timeout_source = {}
|
||||
|
||||
local last_profiles = {}
|
||||
|
||||
local active_streams = {}
|
||||
local previous_streams = {}
|
||||
|
||||
function handlePersistentSetting (enable)
|
||||
if enable and state == nil then
|
||||
-- the state storage
|
||||
state = Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile")
|
||||
and State ("bluetooth-autoswitch") or nil
|
||||
headset_profiles = state and state:load () or Properties()
|
||||
else
|
||||
state = nil
|
||||
headset_profiles = nil
|
||||
end
|
||||
end
|
||||
|
||||
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
||||
handlePersistentSetting (Settings.get_boolean ("bluetooth.use-persistent-storage"))
|
||||
end)
|
||||
|
||||
function saveHeadsetProfile (device, profile_name)
|
||||
function saveHeadsetProfile (device, profile_name, persistent)
|
||||
local key = "saved-headset-profile:" .. device.properties ["device.name"]
|
||||
headset_profiles [key] = profile_name
|
||||
state:save_after_timeout (headset_profiles)
|
||||
if state ~= nil and persistent then
|
||||
state:save_after_timeout (headset_profiles)
|
||||
end
|
||||
end
|
||||
|
||||
function getSavedHeadsetProfile (device)
|
||||
|
|
@ -72,85 +54,72 @@ function getSavedHeadsetProfile (device)
|
|||
return headset_profiles [key]
|
||||
end
|
||||
|
||||
function saveLastProfile (device, profile_name)
|
||||
last_profiles [device.properties ["device.name"]] = profile_name
|
||||
function saveNonHeadsetProfile (device, profile_name)
|
||||
non_headset_profiles [device.properties ["device.name"]] = profile_name
|
||||
end
|
||||
|
||||
function getSavedLastProfile (device)
|
||||
return last_profiles [device.properties ["device.name"]]
|
||||
end
|
||||
|
||||
function isSwitchedToHeadsetProfile (device)
|
||||
return getSavedLastProfile (device) ~= nil
|
||||
function getSavedNonHeadsetProfile (device)
|
||||
return non_headset_profiles [device.properties ["device.name"]]
|
||||
end
|
||||
|
||||
function findProfile (device, index, name)
|
||||
for p in device:iterate_params ("EnumProfile") do
|
||||
local profile = cutils.parseParam (p, "EnumProfile")
|
||||
if not profile then
|
||||
goto skip_enum_profile
|
||||
end
|
||||
|
||||
log:debug ("Profile name: " .. profile.name .. ", priority: "
|
||||
.. tostring (profile.priority) .. ", index: " .. tostring (profile.index))
|
||||
if (index ~= nil and profile.index == index) or
|
||||
(name ~= nil and profile.name == name) then
|
||||
return profile.priority, profile.index, profile.name
|
||||
end
|
||||
|
||||
::skip_enum_profile::
|
||||
end
|
||||
|
||||
return INVALID, INVALID, nil
|
||||
end
|
||||
|
||||
function getCurrentProfile (device)
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile then
|
||||
return profile.name
|
||||
if profile ~= nil then
|
||||
if (index ~= nil and profile.index == index) or
|
||||
(name ~= nil and profile.name == name) then
|
||||
return profile
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function highestPrioProfileWithInputRoute (device)
|
||||
local profile_priority = INVALID
|
||||
local profile_index = INVALID
|
||||
local profile_name = nil
|
||||
function getCurrentProfile (device)
|
||||
for p in device:iterate_params ("Profile") do
|
||||
local profile = cutils.parseParam (p, "Profile")
|
||||
if profile then
|
||||
return profile
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function highestPrioProfileWithInputRoute (device)
|
||||
local found_profile = nil
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
-- Parse pod
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
if route.direction ~= "Input" then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
log:debug ("Route with index: " .. tostring (route.index) .. ", direction: "
|
||||
.. route.direction .. ", name: " .. route.name .. ", description: "
|
||||
.. route.description .. ", priority: " .. route.priority)
|
||||
if route.profiles then
|
||||
if route ~= nil and route.profiles ~= nil and route.direction == "Input" then
|
||||
for _, v in pairs (route.profiles) do
|
||||
local priority, index, name = findProfile (device, v)
|
||||
if priority ~= INVALID then
|
||||
if profile_priority < priority then
|
||||
profile_priority = priority
|
||||
profile_index = index
|
||||
profile_name = name
|
||||
local p = findProfile (device, v)
|
||||
if p ~= nil then
|
||||
if found_profile == nil or found_profile.priority < p.priority then
|
||||
found_profile = p
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::skip_enum_route::
|
||||
end
|
||||
return found_profile
|
||||
end
|
||||
|
||||
return profile_priority, profile_index, profile_name
|
||||
function highestPrioProfileWithoutInputRoute (device)
|
||||
local found_profile = nil
|
||||
for p in device:iterate_params ("EnumRoute") do
|
||||
local route = cutils.parseParam (p, "EnumRoute")
|
||||
if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
|
||||
for _, v in pairs (route.profiles) do
|
||||
local p = findProfile (device, v)
|
||||
if p ~= nil then
|
||||
if found_profile == nil or found_profile.priority < p.priority then
|
||||
found_profile = p
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return found_profile
|
||||
end
|
||||
|
||||
function hasProfileInputRoute (device, profile_index)
|
||||
|
|
@ -177,49 +146,40 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
|
|||
return
|
||||
end
|
||||
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
if hasProfileInputRoute (device, index) then
|
||||
log:info ("Current profile has input route, not switching")
|
||||
-- Do not switch if the current profile is already a headset profile
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil and
|
||||
hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device,
|
||||
"Current profile is already a headset profile, no need to switch")
|
||||
return
|
||||
end
|
||||
|
||||
if isSwitchedToHeadsetProfile (device) then
|
||||
log:info ("Device with id " .. tostring(dev_id).. " is already switched to HSP/HFP")
|
||||
return
|
||||
end
|
||||
|
||||
local saved_headset_profile = getSavedHeadsetProfile (device)
|
||||
|
||||
index = INVALID
|
||||
if saved_headset_profile then
|
||||
priority, index, name = findProfile (device, nil, saved_headset_profile)
|
||||
if index ~= INVALID and not hasProfileInputRoute (device, index) then
|
||||
index = INVALID
|
||||
saveHeadsetProfile (device, nil)
|
||||
-- Get saved headset profile if any, otherwise find the highest priority one
|
||||
local profile = nil
|
||||
local profile_name = getSavedHeadsetProfile (device)
|
||||
if profile_name ~= nil then
|
||||
profile = findProfile (device, nil, profile_name)
|
||||
if profile ~= nil and not hasProfileInputRoute (device, profile.index) then
|
||||
saveHeadsetProfile (device, nil, false)
|
||||
end
|
||||
end
|
||||
if index == INVALID then
|
||||
priority, index, name = highestPrioProfileWithInputRoute (device)
|
||||
if profile == nil then
|
||||
profile = highestPrioProfileWithInputRoute (device)
|
||||
end
|
||||
|
||||
if index ~= INVALID then
|
||||
-- Switch if headset profile was found
|
||||
if profile ~= nil then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
index = profile.index,
|
||||
save = false
|
||||
}
|
||||
|
||||
-- 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)
|
||||
log:info (device, "Switching profile from: " .. cur_profile.name
|
||||
.. " to: " .. profile.name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
log:warning ("Got invalid index when switching profile")
|
||||
log:warning ("Could not find valid headset profile, not switching")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -233,45 +193,40 @@ function restoreProfile (dev_id, device_om)
|
|||
return
|
||||
end
|
||||
|
||||
if not isSwitchedToHeadsetProfile (device) then
|
||||
log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP")
|
||||
-- Do not restore if the current profile is already a non-headset profile
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil and
|
||||
not hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device,
|
||||
"Current profile is already a non-headset profile, no need to restore")
|
||||
return
|
||||
end
|
||||
|
||||
local profile_name = getSavedLastProfile (device)
|
||||
local cur_profile_name = getCurrentProfile (device)
|
||||
local priority, index, name
|
||||
|
||||
if cur_profile_name then
|
||||
priority, index, name = findProfile (device, nil, cur_profile_name)
|
||||
|
||||
if index ~= INVALID and hasProfileInputRoute (device, index) then
|
||||
log:info ("Setting saved headset profile to: " .. cur_profile_name)
|
||||
saveHeadsetProfile (device, cur_profile_name)
|
||||
-- Get saved non-headset profile if any, otherwise find the highest priority one
|
||||
local profile = nil
|
||||
local profile_name = getSavedNonHeadsetProfile (device)
|
||||
if profile_name ~= nil then
|
||||
profile = findProfile (device, nil, profile_name)
|
||||
if profile ~= nil and hasProfileInputRoute (device, profile.index) then
|
||||
saveNonHeadsetProfile (device, nil)
|
||||
end
|
||||
end
|
||||
if profile == nil then
|
||||
profile = highestPrioProfileWithoutInputRoute (device)
|
||||
end
|
||||
|
||||
if profile_name then
|
||||
priority, index, name = findProfile (device, nil, profile_name)
|
||||
|
||||
if index ~= INVALID then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = index
|
||||
}
|
||||
|
||||
-- clear last profile as we will restore it now
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- restore previous profile
|
||||
log:info ("Restoring profile of '"
|
||||
.. device.properties ["device.description"]
|
||||
.. "' from: " .. cur_profile_name
|
||||
.. " to: " .. name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
log:warning ("Failed to restore profile")
|
||||
end
|
||||
-- Restore if non-headset profile was found
|
||||
if profile ~= nil then
|
||||
local pod = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Profile", "Profile",
|
||||
index = profile.index,
|
||||
save = false
|
||||
}
|
||||
log:info (device, "Restoring profile from: " .. cur_profile.name
|
||||
.. " to: " .. profile.name)
|
||||
device:set_params ("Profile", pod)
|
||||
else
|
||||
log:warning ("Could not find valid non-headset profile, not switching")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -280,95 +235,89 @@ function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
|
|||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
||||
end
|
||||
if switch_timeout_source[dev_id] ~= nil then
|
||||
switch_timeout_source[dev_id]:destroy ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
||||
end
|
||||
|
||||
-- create new switch callback
|
||||
switch_timeout_source[dev_id] = Core.timeout_add (profile_switch_timeout_msec, function ()
|
||||
log:info ("Triggering profile switch on device " .. tostring (dev_id))
|
||||
switch_timeout_source[dev_id] = Core.timeout_add (PROFILE_SWITCH_TIMEOUT_MSEC, function ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
switchDeviceToHeadsetProfile (dev_id, device_om)
|
||||
end)
|
||||
end
|
||||
|
||||
function triggerRestoreProfile (dev_id, device_om)
|
||||
-- we never restore the device profiles if there are active streams
|
||||
for _, v in pairs (active_streams) do
|
||||
if v == dev_id then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Always clear any pending restore/switch callbacks when triggering a new restore
|
||||
if switch_timeout_source[dev_id] ~= nil then
|
||||
switch_timeout_source[dev_id]:destroy ()
|
||||
switch_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile switch on device " .. tostring (dev_id))
|
||||
end
|
||||
if restore_timeout_source[dev_id] ~= nil then
|
||||
restore_timeout_source[dev_id]:destroy ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
log:info ("Cancelled profile restore on device " .. tostring (dev_id))
|
||||
end
|
||||
|
||||
-- create new restore callback
|
||||
restore_timeout_source[dev_id] = Core.timeout_add (profile_restore_timeout_msec, function ()
|
||||
log:info ("Triggering profile restore on device " .. tostring (dev_id))
|
||||
restore_timeout_source[dev_id] = Core.timeout_add (PROFILE_RESTORE_TIMEOUT_MSEC, function ()
|
||||
restore_timeout_source[dev_id] = nil
|
||||
restoreProfile (dev_id, device_om)
|
||||
end)
|
||||
end
|
||||
|
||||
-- We consider a Stream of interest if it is linked to a bluetooth loopback
|
||||
-- source filter
|
||||
function checkStreamStatus (stream, node_om, visited_link_groups)
|
||||
-- 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 = node_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
}
|
||||
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
|
||||
function getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om, visited_link_groups)
|
||||
local stream_id = stream["bound-id"]
|
||||
|
||||
return dev_id
|
||||
-- Make sure the node is linked
|
||||
local link = link_om:lookup {
|
||||
Constraint { "link.input.node", "=", stream_id, type = "pw-global"}
|
||||
}
|
||||
if link == nil then
|
||||
return nil
|
||||
end
|
||||
local peer_id = link.properties["link.output.node"]
|
||||
|
||||
-- If the peer node is the BT loopback source node, return its Id.
|
||||
-- Otherwise recursively advance in the graph if it is linked to a filter.
|
||||
local bt_node = node_om:lookup {
|
||||
Constraint { "media.class", "matches", "Audio/Source", type = "pw-global" },
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
}
|
||||
if bt_node ~= nil then
|
||||
return bt_node
|
||||
else
|
||||
local filter_main_node = node_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "node.link-group", "+", type = "pw" }
|
||||
}
|
||||
if filter_main_node ~= nil then
|
||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||
if visited_link_groups == nil then
|
||||
visited_link_groups = {}
|
||||
end
|
||||
else
|
||||
-- Check if it is linked to a filter main node, and recursively advance if so
|
||||
local filter_main_node = node_om:lookup {
|
||||
Constraint { "bound-id", "=", peer_id, type = "gobject" },
|
||||
Constraint { "node.link-group", "+", type = "pw" }
|
||||
}
|
||||
if filter_main_node ~= nil then
|
||||
-- Now check all stream nodes for this filter
|
||||
local filter_link_group = filter_main_node.properties ["node.link-group"]
|
||||
if visited_link_groups == nil then
|
||||
visited_link_groups = {}
|
||||
end
|
||||
if visited_link_groups [filter_link_group] then
|
||||
return nil
|
||||
else
|
||||
visited_link_groups [filter_link_group] = true
|
||||
end
|
||||
for filter_stream_node in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||
} do
|
||||
local dev_id = checkStreamStatus (filter_stream_node, node_om, visited_link_groups)
|
||||
if dev_id ~= nil then
|
||||
return dev_id
|
||||
end
|
||||
if visited_link_groups [filter_link_group] then
|
||||
return nil
|
||||
else
|
||||
visited_link_groups [filter_link_group] = true
|
||||
end
|
||||
for filter_stream_node in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
Constraint { "node.link-group", "=", filter_link_group, type = "pw" }
|
||||
} do
|
||||
local filter_stream_id = filter_stream_node["bound-id"]
|
||||
local bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (filter_stream_id, node_om, link_om, visited_link_groups)
|
||||
if bt_node ~= nil then
|
||||
return bt_node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -377,60 +326,65 @@ function checkStreamStatus (stream, node_om, visited_link_groups)
|
|||
return nil
|
||||
end
|
||||
|
||||
function handleStream (stream, node_om, device_om)
|
||||
if not Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") then
|
||||
return
|
||||
end
|
||||
|
||||
local dev_id = checkStreamStatus (stream, node_om)
|
||||
if dev_id ~= nil then
|
||||
active_streams [stream.id] = dev_id
|
||||
previous_streams [stream.id] = dev_id
|
||||
triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
|
||||
else
|
||||
dev_id = active_streams [stream.id]
|
||||
active_streams [stream.id] = nil
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id, device_om)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function handleAllStreams (node_om, device_om)
|
||||
function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
|
||||
local bt_node_id = bt_node["bound-id"]
|
||||
for stream in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "-", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||
} do
|
||||
handleStream (stream, node_om, device_om)
|
||||
local linked_bt_node = getLinkedBluetoothLoopbackSourceNodeForStream (stream, node_om, link_om)
|
||||
if linked_bt_node ~= nil then
|
||||
local linked_bt_node_id = linked_bt_node ["bound-id"]
|
||||
if tonumber (linked_bt_node_id) == tonumber (bt_node_id) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node-removed@autoswitch-bluetooth-profile",
|
||||
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
|
||||
name = "evaluate-bluetooth-profiles@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" },
|
||||
Constraint { "event.type", "=", "evaluate-bluetooth-profiles" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local stream = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
local device_om = source:call ("get-object-manager", "device")
|
||||
local link_om = source:call ("get-object-manager", "link")
|
||||
|
||||
local dev_id = active_streams[stream.id]
|
||||
active_streams[stream.id] = nil
|
||||
previous_streams[stream.id] = nil
|
||||
if dev_id ~= nil then
|
||||
triggerRestoreProfile (dev_id, device_om)
|
||||
-- Evaluate all bluetooth loopback source nodes, and switch to headset
|
||||
-- profile only if the node is running and linked to a stream that is not a
|
||||
-- monitor, otherwise just restore the profile.
|
||||
--
|
||||
-- If the bluetooth node is linked to a stream that is a monitor, its state
|
||||
-- will be 'running', so we cannot just rely on the state to know if we
|
||||
-- have to switch or not, we also need to check if the node is linked to
|
||||
-- a stream that is not a monitor.
|
||||
for bt_node in node_om:iterate {
|
||||
Constraint { "media.class", "matches", "Audio/Source" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
} do
|
||||
local bt_node_state = bt_node["state"]
|
||||
local bt_dev_id = bt_node.properties ["device.id"]
|
||||
|
||||
if bt_node_state == "running" and
|
||||
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
|
||||
triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
|
||||
else
|
||||
triggerRestoreProfile (bt_dev_id, device_om)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
}
|
||||
|
||||
SimpleEventHook {
|
||||
local link_added_hook = SimpleEventHook {
|
||||
name = "link-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
|
|
@ -438,46 +392,149 @@ SimpleEventHook {
|
|||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local link = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
local device_om = source:call ("get-object-manager", "device")
|
||||
local link_props = link.properties
|
||||
local link = event:get_subject ()
|
||||
local in_stream_id = link.properties["link.input.node"]
|
||||
|
||||
for stream in node_om:iterate {
|
||||
-- Only evaluate bluetooth profiles if a capture stream was linked
|
||||
local stream = node_om:lookup {
|
||||
Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" },
|
||||
Constraint { "node.link-group", "-", type = "pw" },
|
||||
Constraint { "stream.monitor", "!", "true", type = "pw" },
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" }
|
||||
} do
|
||||
local in_id = tonumber(link_props["link.input.node"])
|
||||
local stream_id = tonumber(stream["bound-id"])
|
||||
if in_id == stream_id then
|
||||
handleStream (stream, node_om, device_om)
|
||||
end
|
||||
Constraint { "bluez5.loopback", "!", "true", type = "pw" },
|
||||
Constraint { "bound-id", "=", in_stream_id, type = "gobject" },
|
||||
}
|
||||
if stream ~= nil then
|
||||
capture_stream_links [link.id] = true
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
}
|
||||
|
||||
SimpleEventHook {
|
||||
name = "bluez-device-added@autoswitch-bluetooth-profile",
|
||||
local link_removed_hook = SimpleEventHook {
|
||||
name = "link-removed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-added" },
|
||||
Constraint { "event.type", "=", "link-removed" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local link = event:get_subject ()
|
||||
|
||||
-- Only evaluate bluetooth profiles if a capture stream was unlinked
|
||||
if capture_stream_links [link.id] then
|
||||
capture_stream_links [link.id] = nil
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local state_changed_hook = SimpleEventHook {
|
||||
name = "bluez-loopback-state-changed@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "matches", "Audio/Source" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
}
|
||||
|
||||
local node_added_hook = SimpleEventHook {
|
||||
name = "bluez-loopback-added@autoswitch-bluetooth-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "matches", "Audio/Source" },
|
||||
Constraint { "device.id", "+" },
|
||||
Constraint { "bluez5.loopback", "=", "true", type = "pw" }
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
|
||||
end
|
||||
}
|
||||
|
||||
local device_profile_changed_hook = SimpleEventHook {
|
||||
name = "device/store-user-selected-profile",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "device-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Profile" },
|
||||
Constraint { "device.api", "=", "bluez5" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local device = event:get_subject ()
|
||||
local source = event:get_source ()
|
||||
local node_om = source:call ("get-object-manager", "node")
|
||||
local device_om = source:call ("get-object-manager", "device")
|
||||
|
||||
-- Devices are unswitched initially
|
||||
saveLastProfile (device, nil)
|
||||
|
||||
-- Handle all streams when BT device is added
|
||||
handleAllStreams (node_om, device_om)
|
||||
-- Always save the current profile when it changes
|
||||
local cur_profile = getCurrentProfile (device)
|
||||
if cur_profile ~= nil then
|
||||
if hasProfileInputRoute (device, cur_profile.index) then
|
||||
log:info (device, "Saving headset profile " .. cur_profile.name)
|
||||
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
|
||||
else
|
||||
log:info (device, "Saving non-headset profile " .. cur_profile.name)
|
||||
saveNonHeadsetProfile (device, cur_profile.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
}
|
||||
|
||||
function evaluatePersistentStorage ()
|
||||
if Settings.get_boolean ("bluetooth.use-persistent-storage") and
|
||||
not persistent_storage_hooks_registered then
|
||||
state = State ("bluetooth-autoswitch")
|
||||
headset_profiles = state:load ()
|
||||
persistent_storage_hooks_registered = true
|
||||
elseif persistent_storage_hooks_registered then
|
||||
state = nil
|
||||
headset_profiles = {}
|
||||
persistent_storage_hooks_registered = false
|
||||
end
|
||||
end
|
||||
|
||||
function evaluateAutoswitch ()
|
||||
if Settings.get_boolean ("bluetooth.autoswitch-to-headset-profile") and
|
||||
not autoswitch_hooks_registered then
|
||||
capture_stream_links = {}
|
||||
restore_timeout_source = {}
|
||||
switch_timeout_source = {}
|
||||
evaluate_bluetooth_profiles_hook:register ()
|
||||
link_added_hook:register ()
|
||||
link_removed_hook:register ()
|
||||
state_changed_hook:register ()
|
||||
node_added_hook:register ()
|
||||
device_profile_changed_hook:register ()
|
||||
autoswitch_hooks_registered = true
|
||||
elseif autoswitch_hooks_registered then
|
||||
capture_stream_links = nil
|
||||
restore_timeout_source = nil
|
||||
switch_timeout_source = nil
|
||||
evaluate_bluetooth_profiles_hook:remove ()
|
||||
link_added_hook:remove ()
|
||||
link_removed_hook:remove ()
|
||||
state_changed_hook:remove ()
|
||||
node_added_hook:remove ()
|
||||
device_profile_changed_hook:remove ()
|
||||
autoswitch_hooks_registered = false
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("bluetooth.use-persistent-storage", function ()
|
||||
evaluatePersistentStorage ()
|
||||
end)
|
||||
evaluatePersistentStorage ()
|
||||
|
||||
Settings.subscribe ("bluetooth.autoswitch-to-headset-profile", function ()
|
||||
evaluateAutoswitch ()
|
||||
end)
|
||||
evaluateAutoswitch ()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue