docs: initial design documentation

This commit is contained in:
George Kiagiadakis 2023-01-23 19:31:17 +02:00 committed by Julian Bouzas
parent 3779a92fcc
commit fe3d0b55ac
6 changed files with 309 additions and 0 deletions

View file

@ -0,0 +1,139 @@
.. _events_and_hooks:
Events and Hooks
================
Session management is all about reacting to events and taking neccessary
actions. This is why WirePlumber's logic is all built on events and hooks.
Events
------
Events are objects that represent a change that has just happened on a PipeWire
object, or just a trigger for making a decision and potentially taking some
action.
Every event has a source, a subject and some properties, which include the
event type.
* The ``source`` is a reference to the GObject that created this event.
Typically, this is the ``WpStandardEventSource`` plugin.
* The ``subject`` is an *optional* reference to the object that this event
is about. For example, in a ``node-added`` event, the ``subject`` would be
a reference to the ``WpNode`` object that was just added. Some events,
especially those which are used only to trigger actions, do not have a
subject.
* The ``properties`` is a dictionary that contains information about the event,
including the event type, and also includes all the PipeWire properties of the
``subject``, if there is one.
* The ``event.type`` property describes the nature of the event, for example
``node-added`` or ``metadata-changed`` are some valid event types.
Every event also has a priority. Events with a higher priority are processed
before events with a lower priority. When two or more events have the same
priority, they are processed in a first-in-first-out manner. This logic
is defined in the *event dispatcher*.
Events are short-lived objects. They are created at the time that something is
happening and they are destroyed after they get processed. Processing an event
means executing all the hooks that are associated with it. The next section
explains what hooks are and how they are associated with events.
Hooks
-----
Hooks are objects that represent a runnable action that needs to be executed
when a certain event is processed. Every hook, therefore, consists of a
function - synchronous or asynchronous - that can be executed. Additionally,
every hook has a means to associate itself with specific events. This is
normally done by declaring *interest* to specific event properties or
combinations of them.
There are two main types of hooks: ``SimpleEventHook`` and ``AsyncEventHook``.
* ``SimpleEventHook`` contains a single, synchronous function. As soon as this
function is executed, the hook is completed.
* ``AsyncEventHook`` contains multiple functions, combined together in a state
machine using ``WpTransition`` underneath. The hook is completed only after
the state machine reaches its final state and this can take any amount of time
neccessary.
Every hook also has a name, which can be an arbitrary string of characters.
Additionally, it has two arrays of names, which declare dependencies between
this hook and others. One array is called ``before`` and the other is called
``after``. The hook names in the ``before`` array specify that this hook must
be executed *before* those other hooks. Similarly, the hook names in the
``after`` array specify that this hook must be executed *after* those other
hooks. Using this mechanism, it is possible to define the order in which
hooks will be executed, for a specific event.
Hooks are long-lived objects. They are created once, registered in the
*event dispatcher*, they are attached on events and detached after their
execution. They don't maintain any internal state, so the actions of the hook
depend solely on the event itself.
The Event Dispatcher
--------------------
The event dispatcher is a (per core) singleton object that processes all events
and also maintains a list of all the registered hooks. It has a method to
*push* events on it, which causes them to be scheduled for processing.
Scheduling of events and hooks
------------------------------
The main idea and reasoning behind this architecture is to have everything
execute in a predefined order and always wait for an action to finish before
executing the next one.
Every event has a *priority* and every hook also has an order of execution that
derives from the inter-dependencies between hooks, which are defined with
``before`` and ``after`` (see above). When an event is pushed on the dispatcher,
the dispatcher goes through all the registered hooks and checks which hooks are
configured to run on this event (their event interest matches the event).
It then makes a list of them, sorted by their order of execution, and stores it
on the event. The event is then added on the dispatcher's list of events, which
is sorted by priority.
For example::
List of events
| event1 (prio 99) -> hook1, hook2, hook3
| event2 (prio 50) -> hook5, hook2, hook4
v
The dispatcher has an internal ``GSource`` that is registered with
``G_PRIORITY_HIGH_IDLE`` priority. When there is at least one event in the
list of events, the source is dispatched. Every time it gets dispatched,
it takes the top-most event (the highest priority one) and executes the highest
priority hook in that event. If the hook executes synchronously, it then takes
the next hook and continues until there are no more hooks on this event;
then it goes to the next event, and so on. If the hook, however, executes
asynchronously, processing stops until the hook finishes; after finishing,
processing resumes like before.
It is important to notice here that the list of events may be modified while
events are getting processed. For example, a device is added; that's a
``device-added`` event. Then a hook is executed to set the profile. That creates
nodes, so a couple of ``node-added`` events... But there is also another hook to
set the route, which was attached on the ``device-added`` event for the device.
Suppose that we give the ``node-added`` events lower priority than the
``device-added`` events, then the ``set-route`` hook will execute right after
the ``set-profile`` and before any ``node-added`` events are processed.
Visually, with sample priorities::
List of events
| "device-added" (prio 20) -> set-profile, set-route
| "node-added" (prio 10) -> restore-stream, create-session-item
v
Obviously, there can also be a case where a newly added event has higher
priority than the event that was being processed before. In that case,
processing the hooks of the original event is stopped until all the hooks from
the higher priority event have been processed. For example, a capture stream
node being added may trigger the "bluetooth autoswitch" hook, which will then
change the profile of a device. Changing the profile also has to trigger setting
a new route and also handling the new device nodes, creating session items for
them... After all this is done, processing the original capture stream
``node-added`` event can continue.

