Before you start#
Read PipeWire core and objects and graph scheduling and buffers. This chapter studies WirePlumber 0.5.5 and PipeWire 1.2.0 as separately pinned historical sources, not as a tested compatibility matrix. Its policy exercise is a private model and never queries or modifies a running audio graph.
Learning goals#
Separate WirePlumber policy from PipeWire's graph engine. Follow a core connection, object-manager discovery and a default-node policy hook, then reason about hotplug, permissions, metadata and stale identities. Explain the distinct roles of 0.5 configuration fragments and Lua policy scripts.
Source walkthrough#
- Object manager
Tracks permitted matching objects
- Policy hook
Evaluates available nodes and priorities
- Metadata and links
Publish decisions and establish permitted routing
- Removal
Drop stale references and reevaluate
PipeWire supplies a multimedia graph, protocol and processing machinery. WirePlumber is a session and policy manager that observes objects and applies configured decisions, such as which nodes should be considered defaults or how eligible streams should be linked. The distinction is useful because a graph can exist without one particular policy manager, and the same engine can be used in deployments with different policy. A node becoming visible does not automatically mean that it is selected as a default, linked to a stream, permitted to a client or actually producing sound.
A WpCore wraps a connection and local framework state for WirePlumber components. WpObjectManager tracks objects matching configured interests and emits signals when its observed set changes. Interests can match interface types and properties; the manager's observed objects and proxies have lifetimes governed by the connection and its own references. A local proxy represents a server object; it does not keep the physical device present. A session item is a higher-level policy object that can model a usable endpoint or relationship, with its own activation and teardown lifecycle. See WirePlumber 0.5.5 lib/wp/core.c and object manager.
The manager's GLib-based control context dispatches events, signals and asynchronous operations. Lua scripts can register policy hooks that run in response to appropriate events; they are not real-time audio process callbacks. Reference-counted objects and signal connections must remain valid until callbacks finish, and a policy component must disconnect or release them when the connection or component is torn down. Internal locking and a reference count do not make arbitrary shared state safe if a component introduces additional threads. A callback can observe that an object was removed after an earlier discovery event, so it must revalidate the objects it uses.
Connect, observe, disconnect
From WirePlumber 0.5.5 lib/wp/core.c, wp_core_connect, MIT SPDX file license; property conversion and subsequent listener setup are omitted:
self->pw_core = pw_context_connect (self->pw_context, p, 0);
if (!self->pw_core)
return FALSE;
The first statement attempts to create the underlying PipeWire core connection and stores the result. The condition returns false if no connection was created. The surrounding function avoids reconnecting an already connected core, adds core and proxy listeners on success, and attaches its registry. A caller must handle the false result without pretending that an object manager has discovered a valid graph. Later disconnects are separate events: wp_core_disconnect detaches the registry and asks the underlying core to disconnect, whose destruction path continues cleanup. Local objects created through the registry must not be treated as live remote objects after that transition.
In lib/wp/core.c, WirePlumber 0.5.5, MIT SPDX (surrounding code omitted):
static void
core_error (void *data, uint32_t id, int seq, int res, const char *message)
{
WpCore *self = WP_CORE (data);
/* protocol socket disconnected; schedule disconnecting our core */
if (id == 0 && res == -EPIPE) {
wp_core_idle_add_closure (self, NULL, g_cclosure_new_object (
G_CALLBACK (core_disconnect_async), G_OBJECT (self)));
}
}
A protocol-socket EPIPE on the core object becomes a scheduled disconnect instead of teardown inside the error callback. Other errors are not handled in this function. The excerpt does not show what the disconnect releases, so it does not establish a complete recovery policy.
An object manager's interest is a filter over what its connection can see. Absence from its observed set can mean the object is absent, does not match the interest, is not yet advertised, or is hidden by permissions. A policy script should not equate an empty set with proof that a physical sound device does not exist. When the registry reports removal, the manager updates its set and signals observers. Observers must release references or transition their session items and links appropriately. A numeric global ID can be reused after removal or reconnect, so identity includes connection and lifetime, not just a remembered integer.
In lib/wp/object-manager.c, WirePlumber 0.5.5, MIT SPDX (surrounding code omitted):
void
wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
{
guint index;
if (g_ptr_array_find (self->objects, object, &index)) {
g_ptr_array_remove_index_fast (self->objects, index);
g_signal_emit (self, signals[SIGNAL_OBJECT_REMOVED], 0, object);
self->changed = TRUE;
}
}
Removal does nothing when the object is not in the manager's observed set. When it is, the object leaves the set, object-removed is emitted, and changed is set. The comment above the function says the caller must also run the change-notification step afterwards. The excerpt does not show who else holds references to the removed object.
Configuration and scripts in 0.5
WirePlumber 0.5 uses SPA-JSON configuration files and fragments for configuration. They describe components, profiles and settings under the version's documented rules. Lua remains relevant for policy scripts and hooks; saying that 0.5 uses SPA-JSON configuration does not mean the project contains no Lua. Conversely, copying a 0.4-era Lua configuration file into a 0.5 deployment is not automatically a valid migration. Inspect the pinned 0.5.5 configuration and the pinned policy scripts, and consult the installed version's documentation before changing a real system. This lesson does not install or alter any configuration.
A script registers an interest in an event and runs a callback when the policy engine dispatches that event. It can read event data, compare candidates and write a result for later hooks. Hook ordering and other scripts can affect the final policy. The one script shown below is evidence of its specific candidate-selection step, not proof of the entire default-node algorithm or every deployment's configuration. A later hook can use stored preferences or other metadata, and a different installation can enable a different policy profile.
From WirePlumber 0.5.5 src/scripts/default-nodes/find-best-default-node.lua, MIT SPDX file license; iteration setup and result storage are omitted:
if priority > selected_prio or selected_node == nil then
selected_prio = priority
selected_node = node_props ["node.name"]
end
The condition accepts a candidate when its priority exceeds the currently selected priority or when no candidate has been selected. The next two lines store the candidate's priority and node name. Equal priority does not replace an already selected candidate in this particular loop; ordering and other hooks can therefore matter. The surrounding script parses the available-node data and returns without selecting when it cannot obtain that list. It later writes the selected name and priority back to the event. It does not itself prove that a node is linked, audible or authorized for every client.
Devices, monitors and routes
A monitor component discovers a class of resources and creates or exposes relevant PipeWire objects. ALSA monitoring can use card profiles, routes and UCM-derived information where configured. Bluetooth monitoring can use BlueZ and its available profiles, subject to device, service and permission state. These monitors are different from the application-facing stream itself. A card can expose multiple profiles, and a profile can expose different ports or routes. Selecting a profile or route can change which nodes are available and can trigger removal and addition events. Neither a detected card nor a visible node proves that a physical output is connected or that it can handle every format.
Default-node policy can use metadata to publish a selected target. Metadata is a set of keyed values associated with a subject under the relevant interface; it is not itself an audio buffer or a permanent ownership reference to the node. A client or policy component can observe a default value changing. Persistence mechanisms may remember user preferences across runs, subject to the enabled policy and storage. A remembered name can be absent at startup, refer to a different object after configuration changes, or be inaccessible to a particular client. A robust policy considers current availability and permission rather than assuming stored state is always valid.
Linking policy matches an application's stream and target ports, considering direction, media class, role, format and other properties. The session manager decides which clients should access resources, using PipeWire’s basic resource-permission mechanism; a create or bind request can therefore be denied even if a policy model chooses a target. See PipeWire session-manager requirements. A link can fail negotiation, a port can disappear before activation, or a stream can exit while a link is being created. The policy must handle these events and remove or release its session items, listeners and proxy references. A target name is a policy hint or identity within a particular scope; it is not a capability that bypasses server authorization.
A three-node policy example
The following values are synthetic. The available output nodes are desk with priority 100, headset with priority 200, and virtual with priority 50. With no prior selection, the quoted loop first selects desk, replaces it with headset, and keeps headset when it sees virtual. It records name headset and priority 200 in the event. This demonstrates only the pinned hook's comparison; a complete WirePlumber policy can include other hooks and settings.
Now the headset disappears. The object manager receives a removal event, and a policy implementation must stop treating the removed proxy as a live target. Existing links involving the removed port can be removed or fail, and dependent policy state must be cleaned up. A new selection event with the two remaining candidates can choose desk under the same simplified priority rule. If a newly added object later receives the same numeric global ID as the old headset, it is still a new object with a new lifetime. A callback that cached only the old ID must not act on the replacement as though it were the old headset.
Synthetic teaching data, not a device capture.
- PipeWire coreWpCore
registry global for headset (connected core)
- WpCoreWpObjectManager
object matches the manager interest
- WpObjectManagerPolicy hook
selection event with all candidates
- Policy hookPolicy hook
keep higher priority; record node.name
- PipeWire coreWpObjectManager
global removed
- WpObjectManagerWpObjectManager
wp_object_manager_rm_object removes it
- WpObjectManagerPolicy hook
object-removed; selection runs again
- Policy hookPolicy hook
choose desk from remaining candidates
Time runs downward. Dashed arrows show returns.
Read the text version
- PipeWire core → WpCore: registry global for headset (connected core)
- WpCore → WpObjectManager: object matches the manager interest
- WpObjectManager → Policy hook: selection event with all candidates
- Policy hook → Policy hook: keep higher priority; record node.name
- PipeWire core → WpObjectManager: global removed
- WpObjectManager → WpObjectManager: wp_object_manager_rm_object removes it
- WpObjectManager → Policy hook: object-removed; selection runs again
- Policy hook → Policy hook: choose desk from remaining candidates
Suppose a client cannot create a link to desk because its permissions deny the operation. A default selection is not an authorization bypass; the create/bind request can fail and the client or policy component must report or handle that failure. If format negotiation also fails, an apparently selected and permitted target can still carry no media. A model that prints the highest-priority name has not tested device routing, permissions, link activation or sound output.
State, lifetime and race handling
A policy component often holds a reference to a proxy while an asynchronous operation is pending. When a removal signal arrives, the reference can keep the local wrapper allocated, but it does not resurrect the remote resource. A completion callback can arrive after the component has begun shutdown or after a newer selection replaced the old one. Attach a generation or other operation identity to pending work, check whether it still applies, and release returned resources even when ignoring a stale completion. The exact API calls and cancellation semantics depend on the component; the principle is to make each owned reference and terminal path explicit.
A name stored in metadata is not the same as a proxy reference. A local proxy is not the same as the server resource. A server resource is not the same as a physical device. Each layer can disappear independently: a headset can disconnect, a server can restart, a policy process can reconnect, or a stored preference can remain after all of them. Reconnecting requires a fresh registry view and fresh proxies. Reusing old IDs or callbacks across that boundary can connect a stream to the wrong target or dereference a stale object.
Signal handlers and hook closures also own or borrow state. A component that stores callbacks referencing itself should arrange for disconnection or appropriate weak references during teardown. An object manager can emit changes while a policy decision is being assembled, so a selection based on a snapshot must be revalidated before an operation that requires the chosen object. The event loop serializes some callbacks in its context, but another process can change the graph between a local decision and a server request. The server's result and permission checks remain authoritative.
Policy is not a measured guarantee
A default node can change because of hotplug, profile changes, user preference or policy configuration. Applications can explicitly target a node, follow defaults, or be constrained by a sandbox and server permissions. A client that caches a default indefinitely may not follow later changes; a client that blindly switches on every transient event may interrupt playback. The desired behavior is application policy and user expectation, not something that can be inferred from a single registry event.
The relative priority used by the quoted script is a policy input. It is not signal quality, physical distance, a latency measurement or a security ranking. A node with a high priority can fail to start or lack a compatible format. A real investigation would need authorized observation of the relevant configuration, policy events, link state and permissions. This lesson deliberately uses invented inputs so it does not reveal other users' audio clients or change a default sink.
- Candidate comparison
- In the quoted loop a higher priority replaces the selection; equal priority does not.
- Node name record
- The hook writes the chosen node.name and priority back to the event.
- Link creation
- Ports, direction, role and format must match and negotiate; the create request can still fail.
- Permission and output
- Server permissions, active state and a device path decide whether sound flows.
Policy and graph state can change while an asynchronous operation is pending. A device disappearing after selection can make a previously suitable target unavailable before a link is created. The session manager must observe the actual completion and remove or revise stale references; a preference stored in policy is not proof of a working audio path. Record the relevant object lifetime and failure result when testing a particular release.
Practice#
This Python model copies the comparison in the quoted hook for three invented candidates. It does not run Lua, contact PipeWire, inspect metadata or change policy:
python3 - <<'PY'
candidates = [('desk', 100), ('headset', 200), ('virtual', 50)]
selected, priority = None, 0
for name, p in candidates:
if p > priority or selected is None:
selected, priority = name, p
print('selected:', selected, priority)
candidates = [item for item in candidates if item[0] != 'headset']
selected, priority = None, 0
for name, p in candidates:
if p > priority or selected is None:
selected, priority = name, p
print('after removal:', selected, priority)
PY
Expected output selects headset 200, then desk 100. A counterexample is a denied link request: the model still prints a name but has not established permission or a working link. As a source-only alternative, use the three-node table above, track the selected name and priority after each iteration, and identify which references and links must be reconsidered after removal. Deliver the state table and an authorization-denied cleanup path.
Knowledge check#
- What does
wp_core_connectreturn when the underlying connection is not created? - Does a highest-priority candidate automatically become an audible output?
- What does the shown Lua hook do when a candidate has the same priority as an existing selection?
- Why must a removed global ID not be treated as a permanent hardware identity?
- Does WirePlumber 0.5's SPA-JSON configuration mean that it has no Lua policy scripts?
Answers#
- The shown function returns false and does not proceed to install listeners and attach the registry on that path.
- No. Other policy, permission, link, format and device conditions must succeed before media can flow.
- The strict greater-than comparison does not replace a non-null existing selection for equal priority in that loop.
- IDs are scoped to a connection and object lifetime; a later object can reuse the number while the old proxy is no longer valid.
- No. Configuration fragments use SPA-JSON while Lua policy scripts and hooks can still exist in 0.5.
Checkpoint#
Submit the candidate-iteration table, removal/reselection sequence and denied-link path. Pass when the script's limited comparison is separated from full routing and each stale proxy or callback has a cleanup owner. If not, reread the two pinned source excerpts and rerun the private model. This is practice, not certification.
Sources#
Read WirePlumber core, object manager, default-node hook, and 0.5 configuration. The model does not establish behavior of an installed session manager.