policy-endpoint-client.lua 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. -- WirePlumber
  2. --
  3. -- Copyright © 2021 Collabora Ltd.
  4. -- @author Julian Bouzas <julian.bouzas@collabora.com>
  5. --
  6. -- SPDX-License-Identifier: MIT
  7. -- Receive script arguments from config.lua
  8. local config = ... or {}
  9. config.roles = config.roles or {}
  10. local self = {}
  11. self.scanning = false
  12. self.pending_rescan = false
  13. function rescan ()
  14. for si in linkables_om:iterate() do
  15. handleLinkable (si)
  16. end
  17. end
  18. function scheduleRescan ()
  19. if self.scanning then
  20. self.pending_rescan = true
  21. return
  22. end
  23. self.scanning = true
  24. rescan ()
  25. self.scanning = false
  26. if self.pending_rescan then
  27. self.pending_rescan = false
  28. Core.sync(function ()
  29. scheduleRescan ()
  30. end)
  31. end
  32. end
  33. function findRole(role, tmc)
  34. if role and not config.roles[role] then
  35. -- find the role with matching alias
  36. for r, p in pairs(config.roles) do
  37. -- default media class can be overridden in the role config data
  38. mc = p["media.class"] or "Audio/Sink"
  39. if (type(p.alias) == "table" and tmc == mc) then
  40. for i = 1, #(p.alias), 1 do
  41. if role == p.alias[i] then
  42. return r
  43. end
  44. end
  45. end
  46. end
  47. -- otherwise get the lowest priority role
  48. local lowest_priority_p = nil
  49. local lowest_priority_r = nil
  50. for r, p in pairs(config.roles) do
  51. mc = p["media.class"] or "Audio/Sink"
  52. if tmc == mc and (lowest_priority_p == nil or
  53. p.priority < lowest_priority_p.priority) then
  54. lowest_priority_p = p
  55. lowest_priority_r = r
  56. end
  57. end
  58. return lowest_priority_r
  59. end
  60. return role
  61. end
  62. function findTargetEndpoint (node, media_class, role)
  63. local target_class_assoc = {
  64. ["Stream/Input/Audio"] = "Audio/Source",
  65. ["Stream/Output/Audio"] = "Audio/Sink",
  66. ["Stream/Input/Video"] = "Video/Source",
  67. }
  68. local media_role = nil
  69. local highest_priority = -1
  70. local target = nil
  71. -- get target media class
  72. local target_media_class = target_class_assoc[media_class]
  73. if not target_media_class then
  74. return nil
  75. end
  76. -- find highest priority endpoint by role
  77. media_role = findRole(role, target_media_class)
  78. for si_target_ep in endpoints_om:iterate {
  79. Constraint { "role", "=", media_role, type = "pw-global" },
  80. Constraint { "media.class", "=", target_media_class, type = "pw-global" },
  81. } do
  82. local priority = tonumber(si_target_ep.properties["priority"])
  83. if priority > highest_priority then
  84. highest_priority = priority
  85. target = si_target_ep
  86. end
  87. end
  88. return target
  89. end
  90. function createLink (si, si_target_ep)
  91. local out_item = nil
  92. local in_item = nil
  93. local si_props = si.properties
  94. local target_ep_props = si_target_ep.properties
  95. if si_props["item.node.direction"] == "output" then
  96. -- playback
  97. out_item = si
  98. in_item = si_target_ep
  99. else
  100. -- capture
  101. out_item = si_target_ep
  102. in_item = si
  103. end
  104. Log.info (string.format("link %s <-> %s",
  105. tostring(si_props["node.name"]),
  106. tostring(target_ep_props["name"])))
  107. -- create and configure link
  108. local si_link = SessionItem ( "si-standard-link" )
  109. if not si_link:configure {
  110. ["out.item"] = out_item,
  111. ["in.item"] = in_item,
  112. ["out.item.port.context"] = "output",
  113. ["in.item.port.context"] = "input",
  114. ["is.policy.endpoint.client.link"] = true,
  115. ["media.role"] = target_ep_props["role"],
  116. ["target.media.class"] = target_ep_props["media.class"],
  117. ["item.plugged.usec"] = si_props["item.plugged.usec"],
  118. } then
  119. Log.warning (si_link, "failed to configure si-standard-link")
  120. return
  121. end
  122. -- register
  123. si_link:register()
  124. end
  125. function checkLinkable (si)
  126. -- only handle session items that has a node associated proxy
  127. local node = si:get_associated_proxy ("node")
  128. if not node or not node.properties then
  129. return false
  130. end
  131. -- only handle stream session items
  132. local media_class = node.properties["media.class"]
  133. if not media_class or not string.find (media_class, "Stream") then
  134. return false
  135. end
  136. -- Determine if we can handle item by this policy
  137. if endpoints_om:get_n_objects () == 0 then
  138. Log.debug (si, "item won't be handled by this policy")
  139. return false
  140. end
  141. return true
  142. end
  143. function handleLinkable (si)
  144. if not checkLinkable (si) then
  145. return
  146. end
  147. local node = si:get_associated_proxy ("node")
  148. local media_class = node.properties["media.class"] or ""
  149. local media_role = node.properties["media.role"] or "Default"
  150. Log.info (si, "handling item " .. tostring(node.properties["node.name"]) ..
  151. " with role " .. media_role)
  152. -- find proper target endpoint
  153. local si_target_ep = findTargetEndpoint (node, media_class, media_role)
  154. if not si_target_ep then
  155. Log.info (si, "... target endpoint not found")
  156. return
  157. end
  158. -- Check if item is linked to proper target, otherwise re-link
  159. for link in links_om:iterate() do
  160. local out_id = tonumber(link.properties["out.item.id"])
  161. local in_id = tonumber(link.properties["in.item.id"])
  162. if out_id == si.id or in_id == si.id then
  163. local is_out = out_id == si.id and true or false
  164. for peer_ep in endpoints_om:iterate() do
  165. if peer_ep.id == (is_out and in_id or out_id) then
  166. if peer_ep.id == si_target_ep.id then
  167. Log.info (si, "... already linked to proper target endpoint")
  168. return
  169. end
  170. -- remove old link if active, otherwise schedule rescan
  171. if ((link:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0) then
  172. link:remove ()
  173. Log.info (si, "... moving to new target")
  174. else
  175. scheduleRescan ()
  176. Log.info (si, "... scheduled rescan")
  177. return
  178. end
  179. end
  180. end
  181. end
  182. end
  183. -- create new link
  184. createLink (si, si_target_ep)
  185. end
  186. function unhandleLinkable (si)
  187. if not checkLinkable (si) then
  188. return
  189. end
  190. local node = si:get_associated_proxy ("node")
  191. Log.info (si, "unhandling item " .. tostring(node.properties["node.name"]))
  192. -- remove any links associated with this item
  193. for silink in links_om:iterate() do
  194. local out_id = tonumber (silink.properties["out.item.id"])
  195. local in_id = tonumber (silink.properties["in.item.id"])
  196. if out_id == si.id or in_id == si.id then
  197. silink:remove ()
  198. Log.info (silink, "... link removed")
  199. end
  200. end
  201. end
  202. endpoints_om = ObjectManager { Interest { type = "SiEndpoint" }}
  203. linkables_om = ObjectManager { Interest { type = "SiLinkable",
  204. -- only handle si-audio-adapter and si-node
  205. Constraint {
  206. "item.factory.name", "=", "si-audio-adapter", type = "pw-global" },
  207. Constraint {
  208. "active-features", "!", 0, type = "gobject" },
  209. }
  210. }
  211. links_om = ObjectManager { Interest { type = "SiLink",
  212. -- only handle links created by this policy
  213. Constraint { "is.policy.endpoint.client.link", "=", true, type = "pw-global" },
  214. } }
  215. linkables_om:connect("objects-changed", function (om)
  216. scheduleRescan ()
  217. end)
  218. linkables_om:connect("object-removed", function (om, si)
  219. unhandleLinkable (si)
  220. end)
  221. endpoints_om:activate()
  222. linkables_om:activate()
  223. links_om:activate()