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

openSUSE Build Service is sponsored by