
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.
346 lines
10 KiB
Lua
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"))
|