Files
wireplumber/src/scripts/device/state-routes.lua
Julian Bouzas 5e19722491 scripts: fix regression in state-routes.lua when marking routes as 'active'
Like WirePlumber 0.4.17, we need to mark the current routes as 'active' if they
were previously not active as soon as we detect it. This avoids a possible
infinite loop that restores the routes and saves them constantly, which happens
when the device's Route param has changed more than once before the event
'select-routes' is triggered.
2024-03-11 09:47:31 +00:00

346 lines
10 KiB
Lua

-- WirePlumber
--
-- Copyright © 2021-2022 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- Based on default-routes.c from pipewire-media-session
-- Copyright © 2020 Wim Taymans
--
-- SPDX-License-Identifier: MIT
--
-- This file contains all the logic related to saving device routes and their
-- properties to a state file and restoring both the routes selection and
-- the properties of routes later on.
--
cutils = require ("common-utils")
devinfo = require ("device-info-cache")
log = Log.open_topic ("s-device")
-- the state storage
state = nil
state_table = nil
-- hook to restore routes selection for a newly selected profile
find_stored_routes_hook = SimpleEventHook {
name = "device/find-stored-routes",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-routes" },
Constraint { "profile.changed", "=", "true" },
Constraint { "profile.active-device-ids", "is-present" },
},
},
execute = function (event)
local device = event:get_subject ()
local event_properties = event:get_properties ()
local profile_name = event_properties ["profile.name"]
local active_ids = event_properties ["profile.active-device-ids"]
local selected_routes = event:get_data ("selected-routes") or {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
-- get the stored routes for this profile
-- skip the hook if there are no stored routes, there is no point
local spr = getStoredProfileRoutes (dev_info, profile_name)
if #spr == 0 then
return
end
-- active IDs are exchanged in JSON format
active_ids = Json.Raw (active_ids):parse ()
for _, device_id in ipairs (active_ids) do
-- if a previous hook already selected a route for this device_id, skip it
if selected_routes [tostring (device_id)] then
goto next_device_id
end
log:info (device, "restoring route for device ID " .. tostring (device_id));
local route_info = nil
-- find a route that was previously stored for a device_id
for _, ri in pairs (dev_info.route_infos) do
if cutils.arrayContains (ri.devices, tonumber (device_id)) and
(ri.profiles == nil or cutils.arrayContains (ri.profiles, dev_info.active_profile)) and
cutils.arrayContains (spr, ri.name) then
route_info = ri
break
end
end
if route_info then
-- we found a stored route
if route_info.available == "no" then
log:info (device, "stored route '" .. route_info.name .. "' not available")
-- not available, try to find next best
route_info = nil
else
log:info (device, "found stored route: " .. route_info.name)
-- make sure we save it again
route_info.save = true
end
end
if route_info then
selected_routes [tostring (device_id)] =
Json.Object { index = route_info.index }:to_string ()
end
::next_device_id::
end
-- save the selected routes for the apply-routes hook
event:set_data ("selected-routes", selected_routes)
end
}
-- extract the "selected-routes" event data and augment it to include
-- the route properties, as they were stored in the state file;
-- this is the last step before applying the routes
apply_route_props_hook = SimpleEventHook {
name = "device/apply-route-props",
after = { "device/find-stored-routes", "device/find-best-routes" },
interests = {
EventInterest {
Constraint { "event.type", "=", "select-routes" },
},
},
execute = function (event)
local device = event:get_subject ()
local selected_routes = event:get_data ("selected-routes") or {}
local new_selected_routes = {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
if next (selected_routes) == nil then
log:info (device, "No routes selected to set on " .. dev_info.name)
return
end
for device_id, route in pairs (selected_routes) do
-- JSON to lua table
route = Json.Raw (route):parse ()
local route_info = devinfo.find_route_info (dev_info, route, false)
local props = getStoredRouteProps (dev_info, route_info)
-- convert arrays to Json
if props.channelVolumes then
props.channelVolumes = Json.Array (props.channelVolumes)
end
if props.channelMap then
props.channelMap = Json.Array (props.channelMap)
end
if props.iec958Codecs then
props.iec958Codecs = Json.Array (props.iec958Codecs)
end
local json = Json.Object {
index = route_info.index,
props = Json.Object (props),
}
new_selected_routes [device_id] = json:to_string ()
end
-- save the selected routes for the apply-routes hook
event:set_data ("selected-routes", new_selected_routes)
end
}
store_or_restore_routes_hook = SimpleEventHook {
name = "device/store-or-restore-routes",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "=", "Route" },
},
},
execute = function (event)
local device = event:get_subject ()
local source = event:get_source ()
local selected_routes = {}
local push_select_routes = false
local dev_info = devinfo:get_device_info (device)
if not dev_info then
return
end
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in device:iterate_params ("EnumRoute") do
-- parse pod
local route = cutils.parseParam (p, "EnumRoute")
if not route then
goto skip_enum_route
end
-- find cached route information
local route_info = devinfo.find_route_info (dev_info, route, true)
if not route_info then
goto skip_enum_route
end
-- update properties
route_info.prev_active = route_info.active
route_info.active = false
route_info.save = false
-- store
new_route_infos [route.index] = route_info
::skip_enum_route::
end
-- update route_infos with new prev_active, active and save changes
dev_info.route_infos = new_route_infos
new_route_infos = nil
-- check for changes in the active routes
for p in device:iterate_params ("Route") do
local route = cutils.parseParam (p, "Route")
if not route then
goto skip_route
end
-- get cached route info and at the same time
-- ensure that the route is also in EnumRoute
local route_info = devinfo.find_route_info (dev_info, route, false)
if not route_info then
goto skip_route
end
-- update route_info state
route_info.active = true
route_info.save = route.save
if not route_info.prev_active then
-- a new route is now active, restore the volume and
-- make sure we save this as a preferred route
log:info (device,
string.format ("new active route(%s) found of device(%s)",
route.name, dev_info.name))
route_info.prev_active = true
route_info.active = true
selected_routes [tostring (route.device)] =
Json.Object { index = route_info.index }:to_string ()
push_select_routes = true
elseif route.save and route.props then
-- just save route properties
log:info (device,
string.format ("storing route(%s) props of device(%s)",
route.name, dev_info.name))
saveRouteProps (dev_info, route)
end
::skip_route::
end
-- save selected routes for the active profile
for p in device:iterate_params ("Profile") do
local profile = cutils.parseParam (p, "Profile")
saveProfileRoutes (dev_info, profile.name)
end
-- push a select-routes event to re-apply the routes with new properties
if push_select_routes then
local e = source:call ("create-event", "select-routes", device, nil)
e:set_data ("selected-routes", selected_routes)
EventDispatcher.push_event (e)
end
end
}
function saveRouteProps (dev_info, route)
local props = route.props.properties
local key = dev_info.name .. ":" ..
route.direction:lower () .. ":" ..
route.name
state_table [key] = Json.Object {
volume = props.volume,
mute = props.mute,
channelVolumes = props.channelVolumes and Json.Array (props.channelVolumes),
channelMap = props.channelMap and Json.Array (props.channelMap),
latencyOffsetNsec = props.latencyOffsetNsec,
iec958Codecs = props.iec958Codecs and Json.Array (props.iec958Codecs),
}:to_string ()
state:save_after_timeout (state_table)
end
function getStoredRouteProps (dev_info, route)
local key = dev_info.name .. ":" ..
route.direction:lower () .. ":" ..
route.name
local value = state_table [key]
if value then
local json = Json.Raw (value)
if json and json:is_object () then
return json:parse ()
end
end
return {}
end
-- stores an array with the route names that are selected
-- for the given device and profile
function saveProfileRoutes (dev_info, profile_name)
-- select only routes with save == true
local routes = {}
for idx, ri in pairs (dev_info.route_infos) do
if ri.save then
table.insert (routes, ri.name)
end
end
if #routes > 0 then
local key = dev_info.name .. ":profile:" .. profile_name
state_table [key] = Json.Array (routes):to_string()
state:save_after_timeout (state_table)
end
end
-- returns an array of the route names that were previously selected
-- for the given device and profile
function getStoredProfileRoutes (dev_info, profile_name)
local key = dev_info.name .. ":profile:" .. profile_name
local value = state_table [key]
if value then
local json = Json.Raw (value)
if json and json:is_array () then
return json:parse ()
end
end
return {}
end
function toggleState (enable)
if enable and not state then
state = State ("default-routes")
state_table = state:load ()
find_stored_routes_hook:register ()
apply_route_props_hook:register ()
store_or_restore_routes_hook:register ()
elseif not enable and state then
state = nil
state_table = nil
find_stored_routes_hook:remove ()
apply_route_props_hook:remove ()
store_or_restore_routes_hook:remove ()
end
end
Settings.subscribe ("device.restore-routes", function ()
toggleState (Settings.get_boolean ("device.restore-routes"))
end)
toggleState (Settings.get_boolean ("device.restore-routes"))