Files
wireplumber/src/scripts/node/state-stream.lua
George Kiagiadakis 3b0c0fcd7e state-stream: fix storing/restoring notification volume
We apparently store things in the <media-class>:<key>:<value>
format, while pipewire-pulse expects <media-class>.<key>:<value>
and this detail was missed in the latest refactoring.

It also seems that I forgot to ensure that restoring this metadata
key worked. The hook was there, but not registered.
Remove the hook and directly populate the metadata object when
it is activated, as it seems simpler.

Fixes: #604
2024-03-30 11:53:45 +02:00

453 lines
14 KiB
Lua

-- 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)
if not stored_values then
return
end
-- 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
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"))