Custom Scripts
The locations where WirePlumber searches for scripts is explained in Location of scripts.
Scripts are not loaded automatically; a component must be defined for them, and this component must be included in a profile. See Components & Profiles.
Example 1: a standalone script
Let's assume that ~/.local/share/wireplumber/scripts/90-hello-world.lua
contains the following script:
log = Log.open_topic ("hello-world")
log.info ("Hello world")
In order for it to run, we'll define a component and include it in the default
profile by including the following configuration (for example, in
~/.config/wireplumber/wireplumber.conf.d/90-hello-world.conf):
wireplumber.components = [
{
name = "90-hello-world.lua", type = script/lua
provides = hello-world
}
]
wireplumber.profiles = {
main = {
hello-world = required
}
}
Example 2: an event hook
A script that only runs once at startup is of limited use. Most of the logic in WirePlumber is written as event hooks, which run every time something happens in the graph. See Events and Hooks for the concepts and Events & Hooks for the API.
~/.local/share/wireplumber/scripts/90-log-new-nodes.lua:
log = Log.open_topic ("s-log-new-nodes")
SimpleEventHook {
name = "example/log-new-nodes",
interests = {
EventInterest {
Constraint { "event.type", "=", "node-added" },
Constraint { "media.class", "#", "Stream/*" },
},
},
execute = function (event)
local node = event:get_subject ()
log:info (node, "new stream: " ..
tostring (node.properties ["node.name"]))
end
}:register ()
~/.config/wireplumber/wireplumber.conf.d/90-log-new-nodes.conf:
wireplumber.components = [
{
name = "90-log-new-nodes.lua", type = script/lua
provides = hooks.example.log-new-nodes
}
]
wireplumber.profiles = {
main = {
hooks.example.log-new-nodes = required
}
}
Note
The wireplumber.components.rules in the default configuration
automatically add requires = [ support.lua-scripting ] to every
script/lua component, and make components whose provides matches
hooks.* load before the standard event source (the monitor scripts are
excluded and load after it instead). Naming your feature hooks.*
therefore ensures the hook is registered before any events are dispatched.
Example 3: replacing a shipped hook
Registering a hook with the same name as an existing one does not replace it; both will run. To replace one of WirePlumber's own hooks, disable the feature that provides it in your profile and provide your own component instead.
For instance, linking/find-default-target.lua provides the feature
hooks.linking.target.find-default. To substitute your own target selection
logic:
wireplumber.components = [
{
name = "90-my-find-target.lua", type = script/lua
provides = hooks.example.find-target
}
]
wireplumber.profiles = {
main = {
hooks.linking.target.find-default = disabled
hooks.example.find-target = required
}
}
To add to the selection chain rather than replace part of it, leave the stock
hooks enabled and order your own hook against them with before and
after; see Example 4: participating in target selection below for a
worked example, and Existing Scripts for the list of hooks you
can order against.
Example 4: participating in target selection
When a stream needs to be linked, WirePlumber pushes a select-target event
and a chain of hooks runs on it, each one trying to pick a target node for the
stream. Every hook in that chain bypasses itself if a target has already been
picked, so the first hook that finds a target wins; the hooks that follow
it merely translate and link it. This is why the hook below returns early when
target is already set: without that guard it would override a decision that
a higher-priority hook has already made.
To take part in the chain, register another hook on the select-target event
and order it relative to the stock hooks with before / after. The hook
below runs before linking/find-defined-target, which makes it the first
one to get a say, ahead of all of WirePlumber's own selection logic.
The linking-utils module provides
unwrap_select_target_event (), which unpacks the event into the objects the
hook needs: the source event, the object manager, the session item being linked
(si) along with its properties and flags, and the target picked so far (or
nil). The chosen target is handed to the rest of the chain by storing it on
the event with event:set_data ("target", target).
~/.local/share/wireplumber/scripts/90-find-user-target.lua:
lutils = require ("linking-utils")
log = Log.open_topic ("s-linking")
SimpleEventHook {
name = "linking/find-user-target",
before = "linking/find-defined-target",
interests = {
EventInterest {
Constraint { "event.type", "=", "select-target" },
},
},
execute = function (event)
local source, om, si, si_props, si_flags, target =
lutils:unwrap_select_target_event (event)
-- bypass the hook if the target is already picked up
if target then
return
end
log:info (si, "in find-user-target")
-- implement logic here to find a suitable target
-- store the found target on the event,
-- the next hooks will take care of linking
event:set_data ("target", target)
end
}:register ()
~/.config/wireplumber/wireplumber.conf.d/90-find-user-target.conf:
wireplumber.components = [
{
name = "90-find-user-target.lua", type = script/lua
provides = hooks.example.find-user-target
}
]
wireplumber.profiles = {
main = {
hooks.example.find-user-target = required
}
}
As noted in Example 2 above, naming the feature hooks.* ensures that the
component is loaded, and the hook registered, before any events are dispatched.
The stock hook that this one orders itself against must of course be enabled
for the before key to have an effect; linking/find-defined-target is
provided by hooks.linking.target.find-defined, which is part of the default
profile. See Existing Scripts for the other hooks in the
chain.
Example 5: reading configuration and settings
Scripts read static configuration with Conf and runtime settings with Settings. Static configuration is read once at startup; settings can change while WirePlumber is running.
-- static configuration, from a section in wireplumber.conf or a fragment
config = {}
config.rules = Conf.get_section_as_json ("example.rules", Json.Array {})
-- a runtime setting, declared in wireplumber.settings.schema
local enabled = Settings.get_boolean ("example.enabled")
Settings.subscribe ("example.enabled", function ()
enabled = Settings.get_boolean ("example.enabled")
end)
Debugging your script
Give your script its own log topic with Log.open_topic () and use the
s- prefix, matching the convention used by the shipped scripts. You can
then enable just your messages:
$ WIREPLUMBER_DEBUG=s-log-new-nodes:D wireplumber
Useful things to know while developing a script:
wpctl statusshows the current graph, which is the state your hooks are reacting to.Debug.dump_table ()pretty-prints a Lua table to the log.A script that does not need to run inside the daemon can be executed standalone against a running PipeWire with
wpexec, which is much faster to iterate on. See Testing for examples.If your hook does not seem to run, check that its component is actually loaded — a profile entry naming a feature that does not exist is silently ignored. Raising the log level to
Dshows which components are loaded.
See also Events & Hooks for the full hook API and Existing Scripts for the hooks that ship with WirePlumber.