diff --git a/docs/rst/design/events_and_hooks.rst b/docs/rst/design/events_and_hooks.rst new file mode 100644 index 00000000..84d69441 --- /dev/null +++ b/docs/rst/design/events_and_hooks.rst @@ -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. diff --git a/docs/rst/design/meson.build b/docs/rst/design/meson.build new file mode 100644 index 00000000..7d907837 --- /dev/null +++ b/docs/rst/design/meson.build @@ -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', +) diff --git a/docs/rst/design/understanding_session_management.rst b/docs/rst/design/understanding_session_management.rst new file mode 100644 index 00000000..cd76086e --- /dev/null +++ b/docs/rst/design/understanding_session_management.rst @@ -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. diff --git a/docs/rst/design/understanding_wireplumber.rst b/docs/rst/design/understanding_wireplumber.rst new file mode 100644 index 00000000..1f819682 --- /dev/null +++ b/docs/rst/design/understanding_wireplumber.rst @@ -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 +^^^^^^^ + diff --git a/docs/rst/index.rst b/docs/rst/index.rst index 615a9abb..af613592 100644 --- a/docs/rst/index.rst +++ b/docs/rst/index.rst @@ -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 diff --git a/docs/rst/meson.build b/docs/rst/meson.build index cf9a25b2..5b291d8d 100644 --- a/docs/rst/meson.build +++ b/docs/rst/meson.build @@ -16,3 +16,4 @@ sphinx_files += files( subdir('c_api') subdir('lua_api') subdir('configuration') +subdir('design')