View file

@ -0,0 +1,6 @@
# you need to add here any files you add to the toc directory as well
sphinx_files += files(
'understanding_session_management.rst',
'understanding_wireplumber.rst',
'events_and_hooks.rst',
)

View file

@ -0,0 +1,105 @@
.. _understanding_session_management:
Understanding Session Management
================================
The PipeWire session manager is a tool that is tasked to do a lot of things.
Many people understand the term "session manager" as a tool that is responsible
for managing links between nodes, but that is only one of many tasks. To
understand the entirety of its operation, we need to discuss how PipeWire works,
first.
When PipeWire starts, it loads a set of modules that are defined in its
configuration file. These modules provide functionality to PipeWire, otherwise
it is just an empty process that does nothing. Under normal circumstances,
the modules that are loaded on PipeWire's startup contain object *factories*,
plus the native protocol module that allows inter-process communication.
Other than that, PipeWire does not really load or do anything else. This is
where session management begins.
Session management is basically about setting up PipeWire to do something
useful. This is achieved by utilizing PipeWire's exposed object factories to
create some useful objects, then work with their methods to modify and later
destroy them. Such objects include devices, nodes, ports, links and others.
This is a task that requires continuous monitoring and action taking, reacting
on a large number of different events that happen as the system is being used.
High-level areas of operation
-----------------------------
The session management logic, in WirePlumber, is divided into 6 different areas
of operation:
1. Device Enablement
^^^^^^^^^^^^^^^^^^^^
Enabling devices is a fundamental area of operation. It is achieved by using
the device monitor objects (or just "monitors"), which are typically
implemented as SPA plugins in PipeWire, but they are loaded by WirePlumber.
Their task is to discover available media devices and create objects in PipeWire
that offer a way to interact with them.
Well-known monitors include:
- The ALSA monitor, which enables audio devices
- The ALSA MIDI monitor, which enables MIDI devices
- The libcamera monitor, which enables cameras
- The Video4Linux2 (V4L2) monitor, which also enables cameras, but also
other video capture devices through the V4L2 Linux API
- The BlueZ monitor, which enables bluetooth audio devices
2. Device Configuration
^^^^^^^^^^^^^^^^^^^^^^^
Most devices expose complex functionality, from the computer's perspective, that
needs to be managed in order to provide a simple and smooth user experience.
For that reason, for example, audio devices are organized into *profiles* and
*routes*, which allow setting them up to serve a specific use case. These
need to be configured and managed by the session manager.
3. Client Access Control
^^^^^^^^^^^^^^^^^^^^^^^^
When client applications connect to PipeWire, they need to obtain permissions
in order to be able to access the objects exposed by PipeWire and interact
with them. In some circumstances and configurations, the session manager is also
tasked with deciding which permissions should be granted to each client.
4. Node Configuration
^^^^^^^^^^^^^^^^^^^^^
Nodes are the fundamental elements of media processing. They are typically
created either by the device monitors or by client applications. When they are
created, they are in a state where they cannot be linked. Linking them requires
some configuration, such as configuring the media format and subsequently
the number and the type of ports that should be exposed. Additionally, some
properties and metadata related to the node might need to be set according to
user preferences. All of this is taken care of by the session manager.
5. Link Management
^^^^^^^^^^^^^^^^^^
When nodes are finally ready to use, the session manager is also tasked to
decide how they should be linked together in order for media to flow though.
For instance, an audio playback stream node most likely needs to be linked to
the default audio output device node. The session manager then also needs to
create all these links and monitor all conditions that may affect them so that
dynamic re-linking is possible in case something changes
(ex. if a device disconnects). In some cases, device and node configuration
may also need to change as a result of links being created or destroyed.
6. Metadata Management
^^^^^^^^^^^^^^^^^^^^^^
While in operation, PipeWire and WirePlumber both store some additional
properties about objects and their operation in storage that lives outside
these objects. These properties are referred to as "metadata" and they are
stored in "metadata objects". This metadata can be changed externally by tools
such as `pw-metadata`, but also others.
In some circumstances, this metadata needs to interact with logic inside
the session manager. Most notably, selecting the default audio and video inputs
and outputs is done by setting metadata. The session manager then needs to
validate this information, store it and restore it on the next restart, but also
ensure that the default inputs and outputs stay valid and reasonable when
devices are plugged and unplugged dynamically.

