We're hiring!

WirePlumber: Exploring Lua scripts with Event Dispatcher

Ashok Sidipotu avatar

Ashok Sidipotu
October 30, 2023

Share this post:

Reading time:

This is the third blog post in a series about the upcoming release of WirePlumber 0.5. The first two posts discussed the configuration system refactoring and presented the new Event Dispatcher. This latest entry explores how WirePlumber's Lua scripts are transformed with Event Dispatcher.

WirePlumber lua scripts are transformed with Event Dispatcher, they look and feel completely different. They are much more modular and extensible with very little redundant processing and almost no hacks. Traditionally they used an object manager API and for the reasons explained in the previous blog post, we have moved them to Event Dispatcher. Now they are a bunch of hooks responding to appropriate events.

For all the WirePlumber's system scripts (policy-node.lua, default-nodes, restore-stream, monitor/alsa.lua, etc) you can find their .c counterparts in the PipeWire Media Session project. During the initial days of WirePlumber we translated this C logic into Lua; of course with WirePlumber elements like Object Manager. But with the advent of Event Dispatcher, they are not what they were. Further in this post, I will break down what has happened to them.

Linking scripts are by far the most complex of the WirePlumber system scripts and so I will focus on how they changed over. I will also touch upon the rest of the scripts.

WirePlumber linking scripts


policy-node.lua is the main linking script. It is like the conductor of the whole pipewire symphony and so living up to the complexity of the task, it achieves so much out of one single lua source file. Let me try to sum up its functionality as briefly as I can.

It looks for new streams started, new devices added, and changes in user preferences and rescans the graph, i.e. it scans through all the linkable nodes and links them so that the media starts flowing through the pipeline(rather PipeWire :)).


The whole of this complexity is addressed out of one single file i.e. policy-node.lua and in that file mainly around one single function i.e. handleLinkable. Let me try to outline what this function does.

Not Modular

handleLinkable tries to find a target node for a given stream node(for example pw-play/pw-cat client nodes); for that it first looks for user-preferred targets followed by default targets. If none of them are available, it then looks for the best possible target out of the available targets. Lastly it prepares and links them, so the code is simply not modular.

Redundant Processing

To top it off, there are many hacks and quite a lot of redundant processing. All the WirePlumber Modules and Scripts register for Object Manager Callbacks. Sometimes many of them register for the same callback(for example node-added, default-nodes-changed). Since there is no way to control the order of execution of these callbacks, Modules and Scripts are forced to add redundant checks and processing. As the linking script deals with many generic Object Managers of this kind, it registers so many callbacks and so there are many of these redundant checks and processing, for example the scheduleRescan() is called many times and from many different parts of the code.

This is the not-so-nice part of WirePlumber, coming as I did from my previous company the WirePlumber/PipeWire code & design looked pretty clean, but the way rescan() is handled was difficult to swallow for me. It gets called for way too many events and most of the time the function would return empty-handed. By that, I mean without processing or worse, halfway through the processing, as some condition is not met or some new callback has come that requires abandoning the current pass. Often, it would queue a rescan by itself and exit, which is a classic sign of mediocre design, and so it felt all the more fulfilling and redeeming when we got a chance to clean up the mess with ourselves.


Sometimes the linking script changes the properties/state of the Pipewire object in the Object Manager callbacks and some other scripts might be dependent on the state of the same object, in this way the code is prone to race conditions like this. The way out of these situations is hacks.

Design with Event Dispatcher

Rescan runs last

As I poured out in redundant processing, the rescan event kicking in *at the drop of a hat* is the crux of the problem. With the help of Event Dispatcher, rescan is now converted into an event and is accorded the lowest priority and so it gets a chance only after all the dust settles, meaning after all the higher priority events are processed. I would welcome you to take a look at this video from the previous blog, to get a clear idea of how this is achieved.

So the fact that linking happens towards the end, obviated most of the redundant processing and almost all the hacks simply vanished(Abracadabra :)).


Like small children, we took pleasure in breaking up the policy-node.lua monolith into multiple pieces. Let me briefly outline how we achieved this.

rescan.lua registers a hook for a rescan event, which runs at the lowest priority and raises a select-target event for that particular source stream node(for example the pw-play/pw-cat client node). Please note that the rescan event is only picked up at the end of processing of higher priority events like node-added/removed, etc. If this is confusing you, I would again point you to the video to connect the dots.

select-event traverses through find* hooks, which help pick the target, the picked target is prepared(prepare-link.lua) and eventually linked(link-target.lua). There are three find hooks, the first one(find-defined-target.lua) looks for defined targets, if there are none defined, the second hook comes into the picture and checks for defined targets(find-default-target.lua). If the target is still not found, it picks the best target with the help of the third hook(find-best-target.lua)

All the hooks run in the aforementioned order, this is made possible with the prioritization mechanism built into them. Hooks are defined with before and after tags(inspired by systemd). For a more detailed dissection of a hook, please take a look at our previous blog post(look for "An example hook" section)


Users can override the default handling of WirePlumber by selecting the right event and registering a user-defined hook with the right priority against this event. Just add this new hook in a separate Lua source file, then add an entry in the wireplumber.conf. And boom! Your hook gets executed, without changing even a single line of upstream code.

For example, find-user-target.lua.example is an example hook that demonstrates how to add a custom way to pick a target for linking. This is a hook registered against the select-target event and it will be the first hook to run for this event.

Remaining system scripts

Applying similar reasoning, the remaining system scripts, like the default nodes module, device profile selecting script, and device routes selecting script, are also logically broken down into rescanning, finding, and applying.

WirePlumber user scripts

WirePlumber user scripts are small to fairly large snippets of Lua code written mostly with object manager, here are a bunch of examples. If you haven't tried one, you should give it a shot, it's a very simple way of harnessing the power of PipeWire through the WirePlumber Lua API.

These scripts are simply run with wpexec. Another method is to copy them to the src/scripts folder and add an entry in the wireplumber.conf. With the wpexec it runs as a separate process and in the later method it runs as a part of the WirePlumber daemon.

Now, for this type of use case, we recommend sticking with object manager. However, if the user is interested in influencing the WirePlumber daemon logic, for example linking, default nodes, profile, routes, etc., we invite him/her to do it via events and hooks. Thanks to Event Dispatcher, WirePlumber can be overridden or extended with ease.

Comments (0)

Add a Comment

Allowed tags: <b><i><br>Add a new comment:

Search the newsroom

Latest Blog Posts

Building a Board Farm for Embedded World


With each board running a mainline-first Linux software stack and tested in a CI loop with the LAVA test framework, the Farm showcased Collabora's…

Smart audio filters with WirePlumber 0.5


WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…

The latest on cmtp-responder, a permissively-licensed MTP responder implementation


Part 3 of the cmtp-responder series with a focus on USB gadgets explores several new elements including a unified build environment with…

A roadmap for VirtIO Video on ChromeOS: part 3


The final installment of a series explaining how Collabora is helping shape the video virtualization story for Chromebooks with a focus…

Hacking on the PipeWire GStreamer elements


Last week I attended the GStreamer spring hackfest in Thessaloniki to work on the PipeWire GStreamer elements and connect with the community.

Transforming speech technology with WhisperLive


The world of AI has made leaps and bounds from what It once was, but there are still some adjustments required for the optimal outcome.…

Open Since 2005 logo

Our website only uses a strictly necessary session cookie provided by our CMS system. To find out more please follow this link.

Collabora Limited © 2005-2024. All rights reserved. Privacy Notice. Sitemap.