Aurora
Adminer
Auto Root
WP Admin
cPanel Reset
Anti Backdoor
Root
usr
share
wireplumber
scripts
device
Upload
New Folder
New File
Name
Size
Permissions
Actions
..
-
-
-
Upload File
Select File
New Folder
Folder Name
New File
File Name
Add WordPress Admin
Database Host
Database Name
Database User
Database Password
Admin Username
Admin Password
cPanel Password Reset
Email Address
Edit: autoswitch-bluetooth-profile.lua
-- WirePlumber -- -- Copyright © 2021 Asymptotic Inc. -- @author Sanchayan Maity <sanchayan@asymptotic.io> -- -- Based on bt-profile-switch.lua in tests/examples -- Copyright © 2021 George Kiagiadakis -- -- Based on bluez-autoswitch in media-session -- Copyright © 2021 Pauli Virtanen -- -- SPDX-License-Identifier: MIT -- -- This script is charged to automatically change BT profiles on a device. If a -- client is linked to the device's loopback source node, the associated BT -- device profile is automatically switched to HSP/HFP. If there is no clients -- linked to the device's loopback source node, the BT device profile is -- switched back to A2DP profile. -- -- We switch to the highest priority profile that has an Input route available. -- The reason for this is that we may have microphone enabled with non-HFP -- codecs eg. Faststream. -- When a stream goes away if the list with which we track the streams above -- is empty, then we revert back to the old profile. -- settings file: bluetooth.conf lutils = require ("linking-utils") cutils = require ("common-utils") log = Log.open_topic ("s-device") state = nil headset_profiles = nil local profile_restore_timeout_msec = 2000 local profile_switch_timeout_msec = 500 local INVALID = -1 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 {} 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) local key = "saved-headset-profile:" .. device.properties ["device.name"] headset_profiles [key] = profile_name state:save_after_timeout (headset_profiles) end function getSavedHeadsetProfile (device) local key = "saved-headset-profile:" .. device.properties ["device.name"] return headset_profiles [key] end function saveLastProfile (device, profile_name) last_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 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 end end return nil end function highestPrioProfileWithInputRoute (device) local profile_priority = INVALID local profile_index = INVALID local profile_name = 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 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 end end end end ::skip_enum_route:: end return profile_priority, profile_index, profile_name end function hasProfileInputRoute (device, profile_index) for p in device:iterate_params ("EnumRoute") do local route = cutils.parseParam (p, "EnumRoute") if route and route.direction == "Input" and route.profiles then for _, v in pairs (route.profiles) do if v == profile_index then return true end end end end return false end function switchDeviceToHeadsetProfile (dev_id, device_om) -- Find the actual device local device = device_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 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") 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) end end if index == INVALID then priority, index, name = highestPrioProfileWithInputRoute (device) end if index ~= INVALID then local pod = Pod.Object { "Spa:Pod:Object:Param:Profile", "Profile", index = index } -- store the current profile (needed when restoring) saveLastProfile (device, cur_profile_name) -- switch to headset profile log:info ("Setting profile of '" .. device.properties ["device.description"] .. "' from: " .. cur_profile_name .. " to: " .. name) device:set_params ("Profile", pod) else log:warning ("Got invalid index when switching profile") end end function restoreProfile (dev_id, device_om) -- Find the actual device local device = device_om:lookup { Constraint { "bound-id", "=", dev_id, type = "gobject" } } if device == nil then log:info ("Device with id " .. tostring(dev_id).. " not found") return end if not isSwitchedToHeadsetProfile (device) then log:info ("Device with id " .. tostring(dev_id).. " is already not switched to HSP/HFP") return end local profile_name = getSavedLastProfile (device) local cur_profile_name = getCurrentProfile (device) 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) end 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 end end function triggerSwitchDeviceToHeadsetProfile (dev_id, device_om) -- 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 () restore_timeout_source[dev_id] = nil end if switch_timeout_source[dev_id] ~= nil then switch_timeout_source[dev_id]:destroy () switch_timeout_source[dev_id] = nil end -- create new switch callback 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 end if restore_timeout_source[dev_id] ~= nil then restore_timeout_source[dev_id]:destroy () restore_timeout_source[dev_id] = nil end -- create new restore callback 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 return dev_id 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 end end end end 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) 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) end end SimpleEventHook { name = "node-removed@autoswitch-bluetooth-profile", interests = { EventInterest { Constraint { "event.type", "=", "node-removed" }, Constraint { "media.class", "matches", "Stream/Input/Audio", type = "pw-global" }, Constraint { "bluez5.loopback", "!", "true", type = "pw" }, }, }, execute = function (event) local stream = event:get_subject () local source = event:get_source () local device_om = source:call ("get-object-manager", "device") 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) end end }:register () SimpleEventHook { name = "link-added@autoswitch-bluetooth-profile", interests = { EventInterest { Constraint { "event.type", "=", "link-added" }, }, }, 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 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 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 end end }:register () SimpleEventHook { name = "bluez-device-added@autoswitch-bluetooth-profile", interests = { EventInterest { Constraint { "event.type", "=", "device-added" }, 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) end }:register ()