File 808.patch of Package wireplumber
From 2954e0d5e8d3a0aece6e3f13de0bcae57fcd946e Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 10:11:55 -0400
Subject: [PATCH 1/7] autoswitch-bluetooth-profile: Make sure current profile
is valid before switching
---
src/scripts/device/autoswitch-bluetooth-profile.lua | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index ee41c32e..35e2a0fd 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -153,6 +153,9 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
log:info (device,
"Current profile is already a headset profile, no need to switch")
return
+ elseif cur_profile == nil then
+ log:info (device, "Could not get current profile, not switching")
+ return
end
-- Get saved headset profile if any, otherwise find the highest priority one
@@ -200,6 +203,9 @@ function restoreProfile (dev_id, device_om)
log:info (device,
"Current profile is already a non-headset profile, no need to restore")
return
+ elseif cur_profile == nil then
+ log:info (device, "Could not get current profile, not switching")
+ return
end
-- Get saved non-headset profile if any, otherwise find the highest priority one
--
GitLab
From 8dcf3ce6edda35949cfadfb9d6a287944848dc69 Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 12:32:42 -0400
Subject: [PATCH 2/7] autoswitch-bluetooth-profile: Check profile names to see
if a profile is headset
We cannot only rely on the input routes to check whether a profile is a headset
profile or not because some BT devices can have A2DP source nodes, causing the
autoswitch logic to not work properly.
This patch fixes the problem by also checking if the profile name matches the
'headset-head-unit*' or 'bap-duplex' patterns. If the profile name does not
match those patterns, the profile is not considered headset profile.
See #926
---
.../device/autoswitch-bluetooth-profile.lua | 59 +++++++++++--------
1 file changed, 33 insertions(+), 26 deletions(-)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index 35e2a0fd..6ae6321c 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -86,32 +86,37 @@ function getCurrentProfile (device)
return nil
end
-function highestPrioProfileWithInputRoute (device)
- local found_profile = nil
+function hasProfileInputRoute (device, profile_index)
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
+ if route and route.direction == "Input" and route.profiles 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
+ if v == profile_index then
+ return true
end
end
end
end
- return found_profile
+ return false
+end
+
+function isHeadsetProfile (device, profile)
+ if hasProfileInputRoute (device, profile.index) and
+ (string.find (profile.name, "^headset%-head%-unit") or profile.name == "bap-duplex") then
+ return true
+ else
+ return false
+ end
end
-function highestPrioProfileWithoutInputRoute (device)
+function highestPrioHeadsetProfile (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
+ 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 p ~= nil and isHeadsetProfile (device, p) then
if found_profile == nil or found_profile.priority < p.priority then
found_profile = p
end
@@ -122,18 +127,22 @@ function highestPrioProfileWithoutInputRoute (device)
return found_profile
end
-function hasProfileInputRoute (device, profile_index)
+function highestPrioNonHeadsetProfile (device)
+ local found_profile = nil
for p in device:iterate_params ("EnumRoute") do
local route = cutils.parseParam (p, "EnumRoute")
- if route and route.direction == "Input" and route.profiles then
+ if route ~= nil and route.profiles ~= nil and route.direction ~= "Input" then
for _, v in pairs (route.profiles) do
- if v == profile_index then
- return true
+ local p = findProfile (device, v)
+ if p ~= nil and not isHeadsetProfile (device, p) then
+ if found_profile == nil or found_profile.priority < p.priority then
+ found_profile = p
+ end
end
end
end
end
- return false
+ return found_profile
end
function switchDeviceToHeadsetProfile (dev_id, device_om)
@@ -148,8 +157,7 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
-- 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
+ if cur_profile ~= nil and isHeadsetProfile (device, cur_profile) then
log:info (device,
"Current profile is already a headset profile, no need to switch")
return
@@ -163,12 +171,12 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
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
+ if profile ~= nil and not isHeadsetProfile (device, profile) then
saveHeadsetProfile (device, nil, false)
end
end
if profile == nil then
- profile = highestPrioProfileWithInputRoute (device)
+ profile = highestPrioHeadsetProfile (device)
end
-- Switch if headset profile was found
@@ -198,8 +206,7 @@ function restoreProfile (dev_id, device_om)
-- 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
+ if cur_profile ~= nil and not isHeadsetProfile (device, cur_profile) then
log:info (device,
"Current profile is already a non-headset profile, no need to restore")
return
@@ -213,12 +220,12 @@ function restoreProfile (dev_id, device_om)
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
+ if profile ~= nil and isHeadsetProfile (device, profile) then
saveNonHeadsetProfile (device, nil)
end
end
if profile == nil then
- profile = highestPrioProfileWithoutInputRoute (device)
+ profile = highestPrioNonHeadsetProfile (device)
end
-- Restore if non-headset profile was found
@@ -483,7 +490,7 @@ local device_profile_changed_hook = SimpleEventHook {
-- 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
+ if isHeadsetProfile (device, cur_profile) then
log:info (device, "Saving headset profile " .. cur_profile.name)
saveHeadsetProfile (device, cur_profile.name, cur_profile.save)
else
--
GitLab
From 9fb963d4e5cdeef29abb8ee966adf0f23980b707 Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 12:16:07 -0400
Subject: [PATCH 3/7] autoswitch-bluetooth-profile: Don't evaluate if node
state changes from 'idle' to 'suspended'
This is not needed and can improve performance a little bit.
---
src/scripts/device/autoswitch-bluetooth-profile.lua | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index 6ae6321c..8348b545 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -455,6 +455,17 @@ local state_changed_hook = SimpleEventHook {
},
execute = function (event)
local source = event:get_source ()
+ local node = event:get_subject ()
+ local old_state = event:get_properties ()["event.subject.old-state"]
+ local new_state = event:get_properties ()["event.subject.new-state"]
+
+ log:info (node, "state changed from '" .. old_state .. "' to '" .. new_state .. "'")
+
+ -- Dont evaluate if the state changed from idle to suspended
+ if old_state == "idle" and new_state == "suspended" then
+ return
+ end
+
source:call ("push-event", "evaluate-bluetooth-profiles", nil, nil)
end
}
--
GitLab
From f38f1a2af8e7a691d55e84e14279ae4c6864a478 Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 12:19:34 -0400
Subject: [PATCH 4/7] autoswitch-bluetooth-profile: Rename
device-profile-changed hook name to be more consistent
This makes all the script hooks more consistent.
---
src/scripts/device/autoswitch-bluetooth-profile.lua | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index 8348b545..d32eb781 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -487,7 +487,7 @@ local node_added_hook = SimpleEventHook {
}
local device_profile_changed_hook = SimpleEventHook {
- name = "device/store-user-selected-profile",
+ name = "bluez-profile-changed@autoswitch-bluetooth-profile",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
--
GitLab
From 20238072e2e6eaca352282dc037a46c3d6c4293e Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 12:21:51 -0400
Subject: [PATCH 5/7] autoswitch-bluetooth-profile: Ensure the saved profile is
headset/non-headset before switching/restoring
Otherwise find the highest priority one.
---
src/scripts/device/autoswitch-bluetooth-profile.lua | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index d32eb781..d057b85c 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -173,6 +173,7 @@ function switchDeviceToHeadsetProfile (dev_id, device_om)
profile = findProfile (device, nil, profile_name)
if profile ~= nil and not isHeadsetProfile (device, profile) then
saveHeadsetProfile (device, nil, false)
+ profile = nil
end
end
if profile == nil then
@@ -222,6 +223,7 @@ function restoreProfile (dev_id, device_om)
profile = findProfile (device, nil, profile_name)
if profile ~= nil and isHeadsetProfile (device, profile) then
saveNonHeadsetProfile (device, nil)
+ profile = nil
end
end
if profile == nil then
--
GitLab
From 7023ad0c2cfe2e4fa7cc89eb1bb16fbbf81842be Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Thu, 19 Mar 2026 10:20:08 -0400
Subject: [PATCH 6/7] m-standard-event-source: Add 'autoswitch-*' local event
priority
These local events have lower priority than the 'create-*' and 'select-*' ones,
and are meant to be used when wireplumber wants to automatically switch profiles
or other things.
---
modules/module-standard-event-source.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/modules/module-standard-event-source.c b/modules/module-standard-event-source.c
index fba868b8..e23696bc 100644
--- a/modules/module-standard-event-source.c
+++ b/modules/module-standard-event-source.c
@@ -159,6 +159,8 @@ get_default_event_priority (const gchar *event_type)
if (g_str_has_prefix(event_type, "select-") ||
g_str_has_prefix(event_type, "create-"))
return 500;
+ if (g_str_has_prefix(event_type, "autoswitch-"))
+ return 400;
else if (!g_strcmp0 (event_type, "rescan-for-default-nodes"))
return -490;
else if (!g_strcmp0 (event_type, "rescan-for-linking"))
@@ -209,7 +211,8 @@ static gboolean
is_it_local_event (const gchar *event_type)
{
if (g_str_has_prefix(event_type, "select-") ||
- g_str_has_prefix(event_type, "create-"))
+ g_str_has_prefix(event_type, "create-") ||
+ g_str_has_prefix(event_type, "autoswitch-"))
return TRUE;
return FALSE;
--
GitLab
From b453464320443351bb8b4cc9be66b7fc98636f2c Mon Sep 17 00:00:00 2001
From: Julian Bouzas <julian.bouzas@collabora.com>
Date: Wed, 18 Mar 2026 12:24:54 -0400
Subject: [PATCH 7/7] autoswitch-bluetooth-profile: Switch/restore the profile
using 'autoswitch-*' event hooks
Since 'autoswitch-*' events have lower priority than 'select-*' events,
this guarantees that the autoswitch hooks will always run after the
'device/apply-profile' hook, avoiding possible race conditions.
---
.../device/autoswitch-bluetooth-profile.lua | 77 +++++++++++++++++--
1 file changed, 70 insertions(+), 7 deletions(-)
diff --git a/src/scripts/device/autoswitch-bluetooth-profile.lua b/src/scripts/device/autoswitch-bluetooth-profile.lua
index d057b85c..27c79ddf 100644
--- a/src/scripts/device/autoswitch-bluetooth-profile.lua
+++ b/src/scripts/device/autoswitch-bluetooth-profile.lua
@@ -245,7 +245,7 @@ function restoreProfile (dev_id, device_om)
end
end
-function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
+function triggerSwitchDeviceToHeadsetProfile (source, dev_id)
-- Always clear any pending restore/switch callbacks when triggering a new switch
if restore_timeout_source[dev_id] ~= nil then
restore_timeout_source[dev_id]:destroy ()
@@ -262,11 +262,14 @@ function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om)
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)
+
+ local e = source:call ("create-event", "autoswitch-bluez-headset-profile", nil, nil)
+ e:set_data ("device-id", dev_id)
+ EventDispatcher.push_event (e)
end)
end
-function triggerRestoreProfile (dev_id, device_om)
+function triggerRestoreProfile (source, dev_id)
-- 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 ()
@@ -283,7 +286,10 @@ function triggerRestoreProfile (dev_id, device_om)
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)
+
+ local e = source:call ("create-event", "autoswitch-bluez-a2dp-profile", nil, nil)
+ e:set_data ("device-id", dev_id)
+ EventDispatcher.push_event (e)
end)
end
@@ -359,6 +365,60 @@ function isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om)
return false
end
+local switch_profile_hook = AsyncEventHook {
+ name = "switch-profile@autoswitch-bluetooth-profile",
+ interests = {
+ EventInterest {
+ Constraint { "event.type", "=", "autoswitch-bluez-headset-profile" },
+ },
+ },
+ steps = {
+ start = {
+ next = "none",
+ execute = function (event, transition)
+ local source = event:get_source ()
+ local device_om = source:call ("get-object-manager", "device")
+ local device_id = event:get_data ("device-id")
+
+ -- Switch profile
+ switchDeviceToHeadsetProfile (device_id, device_om)
+
+ -- Wait until the profile is applied
+ Core.sync (function ()
+ transition:advance ()
+ end)
+ end
+ },
+ }
+}
+
+local restore_profile_hook = AsyncEventHook {
+ name = "restore-profile@autoswitch-bluetooth-profile",
+ interests = {
+ EventInterest {
+ Constraint { "event.type", "=", "autoswitch-bluez-a2dp-profile" },
+ },
+ },
+ steps = {
+ start = {
+ next = "none",
+ execute = function (event, transition)
+ local source = event:get_source ()
+ local device_om = source:call ("get-object-manager", "device")
+ local device_id = event:get_data ("device-id")
+
+ -- Restore profile
+ restoreProfile (device_id, device_om)
+
+ -- Wait until the profile is applied
+ Core.sync (function ()
+ transition:advance ()
+ end)
+ end
+ },
+ }
+}
+
local evaluate_bluetooth_profiles_hook = SimpleEventHook {
name = "evaluate-bluetooth-profiles@autoswitch-bluetooth-profile",
interests = {
@@ -369,7 +429,6 @@ local evaluate_bluetooth_profiles_hook = SimpleEventHook {
execute = function (event)
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")
-- Evaluate all bluetooth loopback source nodes, and switch to headset
@@ -390,9 +449,9 @@ local evaluate_bluetooth_profiles_hook = SimpleEventHook {
if bt_node_state == "running" and
isBluetoothLoopbackSourceNodeLinkedToStream (bt_node, node_om, link_om) then
- triggerSwitchDeviceToHeadsetProfile (bt_dev_id, device_om)
+ triggerSwitchDeviceToHeadsetProfile (source, bt_dev_id)
else
- triggerRestoreProfile (bt_dev_id, device_om)
+ triggerRestoreProfile (source, bt_dev_id)
end
end
end
@@ -533,6 +592,8 @@ function evaluateAutoswitch ()
capture_stream_links = {}
restore_timeout_source = {}
switch_timeout_source = {}
+ switch_profile_hook:register ()
+ restore_profile_hook:register ()
evaluate_bluetooth_profiles_hook:register ()
link_added_hook:register ()
link_removed_hook:register ()
@@ -544,6 +605,8 @@ function evaluateAutoswitch ()
capture_stream_links = nil
restore_timeout_source = nil
switch_timeout_source = nil
+ switch_profile_hook:remove ()
+ restore_profile_hook:remove ()
evaluate_bluetooth_profiles_hook:remove ()
link_added_hook:remove ()
link_removed_hook:remove ()
--
GitLab