Update
This commit is contained in:
154
pipewire/.config/wireplumber/scripts/node/create-item.lua
Normal file
154
pipewire/.config/wireplumber/scripts/node/create-item.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author Julian Bouzas <julian.bouzas@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
-- create-item.lua script takes pipewire nodes and creates session items (a.k.a
|
||||
-- linkable) objects out of them.
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-node")
|
||||
|
||||
items = {}
|
||||
|
||||
function configProperties (node)
|
||||
local properties = node.properties
|
||||
local media_class = properties ["media.class"] or ""
|
||||
|
||||
-- ensure a media.type is set
|
||||
if not properties ["media.type"] then
|
||||
for _, i in ipairs ({ "Audio", "Video", "Midi" }) do
|
||||
if media_class:find (i) then
|
||||
properties ["media.type"] = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
properties ["item.node"] = node
|
||||
properties ["item.node.direction"] =
|
||||
cutils.mediaClassToDirection (media_class)
|
||||
properties ["item.node.type"] =
|
||||
media_class:find ("^Stream/") and "stream" or "device"
|
||||
properties ["item.plugged.usec"] = GLib.get_monotonic_time ()
|
||||
properties ["item.features.no-dsp"] =
|
||||
Settings.get_boolean ("node.features.audio.no-dsp")
|
||||
properties ["item.features.monitor"] =
|
||||
Settings.get_boolean ("node.features.audio.monitor-ports")
|
||||
properties ["item.features.control-port"] =
|
||||
Settings.get_boolean ("node.features.audio.control-port")
|
||||
properties ["node.id"] = node ["bound-id"]
|
||||
|
||||
-- set the default media.role, if configured
|
||||
-- avoid Settings.get_string(), as it will parse the default "null" value
|
||||
-- as a string instead of returning nil
|
||||
local default_role = Settings.get ("node.stream.default-media-role")
|
||||
if default_role then
|
||||
default_role = default_role:parse()
|
||||
properties ["media.role"] = properties ["media.role"] or default_role
|
||||
end
|
||||
|
||||
return properties
|
||||
end
|
||||
|
||||
AsyncEventHook {
|
||||
name = "node/create-item",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
||||
Constraint { "wireplumber.is-virtual", "-", type = "pw" },
|
||||
},
|
||||
},
|
||||
steps = {
|
||||
start = {
|
||||
next = "register",
|
||||
execute = function (event, transition)
|
||||
local node = event:get_subject ()
|
||||
local id = node.id
|
||||
local item
|
||||
local item_type
|
||||
|
||||
local media_class = node.properties ['media.class']
|
||||
if string.find (media_class, "Audio") then
|
||||
item_type = "si-audio-adapter"
|
||||
else
|
||||
item_type = "si-node"
|
||||
end
|
||||
|
||||
log:info (node, "creating item for node -> " .. item_type)
|
||||
|
||||
-- create item
|
||||
item = SessionItem (item_type)
|
||||
items [id] = item
|
||||
|
||||
-- configure item
|
||||
if not item:configure (configProperties (node)) then
|
||||
transition:return_error ("failed to configure item for node "
|
||||
.. tostring (id))
|
||||
return
|
||||
end
|
||||
|
||||
-- activate item
|
||||
item:activate (Features.ALL, function (_, e)
|
||||
if e then
|
||||
transition:return_error ("failed to activate item: "
|
||||
.. tostring (e));
|
||||
else
|
||||
transition:advance ()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
},
|
||||
register = {
|
||||
next = "none",
|
||||
execute = function (event, transition)
|
||||
local node = event:get_subject ()
|
||||
local bound_id = node ["bound-id"]
|
||||
local item = items [node.id]
|
||||
|
||||
log:info (item, "activated item for node " .. tostring (bound_id))
|
||||
item:register ()
|
||||
transition:advance ()
|
||||
end,
|
||||
},
|
||||
},
|
||||
}:register ()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/destroy-item",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "#", "Video/*", type = "pw-global" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
Constraint { "media.class", "#", "Audio/*", type = "pw-global" },
|
||||
Constraint { "wireplumber.is-virtual", "-", type = "pw" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local id = node.id
|
||||
if items [id] then
|
||||
items [id]:remove ()
|
||||
items [id] = nil
|
||||
end
|
||||
|
||||
end
|
||||
}:register ()
|
||||
@@ -0,0 +1,111 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022 Collabora Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Logic to "forward" the format set on special filter nodes to their
|
||||
-- virtual device peer node. This is for things like the "loopback" module,
|
||||
-- which always comes in pairs of 2 nodes, one stream and one virtual device.
|
||||
--
|
||||
-- FIXME: this script can be further improved
|
||||
|
||||
lutils = require ("linking-utils")
|
||||
log = Log.open_topic ("s-node")
|
||||
|
||||
function findAssociatedLinkGroupNode (si)
|
||||
local si_props = si.properties
|
||||
local link_group = si_props ["node.link-group"]
|
||||
if link_group == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local std_event_source = Plugin.find ("standard-event-source")
|
||||
local om = std_event_source:call ("get-object-manager", "session-item")
|
||||
|
||||
-- get the associated media class
|
||||
local assoc_direction = cutils.getTargetDirection (si_props)
|
||||
local assoc_media_class = si_props ["media.type"] ..
|
||||
(assoc_direction == "input" and "/Sink" or "/Source")
|
||||
|
||||
-- find the linkable with same link group and matching assoc media class
|
||||
for assoc_si in om:iterate { type = "SiLinkable" } do
|
||||
local assoc_props = assoc_si.properties
|
||||
local assoc_link_group = assoc_props ["node.link-group"]
|
||||
if assoc_link_group == link_group and
|
||||
assoc_media_class == assoc_props ["media.class"] then
|
||||
return assoc_si
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function onLinkGroupPortsStateChanged (si, old_state, new_state)
|
||||
local si_props = si.properties
|
||||
|
||||
-- only handle items with configured ports state
|
||||
if new_state ~= "configured" then
|
||||
return
|
||||
end
|
||||
|
||||
log:info (si, "ports format changed on " .. si_props ["node.name"])
|
||||
|
||||
-- find associated device
|
||||
local si_device = findAssociatedLinkGroupNode (si)
|
||||
if si_device ~= nil then
|
||||
local device_node_name = si_device.properties ["node.name"]
|
||||
|
||||
-- get the stream format
|
||||
local f, m = si:get_ports_format ()
|
||||
|
||||
-- unregister the device
|
||||
log:info (si_device, "unregistering " .. device_node_name)
|
||||
si_device:remove ()
|
||||
|
||||
-- set new format in the device
|
||||
log:info (si_device, "setting new format in " .. device_node_name)
|
||||
si_device:set_ports_format (f, m, function (item, e)
|
||||
if e ~= nil then
|
||||
log:warning (item, "failed to configure ports in " ..
|
||||
device_node_name .. ": " .. e)
|
||||
end
|
||||
|
||||
-- register back the device
|
||||
log:info (item, "registering " .. device_node_name)
|
||||
item:register ()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/filter-forward-format",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "session-item-added" },
|
||||
Constraint { "event.session-item.interface", "=", "linkable" },
|
||||
Constraint { "item.factory.name", "c", "si-audio-adapter", "si-node" },
|
||||
Constraint { "media.class", "#", "Stream/*", type = "pw-global" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local si = event:get_subject ()
|
||||
|
||||
-- Forward filters ports format to associated virtual devices if enabled
|
||||
if Settings.get_boolean ("node.filter.forward-format") then
|
||||
local si_props = si.properties
|
||||
local link_group = si_props ["node.link-group"]
|
||||
local si_flags = lutils:get_flags (si.id)
|
||||
|
||||
-- only listen for ports state changed on audio filter streams
|
||||
if si_flags.ports_state_signal ~= true and
|
||||
si_props ["item.factory.name"] == "si-audio-adapter" and
|
||||
si_props ["item.node.type"] == "stream" and
|
||||
link_group ~= nil then
|
||||
si:connect ("adapter-ports-state-changed", onLinkGroupPortsStateChanged)
|
||||
si_flags.ports_state_signal = true
|
||||
log:info (si, "listening ports state changed on " .. si_props ["node.name"])
|
||||
end
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
92
pipewire/.config/wireplumber/scripts/node/software-dsp.lua
Normal file
92
pipewire/.config/wireplumber/scripts/node/software-dsp.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2022-2023 The WirePlumber project contributors
|
||||
-- @author Dmitry Sharshakov <d3dx12.xx@gmail.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
log = Log.open_topic("s-node")
|
||||
|
||||
config = {}
|
||||
config.rules = Conf.get_section_as_json("node.software-dsp.rules", Json.Array{})
|
||||
|
||||
-- TODO: port from Obj Manager to Hooks
|
||||
clients_om = ObjectManager {
|
||||
Interest { type = "client" }
|
||||
}
|
||||
|
||||
filter_nodes = {}
|
||||
hidden_nodes = {}
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/dsp/create-dsp-node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
},
|
||||
},
|
||||
execute = function(event)
|
||||
local node = event:get_subject()
|
||||
JsonUtils.match_rules (config.rules, node.properties, function (action, value)
|
||||
if action == "create-filter" then
|
||||
local props = value:parse (1)
|
||||
log:debug("DSP rule found for " .. node.properties["node.name"])
|
||||
|
||||
if props["filter-graph"] then
|
||||
log:debug("Loading filter graph for " .. node.properties["node.name"])
|
||||
filter_nodes[node.id] = LocalModule("libpipewire-module-filter-chain", props["filter-graph"], {})
|
||||
elseif props["filter-path"] then
|
||||
log:debug("Loading filter graph for " .. node.properties["node.name"] .. " from disk")
|
||||
local conf = Conf(props["filter-path"], {
|
||||
["as-section"] = "node.software-dsp.graph",
|
||||
["no-fragments"] = true
|
||||
})
|
||||
local err = conf:open()
|
||||
if not err then
|
||||
local args = conf:get_section_as_json("node.software-dsp.graph"):to_string()
|
||||
filter_nodes[node.id] = LocalModule("libpipewire-module-filter-chain", args, {})
|
||||
else
|
||||
log:warning("Unable to load filter graph for " .. node.properties["node.name"])
|
||||
end
|
||||
end
|
||||
|
||||
if props["hide-parent"] then
|
||||
log:debug("Setting permissions to '-' on " .. node.properties["node.name"] .. " for open clients")
|
||||
for client in clients_om:iterate{ type = "client" } do
|
||||
if not client["properties"]["wireplumber.daemon"] then
|
||||
client:update_permissions{ [node["bound-id"]] = "-" }
|
||||
end
|
||||
end
|
||||
hidden_nodes[node["bound-id"]] = node.id
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
}:register()
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/dsp/free-dsp-node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-removed" },
|
||||
},
|
||||
},
|
||||
execute = function(event)
|
||||
local node = event:get_subject()
|
||||
if filter_nodes[node.id] then
|
||||
log:debug("Freeing filter on node " .. node.id)
|
||||
filter_nodes[node.id] = nil
|
||||
hidden_nodes[node["bound-id"]] = nil
|
||||
end
|
||||
end
|
||||
}:register()
|
||||
|
||||
clients_om:connect("object-added", function (om, client)
|
||||
for id, _ in pairs(hidden_nodes) do
|
||||
if not client["properties"]["wireplumber.daemon"] then
|
||||
client:update_permissions { [id] = "-" }
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
clients_om:activate()
|
||||
452
pipewire/.config/wireplumber/scripts/node/state-stream.lua
Normal file
452
pipewire/.config/wireplumber/scripts/node/state-stream.lua
Normal file
@@ -0,0 +1,452 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021-2022 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- Based on restore-stream.c from pipewire-media-session
|
||||
-- Copyright © 2020 Wim Taymans
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
cutils = require ("common-utils")
|
||||
log = Log.open_topic ("s-node")
|
||||
|
||||
config = {}
|
||||
config.rules = Conf.get_section_as_json ("stream.rules", Json.Array {})
|
||||
|
||||
-- the state storage
|
||||
state = nil
|
||||
state_table = nil
|
||||
|
||||
-- Support for the "System Sounds" volume control in pavucontrol
|
||||
rs_metadata = nil
|
||||
|
||||
-- hook to restore stream properties & target
|
||||
restore_stream_hook = SimpleEventHook {
|
||||
name = "node/restore-stream",
|
||||
interests = {
|
||||
-- match stream nodes
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "matches", "Stream/*" },
|
||||
},
|
||||
-- and device nodes that are not associated with any routes
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "matches", "Audio/*" },
|
||||
Constraint { "device.routes", "is-absent" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-added" },
|
||||
Constraint { "media.class", "matches", "Audio/*" },
|
||||
Constraint { "device.routes", "equals", "0" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local stream_props = node.properties
|
||||
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||
|
||||
local key = formKey (stream_props)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
|
||||
local stored_values = getStoredStreamProps (key) or {}
|
||||
|
||||
-- restore node Props (volumes, channelMap, etc...)
|
||||
if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
|
||||
then
|
||||
local props = {
|
||||
"Spa:Pod:Object:Param:Props", "Props",
|
||||
volume = stored_values.volume,
|
||||
mute = stored_values.mute,
|
||||
channelVolumes = stored_values.channelVolumes ~= nil and
|
||||
stored_values.channelVolumes or buildDefaultChannelVolumes (node),
|
||||
channelMap = stored_values.channelMap,
|
||||
}
|
||||
-- convert arrays to Spa Pod
|
||||
if props.channelVolumes then
|
||||
table.insert (props.channelVolumes, 1, "Spa:Float")
|
||||
props.channelVolumes = Pod.Array (props.channelVolumes)
|
||||
end
|
||||
if props.channelMap then
|
||||
table.insert (props.channelMap, 1, "Spa:Enum:AudioChannel")
|
||||
props.channelMap = Pod.Array (props.channelMap)
|
||||
end
|
||||
|
||||
if props.volume or (props.mute ~= nil) or props.channelVolumes or props.channelMap
|
||||
then
|
||||
log:info (node, "restore values from " .. key)
|
||||
|
||||
local param = Pod.Object (props)
|
||||
log:debug (param, "setting props on " .. tostring (stream_props ["node.name"]))
|
||||
node:set_param ("Props", param)
|
||||
end
|
||||
end
|
||||
|
||||
-- restore the node's link target on metadata
|
||||
if Settings.get_boolean ("node.stream.restore-target") and stream_props ["state.restore-target"] ~= "false"
|
||||
then
|
||||
if stored_values.target then
|
||||
-- check first if there is a defined target in the node's properties
|
||||
-- and skip restoring if this is the case (#335)
|
||||
local target_in_props =
|
||||
stream_props ["target.object"] or stream_props ["node.target"]
|
||||
|
||||
if not target_in_props then
|
||||
local source = event:get_source ()
|
||||
local nodes_om = source:call ("get-object-manager", "node")
|
||||
local metadata_om = source:call ("get-object-manager", "metadata")
|
||||
|
||||
local target_node = nodes_om:lookup {
|
||||
Constraint { "node.name", "=", stored_values.target, type = "pw" }
|
||||
}
|
||||
local metadata = metadata_om:lookup {
|
||||
Constraint { "metadata.name", "=", "default" }
|
||||
}
|
||||
|
||||
if target_node and metadata then
|
||||
metadata:set (node ["bound-id"], "target.object", "Spa:Id",
|
||||
target_node.properties ["object.serial"])
|
||||
end
|
||||
else
|
||||
log:debug (node,
|
||||
"Not restoring the target for " ..
|
||||
tostring (stream_props ["node.name"]) ..
|
||||
" because it is already set to " .. target_in_props)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
}
|
||||
|
||||
-- store stream properties on the state file
|
||||
store_stream_props_hook = SimpleEventHook {
|
||||
name = "node/store-stream-props",
|
||||
interests = {
|
||||
-- match stream nodes
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Props" },
|
||||
Constraint { "media.class", "matches", "Stream/*" },
|
||||
},
|
||||
-- and device nodes that are not associated with any routes
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Props" },
|
||||
Constraint { "media.class", "matches", "Audio/*" },
|
||||
Constraint { "device.routes", "is-absent" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-params-changed" },
|
||||
Constraint { "event.subject.param-id", "=", "Props" },
|
||||
Constraint { "media.class", "matches", "Audio/*" },
|
||||
Constraint { "device.routes", "equals", "0" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local stream_props = node.properties
|
||||
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||
|
||||
if Settings.get_boolean ("node.stream.restore-props") and stream_props ["state.restore-props"] ~= "false"
|
||||
then
|
||||
local key = formKey (stream_props)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
|
||||
local stored_values = getStoredStreamProps (key) or {}
|
||||
local hasChanges = false
|
||||
|
||||
log:info (node, "saving stream props for " ..
|
||||
tostring (stream_props ["node.name"]))
|
||||
|
||||
for p in node:iterate_params ("Props") do
|
||||
local props = cutils.parseParam (p, "Props")
|
||||
if not props then
|
||||
goto skip_prop
|
||||
end
|
||||
|
||||
if props.volume ~= stored_values.volume then
|
||||
stored_values.volume = props.volume
|
||||
hasChanges = true
|
||||
end
|
||||
if props.mute ~= stored_values.mute then
|
||||
stored_values.mute = props.mute
|
||||
hasChanges = true
|
||||
end
|
||||
if props.channelVolumes then
|
||||
stored_values.channelVolumes = props.channelVolumes
|
||||
hasChanges = true
|
||||
end
|
||||
if props.channelMap then
|
||||
stored_values.channelMap = props.channelMap
|
||||
hasChanges = true
|
||||
end
|
||||
|
||||
::skip_prop::
|
||||
end
|
||||
|
||||
if hasChanges then
|
||||
saveStreamProps (key, stored_values)
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
-- save "target.node"/"target.object" on metadata changes
|
||||
store_stream_target_hook = SimpleEventHook {
|
||||
name = "node/store-stream-target-metadata-changed",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "default" },
|
||||
Constraint { "event.subject.key", "c", "target.object", "target.node" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local source = event:get_source ()
|
||||
local nodes_om = source:call ("get-object-manager", "node")
|
||||
local props = event:get_properties ()
|
||||
local subject_id = props ["event.subject.id"]
|
||||
local target_key = props ["event.subject.key"]
|
||||
local target_value = props ["event.subject.value"]
|
||||
|
||||
local node = nodes_om:lookup {
|
||||
Constraint { "bound-id", "=", subject_id, type = "gobject" }
|
||||
}
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
|
||||
local stream_props = node.properties
|
||||
stream_props = JsonUtils.match_rules_update_properties (config.rules, stream_props)
|
||||
|
||||
if stream_props ["state.restore-target"] == "false" then
|
||||
return
|
||||
end
|
||||
|
||||
local key = formKey (stream_props)
|
||||
if not key then
|
||||
return
|
||||
end
|
||||
|
||||
local target_name = nil
|
||||
|
||||
if target_value and target_value ~= "-1" then
|
||||
local target_node
|
||||
if target_key == "target.object" then
|
||||
target_node = nodes_om:lookup {
|
||||
Constraint { "object.serial", "=", target_value, type = "pw-global" }
|
||||
}
|
||||
else
|
||||
target_node = nodes_om:lookup {
|
||||
Constraint { "bound-id", "=", target_value, type = "gobject" }
|
||||
}
|
||||
end
|
||||
if target_node then
|
||||
target_name = target_node.properties ["node.name"]
|
||||
end
|
||||
end
|
||||
|
||||
log:info (node, "saving stream target for " ..
|
||||
tostring (stream_props ["node.name"]) .. " -> " .. tostring (target_name))
|
||||
|
||||
local stored_values = getStoredStreamProps (key) or {}
|
||||
stored_values.target = target_name
|
||||
saveStreamProps (key, stored_values)
|
||||
end
|
||||
}
|
||||
|
||||
-- populate route-settings metadata
|
||||
function populateMetadata (metadata)
|
||||
-- copy state into the metadata
|
||||
local key = "Output/Audio:media.role:Notification"
|
||||
local p = getStoredStreamProps (key)
|
||||
if p then
|
||||
p.channels = p.channelMap and Json.Array (p.channelMap)
|
||||
p.volumes = p.channelVolumes and Json.Array (p.channelVolumes)
|
||||
p.channelMap = nil
|
||||
p.channelVolumes = nil
|
||||
p.target = nil
|
||||
|
||||
-- pipewire-pulse expects the key to be
|
||||
-- "restore.stream.Output/Audio.media.role:Notification"
|
||||
key = string.gsub (key, ":", ".", 1);
|
||||
metadata:set (0, "restore.stream." .. key, "Spa:String:JSON",
|
||||
Json.Object (p):to_string ())
|
||||
end
|
||||
end
|
||||
|
||||
-- track route-settings metadata changes
|
||||
route_settings_metadata_changed_hook = SimpleEventHook {
|
||||
name = "node/route-settings-metadata-changed",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "metadata-changed" },
|
||||
Constraint { "metadata.name", "=", "route-settings" },
|
||||
Constraint { "event.subject.key", "=",
|
||||
"restore.stream.Output/Audio.media.role:Notification" },
|
||||
Constraint { "event.subject.spa_type", "=", "Spa:String:JSON" },
|
||||
Constraint { "event.subject.value", "is-present" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local props = event:get_properties ()
|
||||
local subject_id = props ["event.subject.id"]
|
||||
local key = props ["event.subject.key"]
|
||||
local value = props ["event.subject.value"]
|
||||
|
||||
local json = Json.Raw (value)
|
||||
if json == nil or not json:is_object () then
|
||||
return
|
||||
end
|
||||
|
||||
local vparsed = json:parse ()
|
||||
|
||||
-- we store the key as "Output/Audio:media.role:Notification"
|
||||
local key = string.sub (key, string.len ("restore.stream.") + 1)
|
||||
key = string.gsub (key, "%.", ":", 1);
|
||||
|
||||
local stored_values = getStoredStreamProps (key) or {}
|
||||
|
||||
if vparsed.volume ~= nil then
|
||||
stored_values.volume = vparsed.volume
|
||||
end
|
||||
if vparsed.mute ~= nil then
|
||||
stored_values.mute = vparsed.mute
|
||||
end
|
||||
if vparsed.channels ~= nil then
|
||||
stored_values.channelMap = vparsed.channels
|
||||
end
|
||||
if vparsed.volumes ~= nil then
|
||||
stored_values.channelVolumes = vparsed.volumes
|
||||
end
|
||||
saveStreamProps (key, stored_values)
|
||||
end
|
||||
}
|
||||
|
||||
function buildDefaultChannelVolumes (node)
|
||||
local node_props = node.properties
|
||||
local direction = cutils.mediaClassToDirection (node_props ["media.class"] or "")
|
||||
local def_vol = 1.0
|
||||
local channels = 2
|
||||
local res = {}
|
||||
|
||||
local str = node.properties["state.default-volume"]
|
||||
if str ~= nil then
|
||||
def_vol = tonumber (str)
|
||||
elseif direction == "input" then
|
||||
def_vol = Settings.get_float ("node.stream.default-capture-volume")
|
||||
elseif direction == "output" then
|
||||
def_vol = Settings.get_float ("node.stream.default-playback-volume")
|
||||
end
|
||||
|
||||
for pod in node:iterate_params("Format") do
|
||||
local pod_parsed = pod:parse()
|
||||
if pod_parsed ~= nil then
|
||||
channels = pod_parsed.properties.channels
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
log:info (node, "using default volume: " .. tostring(def_vol) ..
|
||||
", channels: " .. tostring(channels))
|
||||
|
||||
while (#res < channels) do
|
||||
table.insert(res, def_vol)
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
function getStoredStreamProps (key)
|
||||
local value = state_table [key]
|
||||
if not value then
|
||||
return nil
|
||||
end
|
||||
|
||||
local json = Json.Raw (value)
|
||||
if not json or not json:is_object () then
|
||||
return nil
|
||||
end
|
||||
|
||||
return json:parse ()
|
||||
end
|
||||
|
||||
function saveStreamProps (key, p)
|
||||
assert (type (p) == "table")
|
||||
|
||||
p.channelMap = p.channelMap and Json.Array (p.channelMap)
|
||||
p.channelVolumes = p.channelVolumes and Json.Array (p.channelVolumes)
|
||||
|
||||
state_table [key] = Json.Object (p):to_string ()
|
||||
state:save_after_timeout (state_table)
|
||||
end
|
||||
|
||||
function formKey (properties)
|
||||
local keys = {
|
||||
"media.role",
|
||||
"application.id",
|
||||
"application.name",
|
||||
"media.name",
|
||||
"node.name",
|
||||
}
|
||||
local key_base = nil
|
||||
|
||||
for _, k in ipairs (keys) do
|
||||
local p = properties [k]
|
||||
if p then
|
||||
key_base = string.format ("%s:%s:%s",
|
||||
properties ["media.class"]:gsub ("^Stream/", ""), k, p)
|
||||
break
|
||||
end
|
||||
end
|
||||
return key_base
|
||||
end
|
||||
|
||||
function toggleState (enable)
|
||||
if enable and not state then
|
||||
state = State ("stream-properties")
|
||||
state_table = state:load ()
|
||||
|
||||
restore_stream_hook:register ()
|
||||
store_stream_props_hook:register ()
|
||||
store_stream_target_hook:register ()
|
||||
route_settings_metadata_changed_hook:register ()
|
||||
|
||||
rs_metadata = ImplMetadata ("route-settings")
|
||||
rs_metadata:activate (Features.ALL, function (m, e)
|
||||
if e then
|
||||
log:warning ("failed to activate route-settings metadata: " .. tostring (e))
|
||||
else
|
||||
populateMetadata (m)
|
||||
end
|
||||
end)
|
||||
|
||||
elseif not enable and state then
|
||||
state = nil
|
||||
state_table = nil
|
||||
restore_stream_hook:remove ()
|
||||
store_stream_props_hook:remove ()
|
||||
store_stream_target_hook:remove ()
|
||||
route_settings_metadata_changed_hook:remove ()
|
||||
rs_metadata = nil
|
||||
end
|
||||
end
|
||||
|
||||
Settings.subscribe ("node.stream.restore-props", function ()
|
||||
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||
Settings.get_boolean ("node.stream.restore-target"))
|
||||
end)
|
||||
|
||||
Settings.subscribe ("node.stream.restore-target", function ()
|
||||
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||
Settings.get_boolean ("node.stream.restore-target"))
|
||||
end)
|
||||
|
||||
toggleState (Settings.get_boolean ("node.stream.restore-props") or
|
||||
Settings.get_boolean ("node.stream.restore-target"))
|
||||
65
pipewire/.config/wireplumber/scripts/node/suspend-node.lua
Normal file
65
pipewire/.config/wireplumber/scripts/node/suspend-node.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- WirePlumber
|
||||
--
|
||||
-- Copyright © 2021 Collabora Ltd.
|
||||
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
||||
--
|
||||
-- SPDX-License-Identifier: MIT
|
||||
|
||||
log = Log.open_topic ("s-node")
|
||||
|
||||
sources = {}
|
||||
|
||||
SimpleEventHook {
|
||||
name = "node/suspend-node",
|
||||
interests = {
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "matches", "Audio/*" },
|
||||
},
|
||||
EventInterest {
|
||||
Constraint { "event.type", "=", "node-state-changed" },
|
||||
Constraint { "media.class", "matches", "Video/*" },
|
||||
},
|
||||
},
|
||||
execute = function (event)
|
||||
local node = event:get_subject ()
|
||||
local new_state = event:get_properties ()["event.subject.new-state"]
|
||||
|
||||
log:debug (node, "changed state to " .. new_state)
|
||||
|
||||
-- Always clear the current source if any
|
||||
local id = node["bound-id"]
|
||||
if sources[id] then
|
||||
sources[id]:destroy()
|
||||
sources[id] = nil
|
||||
end
|
||||
|
||||
-- Add a timeout source if idle for at least 5 seconds
|
||||
if new_state == "idle" or new_state == "error" then
|
||||
-- honor "session.suspend-timeout-seconds" if specified
|
||||
local timeout =
|
||||
tonumber(node.properties["session.suspend-timeout-seconds"]) or 5
|
||||
|
||||
if timeout == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- add idle timeout; multiply by 1000, timeout_add() expects ms
|
||||
sources[id] = Core.timeout_add(timeout * 1000, function()
|
||||
-- Suspend the node
|
||||
-- but check first if the node still exists
|
||||
if (node:get_active_features() & Feature.Proxy.BOUND) ~= 0 then
|
||||
log:info(node, "was idle for a while; suspending ...")
|
||||
node:send_command("Suspend")
|
||||
end
|
||||
|
||||
-- Unref the source
|
||||
sources[id] = nil
|
||||
|
||||
-- false (== G_SOURCE_REMOVE) destroys the source so that this
|
||||
-- function does not get fired again after 5 seconds
|
||||
return false
|
||||
end)
|
||||
end
|
||||
end
|
||||
}:register ()
|
||||
Reference in New Issue
Block a user