policy-node.lua 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  1. -- WirePlumber
  2. --
  3. -- Copyright © 2020 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. -- ensure config.move and config.follow are not nil
  10. config.move = config.move or false
  11. config.follow = config.follow or false
  12. config.filter_forward_format = config["filter.forward-format"] or false
  13. local self = {}
  14. self.scanning = false
  15. self.pending_rescan = false
  16. self.events_skipped = false
  17. self.pending_error_timer = nil
  18. function rescan()
  19. for si in linkables_om:iterate() do
  20. handleLinkable (si)
  21. end
  22. end
  23. function scheduleRescan ()
  24. if self.scanning then
  25. self.pending_rescan = true
  26. return
  27. end
  28. self.scanning = true
  29. rescan ()
  30. self.scanning = false
  31. if self.pending_rescan then
  32. self.pending_rescan = false
  33. Core.sync(function ()
  34. scheduleRescan ()
  35. end)
  36. end
  37. end
  38. function parseBool(var)
  39. return var and (var:lower() == "true" or var == "1")
  40. end
  41. function createLink (si, si_target, passthrough, exclusive)
  42. local out_item = nil
  43. local in_item = nil
  44. local si_props = si.properties
  45. local target_props = si_target.properties
  46. local si_id = si.id
  47. -- break rescan if tried more than 5 times with same target
  48. if si_flags[si_id].failed_peer_id ~= nil and
  49. si_flags[si_id].failed_peer_id == si_target.id and
  50. si_flags[si_id].failed_count ~= nil and
  51. si_flags[si_id].failed_count > 5 then
  52. Log.warning (si, "tried to link on last rescan, not retrying")
  53. return
  54. end
  55. if si_props["item.node.direction"] == "output" then
  56. -- playback
  57. out_item = si
  58. in_item = si_target
  59. else
  60. -- capture
  61. in_item = si
  62. out_item = si_target
  63. end
  64. local passive = parseBool(si_props["node.passive"]) or
  65. parseBool(target_props["node.passive"])
  66. Log.info (
  67. string.format("link %s <-> %s passive:%s, passthrough:%s, exclusive:%s",
  68. tostring(si_props["node.name"]),
  69. tostring(target_props["node.name"]),
  70. tostring(passive), tostring(passthrough), tostring(exclusive)))
  71. -- create and configure link
  72. local si_link = SessionItem ( "si-standard-link" )
  73. if not si_link:configure {
  74. ["out.item"] = out_item,
  75. ["in.item"] = in_item,
  76. ["passive"] = passive,
  77. ["passthrough"] = passthrough,
  78. ["exclusive"] = exclusive,
  79. ["out.item.port.context"] = "output",
  80. ["in.item.port.context"] = "input",
  81. ["is.policy.item.link"] = true,
  82. } then
  83. Log.warning (si_link, "failed to configure si-standard-link")
  84. return
  85. end
  86. si_link:connect("link-error", function (_, error_msg)
  87. local ids = {si_id}
  88. if si_flags[si_id] ~= nil then
  89. table.insert (ids, si_flags[si_id].peer_id)
  90. end
  91. for _, id in ipairs (ids) do
  92. local si = linkables_om:lookup {
  93. Constraint { "id", "=", id, type = "gobject" },
  94. }
  95. if si then
  96. local node = si:get_associated_proxy ("node")
  97. local client_id = node.properties["client.id"]
  98. if client_id then
  99. local client = clients_om:lookup {
  100. Constraint { "bound-id", "=", client_id, type = "gobject" }
  101. }
  102. if client then
  103. Log.info (node, "sending client error: " .. error_msg)
  104. client:send_error (node["bound-id"], -32, error_msg)
  105. end
  106. end
  107. end
  108. end
  109. end)
  110. -- register
  111. si_flags[si_id].peer_id = si_target.id
  112. si_flags[si_id].failed_peer_id = si_target.id
  113. if si_flags[si_id].failed_count ~= nil then
  114. si_flags[si_id].failed_count = si_flags[si_id].failed_count + 1
  115. else
  116. si_flags[si_id].failed_count = 1
  117. end
  118. si_link:register ()
  119. -- activate
  120. si_link:activate (Feature.SessionItem.ACTIVE, function (l, e)
  121. if e then
  122. Log.info (l, "failed to activate si-standard-link: " .. tostring(e))
  123. if si_flags[si_id] ~= nil then
  124. si_flags[si_id].peer_id = nil
  125. end
  126. l:remove ()
  127. else
  128. if si_flags[si_id] ~= nil then
  129. si_flags[si_id].failed_peer_id = nil
  130. si_flags[si_id].failed_count = 0
  131. end
  132. Log.info (l, "activated si-standard-link")
  133. end
  134. scheduleRescan()
  135. end)
  136. end
  137. function isLinked(si_target)
  138. local target_id = si_target.id
  139. local linked = false
  140. local exclusive = false
  141. for l in links_om:iterate() do
  142. local p = l.properties
  143. local out_id = tonumber(p["out.item.id"])
  144. local in_id = tonumber(p["in.item.id"])
  145. linked = (out_id == target_id) or (in_id == target_id)
  146. if linked then
  147. exclusive = parseBool(p["exclusive"]) or parseBool(p["passthrough"])
  148. break
  149. end
  150. end
  151. return linked, exclusive
  152. end
  153. function canPassthrough (si, si_target)
  154. -- both nodes must support encoded formats
  155. if not parseBool(si.properties["item.node.supports-encoded-fmts"])
  156. or not parseBool(si_target.properties["item.node.supports-encoded-fmts"]) then
  157. return false
  158. end
  159. -- make sure that the nodes have at least one common non-raw format
  160. local n1 = si:get_associated_proxy ("node")
  161. local n2 = si_target:get_associated_proxy ("node")
  162. for p1 in n1:iterate_params("EnumFormat") do
  163. local p1p = p1:parse()
  164. if p1p.properties.mediaSubtype ~= "raw" then
  165. for p2 in n2:iterate_params("EnumFormat") do
  166. if p1:filter(p2) then
  167. return true
  168. end
  169. end
  170. end
  171. end
  172. return false
  173. end
  174. function canLink (properties, si_target)
  175. local target_properties = si_target.properties
  176. -- nodes must have the same media type
  177. if properties["media.type"] ~= target_properties["media.type"] then
  178. return false
  179. end
  180. -- nodes must have opposite direction, or otherwise they must be both input
  181. -- and the target must have a monitor (so the target will be used as a source)
  182. local function isMonitor(properties)
  183. return properties["item.node.direction"] == "input" and
  184. parseBool(properties["item.features.monitor"]) and
  185. not parseBool(properties["item.features.no-dsp"]) and
  186. properties["item.factory.name"] == "si-audio-adapter"
  187. end
  188. if properties["item.node.direction"] == target_properties["item.node.direction"]
  189. and not isMonitor(target_properties) then
  190. return false
  191. end
  192. -- check link group
  193. local function canLinkGroupCheck (link_group, si_target, hops)
  194. local target_props = si_target.properties
  195. local target_link_group = target_props["node.link-group"]
  196. if hops == 8 then
  197. return false
  198. end
  199. -- allow linking if target has no link-group property
  200. if not target_link_group then
  201. return true
  202. end
  203. -- do not allow linking if target has the same link-group
  204. if link_group == target_link_group then
  205. return false
  206. end
  207. -- make sure target is not linked with another node with same link group
  208. -- start by locating other nodes in the target's link-group, in opposite direction
  209. for n in linkables_om:iterate {
  210. Constraint { "id", "!", si_target.id, type = "gobject" },
  211. Constraint { "item.node.direction", "!", target_props["item.node.direction"] },
  212. Constraint { "node.link-group", "=", target_link_group },
  213. } do
  214. -- iterate their peers and return false if one of them cannot link
  215. for silink in links_om:iterate() do
  216. local out_id = tonumber(silink.properties["out.item.id"])
  217. local in_id = tonumber(silink.properties["in.item.id"])
  218. if out_id == n.id or in_id == n.id then
  219. local peer_id = (out_id == n.id) and in_id or out_id
  220. local peer = linkables_om:lookup {
  221. Constraint { "id", "=", peer_id, type = "gobject" },
  222. }
  223. if peer and not canLinkGroupCheck (link_group, peer, hops + 1) then
  224. return false
  225. end
  226. end
  227. end
  228. end
  229. return true
  230. end
  231. local link_group = properties["node.link-group"]
  232. if link_group then
  233. return canLinkGroupCheck (link_group, si_target, 0)
  234. end
  235. return true
  236. end
  237. function getTargetDirection(properties)
  238. local target_direction = nil
  239. if properties["item.node.direction"] == "output" or
  240. (properties["item.node.direction"] == "input" and
  241. parseBool(properties["stream.capture.sink"])) then
  242. target_direction = "input"
  243. else
  244. target_direction = "output"
  245. end
  246. return target_direction
  247. end
  248. function getDefaultNode(properties, target_direction)
  249. local target_media_class =
  250. properties["media.type"] ..
  251. (target_direction == "input" and "/Sink" or "/Source")
  252. return default_nodes:call("get-default-node", target_media_class)
  253. end
  254. -- Try to locate a valid target node that was explicitly requsted by the
  255. -- client(node.target) or by the user(target.node)
  256. -- Use the target.node metadata, if config.move is enabled,
  257. -- then use the node.target property that was set on the node
  258. -- `properties` must be the properties dictionary of the session item
  259. -- that is currently being handled
  260. function findDefinedTarget (properties)
  261. local metadata = config.move and metadata_om:lookup()
  262. local target_direction = getTargetDirection(properties)
  263. local target_key
  264. local target_value
  265. local node_defined = false
  266. if properties["target.object"] ~= nil then
  267. target_value = properties["target.object"]
  268. target_key = "object.serial"
  269. node_defined = true
  270. elseif properties["node.target"] ~= nil then
  271. target_value = properties["node.target"]
  272. target_key = "node.id"
  273. node_defined = true
  274. end
  275. if metadata then
  276. local id = metadata:find(properties["node.id"], "target.object")
  277. if id ~= nil then
  278. target_value = id
  279. target_key = "object.serial"
  280. node_defined = false
  281. else
  282. id = metadata:find(properties["node.id"], "target.node")
  283. if id ~= nil then
  284. target_value = id
  285. target_key = "node.id"
  286. node_defined = false
  287. end
  288. end
  289. end
  290. if target_value == "-1" then
  291. return nil, false, node_defined
  292. end
  293. if target_value and tonumber(target_value) then
  294. local si_target = linkables_om:lookup {
  295. Constraint { target_key, "=", target_value },
  296. }
  297. if si_target and canLink (properties, si_target) then
  298. return si_target, true, node_defined
  299. end
  300. end
  301. if target_value then
  302. for si_target in linkables_om:iterate() do
  303. local target_props = si_target.properties
  304. if (target_props["node.name"] == target_value or
  305. target_props["object.path"] == target_value) and
  306. target_props["item.node.direction"] == target_direction and
  307. canLink (properties, si_target) then
  308. return si_target, true, node_defined
  309. end
  310. end
  311. end
  312. return nil, (target_value ~= nil), node_defined
  313. end
  314. function parseParam(param, id)
  315. local route = param:parse()
  316. if route.pod_type == "Object" and route.object_id == id then
  317. return route.properties
  318. else
  319. return nil
  320. end
  321. end
  322. function arrayContains(a, value)
  323. for _, v in ipairs(a) do
  324. if v == value then
  325. return true
  326. end
  327. end
  328. return false
  329. end
  330. -- Does the target device have any active/available paths/routes to
  331. -- the physical device(spkr/mic/cam)?
  332. function haveAvailableRoutes (si_props)
  333. local card_profile_device = si_props["card.profile.device"]
  334. local device_id = si_props["device.id"]
  335. local device = device_id and devices_om:lookup {
  336. Constraint { "bound-id", "=", device_id, type = "gobject"},
  337. }
  338. if not card_profile_device or not device then
  339. return true
  340. end
  341. local found = 0
  342. local avail = 0
  343. -- First check "SPA_PARAM_Route" if there are any active devices
  344. -- in an active profile.
  345. for p in device:iterate_params("Route") do
  346. local route = parseParam(p, "Route")
  347. if not route then
  348. goto skip_route
  349. end
  350. if (route.device ~= tonumber(card_profile_device)) then
  351. goto skip_route
  352. end
  353. if (route.available == "no") then
  354. return false
  355. end
  356. do return true end
  357. ::skip_route::
  358. end
  359. -- Second check "SPA_PARAM_EnumRoute" if there is any route that
  360. -- is available if not active.
  361. for p in device:iterate_params("EnumRoute") do
  362. local route = parseParam(p, "EnumRoute")
  363. if not route then
  364. goto skip_enum_route
  365. end
  366. if not arrayContains(route.devices, tonumber(card_profile_device)) then
  367. goto skip_enum_route
  368. end
  369. found = found + 1;
  370. if (route.available ~= "no") then
  371. avail = avail +1
  372. end
  373. ::skip_enum_route::
  374. end
  375. if found == 0 then
  376. return true
  377. end
  378. if avail > 0 then
  379. return true
  380. end
  381. return false
  382. end
  383. function findDefaultLinkable (si)
  384. local si_props = si.properties
  385. local target_direction = getTargetDirection(si_props)
  386. local def_node_id = getDefaultNode(si_props, target_direction)
  387. return linkables_om:lookup {
  388. Constraint { "node.id", "=", tostring(def_node_id) }
  389. }
  390. end
  391. function checkPassthroughCompatibility (si, si_target)
  392. local si_must_passthrough = parseBool(si.properties["item.node.encoded-only"])
  393. local si_target_must_passthrough = parseBool(si_target.properties["item.node.encoded-only"])
  394. local can_passthrough = canPassthrough(si, si_target)
  395. if (si_must_passthrough or si_target_must_passthrough)
  396. and not can_passthrough then
  397. return false, can_passthrough
  398. end
  399. return true, can_passthrough
  400. end
  401. function findBestLinkable (si)
  402. local si_props = si.properties
  403. local target_direction = getTargetDirection(si_props)
  404. local target_picked = nil
  405. local target_can_passthrough = false
  406. local target_priority = 0
  407. local target_plugged = 0
  408. for si_target in linkables_om:iterate {
  409. Constraint { "item.node.type", "=", "device" },
  410. Constraint { "item.node.direction", "=", target_direction },
  411. Constraint { "media.type", "=", si_props["media.type"] },
  412. } do
  413. local si_target_props = si_target.properties
  414. local si_target_node_id = si_target_props["node.id"]
  415. local priority = tonumber(si_target_props["priority.session"]) or 0
  416. Log.debug(string.format("Looking at: %s (%s)",
  417. tostring(si_target_props["node.name"]),
  418. tostring(si_target_node_id)))
  419. if not canLink (si_props, si_target) then
  420. Log.debug("... cannot link, skip linkable")
  421. goto skip_linkable
  422. end
  423. if not haveAvailableRoutes(si_target_props) then
  424. Log.debug("... does not have routes, skip linkable")
  425. goto skip_linkable
  426. end
  427. local passthrough_compatible, can_passthrough =
  428. checkPassthroughCompatibility (si, si_target)
  429. if not passthrough_compatible then
  430. Log.debug("... passthrough is not compatible, skip linkable")
  431. goto skip_linkable
  432. end
  433. local plugged = tonumber(si_target_props["item.plugged.usec"]) or 0
  434. Log.debug("... priority:"..tostring(priority)..", plugged:"..tostring(plugged))
  435. -- (target_picked == NULL) --> make sure atleast one target is picked.
  436. -- (priority > target_priority) --> pick the highest priority linkable(node)
  437. -- target.
  438. -- (priority == target_priority and plugged > target_plugged) --> pick the
  439. -- latest connected/plugged(in time) linkable(node) target.
  440. if (target_picked == nil or
  441. priority > target_priority or
  442. (priority == target_priority and plugged > target_plugged)) then
  443. Log.debug("... picked")
  444. target_picked = si_target
  445. target_can_passthrough = can_passthrough
  446. target_priority = priority
  447. target_plugged = plugged
  448. end
  449. ::skip_linkable::
  450. end
  451. if target_picked then
  452. Log.info(string.format("... best target picked: %s (%s), can_passthrough:%s",
  453. tostring(target_picked.properties["node.name"]),
  454. tostring(target_picked.properties["node.id"]),
  455. tostring(target_can_passthrough)))
  456. return target_picked, target_can_passthrough
  457. else
  458. return nil, nil
  459. end
  460. end
  461. function findUndefinedTarget (si)
  462. -- Just find the best linkable if default nodes module is not loaded
  463. if default_nodes == nil then
  464. return findBestLinkable (si)
  465. end
  466. -- Otherwise find the default linkable. If the default linkable is not
  467. -- compatible, we find the best one instead. We return nil if the default
  468. -- linkable does not exist.
  469. local si_target = findDefaultLinkable (si)
  470. if si_target then
  471. local passthrough_compatible, can_passthrough =
  472. checkPassthroughCompatibility (si, si_target)
  473. if canLink (si.properties, si_target) and passthrough_compatible then
  474. Log.info(string.format("... default target picked: %s (%s), can_passthrough:%s",
  475. tostring(si_target.properties["node.name"]),
  476. tostring(si_target.properties["node.id"]),
  477. tostring(can_passthrough)))
  478. return si_target, can_passthrough
  479. else
  480. return findBestLinkable (si)
  481. end
  482. end
  483. return nil, nil
  484. end
  485. function lookupLink (si_id, si_target_id)
  486. local link = links_om:lookup {
  487. Constraint { "out.item.id", "=", si_id },
  488. Constraint { "in.item.id", "=", si_target_id }
  489. }
  490. if not link then
  491. link = links_om:lookup {
  492. Constraint { "in.item.id", "=", si_id },
  493. Constraint { "out.item.id", "=", si_target_id }
  494. }
  495. end
  496. return link
  497. end
  498. function checkLinkable(si, handle_nonstreams)
  499. -- only handle stream session items
  500. local si_props = si.properties
  501. if not si_props or (si_props["item.node.type"] ~= "stream"
  502. and not handle_nonstreams) then
  503. return false
  504. end
  505. -- Determine if we can handle item by this policy
  506. if endpoints_om:get_n_objects () > 0 and
  507. si_props["item.factory.name"] == "si-audio-adapter" then
  508. return false
  509. end
  510. return true, si_props
  511. end
  512. si_flags = {}
  513. function checkPending ()
  514. local pending_linkables = pending_linkables_om:get_n_objects ()
  515. -- We cannot process linkables if some of them are pending activation,
  516. -- because linkables do not appear in the same order as nodes,
  517. -- and we cannot resolve target node references until all linkables
  518. -- have appeared.
  519. if self.pending_error_timer then
  520. self.pending_error_timer:destroy ()
  521. self.pending_error_timer = nil
  522. end
  523. if pending_linkables ~= 0 then
  524. -- Wait for linkables to get it sync
  525. Log.debug(string.format("pending %d linkable not ready",
  526. pending_linkables))
  527. self.events_skipped = true
  528. -- To make bugs in activation easier to debug, emit an error message
  529. -- if they occur. policy-node should never be suspended for 20sec.
  530. self.pending_error_timer = Core.timeout_add(20000, function()
  531. self.pending_error_timer = nil
  532. if pending_linkables ~= 0 then
  533. Log.message(string.format("%d pending linkable(s) not activated in 20sec. "
  534. .. "This should never happen.", pending_linkables))
  535. end
  536. end)
  537. return true
  538. elseif self.events_skipped then
  539. Log.debug("pending linkables ready")
  540. self.events_skipped = false
  541. scheduleRescan ()
  542. return true
  543. end
  544. return false
  545. end
  546. function checkFollowDefault (si, si_target, has_node_defined_target)
  547. -- If it got linked to the default target that is defined by node
  548. -- props but not metadata, start ignoring the node prop from now on.
  549. -- This is what Pulseaudio does.
  550. --
  551. -- Pulseaudio skips here filter streams (i->origin_sink and
  552. -- o->destination_source set in PA). Pipewire does not have a flag
  553. -- explicitly for this, but we can use presence of node.link-group.
  554. if not has_node_defined_target then
  555. return
  556. end
  557. local si_props = si.properties
  558. local target_props = si_target.properties
  559. local reconnect = not parseBool(si_props["node.dont-reconnect"])
  560. local is_filter = (si_props["node.link-group"] ~= nil)
  561. if config.follow and default_nodes ~= nil and reconnect and not is_filter then
  562. local def_id = getDefaultNode(si_props, getTargetDirection(si_props))
  563. if target_props["node.id"] == tostring(def_id) then
  564. local metadata = metadata_om:lookup()
  565. -- Set target.node, for backward compatibility
  566. metadata:set(tonumber(si_props["node.id"]), "target.node", "Spa:Id", "-1")
  567. Log.info (si, "... set metadata to follow default")
  568. end
  569. end
  570. end
  571. function handleLinkable (si)
  572. if checkPending () then
  573. return
  574. end
  575. local valid, si_props = checkLinkable(si)
  576. if not valid then
  577. return
  578. end
  579. -- check if we need to link this node at all
  580. local autoconnect = parseBool(si_props["node.autoconnect"])
  581. if not autoconnect then
  582. Log.debug (si, tostring(si_props["node.name"]) .. " does not need to be autoconnected")
  583. return
  584. end
  585. Log.info (si, string.format("handling item: %s (%s)",
  586. tostring(si_props["node.name"]), tostring(si_props["node.id"])))
  587. ensureSiFlags(si)
  588. -- get other important node properties
  589. local reconnect = not parseBool(si_props["node.dont-reconnect"])
  590. local exclusive = parseBool(si_props["node.exclusive"])
  591. local si_must_passthrough = parseBool(si_props["item.node.encoded-only"])
  592. -- find defined target
  593. local si_target, has_defined_target, has_node_defined_target
  594. = findDefinedTarget(si_props)
  595. local can_passthrough = si_target and canPassthrough(si, si_target)
  596. if si_target and si_must_passthrough and not can_passthrough then
  597. si_target = nil
  598. end
  599. -- if the client has seen a target that we haven't yet prepared, schedule
  600. -- a rescan one more time and hope for the best
  601. local si_id = si.id
  602. if has_defined_target
  603. and not si_target
  604. and not si_flags[si_id].was_handled
  605. and not si_flags[si_id].done_waiting then
  606. Log.info (si, "... waiting for target")
  607. si_flags[si_id].done_waiting = true
  608. scheduleRescan()
  609. return
  610. end
  611. -- find fallback target
  612. if not si_target and (reconnect or not has_defined_target) then
  613. si_target, can_passthrough = findUndefinedTarget(si)
  614. end
  615. -- Check if item is linked to proper target, otherwise re-link
  616. if si_flags[si_id].peer_id then
  617. if si_target and si_flags[si_id].peer_id == si_target.id then
  618. Log.debug (si, "... already linked to proper target")
  619. -- Check this also here, in case in default targets changed
  620. checkFollowDefault (si, si_target, has_node_defined_target)
  621. return
  622. end
  623. local link = lookupLink (si_id, si_flags[si_id].peer_id)
  624. if reconnect then
  625. if link ~= nil then
  626. -- remove old link
  627. if ((link:get_active_features() & Feature.SessionItem.ACTIVE) == 0) then
  628. -- Link not yet activated. We don't want to remove it now, as that
  629. -- may cause problems. Instead, give up for now. A rescan is scheduled
  630. -- once the link activates.
  631. Log.info (link, "Link to be moved was not activated, will wait for it.")
  632. return
  633. end
  634. si_flags[si_id].peer_id = nil
  635. link:remove ()
  636. Log.info (si, "... moving to new target")
  637. end
  638. else
  639. if link ~= nil then
  640. Log.info (si, "... dont-reconnect, not moving")
  641. return
  642. end
  643. end
  644. end
  645. -- if the stream has dont-reconnect and was already linked before,
  646. -- don't link it to a new target
  647. if not reconnect and si_flags[si.id].was_handled then
  648. si_target = nil
  649. end
  650. -- check target's availability
  651. if si_target then
  652. local target_is_linked, target_is_exclusive = isLinked(si_target)
  653. if target_is_exclusive then
  654. Log.info(si, "... target is linked exclusively")
  655. si_target = nil
  656. end
  657. if target_is_linked then
  658. if exclusive or si_must_passthrough then
  659. Log.info(si, "... target is already linked, cannot link exclusively")
  660. si_target = nil
  661. else
  662. -- disable passthrough, we can live without it
  663. can_passthrough = false
  664. end
  665. end
  666. end
  667. if not si_target then
  668. Log.info (si, "... target not found, reconnect:" .. tostring(reconnect))
  669. local node = si:get_associated_proxy ("node")
  670. if not reconnect then
  671. Log.info (si, "... destroy node")
  672. node:request_destroy()
  673. elseif si_flags[si.id].was_handled then
  674. Log.info (si, "... waiting reconnect")
  675. return
  676. end
  677. local client_id = node.properties["client.id"]
  678. if client_id then
  679. local client = clients_om:lookup {
  680. Constraint { "bound-id", "=", client_id, type = "gobject" }
  681. }
  682. if client then
  683. client:send_error(node["bound-id"], -2, "no node available")
  684. end
  685. end
  686. else
  687. createLink (si, si_target, can_passthrough, exclusive)
  688. si_flags[si.id].was_handled = true
  689. checkFollowDefault (si, si_target, has_node_defined_target)
  690. end
  691. end
  692. function unhandleLinkable (si)
  693. local valid, si_props = checkLinkable(si, true)
  694. if not valid then
  695. return
  696. end
  697. Log.info (si, string.format("unhandling item: %s (%s)",
  698. tostring(si_props["node.name"]), tostring(si_props["node.id"])))
  699. -- remove any links associated with this item
  700. for silink in links_om:iterate() do
  701. local out_id = tonumber (silink.properties["out.item.id"])
  702. local in_id = tonumber (silink.properties["in.item.id"])
  703. if out_id == si.id or in_id == si.id then
  704. if out_id == si.id and
  705. si_flags[in_id] and si_flags[in_id].peer_id == out_id then
  706. si_flags[in_id].peer_id = nil
  707. elseif in_id == si.id and
  708. si_flags[out_id] and si_flags[out_id].peer_id == in_id then
  709. si_flags[out_id].peer_id = nil
  710. end
  711. silink:remove ()
  712. Log.info (silink, "... link removed")
  713. end
  714. end
  715. si_flags[si.id] = nil
  716. end
  717. default_nodes = Plugin.find("default-nodes-api")
  718. metadata_om = ObjectManager {
  719. Interest {
  720. type = "metadata",
  721. Constraint { "metadata.name", "=", "default" },
  722. }
  723. }
  724. endpoints_om = ObjectManager { Interest { type = "SiEndpoint" } }
  725. clients_om = ObjectManager { Interest { type = "client" } }
  726. devices_om = ObjectManager { Interest { type = "device" } }
  727. linkables_om = ObjectManager {
  728. Interest {
  729. type = "SiLinkable",
  730. -- only handle si-audio-adapter and si-node
  731. Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
  732. Constraint { "active-features", "!", 0, type = "gobject" },
  733. }
  734. }
  735. pending_linkables_om = ObjectManager {
  736. Interest {
  737. type = "SiLinkable",
  738. -- only handle si-audio-adapter and si-node
  739. Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
  740. Constraint { "active-features", "=", 0, type = "gobject" },
  741. }
  742. }
  743. links_om = ObjectManager {
  744. Interest {
  745. type = "SiLink",
  746. -- only handle links created by this policy
  747. Constraint { "is.policy.item.link", "=", true },
  748. }
  749. }
  750. -- listen for default node changes if config.follow is enabled
  751. if config.follow and default_nodes ~= nil then
  752. default_nodes:connect("changed", function ()
  753. scheduleRescan ()
  754. end)
  755. end
  756. -- listen for target.node metadata changes if config.move is enabled
  757. if config.move then
  758. metadata_om:connect("object-added", function (om, metadata)
  759. metadata:connect("changed", function (m, subject, key, t, value)
  760. if key == "target.node" or key == "target.object" then
  761. scheduleRescan ()
  762. end
  763. end)
  764. end)
  765. end
  766. function findAssociatedLinkGroupNode (si)
  767. local si_props = si.properties
  768. local node = si:get_associated_proxy ("node")
  769. local link_group = node.properties["node.link-group"]
  770. if link_group == nil then
  771. return nil
  772. end
  773. -- get the associated media class
  774. local assoc_direction = getTargetDirection(si_props)
  775. local assoc_media_class =
  776. si_props["media.type"] ..
  777. (assoc_direction == "input" and "/Sink" or "/Source")
  778. -- find the linkable with same link group and matching assoc media class
  779. for assoc_si in linkables_om:iterate() do
  780. local assoc_node = assoc_si:get_associated_proxy ("node")
  781. local assoc_link_group = assoc_node.properties["node.link-group"]
  782. if assoc_link_group == link_group and
  783. assoc_media_class == assoc_node.properties["media.class"] then
  784. return assoc_si
  785. end
  786. end
  787. return nil
  788. end
  789. function onLinkGroupPortsStateChanged (si, old_state, new_state)
  790. local new_str = tostring(new_state)
  791. local si_props = si.properties
  792. -- only handle items with configured ports state
  793. if new_str ~= "configured" then
  794. return
  795. end
  796. Log.info (si, "ports format changed on " .. si_props["node.name"])
  797. -- find associated device
  798. local si_device = findAssociatedLinkGroupNode (si)
  799. if si_device ~= nil then
  800. local device_node_name = si_device.properties["node.name"]
  801. -- get the stream format
  802. local f, m = si:get_ports_format()
  803. -- unregister the device
  804. Log.info (si_device, "unregistering " .. device_node_name)
  805. si_device:remove()
  806. -- set new format in the device
  807. Log.info (si_device, "setting new format in " .. device_node_name)
  808. si_device:set_ports_format(f, m, function (item, e)
  809. if e ~= nil then
  810. Log.warning (item, "failed to configure ports in " ..
  811. device_node_name .. ": " .. e)
  812. end
  813. -- register back the device
  814. Log.info (item, "registering " .. device_node_name)
  815. item:register()
  816. end)
  817. end
  818. end
  819. function ensureSiFlags (si)
  820. -- prepare flags table
  821. if not si_flags[si.id] then
  822. si_flags[si.id] = {}
  823. end
  824. end
  825. function checkFiltersPortsState (si)
  826. local si_props = si.properties
  827. local node = si:get_associated_proxy ("node")
  828. local link_group = node.properties["node.link-group"]
  829. ensureSiFlags(si)
  830. -- only listen for ports state changed on audio filter streams
  831. if si_flags[si.id].ports_state_signal ~= true and
  832. si_props["item.factory.name"] == "si-audio-adapter" and
  833. si_props["item.node.type"] == "stream" and
  834. link_group ~= nil then
  835. si:connect("adapter-ports-state-changed", onLinkGroupPortsStateChanged)
  836. si_flags[si.id].ports_state_signal = true
  837. Log.info (si, "listening ports state changed on " .. si_props["node.name"])
  838. end
  839. end
  840. linkables_om:connect("object-added", function (om, si)
  841. local si_props = si.properties
  842. -- Forward filters ports format to associated virtual devices if enabled
  843. if config.filter_forward_format then
  844. checkFiltersPortsState (si)
  845. end
  846. if si_props["item.node.type"] ~= "stream" then
  847. scheduleRescan ()
  848. else
  849. handleLinkable (si)
  850. end
  851. end)
  852. linkables_om:connect("object-removed", function (om, si)
  853. unhandleLinkable (si)
  854. scheduleRescan ()
  855. end)
  856. devices_om:connect("object-added", function (om, device)
  857. device:connect("params-changed", function (d, param_name)
  858. scheduleRescan ()
  859. end)
  860. end)
  861. metadata_om:activate()
  862. endpoints_om:activate()
  863. clients_om:activate()
  864. linkables_om:activate()
  865. pending_linkables_om:activate()
  866. links_om:activate()
  867. devices_om:activate()