mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2026-02-08 19:50:29 +01:00
default-routes: re-implement the default-routes module in lua
using the logic from the default-routes of pipewire-media-session Fixes: #28, #30 Related: !156
This commit is contained in:
parent
d9cd0e4fdf
commit
ee87161d29
2 changed files with 456 additions and 5 deletions
|
|
@ -1,18 +1,22 @@
|
|||
device_defaults = {}
|
||||
|
||||
device_defaults.properties = {
|
||||
-- store preferences to the file system and restore them at startup
|
||||
-- store preferences to the file system and restore them at startup;
|
||||
-- when set to false, default nodes and routes are selected based on
|
||||
-- their priorities and any runtime changes do not persist after restart
|
||||
["use-persistent-storage"] = true,
|
||||
}
|
||||
|
||||
function device_defaults.enable()
|
||||
-- Enables saving and restoring default nodes
|
||||
-- Selects appropriate default nodes and enables saving and restoring them
|
||||
load_module("default-nodes", device_defaults.properties)
|
||||
|
||||
if device_defaults.properties["use-persistent-storage"] then
|
||||
-- Automatically save and restore default routes
|
||||
load_module("default-routes")
|
||||
-- Selects appropriate default routes ("ports" in pulseaudio terminology)
|
||||
-- and enables saving and restoring them together with
|
||||
-- their properties (per-route/port volume levels, channel maps, etc)
|
||||
load_script("default-routes.lua", device_defaults.properties)
|
||||
|
||||
if device_defaults.properties["use-persistent-storage"] then
|
||||
-- Enables functionality to save and restore default device profiles
|
||||
load_module("default-profile")
|
||||
end
|
||||
|
|
|
|||
447
src/scripts/default-routes.lua
Normal file
447
src/scripts/default-routes.lua
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 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
|
||||
|
||||
local config = ...
|
||||
|
||||
-- whether to store state on the file system
|
||||
use_persistent_storage = config["use-persistent-storage"] or false
|
||||
|
||||
-- table of device info
|
||||
dev_infos = {}
|
||||
|
||||
-- the state storage
|
||||
state_name = "default-routes"
|
||||
state = use_persistent_storage and State(state_name) or nil
|
||||
state_table = state and state:load(state_name) or {}
|
||||
|
||||
-- simple serializer {"foo", "bar"} -> "foo;bar;"
|
||||
function serializeArray(a)
|
||||
local str = ""
|
||||
for _, v in ipairs(a) do
|
||||
str = str .. tostring(v) .. ";"
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
-- simple deserializer "foo;bar;" -> {"foo", "bar"}
|
||||
function parseArray(str, convert_value)
|
||||
local array = {}
|
||||
local pos = 1
|
||||
while true do
|
||||
local next = str:find(";", pos, true)
|
||||
if next then
|
||||
local val = str:sub(pos, next-1)
|
||||
val = convert_value and convert_value(val) or val
|
||||
table.insert(array, val)
|
||||
pos = next + 1
|
||||
else
|
||||
break
|
||||
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
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function storeAfterTimeout()
|
||||
if timeout_source then
|
||||
timeout_source:destroy()
|
||||
end
|
||||
timeout_source = Core.timeout_add(1000, function ()
|
||||
state:save(state_name, state_table)
|
||||
timeout_source = nil
|
||||
end)
|
||||
end
|
||||
|
||||
function saveProfile(dev_info, profile_name)
|
||||
if not use_persistent_storage then
|
||||
return
|
||||
end
|
||||
|
||||
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] = serializeArray(routes)
|
||||
storeAfterTimeout()
|
||||
end
|
||||
end
|
||||
|
||||
function saveRouteProps(dev_info, route)
|
||||
if not use_persistent_storage or not route.props then
|
||||
return
|
||||
end
|
||||
|
||||
local props = route.props.properties
|
||||
local key_base = dev_info.name .. ":" ..
|
||||
route.direction:lower() .. ":" ..
|
||||
route.name .. ":"
|
||||
|
||||
state_table[key_base .. "volume"] =
|
||||
props.volume and tostring(props.volume) or nil
|
||||
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
|
||||
state_table[key_base .. "channelMap"] =
|
||||
props.channelMap and serializeArray(props.channelMap) or nil
|
||||
state_table[key_base .. "latencyOffsetNsec"] =
|
||||
props.latencyOffsetNsec and tostring(props.latencyOffsetNsec) or nil
|
||||
|
||||
storeAfterTimeout()
|
||||
end
|
||||
|
||||
function restoreRoute(device, dev_info, device_id, route)
|
||||
-- default props
|
||||
local props = {
|
||||
"Spa:Pod:Object:Param:Props", "Route",
|
||||
channelVolumes = { 0.4 },
|
||||
mute = false,
|
||||
}
|
||||
|
||||
-- restore props from persistent storage
|
||||
if use_persistent_storage then
|
||||
local key_base = dev_info.name .. ":" ..
|
||||
route.direction:lower() .. ":" ..
|
||||
route.name .. ":"
|
||||
|
||||
local str = state_table[key_base .. "volume"]
|
||||
props.volume = str and tonumber(str) or props.volume
|
||||
|
||||
local str = state_table[key_base .. "mute"]
|
||||
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
|
||||
|
||||
local str = state_table[key_base .. "channelMap"]
|
||||
props.channelMap = str and parseArray(str) or props.channelMap
|
||||
|
||||
local str = state_table[key_base .. "latencyOffsetNsec"]
|
||||
props.latencyOffsetNsec = str and math.tointeger(str) or props.latencyOffsetNsec
|
||||
end
|
||||
|
||||
-- convert arrays to Spa Pod
|
||||
if props.channelVolumes then
|
||||
table.insert(props.channelVolumes, 1, "Spa:Float")
|
||||
props.channelVolumes = Pod.Array(props.channelVolumes)
|
||||
end
|
||||
if props.channelMap then
|
||||
table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
|
||||
props.channelMap = Pod.Array(props.channelMap)
|
||||
end
|
||||
|
||||
-- construct Route param
|
||||
local param = Pod.Object {
|
||||
"Spa:Pod:Object:Param:Route", "Route",
|
||||
index = route.index,
|
||||
device = device_id,
|
||||
props = Pod.Object(props),
|
||||
save = route.save,
|
||||
}
|
||||
|
||||
Log.debug(param, "setting route on " .. tostring(device))
|
||||
device:set_param("Route", param)
|
||||
|
||||
route.prev_active = true
|
||||
route.active = true
|
||||
end
|
||||
|
||||
function findActiveDeviceIDs(profile)
|
||||
-- parses the classes from the profile and returns the device IDs
|
||||
----- sample structure, should return { 0, 8 } -----
|
||||
-- classes:
|
||||
-- 1: 2
|
||||
-- 2:
|
||||
-- 1: Audio/Source
|
||||
-- 2: 1
|
||||
-- 3: card.profile.devices
|
||||
-- 4:
|
||||
-- 1: 0
|
||||
-- pod_type: Array
|
||||
-- value_type: Spa:Int
|
||||
-- pod_type: Struct
|
||||
-- 3:
|
||||
-- 1: Audio/Sink
|
||||
-- 2: 1
|
||||
-- 3: card.profile.devices
|
||||
-- 4:
|
||||
-- 1: 8
|
||||
-- pod_type: Array
|
||||
-- value_type: Spa:Int
|
||||
-- pod_type: Struct
|
||||
-- pod_type: Struct
|
||||
local active_ids = {}
|
||||
if type(profile.classes) == "table" and profile.classes.pod_type == "Struct" then
|
||||
for _, p in ipairs(profile.classes) do
|
||||
if type(p) == "table" and p.pod_type == "Struct" then
|
||||
local i = 1
|
||||
while true do
|
||||
local k, v = p[i], p[i+1]
|
||||
i = i + 2
|
||||
if not k or not v then
|
||||
break
|
||||
end
|
||||
if k == "card.profile.devices" and
|
||||
type(v) == "table" and v.pod_type == "Array" then
|
||||
for _, dev_id in ipairs(v) do
|
||||
table.insert(active_ids, dev_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return active_ids
|
||||
end
|
||||
|
||||
-- returns an array of the route names that were previously selected
|
||||
-- for the given device and profile
|
||||
function getStoredProfileRoutes(dev_name, profile_name)
|
||||
local key = dev_name .. ":profile:" .. profile_name
|
||||
local str = state_table[key]
|
||||
return str and 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 arrayContains(spr, ri.name) then
|
||||
return ri
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- find the best route for a given device_id, based on availability and priority
|
||||
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) then
|
||||
if ri.available == "yes" and
|
||||
(best_avail == nil or ri.priority > best_avail.priority) then
|
||||
best_avail = ri
|
||||
elseif ri.available ~= "no" and
|
||||
(best_unk == nil or ri.priority > best_unk.priority) then
|
||||
best_unk = ri
|
||||
end
|
||||
end
|
||||
end
|
||||
return best_avail or best_unk
|
||||
end
|
||||
|
||||
function restoreProfileRoutes(device, dev_info, profile, profile_changed)
|
||||
Log.info(device, "restore routes for profile " .. profile.name)
|
||||
|
||||
local active_ids = findActiveDeviceIDs(profile)
|
||||
local spr = getStoredProfileRoutes(dev_info.name, profile.name)
|
||||
|
||||
for _, device_id in ipairs(active_ids) do
|
||||
Log.info(device, "restoring device " .. device_id);
|
||||
|
||||
local route = nil
|
||||
|
||||
-- restore routes selection for the newly selected profile
|
||||
-- don't bother if spr is empty, there is no point
|
||||
if profile_changed and #spr > 0 then
|
||||
route = findSavedRoute(dev_info, device_id, spr)
|
||||
if route then
|
||||
-- we found a saved route
|
||||
if route.available == "no" then
|
||||
Log.info(device, "saved route '" .. route.name .. "' not available")
|
||||
-- not available, try to find next best
|
||||
route = nil
|
||||
else
|
||||
Log.info(device, "found saved route: " .. route.name)
|
||||
-- make sure we save it again
|
||||
route.save = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- we could not find a saved route, try to find a new best
|
||||
if not route then
|
||||
route = findBestRoute(dev_info, device_id)
|
||||
if not route then
|
||||
Log.info(device, "can't find best route")
|
||||
else
|
||||
Log.info(device, "found best route: " .. route.name)
|
||||
end
|
||||
end
|
||||
|
||||
-- restore route
|
||||
if route then
|
||||
restoreRoute(device, dev_info, device_id, route)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function findRouteInfo(dev_info, route, return_new)
|
||||
local ri = dev_info.route_infos[route.index]
|
||||
if not ri and return_new then
|
||||
ri = {
|
||||
index = route.index,
|
||||
name = route.name,
|
||||
direction = route.direction,
|
||||
devices = route.devices or {},
|
||||
priority = route.priority or 0,
|
||||
available = route.available or "unknown",
|
||||
prev_available = route.available or "unknown",
|
||||
active = false,
|
||||
prev_active = false,
|
||||
save = false,
|
||||
}
|
||||
end
|
||||
return ri
|
||||
end
|
||||
|
||||
function handleDevice(device)
|
||||
local dev_info = dev_infos[device["bound-id"]]
|
||||
local new_route_infos = {}
|
||||
local avail_routes_changed = false
|
||||
|
||||
-- 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")
|
||||
if not route then
|
||||
goto skip_enum_route
|
||||
end
|
||||
|
||||
-- find cached route information
|
||||
local route_info = findRouteInfo(dev_info, route, true)
|
||||
|
||||
-- update properties
|
||||
route_info.prev_available = route_info.available
|
||||
if route_info.available ~= route.available then
|
||||
Log.info(device, "route " .. route.name .. " available changed " ..
|
||||
route_info.available .. " -> " .. route.available)
|
||||
route_info.available = route.available
|
||||
avail_routes_changed = true
|
||||
end
|
||||
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
|
||||
|
||||
-- replace old route_infos to lose old routes
|
||||
-- that no longer exist on the device
|
||||
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 = 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 = findRouteInfo(dev_info, route, false)
|
||||
if not route_info then
|
||||
goto skip_route
|
||||
end
|
||||
|
||||
-- update 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, "new active route found " .. route.name)
|
||||
restoreRoute(device, dev_info, route.device, route_info)
|
||||
elseif route.save then
|
||||
-- just save route properties
|
||||
Log.info(device, "storing route props for " .. route.name)
|
||||
saveRouteProps(dev_info, route)
|
||||
end
|
||||
|
||||
::skip_route::
|
||||
end
|
||||
|
||||
-- now get the profile and restore routes for it
|
||||
for p in device:iterate_params("Profile") do
|
||||
local profile = parseParam(p, "Profile")
|
||||
if profile then
|
||||
local profile_changed = (dev_info.active_profile ~= profile.index)
|
||||
|
||||
-- if the profile changed, restore routes for that profile
|
||||
-- if any of the routes of the current profile changed in availability,
|
||||
-- then try to select a new "best" route for each device and ignore
|
||||
-- what was stored
|
||||
if profile_changed or avail_routes_changed then
|
||||
dev_info.active_profile = profile.index
|
||||
restoreProfileRoutes(device, dev_info, profile, profile_changed)
|
||||
end
|
||||
|
||||
saveProfile(dev_info, profile.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
om = ObjectManager {
|
||||
Interest { type = "device" },
|
||||
}
|
||||
|
||||
om:connect("objects-changed", function (om)
|
||||
local new_dev_infos = {}
|
||||
for device in om:iterate() do
|
||||
if device.properties["device.name"] then
|
||||
local dev_info = dev_infos[device["bound-id"]]
|
||||
-- new device appeared
|
||||
if not dev_info then
|
||||
dev_info = {
|
||||
name = device.properties["device.name"],
|
||||
active_profile = -1,
|
||||
route_infos = {},
|
||||
}
|
||||
dev_infos[device["bound-id"]] = dev_info
|
||||
new_dev_infos[device["bound-id"]] = dev_info
|
||||
|
||||
device:connect("params-changed", handleDevice)
|
||||
handleDevice(device)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- replace list to get rid of dev_info for devices that no longer exist
|
||||
dev_infos = new_dev_infos
|
||||
end)
|
||||
|
||||
om:activate()
|
||||
Loading…
Add table
Reference in a new issue