Moving files:
This commit is contained in:
452
wireplumber/scripts/node/state-stream.lua
Normal file
452
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"))
|
||||
Reference in New Issue
Block a user