View file

@ -0,0 +1,50 @@
.. _understanding_wireplumber:
Understanding WirePlumber
=========================
Knowing the fundamentals of session management, let's see here how WirePlumber
is structured.
The Library
-----------
WirePlumber is built on top of a library that provides some fundamental building
blocks for expressing all the session management logic. This library can also
be used outside the scope of the WirePlumber daemon in order to build external
tools and GUIs that interact with PipeWire.
The Object Model
^^^^^^^^^^^^^^^^
The most fundamental code contained in the WirePlumber library is the object
model, i.e. its representation of PipeWire's objects.
PipeWire exposes several objects, such nodes and ports, via the IPC protocol
in a manner that is hard to interact with using standard object-oriented
principles, because it is asynchronous. For example, when an object is created,
its existence is announced over the protocol, but its properties are announced
later, on a secondary message. If something needs to react on this object
creation event, it typically needs to access the object's properties, so it
must wait until the properties have been sent. Doing this might sound simple,
and it is, but it becomes a tedious repetitive process to be doing this
everywhere instead of focusing on writing the actual event handling logic.
WirePlumber's library solves this by creating proxy objects that cache all the
information and updates received from PipeWire throughout each object's
lifetime. Then, it makes them available via the `WpObjectManager` API, which has
the ability to wait until certain information (ex, the properties) has been
cached on each object before announcing it.
Session management utilities
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Daemon
----------
Modules
^^^^^^^
Scripts
^^^^^^^

View file

@ -12,6 +12,14 @@ Table of Contents
configuration.rst
daemon-logging.rst
.. toctree::
:maxdepth: 2
:caption: WirePlumber's Design
design/understanding_session_management.rst
design/understanding_wireplumber.rst
design/events_and_hooks.rst
.. toctree::
:maxdepth: 2
:caption: The WirePlumber Library

View file

@ -16,3 +16,4 @@ sphinx_files += files(
subdir('c_api')
subdir('lua_api')
subdir('configuration')
subdir('design')