policy-{bluetooth|device-profile|device-routes}.lua: Optimize for Event stack

- Sharpen the hooks.
- Make settings live, apply them when they are changed.
- Move some of the common functions to common_utils.lua
This commit is contained in:
Ashok Sidipotu 2022-09-06 19:24:33 +05:30 committed by Julian Bouzas
parent 8e7611fa9f
commit 6762de3990
7 changed files with 158 additions and 170 deletions

View file

@ -287,7 +287,13 @@ wp_default_profile_enable (WpPlugin * plugin, WpTransition * transition)
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "params-changed",
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "device",
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.param-id", "=s", "EnumProfile",
NULL);
wp_interest_event_hook_add_interest (WP_INTEREST_EVENT_HOOK (hook),
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.type", "=s", "params-changed",
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.type", "=s", "device",
WP_CONSTRAINT_TYPE_PW_PROPERTY, "event.subject.param-id", "=s", "Profile",
NULL);
wp_event_dispatcher_register_hook (dispatcher, hook);
g_clear_object (&hook);
}

View file

@ -93,6 +93,28 @@ function cutils.parseArray (str, convert_value, with_type)
return array
end
function cutils.arrayContains (a, value)
for _, v in ipairs (a) do
if v == value then
return true
end
end
return false
end
function cutils.storeAfterTimeout (state, state_table)
if timeout_source then
timeout_source:destroy ()
end
local timeout_source = Core.timeout_add (1000, function ()
local saved, err = state:save (state_table)
if not saved then
Log.warning (err)
end
timeout_source = nil
end)
end
cutils.default_metadata_om:activate ()
return cutils

View file

@ -256,7 +256,8 @@ function putils.haveAvailableRoutes (si_props)
goto skip_enum_route
end
if not arrayContains (route.devices, tonumber (card_profile_device)) then
if not cutils.arrayContains
(route.devices, tonumber (card_profile_device)) then
goto skip_enum_route
end
found = found + 1;

View file

@ -26,10 +26,50 @@
-- settings file: policy.conf
local use_persistent_storage =
Settings.parse_boolean_safe ("bt-policy-use-persistent-storage", false)
local use_headset_profile =
Settings.parse_boolean_safe ("bt-policy-media-role.use-headset-profile", true)
local cutils = require ("common-utils")
local use_persistent_storage = Settings.parse_boolean_safe
("policy.bluetooth.use-persistent-storage", false)
local use_headset_profile = Settings.parse_boolean_safe
("policy.bluetooth.media-role.use-headset-profile", true)
local apps_setting = Settings.parse_array_safe
("policy.bluetooth.media-role.applications")
state = nil
headset_profiles = nil
function handlePersistantSetting (enable)
if enable and state == nil then
-- the state storage
state = use_persistent_storage and State ("policy-bluetooth") or nil
headset_profiles = state and state:load () or {}
else
state = nil
headset_profiles = nil
end
end
local function settingsChangedCallback (_, setting, _)
if setting == "policy.bluetooth.use-persistent-storage" then
use_persistent_storage = Settings.parse_boolean_safe
("policy.bluetooth.use-persistent-storage", use_persistent_storage)
handlePersistantSetting (use_persistent_storage)
elseif setting == "policy.bluetooth.media-role.use-headset-profile" then
use_headset_profile = Settings.parse_boolean_safe
("policy.bluetooth.media-role.use-headset-profile", use_headset_profile)
elseif setting == "policy.bluetooth.media-role.applications" then
local new_apps_setting = Settings.parse_array_safe
("policy.bluetooth.media-role.applications")
if #new_apps_setting > 0 then
apps_setting = new_apps_setting
loadAppNames (apps_setting)
end
end
end
Settings.subscribe ("policy.bluetooth*", settingsChangedCallback)
handlePersistantSetting (use_persistent_storage)
local applications = {}
local profile_restore_timeout_msec = 2000
@ -38,25 +78,18 @@ local INVALID = -1
local timeout_source = nil
local restore_timeout_source = nil
local state = use_persistent_storage and State ("policy-bluetooth") or nil
local headset_profiles = state and state:load () or {}
local last_profiles = {}
local active_streams = {}
local previous_streams = {}
local apps_setting =
Settings.parse_array_safe ("bt-policy-media-role.applications")
for i = 1, #apps_setting do
applications [apps_setting [i]] = true
function loadAppNames (appNames)
for i = 1, #appNames do
applications [appNames [i]] = true
end
end
metadata_om = ObjectManager {
Interest {
type = "metadata",
Constraint { "metadata.name", "=", "default" },
}
}
loadAppNames (apps_setting)
devices_om = ObjectManager {
Interest {
@ -74,36 +107,10 @@ streams_om = ObjectManager {
}
}
local function parseParam (param_to_parse, id)
local param = param_to_parse:parse ()
if param.pod_type == "Object" and param.object_id == id then
return param.properties
else
return nil
end
end
local function storeAfterTimeout ()
if not use_persistent_storage then
return
end
if timeout_source then
timeout_source:destroy ()
end
timeout_source = Core.timeout_add (1000, function ()
local saved, err = state:save (headset_profiles)
if not saved then
Log.warning (err)
end
timeout_source = nil
end)
end
local function saveHeadsetProfile (device, profile_name)
local key = "saved-headset-profile:" .. device.properties ["device.name"]
headset_profiles [key] = profile_name
storeAfterTimeout ()
cutils.storeAfterTimeout (state, headset_profiles)
end
local function getSavedHeadsetProfile (device)
@ -131,14 +138,14 @@ local function isBluez5AudioSink (sink_name)
end
local function isBluez5DefaultAudioSink ()
local metadata = metadata_om:lookup ()
local metadata = cutils.default_metadata_om:lookup ()
local default_audio_sink = metadata:find (0, "default.audio.sink")
return isBluez5AudioSink (default_audio_sink)
end
local function findProfile (device, index, name)
for p in device:iterate_params ("EnumProfile") do
local profile = parseParam (p, "EnumProfile")
local profile = cutils.parseParam (p, "EnumProfile")
if not profile then
goto skip_enum_profile
end
@ -158,7 +165,7 @@ end
local function getCurrentProfile (device)
for p in device:iterate_params ("Profile") do
local profile = parseParam (p, "Profile")
local profile = cutils.parseParam (p, "Profile")
if profile then
return profile.name
end
@ -173,7 +180,7 @@ local function highestPrioProfileWithInputRoute (device)
local profile_name = nil
for p in device:iterate_params ("EnumRoute") do
local route = parseParam (p, "EnumRoute")
local route = cutils.parseParam (p, "EnumRoute")
-- Parse pod
if not route then
goto skip_enum_route
@ -207,7 +214,7 @@ end
local function hasProfileInputRoute (device, profile_index)
for p in device:iterate_params ("EnumRoute") do
local route = parseParam (p, "EnumRoute")
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
@ -448,12 +455,11 @@ SimpleEventHook {
},
execute = function (event)
if (use_headset_profile) then
-- If bluez sink is set as default, rescan for active input streams
handleAllStreams ()
-- If bluez sink is set as default, rescan for active input streams
handleAllStreams ()
end
end
}:register ()
metadata_om:activate ()
devices_om:activate ()
streams_om:activate ()

