wireplumber/src/scripts/device/state-routes.lua

369 lines
12 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 Properties()
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)
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 Properties()
local new_selected_routes = {}
local dev_info = devinfo:get_device_info (device)
assert (dev_info)
if selected_routes:get_count () == 0 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 = AsyncEventHook {
name = "device/store-or-restore-routes",
interests = {
EventInterest {
Constraint { "event.type", "=", "device-params-changed" },
Constraint { "event.subject.param-id", "=", "Route" },
},
},
steps = {
start = {
next = "none",
execute = function (event, transition)
local source = event:get_source ()
local device = event:get_subject ()
-- Make sure the routes are always updated before evaluating them.
-- https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/762
device:enum_params ("EnumRoute", function (enum_route_it, e)
local selected_routes = {}
local push_select_routes = false
-- check for error
if e then
transition:return_error ("failed to enum routes: "
.. tostring (e));
return
end
-- Make sure the device is still valid
if (device:get_active_features() & Feature.Proxy.BOUND) == 0 then
transition:advance ()
return
end
local dev_info = devinfo:get_device_info (device)
if not dev_info then
transition:advance ()
return
end
local new_route_infos = {}
-- look at all the routes and update/reset cached information
for p in enum_route_it:iterate() 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.available ~= "no" and 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
transition:advance ()
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"))