policy-device-routes.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. -- WirePlumber
  2. --
  3. -- Copyright © 2021 Collabora Ltd.
  4. -- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
  5. --
  6. -- Based on default-routes.c from pipewire-media-session
  7. -- Copyright © 2020 Wim Taymans
  8. --
  9. -- SPDX-License-Identifier: MIT
  10. local config = ... or {}
  11. -- whether to store state on the file system
  12. use_persistent_storage = config["use-persistent-storage"] or false
  13. -- the default volume to apply
  14. default_volume = tonumber(config["default-volume"] or 0.4^3)
  15. default_input_volume = tonumber(config["default-input-volume"] or 1.0)
  16. -- table of device info
  17. dev_infos = {}
  18. -- the state storage
  19. state = use_persistent_storage and State("default-routes") or nil
  20. state_table = state and state:load() or {}
  21. -- simple serializer {"foo", "bar"} -> "foo;bar;"
  22. function serializeArray(a)
  23. local str = ""
  24. for _, v in ipairs(a) do
  25. str = str .. tostring(v):gsub(";", "\\;") .. ";"
  26. end
  27. return str
  28. end
  29. -- simple deserializer "foo;bar;" -> {"foo", "bar"}
  30. function parseArray(str, convert_value)
  31. local array = {}
  32. local val = ""
  33. local escaped = false
  34. for i = 1, #str do
  35. local c = str:sub(i,i)
  36. if c == '\\' then
  37. escaped = true
  38. elseif c == ';' and not escaped then
  39. val = convert_value and convert_value(val) or val
  40. table.insert(array, val)
  41. val = ""
  42. else
  43. val = val .. tostring(c)
  44. escaped = false
  45. end
  46. end
  47. return array
  48. end
  49. function arrayContains(a, value)
  50. for _, v in ipairs(a) do
  51. if v == value then
  52. return true
  53. end
  54. end
  55. return false
  56. end
  57. function parseParam(param, id)
  58. local route = param:parse()
  59. if route.pod_type == "Object" and route.object_id == id then
  60. return route.properties
  61. else
  62. return nil
  63. end
  64. end
  65. function storeAfterTimeout()
  66. if timeout_source then
  67. timeout_source:destroy()
  68. end
  69. timeout_source = Core.timeout_add(1000, function ()
  70. local saved, err = state:save(state_table)
  71. if not saved then
  72. Log.warning(err)
  73. end
  74. timeout_source = nil
  75. end)
  76. end
  77. function saveProfile(dev_info, profile_name)
  78. if not use_persistent_storage then
  79. return
  80. end
  81. local routes = {}
  82. for idx, ri in pairs(dev_info.route_infos) do
  83. if ri.save then
  84. table.insert(routes, ri.name)
  85. end
  86. end
  87. if #routes > 0 then
  88. local key = dev_info.name .. ":profile:" .. profile_name
  89. state_table[key] = serializeArray(routes)
  90. storeAfterTimeout()
  91. end
  92. end
  93. function saveRouteProps(dev_info, route)
  94. if not use_persistent_storage or not route.props then
  95. return
  96. end
  97. local props = route.props.properties
  98. local key_base = dev_info.name .. ":" ..
  99. route.direction:lower() .. ":" ..
  100. route.name .. ":"
  101. state_table[key_base .. "volume"] =
  102. props.volume and tostring(props.volume) or nil
  103. state_table[key_base .. "mute"] =
  104. props.mute and tostring(props.mute) or nil
  105. state_table[key_base .. "channelVolumes"] =
  106. props.channelVolumes and serializeArray(props.channelVolumes) or nil
  107. state_table[key_base .. "channelMap"] =
  108. props.channelMap and serializeArray(props.channelMap) or nil
  109. state_table[key_base .. "latencyOffsetNsec"] =
  110. props.latencyOffsetNsec and tostring(props.latencyOffsetNsec) or nil
  111. state_table[key_base .. "iec958Codecs"] =
  112. props.iec958Codecs and serializeArray(props.iec958Codecs) or nil
  113. storeAfterTimeout()
  114. end
  115. function restoreRoute(device, dev_info, device_id, route)
  116. -- default props
  117. local props = {
  118. "Spa:Pod:Object:Param:Props", "Route",
  119. mute = false,
  120. }
  121. if route.direction == "Input" then
  122. props.channelVolumes = { default_input_volume }
  123. else
  124. props.channelVolumes = { default_volume }
  125. end
  126. -- restore props from persistent storage
  127. if use_persistent_storage then
  128. local key_base = dev_info.name .. ":" ..
  129. route.direction:lower() .. ":" ..
  130. route.name .. ":"
  131. local str = state_table[key_base .. "volume"]
  132. props.volume = str and tonumber(str) or props.volume
  133. local str = state_table[key_base .. "mute"]
  134. props.mute = str and (str == "true") or false
  135. local str = state_table[key_base .. "channelVolumes"]
  136. props.channelVolumes = str and parseArray(str, tonumber) or props.channelVolumes
  137. local str = state_table[key_base .. "channelMap"]
  138. props.channelMap = str and parseArray(str) or props.channelMap
  139. local str = state_table[key_base .. "latencyOffsetNsec"]
  140. props.latencyOffsetNsec = str and math.tointeger(str) or props.latencyOffsetNsec
  141. local str = state_table[key_base .. "iec958Codecs"]
  142. props.iec958Codecs = str and parseArray(str) or props.iec958Codecs
  143. end
  144. -- convert arrays to Spa Pod
  145. if props.channelVolumes then
  146. table.insert(props.channelVolumes, 1, "Spa:Float")
  147. props.channelVolumes = Pod.Array(props.channelVolumes)
  148. end
  149. if props.channelMap then
  150. table.insert(props.channelMap, 1, "Spa:Enum:AudioChannel")
  151. props.channelMap = Pod.Array(props.channelMap)
  152. end
  153. if props.iec958Codecs then
  154. table.insert(props.iec958Codecs, 1, "Spa:Enum:AudioIEC958Codec")
  155. props.iec958Codecs = Pod.Array(props.iec958Codecs)
  156. end
  157. -- construct Route param
  158. local param = Pod.Object {
  159. "Spa:Pod:Object:Param:Route", "Route",
  160. index = route.index,
  161. device = device_id,
  162. props = Pod.Object(props),
  163. save = route.save,
  164. }
  165. Log.debug(param, "setting route on " .. tostring(device))
  166. device:set_param("Route", param)
  167. route.prev_active = true
  168. route.active = true
  169. end
  170. function findActiveDeviceIDs(profile)
  171. -- parses the classes from the profile and returns the device IDs
  172. ----- sample structure, should return { 0, 8 } -----
  173. -- classes:
  174. -- 1: 2
  175. -- 2:
  176. -- 1: Audio/Source
  177. -- 2: 1
  178. -- 3: card.profile.devices
  179. -- 4:
  180. -- 1: 0
  181. -- pod_type: Array
  182. -- value_type: Spa:Int
  183. -- pod_type: Struct
  184. -- 3:
  185. -- 1: Audio/Sink
  186. -- 2: 1
  187. -- 3: card.profile.devices
  188. -- 4:
  189. -- 1: 8
  190. -- pod_type: Array
  191. -- value_type: Spa:Int
  192. -- pod_type: Struct
  193. -- pod_type: Struct
  194. local active_ids = {}
  195. if type(profile.classes) == "table" and profile.classes.pod_type == "Struct" then
  196. for _, p in ipairs(profile.classes) do
  197. if type(p) == "table" and p.pod_type == "Struct" then
  198. local i = 1
  199. while true do
  200. local k, v = p[i], p[i+1]
  201. i = i + 2
  202. if not k or not v then
  203. break
  204. end
  205. if k == "card.profile.devices" and
  206. type(v) == "table" and v.pod_type == "Array" then
  207. for _, dev_id in ipairs(v) do
  208. table.insert(active_ids, dev_id)
  209. end
  210. end
  211. end
  212. end
  213. end
  214. end
  215. return active_ids
  216. end
  217. -- returns an array of the route names that were previously selected
  218. -- for the given device and profile
  219. function getStoredProfileRoutes(dev_name, profile_name)
  220. local key = dev_name .. ":profile:" .. profile_name
  221. local str = state_table[key]
  222. return str and parseArray(str) or {}
  223. end
  224. -- find a route that was previously stored for a device_id
  225. -- spr needs to be the array returned from getStoredProfileRoutes()
  226. function findSavedRoute(dev_info, device_id, spr)
  227. for idx, ri in pairs(dev_info.route_infos) do
  228. if arrayContains(ri.devices, device_id) and
  229. (ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) and
  230. arrayContains(spr, ri.name) then
  231. return ri
  232. end
  233. end
  234. return nil
  235. end
  236. -- find the best route for a given device_id, based on availability and priority
  237. function findBestRoute(dev_info, device_id)
  238. local best_avail = nil
  239. local best_unk = nil
  240. for idx, ri in pairs(dev_info.route_infos) do
  241. if arrayContains(ri.devices, device_id) and
  242. (ri.profiles == nil or arrayContains(ri.profiles, dev_info.active_profile)) then
  243. if ri.available == "yes" or ri.available == "unknown" then
  244. if ri.direction == "Output" and ri.available ~= ri.prev_available then
  245. best_avail = ri
  246. ri.save = true
  247. break
  248. elseif ri.available == "yes" then
  249. if (best_avail == nil or ri.priority > best_avail.priority) then
  250. best_avail = ri
  251. end
  252. elseif best_unk == nil or ri.priority > best_unk.priority then
  253. best_unk = ri
  254. end
  255. end
  256. end
  257. end
  258. return best_avail or best_unk
  259. end
  260. function restoreProfileRoutes(device, dev_info, profile, profile_changed)
  261. Log.info(device, "restore routes for profile " .. profile.name)
  262. local active_ids = findActiveDeviceIDs(profile)
  263. local spr = getStoredProfileRoutes(dev_info.name, profile.name)
  264. for _, device_id in ipairs(active_ids) do
  265. Log.info(device, "restoring device " .. device_id);
  266. local route = nil
  267. -- restore routes selection for the newly selected profile
  268. -- don't bother if spr is empty, there is no point
  269. if profile_changed and #spr > 0 then
  270. route = findSavedRoute(dev_info, device_id, spr)
  271. if route then
  272. -- we found a saved route
  273. if route.available == "no" then
  274. Log.info(device, "saved route '" .. route.name .. "' not available")
  275. -- not available, try to find next best
  276. route = nil
  277. else
  278. Log.info(device, "found saved route: " .. route.name)
  279. -- make sure we save it again
  280. route.save = true
  281. end
  282. end
  283. end
  284. -- we could not find a saved route, try to find a new best
  285. if not route then
  286. route = findBestRoute(dev_info, device_id)
  287. if not route then
  288. Log.info(device, "can't find best route")
  289. else
  290. Log.info(device, "found best route: " .. route.name)
  291. end
  292. end
  293. -- restore route
  294. if route then
  295. restoreRoute(device, dev_info, device_id, route)
  296. end
  297. end
  298. end
  299. function findRouteInfo(dev_info, route, return_new)
  300. local ri = dev_info.route_infos[route.index]
  301. if not ri and return_new then
  302. ri = {
  303. index = route.index,
  304. name = route.name,
  305. direction = route.direction,
  306. devices = route.devices or {},
  307. profiles = route.profiles,
  308. priority = route.priority or 0,
  309. available = route.available or "unknown",
  310. prev_available = route.available or "unknown",
  311. active = false,
  312. prev_active = false,
  313. save = false,
  314. }
  315. end
  316. return ri
  317. end
  318. function handleDevice(device)
  319. local dev_info = dev_infos[device["bound-id"]]
  320. local new_route_infos = {}
  321. local avail_routes_changed = false
  322. local profile = nil
  323. -- get current profile
  324. for p in device:iterate_params("Profile") do
  325. profile = parseParam(p, "Profile")
  326. end
  327. -- look at all the routes and update/reset cached information
  328. for p in device:iterate_params("EnumRoute") do
  329. -- parse pod
  330. local route = parseParam(p, "EnumRoute")
  331. if not route then
  332. goto skip_enum_route
  333. end
  334. -- find cached route information
  335. local route_info = findRouteInfo(dev_info, route, true)
  336. -- update properties
  337. route_info.prev_available = route_info.available
  338. if route_info.available ~= route.available then
  339. Log.info(device, "route " .. route.name .. " available changed " ..
  340. route_info.available .. " -> " .. route.available)
  341. route_info.available = route.available
  342. if profile and arrayContains(route.profiles, profile.index) then
  343. avail_routes_changed = true
  344. end
  345. end
  346. route_info.prev_active = route_info.active
  347. route_info.active = false
  348. route_info.save = false
  349. -- store
  350. new_route_infos[route.index] = route_info
  351. ::skip_enum_route::
  352. end
  353. -- replace old route_infos to lose old routes
  354. -- that no longer exist on the device
  355. dev_info.route_infos = new_route_infos
  356. new_route_infos = nil
  357. -- check for changes in the active routes
  358. for p in device:iterate_params("Route") do
  359. local route = parseParam(p, "Route")
  360. if not route then
  361. goto skip_route
  362. end
  363. -- get cached route info and at the same time
  364. -- ensure that the route is also in EnumRoute
  365. local route_info = findRouteInfo(dev_info, route, false)
  366. if not route_info then
  367. goto skip_route
  368. end
  369. -- update state
  370. route_info.active = true
  371. route_info.save = route.save
  372. if not route_info.prev_active then
  373. -- a new route is now active, restore the volume and
  374. -- make sure we save this as a preferred route
  375. Log.info(device, "new active route found " .. route.name)
  376. restoreRoute(device, dev_info, route.device, route_info)
  377. elseif route.save then
  378. -- just save route properties
  379. Log.info(device, "storing route props for " .. route.name)
  380. saveRouteProps(dev_info, route)
  381. end
  382. ::skip_route::
  383. end
  384. -- restore routes for profile
  385. if profile then
  386. local profile_changed = (dev_info.active_profile ~= profile.index)
  387. -- if the profile changed, restore routes for that profile
  388. -- if any of the routes of the current profile changed in availability,
  389. -- then try to select a new "best" route for each device and ignore
  390. -- what was stored
  391. if profile_changed or avail_routes_changed then
  392. dev_info.active_profile = profile.index
  393. restoreProfileRoutes(device, dev_info, profile, profile_changed)
  394. end
  395. saveProfile(dev_info, profile.name)
  396. end
  397. end
  398. om = ObjectManager {
  399. Interest {
  400. type = "device",
  401. Constraint { "device.name", "is-present", type = "pw-global" },
  402. }
  403. }
  404. om:connect("objects-changed", function (om)
  405. local new_dev_infos = {}
  406. for device in om:iterate() do
  407. local dev_info = dev_infos[device["bound-id"]]
  408. -- new device appeared
  409. if not dev_info then
  410. dev_info = {
  411. name = device.properties["device.name"],
  412. active_profile = -1,
  413. route_infos = {},
  414. }
  415. dev_infos[device["bound-id"]] = dev_info
  416. device:connect("params-changed", handleDevice)
  417. handleDevice(device)
  418. end
  419. new_dev_infos[device["bound-id"]] = dev_info
  420. end
  421. -- replace list to get rid of dev_info for devices that no longer exist
  422. dev_infos = new_dev_infos
  423. end)
  424. om:activate()