View file

@ -10,6 +10,8 @@
-- Settings file: device.conf
local cutils = require ("common-utils")
local self = {}
self.active_profiles = {}
self.default_profile_plugin = Plugin.find ("default-profile")
@ -27,16 +29,6 @@ function isProfilePersistent (device_props, profile_name)
return false
end
function parseParam (param, id)
local parsed = param:parse ()
if parsed.pod_type == "Object" and parsed.object_id == id then
return parsed.properties
else
return nil
end
end
function setDeviceProfile (device, dev_id, dev_name, profile)
if self.active_profiles [dev_id] and
self.active_profiles [dev_id].index == profile.index then
@ -63,7 +55,7 @@ function findDefaultProfile (device)
end
for p in device:iterate_params ("EnumProfile") do
local profile = parseParam (p, "EnumProfile")
local profile = cutils.parseParam (p, "EnumProfile")
if profile.name == def_name then
return profile
end
@ -78,7 +70,7 @@ function findBestProfile (device)
local unk_profile = nil
for p in device:iterate_params ("EnumProfile") do
profile = parseParam (p, "EnumProfile")
profile = cutils.parseParam (p, "EnumProfile")
if profile and profile.name ~= "pro-audio" then
if profile.name == "off" then
off_profile = profile
@ -150,10 +142,8 @@ function handleProfiles (device, new_device)
end
end
function onDeviceParamsChanged (device, param_name)
if param_name == "EnumProfile" then
handleProfiles (device, false)
end
function onDeviceParamsChanged (device)
handleProfiles (device, false)
end
SimpleEventHook {
@ -179,14 +169,11 @@ SimpleEventHook {
EventInterest {
Constraint { "event.type", "=", "params-changed" },
Constraint { "event.subject.type", "=", "device" },
Constraint { "event.subject.param-id", "=", "EnumProfile" },
},
},
execute = function (event)
local device = event:get_subject ()
local props = event:get_properties()
local param_name = props ["event.subject.param-id"]
onDeviceParamsChanged (device, param_name)
onDeviceParamsChanged (event:get_subject ())
end
}:register()

View file

