policy-endpoint-client-links.lua 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. -- WirePlumber
  2. --
  3. -- Copyright © 2021 Collabora Ltd.
  4. -- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
  5. --
  6. -- SPDX-License-Identifier: MIT
  7. local config = ... or {}
  8. config.roles = config.roles or {}
  9. config["duck.level"] = config["duck.level"] or 0.3
  10. function findRole(role)
  11. if role and not config.roles[role] then
  12. for r, p in pairs(config.roles) do
  13. if type(p.alias) == "table" then
  14. for i = 1, #(p.alias), 1 do
  15. if role == p.alias[i] then
  16. return r
  17. end
  18. end
  19. end
  20. end
  21. end
  22. return role
  23. end
  24. function priorityForRole(role)
  25. local r = role and config.roles[role] or nil
  26. return r and r.priority or 0
  27. end
  28. function getAction(dominant_role, other_role)
  29. -- default to "mix" if the role is not configured
  30. if not dominant_role or not config.roles[dominant_role] then
  31. return "mix"
  32. end
  33. local role_config = config.roles[dominant_role]
  34. return role_config["action." .. other_role]
  35. or role_config["action.default"]
  36. or "mix"
  37. end
  38. function restoreVolume(role, media_class)
  39. if not mixer_api then return end
  40. local ep = endpoints_om:lookup {
  41. Constraint { "media.role", "=", role, type = "pw" },
  42. Constraint { "media.class", "=", media_class, type = "pw" },
  43. }
  44. if ep and ep.properties["node.id"] then
  45. Log.debug(ep, "restore role " .. role)
  46. mixer_api:call("set-volume", ep.properties["node.id"], {
  47. monitorVolume = 1.0,
  48. })
  49. end
  50. end
  51. function duck(role, media_class)
  52. if not mixer_api then return end
  53. local ep = endpoints_om:lookup {
  54. Constraint { "media.role", "=", role, type = "pw" },
  55. Constraint { "media.class", "=", media_class, type = "pw" },
  56. }
  57. if ep and ep.properties["node.id"] then
  58. Log.debug(ep, "duck role " .. role)
  59. mixer_api:call("set-volume", ep.properties["node.id"], {
  60. monitorVolume = config["duck.level"],
  61. })
  62. end
  63. end
  64. function getSuspendPlaybackMetadata ()
  65. local suspend = false
  66. local metadata = metadata_om:lookup()
  67. if metadata then
  68. local value = metadata:find(0, "suspend.playback")
  69. if value then
  70. suspend = value == "1" and true or false
  71. end
  72. end
  73. return suspend
  74. end
  75. function rescan()
  76. local links = {
  77. ["Audio/Source"] = {},
  78. ["Audio/Sink"] = {},
  79. ["Video/Source"] = {},
  80. }
  81. Log.info("Rescan endpoint links")
  82. -- deactivate all links if suspend playback metadata is present
  83. local suspend = getSuspendPlaybackMetadata()
  84. for silink in silinks_om:iterate() do
  85. if suspend then
  86. silink:deactivate(Feature.SessionItem.ACTIVE)
  87. end
  88. end
  89. -- gather info about links
  90. for silink in silinks_om:iterate() do
  91. local props = silink.properties
  92. local role = props["media.role"]
  93. local target_class = props["target.media.class"]
  94. local plugged = props["item.plugged.usec"]
  95. local active =
  96. ((silink:get_active_features() & Feature.SessionItem.ACTIVE) ~= 0)
  97. if links[target_class] then
  98. table.insert(links[target_class], {
  99. silink = silink,
  100. role = findRole(role),
  101. active = active,
  102. priority = priorityForRole(role),
  103. plugged = plugged and tonumber(plugged) or 0
  104. })
  105. end
  106. end
  107. local function compareLinks(l1, l2)
  108. return (l1.priority > l2.priority) or
  109. ((l1.priority == l2.priority) and (l1.plugged > l2.plugged))
  110. end
  111. for media_class, v in pairs(links) do
  112. -- sort on priority and stream creation time
  113. table.sort(v, compareLinks)
  114. -- apply actions
  115. local first_link = v[1]
  116. if first_link then
  117. for i = 2, #v, 1 do
  118. local action = getAction(first_link.role, v[i].role)
  119. if action == "cork" then
  120. if v[i].active then
  121. v[i].silink:deactivate(Feature.SessionItem.ACTIVE)
  122. end
  123. elseif action == "mix" then
  124. if not v[i].active and not suspend then
  125. v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
  126. end
  127. restoreVolume(v[i].role, media_class)
  128. elseif action == "duck" then
  129. if not v[i].active and not suspend then
  130. v[i].silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
  131. end
  132. duck(v[i].role, media_class)
  133. else
  134. Log.warning("Unknown action: " .. action)
  135. end
  136. end
  137. if not first_link.active and not suspend then
  138. first_link.silink:activate(Feature.SessionItem.ACTIVE, pendingOperation())
  139. end
  140. restoreVolume(first_link.role, media_class)
  141. end
  142. end
  143. end
  144. pending_ops = 0
  145. pending_rescan = false
  146. function pendingOperation()
  147. pending_ops = pending_ops + 1
  148. return function()
  149. pending_ops = pending_ops - 1
  150. if pending_ops == 0 and pending_rescan then
  151. pending_rescan = false
  152. rescan()
  153. end
  154. end
  155. end
  156. function maybeRescan()
  157. if pending_ops == 0 then
  158. rescan()
  159. else
  160. pending_rescan = true
  161. end
  162. end
  163. silinks_om = ObjectManager {
  164. Interest {
  165. type = "SiLink",
  166. Constraint { "is.policy.endpoint.client.link", "=", true },
  167. },
  168. }
  169. silinks_om:connect("objects-changed", maybeRescan)
  170. silinks_om:activate()
  171. -- enable ducking if mixer-api is loaded
  172. mixer_api = Plugin.find("mixer-api")
  173. if mixer_api then
  174. endpoints_om = ObjectManager {
  175. Interest { type = "endpoint" },
  176. }
  177. endpoints_om:activate()
  178. end
  179. metadata_om = ObjectManager {
  180. Interest {
  181. type = "metadata",
  182. Constraint { "metadata.name", "=", "default" },
  183. }
  184. }
  185. metadata_om:connect("object-added", function (om, metadata)
  186. metadata:connect("changed", function (m, subject, key, t, value)
  187. if key == "suspend.playback" then
  188. maybeRescan()
  189. end
  190. end)
  191. end)
  192. metadata_om:activate()