Crate pipewire

Source
Expand description

§Rust bindings for pipewire

pipewire is a crate offering Rust bindings to libpipewire, the library for interacting with the pipewire server.

Programs that interact with pipewire usually react to events from the server by registering callbacks and invoke methods on objects on the server by calling methods on local proxy objects.

§Getting started

Most programs that interact with pipewire will need the same few basic objects:

  • A MainLoop that drives the program, reacting to any incoming events and dispatching method calls. Most of a time, the program/thread will sit idle in this loop, waiting on events to occur.
  • A Context that keeps track of any pipewire resources.
  • A Core that is a proxy for the remote pipewire instance, used to send messages to and receive events from the remote server.
  • Optionally, a Registry that can be used to manage and track available objects on the server.

This is how they can be created:

use pipewire::{main_loop::MainLoopBox, context::ContextBox};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mainloop = MainLoopBox::new(None)?;
    let context = ContextBox::new(&mainloop.loop_(), None)?;
    let core = context.connect(None)?;
    let registry = core.get_registry()?;

    Ok(())
}

§Smart pointers to PipeWire objects

The example above uses std::boxed::Box-like smart pointers to create the needed objects. Those boxes use lifetimes to ensure the objects dependencies (e.g. Core depends on MainLoop) outlive the object itself. If more flexibility is needed, std::rc::Rc-like reference-counting smart pointers also exist. Those will automatically keep the objects dependencies alive until the object is destroyed.

Both of these kinds of types will automatically dereference to non-owning references for shared functionality, e.g. &MainLoop or &Core.

The same example as above, but using Rc types:

use pipewire::{main_loop::MainLoopRc, context::ContextRc};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mainloop = MainLoopRc::new(None)?;
    let context = ContextRc::new(&mainloop, None)?;
    let core = context.connect_rc(None)?;
    let registry = core.get_registry_rc()?;

    Ok(())
}

§Listening for events

Once the needed objects are created, you can start hooking up different kinds of callbacks to them to react to events, and call methods to change the state of the remote.

use pipewire::{main_loop::MainLoopBox, context::ContextBox};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mainloop = MainLoopBox::new(None)?;
    let context = ContextBox::new(&mainloop.loop_(), None)?;
    let core = context.connect(None)?;
    let registry = core.get_registry()?;

    // Register a callback to the `global` event on the registry, which notifies of any new global objects
    // appearing on the remote.
    // The callback will only get called as long as we keep the returned listener alive.
    let _listener = registry
        .add_listener_local()
        .global(|global| println!("New global: {:?}", global))
        .register();

    // Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
    // We don't have a specific object to destroy now, so this is commented out.
    // registry.destroy_global(313).into_result()?;

    mainloop.run();

    Ok(())
}

Note that registering any callback requires the closure to have the 'static lifetime, so if you need to capture any variables, use move || closures, and use std::rc::Rcs to access shared variables and some std::cell variant if you need to mutate them.

Also note that we called mainloop.run() at the end. This will enter the loop, and won’t return until we call mainloop.quit() from some event. If we didn’t run the loop, events and method invocations would not be processed, so the program would terminate without doing much.

§The main loop

Sometimes, other stuff needs to be done even though we are waiting inside the main loop.
This can be done by adding sources to the loop.

For example, we can call a function on an interval:

use pipewire::main_loop::MainLoopBox;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mainloop = MainLoopBox::new(None)?;

    let timer = mainloop.loop_().add_timer(|_| println!("Hello"));
    // Call the first time in half a second, and then in a one second interval.
    timer.update_timer(Some(Duration::from_millis(500)), Some(Duration::from_secs(1))).into_result()?;

    mainloop.run();

    Ok(())
}

This program will print out “Hello” every second forever.

Using similar methods, you can also react to IO or Signals, or call a callback whenever the loop is idle.

§Multithreading

The pipewire library is not really thread-safe, so pipewire objects do not implement Send or Sync.

However, you can spawn a MainLoop in another thread and do bidirectional communication using two channels.

To send messages to the main thread, we can easily use a std::sync::mpsc. Because we are stuck in the main loop in the pipewire thread and can’t just block on receiving a message, we use a pipewire::channel instead.

See the pipewire::channel module for details.

For info on more general concepts about PipeWire as well as the C library libpipewire, see PipeWire’s documentation. Some notable pages are:

Re-exports§

pub use pw_sys as sys;
pub use spa;

Modules§

buffer
channel
A channel for communicating with a thread running a PipeWire loop.
client
Clients represent an open connection of a client process with the server.
constants
Pipewire constants.
context
The context manages all locally available resources.
core
The core singleton object used in communication with a PipeWire instance.
device
Devices model physical hardware or software devices in the system and can create other objects such as nodes or other devices.
factory
Factories are objects that create other objects.
keys
A collection of keys that are used to add extra information on objects.
link
Links connect two ports of opposite direction, making media flow from the output port to the input port.
loop_
The PipeWire event loop responsible for listening to and handling events.
main_loop
Wrapper that runs a loop in the current thread.
metadata
module
Modules implement functionality such as providing new objects or policies.
node
Nodes are media processing elements that consume or produce buffers with data such as audio or video.
permissions
Permissions are used to implement access control for PipeWire clients.
port
Ports are attached on nodes and provide interfaces for input or output of data on the node.
properties
Properties are used to pass around arbitrary key/value pairs.
proxy
Proxy are client side representations of resources that live on a remote PipeWire instance.
registry
The registry is a singleton object that keeps track of global objects on the PipeWire instance.
stream
Streams are higher-level objects providing a convenient way to send and receive data streams to/from PipeWire.
thread_loop
types

Macros§

__properties__
A macro for creating a new Properties struct with predefined key-value pairs.

Enums§

Error

Functions§

deinit
Deinitialize PipeWire
init
Initialize PipeWire