@ -12,87 +12,58 @@
-- device profiles. It selects and enables the routes(Route here is a path on
-- soundcard/Pipewire Device(PipeWire:Interface:Device), for example: speaker,
-- mic, headset with in a soundcard) needed for a given profile. It also caches
-- the route properties(Volume, Mute, channelVolumes, channelMap etc) and
-- restores them when the route appears afresh. The cached properties are
-- the route specific properties(Volume, Mute, channelVolumes, channelMap etc)
-- and restores them when the route appears afresh. The cached properties are
-- remembered across reboots if persistancy(use_persistent_storage) is enabled.
-- settings file: device.conf
local cutils = require ("common-utils")
local use_persistent_storage =
Settings.parse_boolean_safe ("device.use-persistent-storage", true)
local default_volume =
Settings.parse_float_safe ("device.default-volume", 0.4^3)
local default_input_volume =
Settings.parse_float_safe ("default-input-volume", 1.0)
Settings.parse_float_safe ("device.default-input-volume", 1.0)
-- table of device info
dev_infos = {}
-- the state storage
state = use_persistent_storage and State ("default-routes") or nil
state_table = state and state:load () or {}
state = nil
state_table = nil
-- simple serializer {"foo", "bar"} -> "foo;bar;"
function serializeArray (a)
local str = ""
for _, v in ipairs (a) do
str = str .. tostring (v):gsub (";", "\\;") .. ";"
end
return str
end
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
function parseArray (str, convert_value)
local array = {}
local val = ""
local escaped = false
for i = 1, #str do
local c = str:sub (i,i)
if c == '\\' then
escaped = true
elseif c == ';' and not escaped then
val = convert_value and convert_value (val) or val
table.insert (array, val)
val = ""
else
val = val .. tostring (c)
escaped = false
end
end
return array
end
function arrayContains (a, value)
for _, v in ipairs (a) do
if v == value then
return true
end
end
return false
end
function parseParam (param, id)
local route = param:parse ()
if route.pod_type == "Object" and route.object_id == id then
return route.properties
function handlePersistantSetting (enable)
if enable and state == nil then
-- the state storage
state = use_persistent_storage and State ("default-routes") or nil
state_table = state and state:load () or {}
else
return nil
state = nil
state_table = nil
end
end
function storeAfterTimeout ()
if timeout_source then
timeout_source:destroy ()
local function settingsChangedCallback (_, setting, _)
local value = Settings.get (setting):parse ()
if setting == "device.use-persistent-storage" then
use_persistent_storage = Settings.parse_boolean_safe
("device.use-persistent-storage", use_persistent_storage)
handlePersistantSetting (use_persistent_storage)
elseif setting == "device.default-volume" then
default_volume = Settings.parse_float_safe ("device.default-volume",
default_volume)
elseif setting == "device.default-input-volume" then
default_input_volume = Settings.parse_float_safe
("device.default-input-volume", default_input_volume)
end
timeout_source = Core.timeout_add (1000, function ()
local saved, err = state:save (state_table)
if not saved then
Log.warning (err)
end
timeout_source = nil
end)
end
Settings.subscribe ("device*", settingsChangedCallback)
handlePersistantSetting (use_persistent_storage)
function saveProfile (dev_info, profile_name)
if not use_persistent_storage then
return
@ -107,8 +78,8 @@ function saveProfile (dev_info, profile_name)
if #routes > 0 then
local key = dev_info.name .. ":profile:" .. profile_name
state_table [key] = serializeArray (routes)
storeAfterTimeout ()
state_table [key] = cutils.serializeArray (routes)
cutils.storeAfterTimeout (state, state_table)
end
end
@ -127,15 +98,15 @@ function saveRouteProps (dev_info, route)
state_table [key_base .. "mute"] =
props.mute and tostring (props.mute) or nil
state_table [key_base .. "channelVolumes"] =
props.channelVolumes and serializeArray (props.channelVolumes) or nil
props.channelVolumes and cutils.serializeArray (props.channelVolumes) or nil
state_table [key_base .. "channelMap"] =
props.channelMap and serializeArray (props.channelMap) or nil
props.channelMap and cutils.serializeArray (props.channelMap) or nil
state_table [key_base .. "latencyOffsetNsec"] =
props.latencyOffsetNsec and tostring (props.latencyOffsetNsec) or nil
state_table [key_base .. "iec958Codecs"] =
props.iec958Codecs and serializeArray (props.iec958Codecs) or nil
props.iec958Codecs and cutils.serializeArray (props.iec958Codecs) or nil
storeAfterTimeout ()
cutils.storeAfterTimeout (state, state_table)
end
function restoreRoute (device, dev_info, device_id, route)
@ -164,16 +135,17 @@ function restoreRoute (device, dev_info, device_id, route)
props.mute = str and (str == "true") or false
local str = state_table [key_base .. "channelVolumes"]
props.channelVolumes = str and parseArray (str, tonumber) or props.channelVolumes
props.channelVolumes =
str and cutils.parseArray (str, tonumber) or props.channelVolumes
local str = state_table [key_base .. "channelMap"]
props.channelMap = str and parseArray (str) or props.channelMap
props.channelMap = str and cutils.parseArray (str) or props.channelMap
local str = state_table [key_base .. "latencyOffsetNsec"]
props.latencyOffsetNsec = str and math.tointeger (str) or props.latencyOffsetNsec
local str = state_table [key_base .. "iec958Codecs"]
props.iec958Codecs = str and parseArray (str) or props.iec958Codecs
props.iec958Codecs = str and cutils.parseArray (str) or props.iec958Codecs
end
-- convert arrays to Spa Pod
@ -264,16 +236,16 @@ end
function getStoredProfileRoutes (dev_name, profile_name)
local key = dev_name .. ":profile:" .. profile_name
local str = state_table [key]
return str and parseArray (str) or {}
return str and cutils.parseArray (str) or {}
end
-- find a route that was previously stored for a device_id
-- spr needs to be the array returned from getStoredProfileRoutes()
function findSavedRoute(dev_info, device_id, spr)
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) and
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) and
arrayContains(spr, ri.name) then
if cutils.arrayContains (ri.devices, device_id) and
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) and
cutils.arrayContains (spr, ri.name) then
return ri
end
end
@ -285,8 +257,8 @@ function findBestRoute (dev_info, device_id)
local best_avail = nil
local best_unk = nil
for idx, ri in pairs(dev_info.route_infos) do
if arrayContains(ri.devices, device_id) and
(ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) then
if cutils.arrayContains (ri.devices, device_id) and
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) then
if ri.available == "yes" or ri.available == "unknown" then
if ri.direction == "Output" and ri.available ~= ri.prev_available then
best_avail = ri
@ -395,13 +367,13 @@ function handleDevice (device)
-- get current profile
for p in device:iterate_params ("Profile") do
profile = parseParam (p, "Profile")
profile = cutils.parseParam (p, "Profile")
end
-- look at all the routes and update/reset cached information
for p in device:iterate_params ("EnumRoute") do
-- parse pod
local route = parseParam (p, "EnumRoute")
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
@ -415,7 +387,7 @@ function handleDevice (device)
Log.info (device, "route " .. route.name .. " available changed " ..
route_info.available .. " -> " .. route.available)
route_info.available = route.available
if profile and arrayContains (route.profiles, profile.index) then
if profile and cutils.arrayContains (route.profiles, profile.index) then
avail_routes_changed = true
end
end
@ -436,7 +408,7 @@ function handleDevice (device)
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = parseParam (p, "Route")
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
@ -539,9 +511,16 @@ SimpleEventHook {
EventInterest {
Constraint { "event.type", "=", "params-changed" },
Constraint { "event.subject.type", "=", "device" },
Constraint { "event.subject.param-id", "=", "Route" },
},
EventInterest {
Constraint { "event.type", "=", "params-changed" },
Constraint { "event.subject.type", "=", "device" },
Constraint { "event.subject.param-id", "=", "EnumRoute" },
},
},
execute = function (event)
handleDevice (event:get_subject())
local props = event:get_properties ()
handleDevice (event:get_subject ())
end
}:register()

