From 6762de3990e3d6679ab52b9739322525ba94cb0e Mon Sep 17 00:00:00 2001 From: Ashok Sidipotu Date: Tue, 6 Sep 2022 19:24:33 +0530 Subject: [PATCH] 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 --- modules/module-default-profile.c | 6 ++ src/scripts/lib/common-utils.lua | 22 ++++ src/scripts/lib/policy-utils.lua | 3 +- src/scripts/policy-bluetooth.lua | 108 ++++++++++---------- src/scripts/policy-device-profile.lua | 29 ++---- src/scripts/policy-device-routes.lua | 139 +++++++++++--------------- src/scripts/restore-stream.lua | 21 +--- 7 files changed, 158 insertions(+), 170 deletions(-) diff --git a/modules/module-default-profile.c b/modules/module-default-profile.c index 1333b8b9..bff78eea 100644 --- a/modules/module-default-profile.c +++ b/modules/module-default-profile.c @@ -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); } diff --git a/src/scripts/lib/common-utils.lua b/src/scripts/lib/common-utils.lua index 11f6d3be..6142186d 100644 --- a/src/scripts/lib/common-utils.lua +++ b/src/scripts/lib/common-utils.lua @@ -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 \ No newline at end of file diff --git a/src/scripts/lib/policy-utils.lua b/src/scripts/lib/policy-utils.lua index 2b76d69d..c4e61764 100644 --- a/src/scripts/lib/policy-utils.lua +++ b/src/scripts/lib/policy-utils.lua @@ -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; diff --git a/src/scripts/policy-bluetooth.lua b/src/scripts/policy-bluetooth.lua index 2aa982ed..07b739ef 100644 --- a/src/scripts/policy-bluetooth.lua +++ b/src/scripts/policy-bluetooth.lua @@ -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 () diff --git a/src/scripts/policy-device-profile.lua b/src/scripts/policy-device-profile.lua index 829af25d..f5c567b1 100644 --- a/src/scripts/policy-device-profile.lua +++ b/src/scripts/policy-device-profile.lua @@ -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() diff --git a/src/scripts/policy-device-routes.lua b/src/scripts/policy-device-routes.lua index e9dfc73e..b516f176 100644 --- a/src/scripts/policy-device-routes.lua +++ b/src/scripts/policy-device-routes.lua @@ -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() diff --git a/src/scripts/restore-stream.lua b/src/scripts/restore-stream.lua index a4229b2b..8b245676 100644 --- a/src/scripts/restore-stream.lua +++ b/src/scripts/restore-stream.lua @@ -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