View file

@ -29,19 +29,6 @@ default_channel_volume = Settings.parse_float_safe (
state = State ("restore-stream")
state_table = state:load ()
function storeAfterTimeout ()
if timeout_source then
timeout_source:destroy ()
end
timeout_source = Core.timeout_add (1000, function ()
local saved, err = state:save (state_table)
if not saved then
Log.warning (err)
end
timeout_source = nil
end)
end
function formKeyBase (properties)
local keys = {
"media.role",
@ -116,7 +103,7 @@ function saveTarget (subject, target_key, type, value)
Log.info (node, "saving stream target for " ..
tostring (stream_props ["node.name"]) .. " -> " .. tostring (target_name))
storeAfterTimeout ()
cutils.storeAfterTimeout (state, state_table)
end
function restoreTarget(node, target_name)
@ -260,7 +247,7 @@ function saveStream (node)
::skip_prop::
end
storeAfterTimeout ()
cutils.storeAfterTimeout (state, state_table)
end
end
@ -445,7 +432,7 @@ function handleRouteSettings (subject, key, type, value)
state_table [key_base .. ":channelVolumes"] = cutils.serializeArray (vparsed.volumes)
end
storeAfterTimeout ()
cutils.storeAfterTimeout (state, state_table)
end
@ -561,7 +548,7 @@ local function settingsChangedCallback (_, setting, _)
end
end
local settings_sub_id = Settings.subscribe ("stream*", settingsChangedCallback)
Settings.subscribe ("stream*", settingsChangedCallback)
streams_om = ObjectManager {
-- match stream nodes