diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:28:17 +0000 |
commit | 7a46c07230b8d8108c0e8e80df4522d0ac116538 (patch) | |
tree | d483300dab478b994fe199a5d19d18d74153718a /doc | |
parent | Initial commit. (diff) | |
download | pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.tar.xz pipewire-7a46c07230b8d8108c0e8e80df4522d0ac116538.zip |
Adding upstream version 0.3.65.upstream/0.3.65upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'doc')
45 files changed, 6133 insertions, 0 deletions
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in new file mode 100644 index 0000000..15f9ae1 --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,68 @@ +PROJECT_NAME = PipeWire +PROJECT_NUMBER = @PACKAGE_VERSION@ +OUTPUT_DIRECTORY = "@output_directory@" +FULL_PATH_NAMES = NO +JAVADOC_AUTOBRIEF = YES +TAB_SIZE = 8 +OPTIMIZE_OUTPUT_FOR_C = YES +EXTRACT_ALL = YES +EXTRACT_STATIC = YES +STRIP_FROM_PATH = @path_prefixes@ +STRIP_FROM_INC_PATH = @path_prefixes@ +SHOW_FILES = NO +SHOW_INCLUDE_FILES = NO +GENERATE_TODOLIST = NO +GENERATE_TESTLIST = NO +GENERATE_BUGLIST = NO +QUIET = YES +WARN_NO_PARAMDOC = YES +HAVE_DOT = @HAVE_DOT@ +INPUT = @inputs@ +FILTER_PATTERNS = "*.c=@c_input_filter@" "*.h=@h_input_filter@" +FILE_PATTERNS = "*.h" "*.c" +RECURSIVE = YES +EXAMPLE_PATH = "@top_srcdir@/src/examples" \ + "@top_srcdir@/spa/examples" \ + "@top_srcdir@/doc" +EXAMPLE_PATTERNS = "*.c" + +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +IGNORE_PREFIX = pw_ \ + PW_ \ + spa_ \ + SPA_ +GENERATE_TREEVIEW = YES +SEARCHENGINE = YES +GENERATE_LATEX = NO + +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = PA_C_DECL_BEGIN= \ + PA_C_DECL_END= \ + __USE_ISOC11 \ + SPA_EXPORT \ + SPA_PRINTF_FUNC \ + SPA_DEPRECATED \ + SPA_SENTINEL \ + SPA_UNUSED \ + SPA_NORETURN \ + SPA_RESTRICT +HTML_EXTRA_STYLESHEET = @cssfiles@ + +MAX_INITIALIZER_LINES = 1 +SORT_MEMBER_DOCS = NO + +CALL_GRAPH = NO +CALLER_GRAPH = NO +CLASS_GRAPH = NO +COLLABORATION_GRAPH = NO +GROUP_GRAPHS = NO +INCLUDED_BY_GRAPH = NO +INCLUDE_GRAPH = NO +GRAPHICAL_HIERARCHY = NO +DIRECTORY_GRAPH = NO +TEMPLATE_RELATIONS = NO + +# Fix up some apparent Doxygen mis-parsing +EXCLUDE_SYMBOLS = "desc" "methods" "msgid_plural" "n" "name" "props" "utils" "start" diff --git a/doc/api-tree.dox b/doc/api-tree.dox new file mode 100644 index 0000000..0c5c8fe --- /dev/null +++ b/doc/api-tree.dox @@ -0,0 +1,126 @@ +/** + +\defgroup api_pw_core Core API +\brief PipeWire Core API +\{ +\addtogroup pw_pipewire +\addtogroup pw_main_loop +\addtogroup pw_context +\addtogroup pw_client +\addtogroup pw_core +\addtogroup pw_device +\addtogroup pw_factory +\addtogroup pw_link +\addtogroup pw_loop +\addtogroup pw_module +\addtogroup pw_node +\addtogroup pw_permission +\addtogroup pw_port +\addtogroup pw_proxy +\addtogroup pw_registry +\addtogroup pw_type +\addtogroup pw_keys +\} + +\defgroup api_pw_impl Implementation API +\brief PipeWire Object Implementation API +\{ +\addtogroup pw_impl_client +\addtogroup pw_impl_core +\addtogroup pw_impl_device +\addtogroup pw_impl_factory +\addtogroup pw_impl_link +\addtogroup pw_impl_metadata +\addtogroup pw_impl_module +\addtogroup pw_impl_node +\addtogroup pw_impl_port +\addtogroup pw_buffers +\addtogroup pw_control +\addtogroup pw_data_loop +\addtogroup pw_global +\addtogroup pw_protocol +\addtogroup pw_resource +\addtogroup pw_thread_loop +\addtogroup pw_work_queue +\} + +\defgroup api_pw_util Utilities +\brief PipeWire Utilities +\{ +\addtogroup pw_array +\addtogroup pw_conf +\addtogroup pw_gettext +\addtogroup pw_log +\addtogroup pw_map +\addtogroup pw_memblock +\addtogroup pw_properties +\addtogroup pw_thread +\addtogroup pw_utils +\} + +\defgroup api_pw_ext Extensions +\brief PipeWire Extensions +\{ +\addtogroup pw_client_node +\addtogroup pw_metadata +\addtogroup pw_profiler +\addtogroup pw_protocol_native +\addtogroup pw_session_manager +\} + +\defgroup api_spa SPA +\brief Simple Plugin API +\{ +\addtogroup spa_buffer +\addtogroup spa_control +\addtogroup spa_debug +\addtogroup spa_device +\addtogroup spa_graph +\addtogroup spa_node +\addtogroup spa_param +\addtogroup spa_pod +\defgroup spa_utils Utilities +Utility data structures, macros, etc. +\{ +\addtogroup spa_ansi +\addtogroup spa_utils_defs +\addtogroup spa_dict +\addtogroup spa_list +\addtogroup spa_hooks +\addtogroup spa_interfaces +\addtogroup spa_json +\addtogroup spa_keys +\addtogroup spa_names +\addtogroup spa_result +\addtogroup spa_ringbuffer +\addtogroup spa_string +\addtogroup spa_types +\} +\defgroup spa_support Support +Support interfaces provided by host +\{ +\addtogroup spa_cpu +\addtogroup spa_dbus +\addtogroup spa_i18n +\addtogroup spa_log +\addtogroup spa_loop +\addtogroup spa_handle +\addtogroup spa_plugin_loader +\addtogroup spa_system +\addtogroup spa_thread +\} +\} + +\defgroup pw_stream Stream +\{ +\} + +\defgroup pw_filter Filter +\{ +\} + +\defgroup pwtest Test Suite +\{ +\} + +*/ diff --git a/doc/api.dox b/doc/api.dox new file mode 100644 index 0000000..880127e --- /dev/null +++ b/doc/api.dox @@ -0,0 +1,89 @@ +/** \page page_api PipeWire API + +The PipeWire API consists of several parts: + +- The \ref pw_stream for a convenient way to send and receive data streams from/to PipeWire. + +- The \ref pw_filter for a convenient way to implement processing filters. + +- The \ref api_pw_core to access a PipeWire instance. This API is used +by all clients that need to communicate with the \ref page_daemon and provides +the necessary structs to interface with the daemon. + +- The \ref api_pw_impl is primarily used by the \ref page_daemon itself but also by the +\ref page_session_manager and modules/extensions that need to build objects in +the graph. + +- The \ref api_pw_util containing various utility functions and structures. + +- The \ref api_pw_ext for interfacing with certain extension modules. + +The APIs work through proxy objects, so that calling a method on an object +invokes that same method on the remote side. Marshalling and de-marshalling is +handled transparently by the \ref page_module_protocol_native. +The below graph illustrates this approach: + +\dot +digraph API { + compound=true; + node [shape="box"]; + rankdir="RL"; + + subgraph cluster_daemon { + rankdir="TB"; + label="PipeWire daemon"; + style="dashed"; + + impl_core [label="Core Impl. Object"]; + impl_device [label="Device Impl. Object"]; + impl_node [label="Node Impl. Object"]; + } + + subgraph cluster_client { + rankdir="TB"; + label="PipeWire client"; + style="dashed"; + + obj_core [label="Core Object"]; + obj_device [label="Device Object"]; + obj_node [label="Node Object"]; + } + + obj_core -> impl_core; + obj_device -> impl_device; + obj_node -> impl_node; + +} +\enddot + +It is common for clients to use both the \ref api_pw_core and the \ref api_pw_impl +and both APIs are provided by the same library. + +- \subpage page_client_impl +- \subpage page_proxy +- \subpage page_streams +- \subpage page_thread_loop + + +\addtogroup api_pw_core Core API + +The Core API to access a PipeWire instance. This API is used by all +clients to communicate with the \ref page_daemon. + +If you are familiar with Wayland implementation, the Core API is +roughly equivalent to libwayland-client. + +See: \ref page_api + + +\addtogroup api_pw_impl Implementation API + +The implementation API provides the tools to build new objects and +modules. + +If you are familiar with Wayland implementation, the Implementation API is +roughly equivalent to libwayland-server. + +See: \ref page_api + +*/ diff --git a/doc/custom.css b/doc/custom.css new file mode 100644 index 0000000..43690cb --- /dev/null +++ b/doc/custom.css @@ -0,0 +1,19 @@ +:root { + /* --page-background-color: #729fcf; */ + --primary-color: #729fcf; + --primary-dark-color: #729fcf; + --header-background: #729fcf; + --header-foreground: rgba(255, 255, 255, 0.7); + --font-family: 'Source Sans Pro', 'Source Sans', sans-serif; +} + +@media (prefers-color-scheme: light) { + :root { + --code-background: #f5f5f5; + --code-foreground: #333333; + --fragment-background: #f5f5f5; + --fragment-foreground: #333333; + --fragment-keyword: #c7254e; + --fragment-link: #729fcf; + } +} diff --git a/doc/dma-buf.dox b/doc/dma-buf.dox new file mode 100644 index 0000000..87d61eb --- /dev/null +++ b/doc/dma-buf.dox @@ -0,0 +1,163 @@ +/** \page page_dma_buf DMA-BUF Sharing + +PipeWire supports sharing Direct Memory Access buffers (DMA-BUFs) between +clients via the `SPA_DATA_DmaBuf` data type. However properly negotiating +DMA-BUF support on both the producer and the consumer side require following +a specific procedure. This page describes said procedure by using events and +methods from the filter or stream API. + +Note: This article focuses mostly on DMA-BUF sharing from arbitrary devices, +like discrete GPUs. For using DMA-BUFs created by v4l2 please refer to the +corresponding paragraph. + +# Capability Negotiations + +The capability negotiation for DMA-BUFs is complicated by the fact that a +usable and preferred optimal modifier for a given format can only be +determined by the allocator. This allocator has to be invoked with the intersection +of all supported modifiers for every client. As a result, the fixation of the +modifier is delegated from PipeWire to the node responsible for +allocating the buffers. + +## pw_stream_connect + +The stream parameters should contain two `SPA_PARAM_EnumFormat` objects for +each format: one for DMA-BUFs, one for shared memory buffers as a fallback. + +Query the list of all supported modifiers from your graphics API of choice. +Add a `SPA_FORMAT_VIDEO_modifier` property to the first stream parameter with +the flags `SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE`. The +value of the property should be set to a `SPA_CHOICE_Enum` containing one +`long` choice per supported modifier, plus `DRM_FORMAT_MOD_INVALID` if the +graphics API supports modifier-less buffers. + +Note: When a producer is only supporting modifier-less buffers it can omit +the `SPA_POD_PROP_FLAG_DONT_FIXATE` (see param_changed hook, For producers). + +The second stream parameter should not contain any `SPA_FORMAT_VIDEO_modifier` +property. + +To prioritise DMA-BUFs place those `SPA_PARAM_EnumFormat` containing modifiers +first, when emitting them to PipeWire. + +## param_changed Hook + +When the `param_changed` hook is called for a `SPA_PARAM_Format` the client +has to parse the `spa_pod` directly. Use +`spa_pod_find_prop(param, NULL, SPA_FORMAT_VIDEO_modifier)` to check +whether modifiers were negotiated. If they were negotiated, set the +`SPA_PARAM_BUFFERS_dataType` property to `1 << SPA_DATA_DmaBuf`. If they were +not negotiated, fall back to shared memory by setting the +`SPA_PARAM_BUFFERS_dataType` property to `1 << SPA_DATA_MemFd`, +`1 << SPA_DATA_MemPtr`, or both. + +While consumers only have to parse the resulting `SPA_PARAM_Format` for any +format related information, it's up to the producer to fixate onto a single +format modifier pair. The producer is also responsible to check if all clients +announce sufficient capabilities or fallback to shared memory buffers when +possible. + +### For Consumers + +Use `spa_format_video_raw_parse` to get the format and modifier. + +### For Producers + +Producers have to handle two cases when it comes to modifiers wrt. to the +previous announced capabilities: Using only the modifier-less API, only the +modifier-aware one, or supporting both. + +- modifier-less: + In this case only the modifier `DRM_FORMAT_MOD_INVALID` was announced with + the format. + It is sufficient to check if the `SPA_PARAM_Format` contains the modifier + property as described above. If that is the case, use DMA-BUFs for screen-sharing, + else fall back to SHM, if possible. +- modifier-aware: + In this case a list with all supported modifiers will be returned in the format. + (using `DRM_FORMAT_MOD_INVALID` as the token for the modifier-less API). + On the `param_changed` event check if the modifier key is present and has the flag + `SPA_POD_PROP_FLAG_DONT_FIXATE` attached to it. In this case, extract all modifiers + from the list and do a test allocation with your allocator to choose the preferred + modifier. Fixate on that `EnumFormat` by announcing a `SPA_PARAM_EnumFormat` with + only one modifier in the `SPA_CHOICE_Enum` and without the + `SPA_POD_PROP_FLAG_DONT_FIXATE` flag, followed by the previous announced + `EnumFormat`. This will retrigger the `param_changed` event with an + `SPA_PARAM_Format` as described below. + If the `SPA_PARAM_Format` contains a modifier key, without the flag + `SPA_POD_PROP_FLAG_DONT_FIXATE`, it should only contain one value in the + `SPA_CHOICE_Enum`. In this case announce the `SPA_PARAM_Buffers` accordingly + to the selected format and modifier. It is important to query the plane count + of the used format modifier pair and set `SPA_PARAM_BUFFERS_blocks` accordingly. + +Note: When test allocating a buffer, collect all possible modifiers, while omitting +`DRM_FORMAT_MOD_INVALID` from the `SPA_FORMAT_VIDEO_modifier` property and +pass them all to the graphics API. If the allocation fails and the list of +possible modifiers contains `DRM_FORMAT_MOD_INVALID`, fall back to allocating +without an explicit modifier if the graphics API allows it. + +## add_buffer Hook + +This is relevant for producers. + +Allocate a DMA-BUF only using the negotiated format and modifier. + +## on_event Hook + +This is relevant for consumers. + +Check the type of the dequeued buffer. If its `SPA_DATA_MemFd` or +`SPA_DATA_MemPtr` use the fallback SHM import mechanism. +If it's `SPA_DATA_DmaBuf` +get the DMA-BUF FDs (the plane count is encoded in the `n_datas` variable of the +`spa_buffer` struct) and import them with the graphics API. + +Note: Some graphics APIs have separated functions for the modifier-less case +(`DRM_FORMAT_MOD_INVALID`) or are omitting the modifier, since it might be used +for error handling. + +## Example Programs + +- \ref video-src-fixate.c "": \snippet{doc} video-src-fixate.c title +- \ref video-play-fixate.c "": \snippet{doc} video-play-fixate.c title + +# DMA-BUF Mapping Warning + +It's important to make sure all consumers of the PipeWire stream are prepared +to deal with DMA-BUFs. Most DMA-BUFs cannot be treated like shared memory in general +because of the following issues: + +- DMA-BUFs can use hardware-specific tiling and compression as described by + modifiers. Thus, a `mmap(3)` on the DMA-BUF FD will not give a linear view of + the buffer contents. +- DMA-BUFs need to be properly synchronized with the asynchronous reads and + writes of the hardware. A `mmap(3)` call is not enough to guarantee proper + synchronization. (Maybe add link to linux syscall doc??) +- Blindly accessing the DMA-BUFs via `mmap(3)` can be extremely slow if the + buffer has been allocated on discrete hardware. Consumers are better off + using a proper graphics API (such as EGL, Vulkan or VA-API) to process the + DMA-BUFs. + +# Size of DMA-BUFs + +When importing a DMA-BUF with a proper graphics API the size of a single buffer plane +is no relevant property since it will be derived by the driver from the other properties. +Therefore consumers should ignore the field `maxsize` of a `spa_data` and the field +`size` of a `spa_chunk` struct. Producers are allowed to set both to 0. +In cases where mapping a single plane is required the size should be obtained locally +via the filedescriptor. + +# v4l2 + +Another use case for streaming via DMA-BUFs are exporting a camera feed from v4l2 +as DMA-BUFs. Those are located in the main memory where it is possible to mmap them. +This should be done as follows: Neither producer nor consumer should announce a +modifier, but both should include `1 << SPA_DATA_DmaBuf` in the +`SPA_PARAM_BUFFERS_dataType` property. It's the the responsibility of the producer +while the `add_buffer` event to choose DMA-BUF as the used buffer type even though +no modifier is present, if it can guarantee, that the used buffer is mmapable. + +Note: For now v4l2 uses planar buffers without modifiers. This is the reason for +this special case. + +*/ diff --git a/doc/doxygen-awesome.css b/doc/doxygen-awesome.css new file mode 100644 index 0000000..6000d2f --- /dev/null +++ b/doc/doxygen-awesome.css @@ -0,0 +1,1364 @@ +/** + +Doxygen Awesome +https://github.com/jothepro/doxygen-awesome-css + +MIT License + +Copyright (c) 2021 jothepro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +:root { + /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */ + --primary-color: #1982d2; + --primary-dark-color: #00559f; + --primary-light-color: #7aabd6; + --primary-lighter-color: #cae1f1; + --primary-lightest-color: #e9f1f8; + + /* page base colors */ + --page-background-color: white; + --page-foreground-color: #2c3e50; + --page-secondary-foreground-color: #67727e; + + /* color for all separators on the website: hr, borders, ... */ + --separator-color: #dedede; + + /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */ + --border-radius-large: 8px; + --border-radius-small: 4px; + --border-radius-medium: 6px; + + /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */ + --spacing-small: 5px; + --spacing-medium: 10px; + --spacing-large: 16px; + + /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */ + --box-shadow: 0 2px 10px 0 rgba(0,0,0,.1); + + --odd-color: rgba(0,0,0,.03); + + /* font-families. will affect all text on the website + * font-family: the normal font for text, headlines, menus + * font-family-monospace: used for preformatted text in memtitle, code, fragments + */ + --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; + --font-family-monospace: source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace; + + /* font sizes */ + --page-font-size: 15.6px; + --navigation-font-size: 14.4px; + --code-font-size: 14.4px; /* affects code, fragment */ + --title-font-size: 22px; + + /* content text properties. These only affect the page content, not the navigation or any other ui elements */ + --content-line-height: 27px; + /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/ + --content-maxwidth: 900px; + + /* colors for various content boxes: @warning, @note, @deprecated @bug */ + --warning-color: #fca49b; + --warning-color-dark: #b61825; + --warning-color-darker: #75070f; + --note-color: rgba(255,229,100,.3); + --note-color-dark: #c39900; + --note-color-darker: #8d7400; + --deprecated-color: rgb(214, 216, 224); + --deprecated-color-dark: #5b6269; + --deprecated-color-darker: #43454a; + --bug-color: rgb(246, 208, 178); + --bug-color-dark: #a53a00; + --bug-color-darker: #5b1d00; + --invariant-color: #b7f8d0; + --invariant-color-dark: #00ba44; + --invariant-color-darker: #008622; + + /* blockquote colors */ + --blockquote-background: #f5f5f5; + --blockquote-foreground: #727272; + + /* table colors */ + --tablehead-background: #f1f1f1; + --tablehead-foreground: var(--page-foreground-color); + + /* menu-display: block | none + * Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible. + * `GENERATE_TREEVIEW` MUST be enabled! + */ + --menu-display: block; + + --menu-focus-foreground: var(--page-background-color); + --menu-focus-background: var(--primary-color); + --menu-selected-background: rgba(0,0,0,.05); + + + --header-background: var(--page-background-color); + --header-foreground: var(--page-foreground-color); + + /* searchbar colors */ + --searchbar-background: var(--side-nav-background); + --searchbar-foreground: var(--page-foreground-color); + + /* searchbar size + * (`searchbar-width` is only applied on screens >= 768px. + * on smaller screens the searchbar will always fill the entire screen width) */ + --searchbar-height: 33px; + --searchbar-width: 210px; + + /* code block colors */ + --code-background: #f5f5f5; + --code-foreground: var(--page-foreground-color); + + /* fragment colors */ + --fragment-background: #282c34; + --fragment-foreground: #ffffff; + --fragment-keyword: #cc99cd; + --fragment-keywordtype: #ab99cd; + --fragment-keywordflow: #e08000; + --fragment-token: #7ec699; + --fragment-comment: #999999; + --fragment-link: #98c0e3; + --fragment-preprocessor: #65cabe; + --fragment-linenumber-color: #cccccc; + --fragment-linenumber-background: #35393c; + --fragment-linenumber-border: #1f1f1f; + --fragment-lineheight: 20px; + + /* sidebar navigation (treeview) colors */ + --side-nav-background: #fbfbfb; + --side-nav-foreground: var(--page-foreground-color); + --side-nav-arrow-color: var(--page-background-color); + + /* height of an item in any tree / collapsible table */ + --tree-item-height: 30px; +} + +@media screen and (max-width: 767px) { + :root { + --page-font-size: 16px; + --navigation-font-size: 16px; + --code-font-size: 15px; /* affects code, fragment */ + --title-font-size: 22px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --primary-color: #00559f; + --primary-dark-color: #1982d2; + --primary-light-color: #4779ac; + --primary-lighter-color: #191e21; + --primary-lightest-color: #191a1c; + + --box-shadow: 0 2px 10px 0 rgba(0,0,0,.35); + + --odd-color: rgba(0,0,0,.1); + + --menu-selected-background: rgba(0,0,0,.4); + + --page-background-color: #1C1D1F; + --page-foreground-color: #d2dbde; + --page-secondary-foreground-color: #859399; + --separator-color: #000000; + --side-nav-background: #252628; + + --code-background: #2a2c2f; + + --tablehead-background: #2a2c2f; + + --blockquote-background: #1f2022; + --blockquote-foreground: #77848a; + + --warning-color: #b61825; + --warning-color-dark: #510a02; + --warning-color-darker: #f5b1aa; + --note-color: rgb(255, 183, 0); + --note-color-dark: #9f7300; + --note-color-darker: #fff6df; + --deprecated-color: rgb(88, 90, 96); + --deprecated-color-dark: #262e37; + --deprecated-color-darker: #a0a5b0; + --bug-color: rgb(248, 113, 0); + --bug-color-dark: #812a00; + --bug-color-darker: #ffd3be; + } +} + +body { + color: var(--page-foreground-color); + background-color: var(--page-background-color); + font-size: var(--page-font-size); +} + +body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover { + font-family: var(--font-family); +} + +h1, h2, h3, h4, h5 { + margin-top: .9em; + font-weight: 600; + line-height: initial; +} + +p, div, table, dl { + font-size: var(--page-font-size); +} + +a, a.el:visited, a.el:hover, a.el:focus, a.el:active { + color: var(--primary-dark-color); +} + +/* + Title and top navigation + */ + +#top { + background: var(--header-background); + border-bottom: 1px solid var(--separator-color); +} + +@media screen and (min-width: 768px) { + #top { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + } +} + +#main-nav { + flex-grow: 5; + padding: var(--spacing-small) var(--spacing-medium); +} + +#titlearea { + width: auto; + padding: var(--spacing-medium) var(--spacing-large); + background: none; + color: var(--header-foreground); + border-bottom: none; +} + +@media screen and (max-width: 767px) { + #titlearea { + padding-bottom: var(--spacing-small); + } +} + +#titlearea table tbody tr { + height: auto !important; +} + +#projectname { + font-size: var(--title-font-size); + font-weight: 600; +} + +#projectnumber { + font-family: inherit; + font-size: 60%; +} + +#projectbrief { + font-family: inherit; + font-size: 80%; +} + +#projectlogo { + vertical-align: middle; +} + +#projectlogo img { + max-height: calc(var(--title-font-size) * 2); + margin-right: var(--spacing-small); +} + +.sm-dox, .tabs, .tabs2, .tabs3 { + background: none; + padding: 0; +} + +.tabs, .tabs2, .tabs3 { + border-bottom: 1px solid var(--separator-color); + margin-bottom: -1px; +} + +@media screen and (max-width: 767px) { + .sm-dox a span.sub-arrow { + background: var(--code-background); + } +} + +@media screen and (min-width: 768px) { + .sm-dox li, .tablist li { + display: var(--menu-display); + } + + .sm-dox a span.sub-arrow { + border-color: var(--header-foreground) transparent transparent transparent; + } + + .sm-dox a:hover span.sub-arrow { + border-color: var(--menu-focus-foreground) transparent transparent transparent; + } + + .sm-dox ul a span.sub-arrow { + border-color: transparent transparent transparent var(--header-foreground); + } + + .sm-dox ul a:hover span.sub-arrow { + border-color: transparent transparent transparent var(--menu-focus-foreground); + } +} + +.sm-dox ul { + background: var(--page-background-color); + box-shadow: var(--box-shadow); + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium) !important; + padding: var(--spacing-small); + animation: ease-out 150ms slideInMenu; +} + +@keyframes slideInMenu { + from { + opacity: 0; + transform: translate(0px, -2px); + } + + to { + opacity: 1; + transform: translate(0px, 0px); + } +} + +.sm-dox ul a { + color: var(--page-foreground-color); + background: var(--page-background-color); + font-size: var(--navigation-font-size); +} + +.sm-dox>li>ul:after { + border-bottom-color: var(--page-background-color) !important; +} + +.sm-dox>li>ul:before { + border-bottom-color: var(--separator-color) !important; +} + +.sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus { + font-size: var(--navigation-font-size); + color: var(--menu-focus-foreground); + text-shadow: none; + background-color: var(--menu-focus-background); + border-radius: var(--border-radius-small) !important; +} + +.sm-dox a, .sm-dox a:focus, .tablist li, .tablist li a, .tablist li.current a { + text-shadow: none; + background: transparent; + background-image: none !important; + color: var(--header-foreground); + font-weight: normal; + font-size: var(--navigation-font-size); +} + +.sm-dox a:focus { + outline: auto; +} + +.sm-dox a:hover, .sm-dox a:active, .tablist li a:hover { + text-shadow: none; + font-weight: normal; + background: var(--menu-focus-background); + color: var(--menu-focus-foreground); + border-radius: var(--border-radius-small) !important; + font-size: var(--navigation-font-size); +} + +.tablist li.current { + border-radius: var(--border-radius-small); + background: var(--menu-selected-background); +} + +.tablist li { + margin: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small); +} + +.tablist a { + padding: 0 var(--spacing-large); +} + + +/* + Search box + */ + +#MSearchBox { + height: var(--searchbar-height); + background: var(--searchbar-background); + border-radius: var(--searchbar-height); + border: 1px solid var(--separator-color); + overflow: hidden; + width: var(--searchbar-width); + position: relative; + box-shadow: none; + display: block; + margin-top: 0; +} + +.left #MSearchSelect { + left: 0; +} + +.tabs .left #MSearchSelect { + padding-left: 0; +} + +.tabs #MSearchBox { + position: absolute; + right: var(--spacing-medium); +} + +@media screen and (max-width: 767px) { + .tabs #MSearchBox { + position: relative; + right: 0; + margin-left: var(--spacing-medium); + margin-top: 0; + } +} + +#MSearchSelectWindow, #MSearchResultsWindow { + z-index: 9999; +} + +#MSearchBox.MSearchBoxActive { + border-color: var(--primary-color); + box-shadow: inset 0 0 0 1px var(--primary-color); +} + +#main-menu > li:last-child { + margin-right: 0; +} + +@media screen and (max-width: 767px) { + #main-menu > li:last-child { + height: 50px; + } +} + +#MSearchField { + font-size: var(--navigation-font-size); + height: calc(var(--searchbar-height) - 2px); + background: transparent; + width: calc(var(--searchbar-width) - 64px); +} + +.MSearchBoxActive #MSearchField { + color: var(--searchbar-foreground); +} + +#MSearchSelect { + top: calc(calc(var(--searchbar-height) / 2) - 11px); +} + +.left #MSearchSelect { + padding-left: 8px; +} + +#MSearchBox span.left, #MSearchBox span.right { + background: none; +} + +#MSearchBox span.right { + padding-top: calc(calc(var(--searchbar-height) / 2) - 12px); +} + +.tabs #MSearchBox span.right { + top: calc(calc(var(--searchbar-height) / 2) - 12px); +} + +@keyframes slideInSearchResults { + from { + opacity: 0; + transform: translate(0, 15px); + } + + to { + opacity: 1; + transform: translate(0, 20px); + } +} + +#MSearchResultsWindow { + left: auto !important; + right: var(--spacing-medium); + border-radius: var(--border-radius-large); + border: 1px solid var(--separator-color); + transform: translate(0, 20px); + box-shadow: var(--box-shadow); + animation: ease-out 280ms slideInSearchResults; + background: var(--page-background-color); +} + +iframe#MSearchResults { + background: var(--page-background-color); + margin: 4px; +} + +#MSearchSelectWindow { + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + box-shadow: var(--box-shadow); + background: var(--page-background-color); +} + +#MSearchSelectWindow a.SelectItem { + font-size: var(--navigation-font-size); + line-height: var(--content-line-height); + margin: 0 var(--spacing-small); + border-radius: var(--border-radius-small); + color: var(--page-foreground-color); +} + +#MSearchSelectWindow a.SelectItem:hover { + background: var(--menu-focus-background); + color: var(--menu-focus-foreground); +} + +@media screen and (max-width: 767px) { + #MSearchBox { + margin-top: var(--spacing-medium); + margin-bottom: var(--spacing-medium); + width: calc(100vw - 30px); + } + + #main-menu > li:last-child { + float: none !important; + } + + #MSearchField { + width: calc(100vw - 95px); + } + + @keyframes slideInSearchResultsMobile { + from { + opacity: 0; + transform: translate(0, 15px); + } + + to { + opacity: 1; + transform: translate(0, 20px); + } + } + + #MSearchResultsWindow { + left: var(--spacing-medium) !important; + right: var(--spacing-medium); + overflow: auto; + transform: translate(0, 20px); + animation: ease-out 280ms slideInSearchResultsMobile; + } +} + +/* + Tree view + */ + +#side-nav { + padding: 0 !important; + background: var(--side-nav-background); +} + +@media screen and (max-width: 767px) { + #side-nav { + display: none; + } + + #doc-content { + margin-left: 0 !important; + height: auto !important; + padding-bottom: calc(2 * var(--spacing-large)); + } +} + +#nav-tree { + background: transparent; +} + +#nav-tree .label { + font-size: var(--navigation-font-size); +} + +#nav-tree .item { + height: var(--tree-item-height); + line-height: var(--tree-item-height); +} + +#nav-sync { + top: 12px !important; + right: 12px; +} + +#nav-tree .selected { + text-shadow: none; + background-image: none; + background-color: transparent; + box-shadow: inset 4px 0 0 0 var(--primary-dark-color); +} + +#nav-tree a { + color: var(--side-nav-foreground); +} + +#nav-tree a:focus { + outline-style: auto; +} + +.arrow { + color: var(--primary-light-color); + font-family: serif; + height: auto; + text-align: right; +} + +#nav-tree .arrow { + opacity: 0; +} + +#nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow { + opacity: 1; +} + +#nav-tree .selected a { + color: var(--primary-dark-color); + font-weight: bolder; +} + +.ui-resizable-e { + background: var(--separator-color); + width: 1px; +} + +/* + Contents + */ + +div.header { + border-bottom: 1px solid var(--separator-color); + background-color: var(--page-background-color); + background-image: none; +} + +div.contents, div.header .title, div.header .summary { + max-width: var(--content-maxwidth); +} + +div.contents, div.header .title { + line-height: initial; + margin: calc(var(--spacing-medium) + .2em) auto var(--spacing-medium) auto; +} + +div.header .summary { + margin: var(--spacing-medium) auto 0 auto; +} + +div.headertitle { + padding: 0; +} + +div.header .title { + font-weight: 600; + font-size: 210%; + padding: var(--spacing-medium) var(--spacing-large); + word-break: break-word; +} + +div.header .summary { + width: auto; + display: block; + float: none; + padding: 0 var(--spacing-large); +} + +td.memSeparator { + border-color: var(--separator-color); +} + +.mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams { + background: var(--code-background); +} + +.mdescRight { + color: var(--page-secondary-foreground-color); +} + +span.mlabel { + background: var(--primary-color); + border: none; + padding: 4px 9px; + border-radius: 12px; + margin-right: var(--spacing-medium); +} + +span.mlabel:last-of-type { + margin-right: 2px; +} + +div.contents { + padding: 0 var(--spacing-large); +} + +div.contents p, div.contents li { + line-height: var(--content-line-height); +} + +div.contents div.dyncontent { + margin: var(--spacing-medium) 0; +} + +@media (prefers-color-scheme: dark) { + div.contents div.dyncontent img { + filter: hue-rotate(180deg) invert(); + } +} + +h2.groupheader { + border-bottom: 1px solid var(--separator-color); + color: var(--page-foreground-color); +} + +blockquote { + padding: var(--spacing-small) var(--spacing-medium); + background: var(--blockquote-background); + color: var(--blockquote-foreground); + border-left: 2px solid var(--blockquote-foreground); + margin: 0; +} + +blockquote p { + margin: var(--spacing-small) 0 var(--spacing-medium) 0; +} +.paramname { + color: var(--primary-dark-color); +} + +.glow { + text-shadow: 0 0 15px var(--primary-light-color) !important; +} + +.alphachar a { + color: var(--page-foreground-color); +} + +/* + Table of Contents + */ + +div.toc { + background-color: var(--side-nav-background); + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + box-shadow: var(--box-shadow); + padding: 0 var(--spacing-large); + margin: 0 0 var(--spacing-medium) var(--spacing-medium); +} + +div.toc h3 { + color: var(--side-nav-foreground); + font-size: var(--navigation-font-size); + margin: var(--spacing-large) 0; +} + +div.toc li { + font-size: var(--navigation-font-size); + padding: 0; + background: none; +} + +div.toc li:before { + content: '↓'; + font-weight: 800; + font-family: var(--font-family); + margin-right: var(--spacing-small); + color: var(--side-nav-foreground); + opacity: .4; +} + +div.toc ul li.level1 { + margin: 0; +} + +div.toc ul li.level2, div.toc ul li.level3 { + margin-top: 0; +} + + +@media screen and (max-width: 767px) { + div.toc { + float: none; + width: auto; + margin: 0 0 var(--spacing-medium) 0; + } +} + +/* + Code & Fragments + */ + +code, div.fragment, pre.fragment { + border-radius: var(--border-radius-small); + border: none; + overflow: hidden; +} + +code { + display: inline; + background: var(--code-background); + color: var(--code-foreground); + padding: 2px 6px; + word-break: break-word; +} + +div.fragment, pre.fragment { + margin: var(--spacing-medium) 0; + padding: 14px 16px; + background: var(--fragment-background); + color: var(--fragment-foreground); + overflow-x: auto; +} + +@media screen and (max-width: 767px) { + div.fragment, pre.fragment { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .contents > div.fragment, .textblock > div.fragment, .textblock > pre.fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-large)); + border-radius: 0; + } + + .textblock li > .fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-large)); + } + + .memdoc li > .fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); + } + + .memdoc > div.fragment, .memdoc > pre.fragment, dl dd > div.fragment, dl dd pre.fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); + border-radius: 0; + } +} + +code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size) !important; +} + +div.line:after { + margin-right: var(--spacing-medium); +} + +div.fragment .line, pre.fragment { + white-space: pre; + word-wrap: initial; + line-height: var(--fragment-lineheight); +} + +div.fragment span.keyword { + color: var(--fragment-keyword); +} + +div.fragment span.keywordtype { + color: var(--fragment-keywordtype); +} + +div.fragment span.keywordflow { + color: var(--fragment-keywordflow); +} + +div.fragment span.stringliteral { + color: var(--fragment-token) +} + +div.fragment span.comment { + color: var(--fragment-comment); +} + +div.fragment a.code { + color: var(--fragment-link); +} + +div.fragment span.preprocessor { + color: var(--fragment-preprocessor); +} + +div.fragment span.lineno { + display: inline-block; + width: 27px; + border-right: none; + background: var(--fragment-linenumber-background); + color: var(--fragment-linenumber-color); +} + +div.fragment span.lineno a { + background: none; + color: var(--fragment-link); +} + +div.fragment .line:first-child .lineno { + box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border); +} + +/* + dl warning, attention, note, deprecated, bug, ... + */ + +dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre { + padding: var(--spacing-medium); + margin: var(--spacing-medium) 0; + color: var(--page-background-color); + overflow: hidden; + margin-left: 0; + border-radius: var(--border-radius-small); +} + +dl.section dd { + margin-bottom: 2px; +} + +dl.warning, dl.attention { + background: var(--warning-color); + border-left: 8px solid var(--warning-color-dark); + color: var(--warning-color-darker); +} + +dl.warning dt, dl.attention dt { + color: var(--warning-color-dark); +} + +dl.note { + background: var(--note-color); + border-left: 8px solid var(--note-color-dark); + color: var(--note-color-darker); +} + +dl.note dt { + color: var(--note-color-dark); +} + +dl.bug { + background: var(--bug-color); + border-left: 8px solid var(--bug-color-dark); + color: var(--bug-color-darker); +} + +dl.bug dt a { + color: var(--bug-color-dark) !important; +} + +dl.deprecated { + background: var(--deprecated-color); + border-left: 8px solid var(--deprecated-color-dark); + color: var(--deprecated-color-darker); +} + +dl.deprecated dt a { + color: var(--deprecated-color-dark) !important; +} + +dl.section dd, dl.bug dd, dl.deprecated dd { + margin-inline-start: 0px; +} + +dl.invariant, dl.pre { + background: var(--invariant-color); + border-left: 8px solid var(--invariant-color-dark); + color: var(--invariant-color-darker); +} + +/* + memitem + */ + +div.memdoc, div.memproto, h2.memtitle { + box-shadow: none; + background-image: none; + border: none; +} + +div.memdoc { + padding: 0 var(--spacing-medium); + background: var(--page-background-color); +} + +h2.memtitle, div.memitem { + border: 1px solid var(--separator-color); +} + +div.memproto, h2.memtitle { + background: var(--code-background); + text-shadow: none; +} + +h2.memtitle { + font-weight: 500; + font-family: monospace, fixed; + border-bottom: none; + border-top-left-radius: var(--border-radius-medium); + border-top-right-radius: var(--border-radius-medium); + word-break: break-all; +} + +a:target + h2.memtitle, a:target + h2.memtitle + div.memitem { + border-color: var(--primary-light-color); +} + +a:target + h2.memtitle { + box-shadow: -3px -3px 3px 0 var(--primary-lightest-color), 3px -3px 3px 0 var(--primary-lightest-color); +} + +a:target + h2.memtitle + div.memitem { + box-shadow: 0 0 10px 0 var(--primary-lighter-color); +} + +div.memitem { + border-top-right-radius: var(--border-radius-medium); + border-bottom-right-radius: var(--border-radius-medium); + border-bottom-left-radius: var(--border-radius-medium); + overflow: hidden; + display: block !important; +} + +div.memdoc { + border-radius: 0; +} + +div.memproto { + border-radius: 0 var(--border-radius-small) 0 0; + overflow: auto; + border-bottom: 1px solid var(--separator-color); + padding: var(--spacing-medium); + margin-bottom: -1px; +} + +div.memtitle { + border-top-right-radius: var(--border-radius-medium); + border-top-left-radius: var(--border-radius-medium); +} + +div.memproto table.memname { + font-family: monospace, fixed; + color: var(--page-foreground-color); +} + +table.mlabels, table.mlabels > tbody { + display: block; +} + +td.mlabels-left { + width: auto; +} + +table.mlabels > tbody > tr:first-child { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.memname, .memitem span.mlabels { + margin: 0 +} + +/* + reflist + */ + +dl.reflist { + border-radius: var(--border-radius-medium); + border: 1px solid var(--separator-color); + overflow: hidden; + padding: 0; +} + + +dl.reflist dt, dl.reflist dd { + box-shadow: none; + text-shadow: none; + background-image: none; + border: none; + padding: 12px; +} + + +dl.reflist dt { + border-radius: 0; + background: var(--code-background); + border-bottom: 1px solid var(--separator-color); + color: var(--page-foreground-color) +} + + +dl.reflist dd { + background: none; +} + +/* + Table + */ + +table.markdownTable, table.fieldtable { + width: 100%; + border: 1px solid var(--separator-color); + margin: var(--spacing-medium) 0; +} + +table.fieldtable { + box-shadow: none; + border-radius: var(--border-radius-small); +} + +th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { + background: var(--tablehead-background); + color: var(--tablehead-foreground); + font-weight: 600; +} + +table.markdownTable td, table.markdownTable th, table.fieldtable dt { + border: 1px solid var(--separator-color); + padding: var(--spacing-small) var(--spacing-medium); +} + +table.fieldtable th { + font-size: var(--page-font-size); + font-weight: 600; + background-image: none; + background-color: var(--tablehead-background); + color: var(--tablehead-foreground); + border-bottom: 1px solid var(--separator-color); +} + +.fieldtable td.fieldtype, .fieldtable td.fieldname { + border-bottom: 1px solid var(--separator-color); + border-right: 1px solid var(--separator-color); +} + +.fieldtable td.fielddoc { + border-bottom: 1px solid var(--separator-color); +} + +.memberdecls td.glow, .fieldtable tr.glow { + background-color: var(--primary-light-color); + box-shadow: 0 0 15px var(--primary-lighter-color); +} + +table.memberdecls { + display: block; + overflow-x: auto; + overflow-y: hidden; +} + + +/* + Horizontal Rule + */ + +hr { + margin-top: var(--spacing-large); + margin-bottom: var(--spacing-large); + border-top:1px solid var(--separator-color); +} + +.contents hr { + box-shadow: var(--content-maxwidth) 0 0 0 var(--separator-color), calc(0px - var(--content-maxwidth)) 0 0 0 var(--separator-color); +} + +.contents img { + max-width: 100%; +} + +/* + Directories + */ +div.directory { + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + width: auto; +} + +table.directory { + font-family: var(--font-family); + font-size: var(--page-font-size); + font-weight: normal; +} + +.directory td.entry { + padding: var(--spacing-small); + display: flex; + align-items: center; +} + +.directory tr.even { + background-color: var(--odd-color); +} + +.icona { + width: auto; + height: auto; + margin: 0 var(--spacing-small); +} + +.icon { + background: var(--primary-dark-color); + width: 18px; + height: 18px; + line-height: 18px; +} + +.iconfopen, .icondoc, .iconfclosed { + background-position: center; + margin-bottom: 0; +} + +.icondoc { + filter: saturate(0.2); +} + +@media screen and (max-width: 767px) { + div.directory { + margin-left: calc(0px - var(--spacing-medium)); + margin-right: calc(0px - var(--spacing-medium)); + } +} + +@media (prefers-color-scheme: dark) { + .iconfopen, .iconfclosed { + filter: hue-rotate(180deg) invert(); + } +} + +/* + Class list + */ + +.classindex dl.odd { + background: var(--odd-color); + border-radius: var(--border-radius-small); +} + +@media screen and (max-width: 767px) { + .classindex { + margin: 0 calc(0px - var(--spacing-small)); + } +} + +/* + Footer and nav-path + */ + +#nav-path { + margin-bottom: -1px; + width: 100%; +} + +#nav-path ul { + background-image: none; + background: var(--page-background-color); + border: none; + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + font-size: var(--navigation-font-size); +} + +img.footer { + width: 60px; +} + +.navpath li.footer { + color: var(--page-secondary-foreground-color); +} + +address.footer { + margin-bottom: var(--spacing-large); +} + +#nav-path li.navelem { + background-image: none; + display: flex; + align-items: center; +} + +.navpath li.navelem a { + text-shadow: none; + display: inline-block; + color: var(--primary-dark-color) +} + +li.navelem { + padding: 0; + margin-left: -8px; +} + +li.navelem:first-child { + margin-left: var(--spacing-large); +} + +li.navelem:first-child:before { + display: none; +} + +#nav-path li.navelem:after { + content: ''; + border: 5px solid var(--page-background-color); + border-bottom-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + transform: scaleY(4.2); + z-index: 10; + margin-left: 6px; +} + +#nav-path li.navelem:before { + content: ''; + border: 5px solid var(--separator-color); + border-bottom-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + transform: scaleY(3.2); + margin-right: var(--spacing-small); +} + +@media (prefers-color-scheme: dark) { + #nav-path li.navelem:after { + text-shadow: 3px 0 0 var(--separator-color), 8px 0 6px rgba(0,0,0,0.4); + } +} + +.navpath li.navelem a:hover { + color: var(--primary-color); +} diff --git a/doc/examples.dox.in b/doc/examples.dox.in new file mode 100644 index 0000000..fd11ac8 --- /dev/null +++ b/doc/examples.dox.in @@ -0,0 +1,9 @@ +/** + +\page page_examples List of example programs + +@example_ref@ + +@example_doxygen@ + +*/ diff --git a/doc/index.dox b/doc/index.dox new file mode 100644 index 0000000..1602b7b --- /dev/null +++ b/doc/index.dox @@ -0,0 +1,45 @@ +/** \mainpage PipeWire + +PipeWire is low-level multimedia framework that provides: + +- Graph based processing. +- Support for out-of-process processing graphs with minimal overhead. +- Flexible and extensible media format negotiation and buffer allocation. +- Hard real-time capable plugins. +- Very low-latency for both audio and video processing. + +See \ref page_overview for an overview of PipeWire and \ref page_design +for the design principles guiding PipeWire. + +# Components + +PipeWire ships with the following components: + +- A \ref page_daemon that implements the IPC and graph processing. +- An example \ref page_session_manager that manages objects in the \ref page_daemon. +- A set of \ref page_tools to introspect and use the \ref page_daemon. +- A \ref page_library to develop PipeWire applications and plugins (\ref + page_tutorial "tutorial"). +- The \ref page_spa used by both the \ref page_daemon and in the \ref + page_library. + +# API Documentation + +See \ref page_api. + +# More Documentation + +See our [Wiki](https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/home) for +More information on how to configure and use PipeWire. + +# Resources + +- [PipeWire and AGL](https://wiki.automotivelinux.org/_media/pipewire_agl_20181206.pdf) +- [LAC 2020 Paper](https://lac2020.sciencesconf.org/307881/document) +- [PipeWire Under The Hood](https://venam.nixers.net/blog/unix/2021/06/23/pipewire-under-the-hood.html) +- [PipeWire: The Linux audio/video bus (LWN)](https://lwn.net/Articles/847412) +- [PipeWire Wikipedia](https://en.wikipedia.org/wiki/PipeWire) +- [Bluetooth, PipeWire and Whatsapp calls](https://gjhenrique.com/pipewire.html) +- [Intoduction to PipeWire](https://bootlin.com/blog/an-introduction-to-pipewire/) + +*/ diff --git a/doc/input-filter-h.sh b/doc/input-filter-h.sh new file mode 100755 index 0000000..dc4604a --- /dev/null +++ b/doc/input-filter-h.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Doxygen input filter, which tries to fix documentation of callback +# method macros. +# +# This is used for .h files. +# + +FILENAME="$1" + +# Add \ingroup commands for the file, for each \addgroup in it +BASEFILE=$(echo "$FILENAME" | sed -e 's@.*src/pipewire/@pipewire/@; s@.*spa/include/spa/@spa/@; s@.*src/test/@test/@;') + +echo "/** \file" +echo "\`$BASEFILE\`" +sed -n -e '/.*\\addtogroup [a-zA-Z0-9_].*/ { s/.*addtogroup /\\ingroup /; p; }' < "$FILENAME" | sort | uniq +echo " */" + +# Add \sa and \copydoc for (struct *methods) callback macros. +# #define pw_core_add_listener(...) pw_core_method(c,add_listener,...) -> add \sa and \copydoc +# #define spa_system_read(...) spa_system_method_r(c,read,...) -> add \sa and \copydoc +# +# Also: +# Ensure all macros are included (also those defined inside a struct), +# by adding /** \ingroup XXX */ before each definition. +# Also ensure all opaque structs get included. +sed -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\((.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \ + -e 's@^\(#define .*[[:space:]]\)\(.*_method\)\(_[rvs](.,[[:space:]]*\)\([a-z_]\+\)\(.*)[[:space:]]*\)$@\1\2\3\4\5 /**< \\copydoc \2s.\4\n\n\\sa \2s.\4 */@;' \ + -e '/\\addtogroup/ { h; s@.*\\addtogroup \(.*\).*@/** \\ingroup \1 */@; x; }' \ + -e '/#define \(PW\|SPA\)_[A-Z].*[^\\][ ]*$/ { x; p; x; }' \ + -e 's@^\([ ]*struct \)\([a-zA-Z0-9_]*\)\(;.*\)$@/** \\struct \2 */\n\1\2\3@;' \ +< "$FILENAME" diff --git a/doc/input-filter.sh b/doc/input-filter.sh new file mode 100755 index 0000000..8c71bef --- /dev/null +++ b/doc/input-filter.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# +# Doxygen input filter that adds \privatesection to all files, +# and removes macros. +# +# This is used for .c files, and causes Doxygen to not include +# any symbols from them, unless they also appeared in a header file. +# +echo -n "/** \privatesection */ " +sed -e 's/#define.*//' < "$1" diff --git a/doc/manpage.dox.in b/doc/manpage.dox.in new file mode 100644 index 0000000..9e6df78 --- /dev/null +++ b/doc/manpage.dox.in @@ -0,0 +1,5 @@ +/** \page @pagename@ @title@ + +\verbinclude @filename@ + +*/ diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..b1cb115 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,162 @@ +doxyfile_conf = configuration_data() +doxyfile_conf.set('PACKAGE_NAME', meson.project_name()) +doxyfile_conf.set('PACKAGE_VERSION', meson.project_version()) +doxyfile_conf.set('top_srcdir', meson.project_source_root()) +doxyfile_conf.set('top_builddir', meson.project_build_root()) +doxyfile_conf.set('output_directory', meson.current_build_dir()) + +dot_found = find_program('dot', required: false).found() +summary({'dot (used with doxygen)': dot_found}, bool_yn: true, section: 'Optional programs') +if dot_found + doxyfile_conf.set('HAVE_DOT', 'YES') +else + doxyfile_conf.set('HAVE_DOT', 'NO') +endif + +# Note: order here is how doxygen will expose the pages in the sidebar +# api-tree.dox should be first to determine ordering of Modules. +extra_docs = [ + 'api-tree.dox', + 'index.dox', + 'overview.dox', + 'pipewire.dox', + 'pipewire-design.dox', + 'pipewire-access.dox', + 'pipewire-midi.dox', + 'pipewire-portal.dox', + 'pipewire-daemon.dox', + 'pipewire-library.dox', + 'pipewire-modules.dox', + 'pipewire-session-manager.dox', + 'pipewire-objects-design.dox', + 'pipewire-audio.dox', + 'tutorial.dox', + 'tutorial1.dox', + 'tutorial2.dox', + 'tutorial3.dox', + 'tutorial4.dox', + 'tutorial5.dox', + 'tutorial6.dox', + 'api.dox', + 'spa-index.dox', + 'spa-plugins.dox', + 'spa-design.dox', + 'spa-pod.dox', + 'spa-buffer.dox', + 'pulseaudio.dox', + 'dma-buf.dox', +] + +inputs = [] +foreach extra : extra_docs + inputs += meson.project_source_root() / 'doc' / extra +endforeach +foreach h : pipewire_headers + inputs += meson.project_source_root() / 'src' / 'pipewire' / h +endforeach +foreach h : pipewire_ext_headers + inputs += meson.project_source_root() / 'src' / 'pipewire' / 'extensions' / h +endforeach +foreach h : pipewire_ext_sm_headers + inputs += meson.project_source_root() / 'src' / 'pipewire' / 'extensions' / h +endforeach +foreach h : pipewire_sources + inputs += meson.project_source_root() / 'src' / 'pipewire' / h +endforeach +foreach h : module_sources + inputs += meson.project_source_root() / 'src' / 'modules' / h +endforeach +inputs += meson.project_source_root() / 'test' / 'pwtest.h' +input_dirs = [ meson.project_source_root() / 'spa' / 'include' / 'spa' ] + +path_prefixes = [ + meson.project_source_root() / 'src', + meson.project_source_root() / 'spa' / 'include', + meson.project_source_root(), +] + +cssfiles = [ + meson.project_source_root() / 'doc' / 'doxygen-awesome.css', + meson.project_source_root() / 'doc' / 'custom.css' +] + +# Example files (in order from simple to esoteric) +example_files = [ + 'tutorial1.c', + 'tutorial2.c', + 'tutorial3.c', + 'tutorial4.c', + 'tutorial5.c', + 'tutorial6.c', +] +foreach h : examples + example_files += [h + '.c'] +endforeach +foreach h : spa_examples + example_files += ['spa/examples/' + h + '.c'] +endforeach + +example_doxygen = [] +example_ref = [] +foreach h : example_files + example_doxygen += ['\\example ' + h, + '\\snippet{doc} ' + h + ' title', + '<br>', + '\\snippet{doc} ' + h + ' doc'] + example_ref += ['- \\ref ' + h + ' "": \snippet{doc} ' + h + ' title'] +endforeach + +examples_dox_conf = configuration_data() +examples_dox_conf.set('example_doxygen', '\n'.join(example_doxygen)) +examples_dox_conf.set('example_ref', '\n'.join(example_ref)) +examples_dox = configure_file(input: 'examples.dox.in', + output: 'examples.dox', + configuration: examples_dox_conf) + +input_dirs += [ 'doc/examples.dox' ] + +man_doxygen = [] +man_subpages = [] +foreach m : manpages + manconf = configuration_data() + pagename = 'page_man_' + m.split('.rst.in').get(0).replace('.', '_').replace('-', '_') + filename = m.split('.rst.in').get(0) + '.dox' + manconf.set('pagename', pagename) + manconf.set('title', m.split('.rst.in').get(0).replace('.1','').replace('.5','')) + manconf.set('filename', meson.project_source_root() / 'man' / m) + manfile = configure_file(input: 'manpage.dox.in', + output: filename, + configuration: manconf) + man_doxygen += [manfile] + man_subpages += ['- \subpage ' + pagename] + input_dirs += [ 'doc/' + filename ] +endforeach + +pw_tools_dox_conf = configuration_data() +pw_tools_dox_conf.set('man_subpages', '\n'.join(man_subpages)) +pw_tools_dox = configure_file(input: 'pipewire-tools.dox.in', + output: 'pipewire-tools.dox', + configuration: pw_tools_dox_conf) +input_dirs += [ 'doc/pipewire-tools.dox' ] + +doxyfile_conf.set('inputs', ' '.join(inputs + input_dirs)) +doxyfile_conf.set('cssfiles', ' '.join(cssfiles)) +doxyfile_conf.set('path_prefixes', ' '.join(path_prefixes)) +doxyfile_conf.set('c_input_filter', meson.project_source_root() / 'doc' / 'input-filter.sh') +doxyfile_conf.set('h_input_filter', meson.project_source_root() / 'doc' / 'input-filter-h.sh') + +doxyfile = configure_file(input: 'Doxyfile.in', + output: 'Doxyfile', + configuration: doxyfile_conf) + +docdir = get_option('docdir') +if docdir == '' + docdir = pipewire_datadir / 'doc' / meson.project_name() +endif + +html_target = custom_target('pipewire-docs', + input: [ doxyfile, examples_dox, pw_tools_dox ] + inputs + cssfiles + man_doxygen, + output: [ 'html' ], + command: [ doxygen, doxyfile ], + install: true, + install_dir: docdir) diff --git a/doc/overview.dox b/doc/overview.dox new file mode 100644 index 0000000..b96caf2 --- /dev/null +++ b/doc/overview.dox @@ -0,0 +1,42 @@ +/** \page page_overview Overview + +PipeWire is a new low-level multimedia framework designed from scratch that +aims to provide: + +- Graph based processing. +- Support for out-of-process processing graphs with minimal overhead. +- Flexible and extensible media format negotiation and buffer allocation. +- Hard real-time capable plugins. +- Achieve very low-latency for both audio and video processing. + +The framework is used to build a modular daemon that can be configured to: + +- Be a low-latency audio server with features like PulseAudio and/or JACK. +- A video capture server that can manage hardware video capture devices and + provide access to them. +- A central hub where video can be made available for other applications + such as the gnome-shell screencast API. + + +# Motivation + +Linux has no unified framework for exchanging multimedia content between +applications or even devices. In most cases, developers realized that +a user-space daemon is needed to make this possible: + +- For video content, we typically rely on the compositor to render our + data. +- For video capture, we usually go directly to the hardware devices, with + all security implications and inflexible routing that this brings. +- For consumer audio, we use PulseAudio to manage and mix multiple streams + from clients. +- For Pro audio, we use JACK to manage the graph of nodes. + +None of these solutions (except perhaps to some extent Wayland) however +were designed to support the security features that are required when +dealing with flatpaks or other containerized applications. PipeWire +aims to solve this problem and provides a unified framework to run both +consumer and pro audio as well as video capture and processing in a +secure way. + +*/ diff --git a/doc/pipewire-access.dox b/doc/pipewire-access.dox new file mode 100644 index 0000000..3632280 --- /dev/null +++ b/doc/pipewire-access.dox @@ -0,0 +1,126 @@ +/** \page page_access Access Control + +This document explains how access control is designed and implemented. + +PipeWire implements per client permissions on the objects in the graph. +Permissions include `R` (read), `W` (write), `X` (execute) and `M` (metadata). + +- `R`: An object with permission `R` is visible to the client. The client will receive + registry events for the object and can interact with it. +- `W`: An object with permission `W` can be modified. This is usually done + through a method that modifies the state of the object. The `W` permission + usually implies the `X` permission. +- `X`: An object with permission `X` allows invoking methods on the object. + Some of those methods will only query state, others will modify the object. + As said above, modifying the object through one of these methods requires + the `W` permission. +- `M`: An object with `M` permission can be used as the subject in metadata. + +Clients with all permissions set are referred to as "ALL" in the +documentation. + + +# Use Cases + +## New Clients Need Their Permissions Configured + +A new client is not allowed to communicate with the PipeWire daemon until +it has been configured with permissions. + +## Flatpaks Can't Modify Other Stream/Device Volumes + +An application running as Flatpak should not be able to modify the state of +certain objects. Permissions of the relevant PipeWire objects should not have +the `W` permission to avoid this. + +## Flatpaks Can't Move Other Streams To Different Devices + +Streams are moved to another device by setting the `target.node` metadata +on the node ID. By not setting the `M` bit on the other objects, this can be +avoided. + +## Application Should Be Restricted In What They Can See + +In general, applications should only be able to see the objects that they are +allowed to see. For example, a web browser that was given access to a camera +should not be able to see (and thus receive input data from) audio devices. + +## "Manager" Applications Require Full Access + +Some applications require full access to the PipeWire graph, including +moving streams between nodes (by setting metadata) and modifying properties +(eg. volume). These applications must work even when running as Flatpak. + + +# Design + +## The PipeWire Daemon + +Immediately after a new client connects to the PipeWire daemon and updates +its properties, the client will be registered and made visible to other +clients. + +The PipeWire core will emit a `check_access` event in the \ref pw_context_events +context for the the new client. The implementer of this event is responsible +for assigning permissions to the client. + +Clients with permission `R` on the core object can continue communicating +with the daemon. Clients without permission `R` on the core are suspended +and are not able to send more messages. + +A suspended client can only resume processing after some other client +sets the core permissions to `R`. This other client is usually a session +manager, see e.g. \ref page_session_manager. + +## The PipeWire Access Module + +The \ref page_module_access hooks into the `check_access` event that is +emitted when a new client is registered. The module checks the permissions of +the client and stores those in the \ref PW_KEY_ACCESS +property on the client object. If this property is already set, the access +module does nothing. + +If the property is not set it will go through a set of checks to determine +the permissions for a client. See the \ref page_module_access documentation +for details, particularly on the values documented below. Depending on the +value of the \ref PW_KEY_ACCESS property one the following happens: + +- `"allowed"`, `"unrestricted"`: ALL permissions are set on the core + object and the client will be able to resume. +- `"restricted"`, `"flatpak"`, `"$access.force"`: No permissions are set on + the core object and the client will be suspended. +- `"rejected"`: An error is sent to the client and the client is + suspended. + +As detailed above, the client may be suspended. In that case the session +manager or another client is required to configure permissions on the object +for it to resume. + +## The Session Manager + +The session manager listens for new clients to appear. It will use the +\ref PW_KEY_ACCESS property to determine what to do. + +For clients that are suspended with `"restricted"`, `"flatpak"` or +`"$access.force"` access, the session manager needs to set permissions on the +client for the various PipeWire objects in the graph that it is allowed to +interact with. To resume a client, the session manager needs to set +permission `R` on the core object for the client. + +Permissions of objects for a client can be changed at any time by the +session manager. Removing the client core permission `R` will suspend the +client. + +The session manager needs to do additional checks to determine if the +manager permissions can be given to the particular client and then +configure ALL permissions on the client. Possible checks include +permission store checks or ask the user if the application is allowed +full access. + +Manager applications (ie. applications that need to modify the graph) will +set the \ref PW_KEY_MEDIA_CATEGORY property in the client object to "Manager". + +For details on the pipewire-media-session implementation of access control, +see \ref page_media_session. + +*/ diff --git a/doc/pipewire-architecture.dox b/doc/pipewire-architecture.dox new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/doc/pipewire-architecture.dox diff --git a/doc/pipewire-audio.dox b/doc/pipewire-audio.dox new file mode 100644 index 0000000..b39e869 --- /dev/null +++ b/doc/pipewire-audio.dox @@ -0,0 +1,127 @@ +/** \page page_audio Audio + +This document explains how Audio is implemented. + +# Use Cases + +## Audio Devices Are Made Available As Processing Nodes/Ports + +Applications need to be able to see a port for each stream of an +audio device. + +## Audio Devices Can Be Plugged and Unplugged + +When devices are plugged and unplugged the associated nodes/ports +need to be created and removed. + +## Audio Port In Canonical Format + +It must be possible to make individual audio channels available +as a single mono stream with a fixed format and samplerate. + +This makes it possible to link any of the audio ports together +without doing conversions. + +## Applications Can Connect To Audio Devices + +Applications can create ports that can connect to the audio ports +so that data can be provided to or consumed from them. + +It should be possible to automatically connect an application to +a sink/source when it requests this. + +## Default Audio Sink and Sources + +It should be possible to mark a source or sink as the default source +and sink so that applications are routed to them by default. + +It should be possible to change the default audio sink/source. + +## Application Should Be Able To Move Between Sinks/Sources + +It should be possible to move an application from one device to +another dynamically. + +## Exclusive Access + +Application should be able to connect to a device in exclusive mode. +This allows the application to negotiate a specific format with the +device such as a compressed format. + +Exclusive access means that only one application can access the device +because mixing is in general not possible when negotiating +compressed formats. + + +# Design + +## SPA + +Audio devices are implemented with an \ref spa_device "SPA Device" object. + +This object is then responsible for controlling the \ref spa_node "SPA Nodes" that +provide the audio ports to interface with the device. + +The nodes operate on the native audio formats supported by the device. +This includes the sample rate as well as the number of channels and +the audio format. + +## Audio Adapter + +An SPA Node called the "adapter" is usually used with the SPA device node as +the internal node. + +The function of the adapter is to convert the device native format to +the required external format. This can include format or samplerate +conversion but also channel remixing/remapping. + +The audio adapter is also responsible for exposing the audio channels +as separate mono ports. This is called the DSP setup. + +The audio adapter can also be configured in passthrough mode when it +will not do any conversions but simply pass through the port information +of the internal node. This can be used to implement exclusive access. + +Setup of the different configurations of the adapter can be done with +the PortConfig parameter. + +## The Session Manager + +The session manager is responsible for creating nodes and ports for +the various audio devices. It will need to wrap them into an audio +adapter so that the specific configuration of the node can be +decided by the policy mode. + +The session manager should create name and description for the +devices and nodes. + +The session manager is responsible for assigning priorities to the +nodes. At least \ref PW_KEY_PRIORITY_SESSION and \ref PW_KEY_PRIORITY_DRIVER +need to be set. + +The session manager might need to work with other services to gain +exclusive access to the device (eg. DBus). + + +# Implementation + +## PipeWire Media Session (alsa-monitor) + +PipeWire media session uses the \ref SPA_NAME_API_ALSA_ENUM_UDEV plugin +for enumerating the ALSA devices. For each device it does: + +- Try to acquire the DBus device reservation object to gain exclusive + access to the device. +- Create an SPA device instance for the device and monitor this device instance. +- For each node created by the device, create an adapter with + an ALSA PCM node in the context of the PipeWire daemon. + +The session manager will also create suitable names and descriptions +for the devices and nodes that it creates as well as assign session +and driver priorities. + +The session manager has the option to add extra properties on the +devices and nodes that it creates to control their behavior. This +is implemented with match rules. + +*/ diff --git a/doc/pipewire-daemon.dox b/doc/pipewire-daemon.dox new file mode 100644 index 0000000..c1a0649 --- /dev/null +++ b/doc/pipewire-daemon.dox @@ -0,0 +1,175 @@ +/** \page page_daemon PipeWire Daemon + +The PipeWire daemon is the central process that manages data exchange between +devices and clients. + +Typically general, users run one PipeWire daemon that listens for incoming +connections and manages devices. Clients (including the \ref +page_session_manager) are separate processes that talk to the daemon using the +PipeWire socket (default: `$XDG_RUNTIME_DIR/pipewire-0`). This approach +provides address-space separation between the privileged daemon and +non-privileged clients. + +\dot +digraph pw { + compound=true; + node [shape="box"]; + + subgraph cluster_pw { + rankdir="TB"; + label="PipeWire daemon"; + style="dashed"; + + subgraph cluster_prot_native { + label="pipewire-module-protocol-native"; + style="solid"; + socket [label="$XDG_RUNTIME_DIR/pipewire-0"]; + mod_impl [label="module implementation"]; + + socket -> mod_impl; + } + core [label="PipeWire Core"]; + alsa [label="PipeWire ALSA support"]; + + mod_impl -> core; + core -> alsa; + } + + kernel + + client1 [ label="Media Player" ]; + client2 [ label="Audio Software" ]; + sm [ label="Session Manager", style="dotted" ]; + + client1 -> socket; + client2 -> socket; + sm -> socket; + alsa -> kernel; +} +\enddot + +As shown above, the protocol is handled by the \ref +page_module_protocol_native. From PipeWire's point-of-view this module is just +another module. + +# Configuration Files + +On startup, the daemon reads a configuration file to configure itself. +It executes a series of commands listed in the config file. The lookup order +for configuration files are: + +- `$XDG_CONFIG_HOME/pipewire/pipewire.conf` (usually `$HOME/.config/pipewire/pipewire.conf`) +- `$sysconfdir/pipewire/pipewire.conf` (usually `/etc/pipewire/pipewire.conf`) +- `$datadir/pipewire/pipewire.conf` (usually `/usr/share/pipewire/pipewire.conf`) + +The first configuration file found is loaded as the base configuration. + +Next, configuration sections are collected in the directories in this +order: + +- `$datadir/pipewire/pipewire.conf.d/` (usually `/usr/share/pipewire/pipewire.conf.d/`) +- `$sysconfdir/pipewire/pipewire.conf.d/` (usually `/etc/pipewire/pipewire.conf.d/`) +- `$XDG_CONFIG_HOME/pipewire/pipewire.conf.d/` (usually `$HOME/.config/pipewire/pipewire.conf.d/`) + +They are applied to the global configuration file. Properties are overwritten +and array elements are appended. This makes it possible to make small custom customizations +or additions to the main configuration file. + +The environment variables `PIPEWIRE_CONFIG_DIR`, `PIPEWIRE_CONFIG_PREFIX`, +and `PIPEWIRE_CONFIG_NAME`. Can be used to specify an alternative configuration +directory, subdirectory, and filename respectively. + +## Configuration File Format + +PipeWire's configuration file format is JSON. In addition to true JSON +PipeWire also understands a more compact JSON representation. Where +`"` can be omitted around strings, no trailing commas are required and +`:` or `=` can be used to separate object keys from their values. +Also, `#` can be used to start a comment until the end of the line. + +The configuration file format is grouped into sections. A section is +either a dictionary (`{}`) or an array (`[]`). Dictionary and array entries +are separated by whitespace and may be simple value assignment, an array or +a dictionary. For example: + +``` +# A dictionary section +context.properties = { + # Keys often have a dot notation + core.daemon = true +} + +# An array section containing three dictionary objects +context.modules = [ + # a dictionary object with one key assigned to a string + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-profiler } + + # a dictionary object with two keys, one assigned to a string + # the other one to an array of strings + { name = libpipewire-module-portal + flags = [ ifexists nofail ] + } +] +``` + +Allowed configuration file sections are: + +- **context.properties** (dictionary): These properties configure the + pipewire instance. +- **context.spa-libs** (dictionary): Maps plugin features with globs to a + spa library. +- **context.modules** (array): Each entry in the array is a dictionary with + the name of the module to load, including optional args and flags. Most + modules support being loaded multiple times. +- **context.objects** (array): Each entry in the array is a dictionary con‐ + taining the factory to create an object from and optional extra argu‐ + ments specific to that factory. +- **context.exec** (array): Each entry in the array is dictionary containing + the path of a program to execute on startup and optional args. This ar‐ + ray usually contains an entry to start the session manager. + + +# Logging + +The `PIPEWIRE_DEBUG` environment variable can be used to enable +more debugging. This variable supports the following format: + +- `PIPEWIRE_DEBUG=[<level>][,<glob1>:<level1>][,<glob2>:<level2>,...]` where the globs are + shell globs to match on log topics and the levels are the respective + log level to set for that topic. Globs are applied in order and a matching + glob overrides an earlier glob for that category. A level without a glob + prefix will set the global log level and is a more preformant version of + `*:<level>`. For example, `PIPEWIRE_DEBUG=E,mod.*:D,mod.foo:X` enables global error messages, + debugging on all modules but no messages on the foo module. +- `<level>` specifies the log level: + + + `X` or `0`: No logging is enabled. + + `E` or `1`: Error logging is enabled. + + `W` or `2`: Warnings are enabled. + + `I` or `3`: Informational messages are enabled. + + `D` or `4`: Debug messages are enabled. + + `T` or `5`: Trace messages are enabled. These messages can be logged + from the realtime threads. + +PipeWire uses a `category.topic` naming scheme, with the following categories: + +- `pw.*`: PipeWire internal topics. +- `mod.*`: Module topics, for example `mod.foo` would usually refer to the + `foo` module. +- `ms.*`: Media session topics. +- `ms.mod.*`: Media session modules, for example `ms.foo` would usually refer + to the `media-session-foo` module. +- `conn.*`: Connection specific topics such as printing raw messages sent over + a communication socket. These are in a separate namespace as they are + usually vastly more verbose than the normal debugging topics. + This namespace must be explicitly enabled with a `conn.<glob>` glob. + +The behavior of the logging can be further controlled with the following +environment variables: + +- `PIPEWIRE_LOG_SYSTEMD=false`: Disable logging to the systemd journal. +- `PIPEWIRE_LOG=<filename>`: Redirect the log to the given filename. +- `PIPEWIRE_LOG_LINE=false`: Don't log filename, function, and source code line. + +*/ diff --git a/doc/pipewire-design.dox b/doc/pipewire-design.dox new file mode 100644 index 0000000..59b29ed --- /dev/null +++ b/doc/pipewire-design.dox @@ -0,0 +1,70 @@ +/** \page page_design Design + +A short overview of PipeWire's design. + +PipeWire is a media server that can run graphs of multimedia nodes. +Nodes can run inside the server process or in separate processes, +communicating with the server. + +PipeWire was designed to: + +- Be efficient for raw video using fd passing and audio with + shared ringbuffers. +- Be able to provide/consume/process media from any process. +- Provide policy to restrict access to devices and streams. +- Be easily extensible. + +Although an initial goal, the design is not limited to raw video +only and should be able to handle compressed video and other +media as well. + +PipeWire uses the \ref page_spa "SPA plugin API" for the nodes in the graph. +SPA is designed for low-latency and efficient processing of any multimedia +format. SPA also provides a number of helper utilities that are not available +in the standard C library. + +Some of the application we intend to build: + +- v4l2 device provider: Provide controlled access to v4l2 devices + and share one device between multiple processes. +- gnome-shell video provider: GNOME Shell provides a node that + gives the contents of the frame buffer for screen sharing or + screen recording. +- Audio server: Mix and playback multiple audio streams. The design + is more like CRAS (Chromium audio server) than PulseAudio and with + the added benefit that processing can be arranged in a graph. +- Professional audio graph processing like JACK. +- Media playback backend. + + +# Protocol + +The native protocol and object model is similar to +[Wayland](https://wayland.freedesktop.org) but with custom +serialization/deserialization of messages. This is because the data structures +in the messages are more complicated and not easily expressible in XML. +See \ref page_module_protocol_native for details. + + +# Extensibility + +The functionality of the server is implemented and extended with modules and +extensions. Modules are server side bits of logic that hook into various +places to provide extra features. This mostly means controlling the processing +graph in some way. See \ref page_pipewire_modules for a list of current +modules. + +Extensions are the client side version of the modules. Most extensions provide +both a client side and server side init function. New interfaces or new object +implementation can easily be added with modules/extensions. + +Some of the extensions that can be written: + +- Protocol extensions: A client/server side API (.h) together with protocol + extensions and server/client side logic to implement a new object or + interface. +- A module to check security of method calls. +- A module to automatically create, link or relink nodes. +- A module to suspend idle nodes. + +*/ diff --git a/doc/pipewire-library.dox b/doc/pipewire-library.dox new file mode 100644 index 0000000..cecb930 --- /dev/null +++ b/doc/pipewire-library.dox @@ -0,0 +1,240 @@ +/** \page page_library PipeWire Library + +There are two main components that make up the PipeWire library: + +1. An implementation of a graph based media processing engine. +2. An asynchronous IPC mechanism to manipulate and introspect + a graph in another process. + +There is usually a daemon that implements the global graph and +clients that operate on this graph. + +The IPC mechanism in PipeWire is inspired by Wayland in that it +follows the same design principles of objects and methods/events +along with how this API is presented to the user. + +PipeWire has a plugin architecture that allows new features to +be added (or removed) by the user. Plugins can hook into many +aspects of PipeWire and change the behaviour or number of +features dynamically. + + +# Principles + +The PipeWire API is an object oriented asynchronous protocol. +All requests and replies are method invocations on some object. + +Objects are identified with a unique ID. Each object implements an +interface and requests result in invocations of methods on the +interface. + +The protocol is message based. A message sent by a client to the +server is called a method. A message from the server to the client +is called an event. Unlike Wayland, these messages are not (yet) +described in an external protocol file but implemented directly in +a protocol plugin. Protocol plugins can be added to add new +objects or even protocols when required. + +Messages are encoded with \ref page_spa_pod, which make it +possible to encode complex objects with right types. + +Events from the server can be a reply to a method or can be emitted +when the server state changes. + +Upon connecting to a server, it will broadcast its state. Clients +should listen for these state changes and cache them. There is no +need (or mechanism) to query the state of the server. + +The server also has a registry object that, when listening to, +will broadcast the presence of global objects and any changes in +their state. + +State about objects can be obtained by binding to them and listening +for state changes. + + +# Versioning + +All interfaces have a version number. The maximum supported version +number of an interface is advertized in the registry global event. + +A client asks for a specific version of an interface when it binds +to them. It is the task of the server to adapt to the version of the +client. + +Interfaces increase their version number when new methods or events +are added. Methods or events should never be removed or changed for +simplicity. + + +# Proxies and Resources + +When a client connects to a PipeWire daemon, a new `struct pw_proxy` +object is created with ID 0. The `struct pw_core` interface is +assigned to the proxy. + +On the server side there is an equivalent `struct pw_resource` with +ID 0. Whenever the client sends a message on the proxy (by calling +a method on the interface of the proxy) it will transparently result +in a callback on the resource with the same ID. + +Likewise if the server sends a message (an event) on a resource, it +will result in an event on the client proxy with the same ID. + +PipeWire will notify a client when a resource ID (and thus also proxy +ID) becomes unused. The client is responsible for destroying the +proxy when it no longer wants to use it. + + +# Interfaces + +## struct pw_loop + +An abstraction for a `poll(2)` loop. It is usually part of one of: + +- `struct pw_main_loop`: A helper that can run and stop a `pw_loop`. +- `struct pw_thread_loop`: A helper that can run and stop a `pw_loop` + in a different thread. It also has some helper + functions for various thread related synchronization + issues. +- `struct pw_data_loop`: A helper that can run and stop a `pw_loop` + in a real-time thread along with some useful helper + functions. + +## struct pw_context + +The main context for PipeWire resources. It keeps track of the mainloop, +loaded modules, the processing graph and proxies to remote PipeWire +instances. + +An application has to select an implementation of a `struct pw_loop` +when creating a context. + +The context has methods to create the various objects you can use to +build a server or client application. + +## struct pw_core + +A proxy to a remote PipeWire instance. This is used to send messages +to a remote PipeWire daemon and to receive events from it. + +A core proxy can be used to receive errors from the remote daemon +or to perform a roundtrip message to flush out pending requests. + +Other core methods and events are used internally for the object +life cycle management. + +## struct pw_registry + +A proxy to a PipeWire registry object. It emits events about the +available objects on the server and can be used to bind to those +objects in order to call methods or receive events from them. + +## struct pw_module + +A proxy to a loadable module. Modules implement functionality such +as provide new objects or policy. + +## struct pw_factory + +A proxy to an object that can create other objects. + +## struct pw_device + +A proxy to a device object. Device objects model a physical hardware +or software device in the system and can create other objects +such as nodes or other devices. + +## struct pw_node + +A Proxy to a processing node in the graph. Nodes can have input and +output ports and the ports can be linked together to form a graph. + +## struct pw_port + +A Proxy to an input or output port of a node. They can be linked +together to form a processing graph. + +## struct pw_link + +A proxy to a link between in output and input port. A link negotiates +a format and buffers between ports. A port can be linked to many other +ports and PipeWire will manage mixing and duplicating the buffers. + + +# High Level Helper Objects + +Some high level objects are implemented to make it easier to interface +with a PipeWire graph. + +## struct pw_filter + +A `struct pw_filter` allows you implement a processing filter that can +be added to a PipeWire graph. It is comparable to a JACK client. + +## struct pw_stream + +A `struct pw_stream` makes it easy to implement a playback or capture +client for the graph. It takes care of format conversion and buffer +sizes. It is comparable to Core Audio AudioQueue or a PulseAudio +stream. + + +# Security + +With the default native protocol, clients connect to PipeWire using +a named socket. This results in a client socket that is used to +send messages. + +For sandboxed clients, it is possible to get the client socket via +other ways, like using the portal. In that case, a portal will +do the connection for the client and then hands the connection socket +to the client. + +All objects in PipeWire have per client permission bits, currently +READ, WRITE, EXECUTE and METADATA. A client can not see an object +unless it has READ permissions. Similarly, a client can only execute +methods on an object when the EXECUTE bit is set and to modify the +state of an object, the client needs WRITE permissions. + +A client (the portal after it makes a connection) can drop permissions +on an object. Once dropped, it can never reacquire the permission. + +Clients with WRITE/EXECUTE permissions on another client can +add and remove permissions for the client at will. + +Clients with MODIFY permissions on another object can set or remove +metadata on that object. + +Clients that need permissions assigned to them can be started in +blocked mode and resume when permissions are assigned to them by +a session manager or portal, for example. + +PipeWire uses memfd (`memfd_create(2)`) or DMA-BUF for sharing media +and data between clients. Clients can thus not look at other clients +data unless they can see the objects and connect to them. + + +# Implementation + +PipeWire also exposes an API to implement the server side objects in +a graph. + + +# Error Reporting + +Functions return either NULL with errno set or a negative int error +code when an error occurs. Error codes are used from the SPA plugin +library on which PipeWire is built. + +Some functions might return asynchronously. The error code for such +functions is positive and SPA_RESULT_IS_ASYNC() will return true. +SPA_RESULT_ASYNC_SEQ() can be used to get the unique sequence number +associated with the async operation. + +The object returning the async result code will have some way to +signal the completion of the async operation (with, for example, a +callback). The sequence number can be used to see which operation +completed. + +*/ diff --git a/doc/pipewire-midi.dox b/doc/pipewire-midi.dox new file mode 100644 index 0000000..77c2b27 --- /dev/null +++ b/doc/pipewire-midi.dox @@ -0,0 +1,103 @@ +/** \page page_midi MIDI Support + +This document explains how MIDI is implemented. + + +# Use Cases + +## MIDI Devices Are Made Available As Processing Nodes/Ports + +Applications need to be able to see a port for each stream of a +MIDI device. + +## MIDI Devices Can Be Plugged and Unplugged + +When devices are plugged and unplugged the associated nodes/ports +need to be created and removed. + +## Applications Can Connect To MIDI Devices + +Applications can create ports that can connect to the MIDI ports +so that data can be provided to or consumed from them. + +## Some MIDI Devices Are Sinks Or Sources For MIDI Data + +It should be possible to create a MIDI sink or source that routes the +MIDI events to specific MIDI ports. + +One example of such a sink would be in front of a software MIDI +renderer. + +An example of a MIDI source would be after a virtual keyboard or +as a mix from many MIDI input devices. + +## Applications Should Auto-connect To MIDI Sinks Or Sources + +An application should be able to be connected to a MIDI sink when +it wants to play MIDI data. + +An application should be able to connect to a MIDI source when it +wants to capture MIDI data. + + +# Design + +## SPA + +MIDI devices/streams are implemented with an \ref spa_node with generic +control input and output Ports. These ports have a media type of +`"application/control"` and the data transported over these ports +are of type \ref spa_pod_sequence with the \ref spa_pod_control type set to +\ref SPA_CONTROL_Midi. + +This means that every MIDI event is timestamped with the sample +offset against the current graph clock cycle to get sample accurate +midi events that can be aligned with the corresponding sample data. + +Since the MIDI events are embedded in the generic control stream, +they can be interleaved with other control message types, such as +property updates or OSC messages. + +## The PipeWire Daemon + +Nothing special is implemented for MIDI. Negotiation of formats +happens between `"application/control"` media types and buffers are +negotiated in the same way as any generic format. + +## The Session Manager + +The session manager needs to create the MIDI nodes/ports for the available +devices. + +This can either be done as a single node with ports per device/stream +or as separate nodes created by a MIDI device monitor. + +The session manager needs to be aware of the various MIDI sinks and sources +in order to route MIDI streams to them from applications that want this. + + +# Implementation + +## PipeWire Media Session + +PipeWire media session uses the \ref SPA_NAME_API_ALSA_SEQ_BRIDGE plugin for +the MIDI features. This creates a single SPA Node with ports per +MIDI client/stream. + +The media session will check the permissions on `/dev/snd/seq` before +attempting to create this node. It will also use inotify to wait +until the sequencer device node is accessible. + +## JACK + +JACK assumes all `"application/control"` ports are MIDI ports. + +The control messages are converted to the JACK event format by +filtering out the \ref SPA_CONTROL_Midi types. On output ports, the JACK +event stream is converted to control messages in a similar way. + +There is a 1 to 1 mapping between the JACK events and control +messages so there is no information loss or need for complicated +conversions. + +*/ diff --git a/doc/pipewire-modules.dox b/doc/pipewire-modules.dox new file mode 100644 index 0000000..322d258 --- /dev/null +++ b/doc/pipewire-modules.dox @@ -0,0 +1,84 @@ +/** \page page_pipewire_modules PipeWire Modules + +A PipeWire module is effectively a PipeWire client in an `.so` file that +shares the \ref pw_context with the loading entity. Usually modules are +loaded when they are listed in the configuration files. For example the +default configuration file loads several modules: + +``` +context.modules = [ + ... + # The native communication protocol. + { name = libpipewire-module-protocol-native } + + # The profile module. Allows application to access profiler + # and performance data. It provides an interface that is used + # by pw-top and pw-profiler. + { name = libpipewire-module-profiler } + + # Allows applications to create metadata objects. It creates + # a factory for Metadata objects. + { name = libpipewire-module-metadata } + + # Creates a factory for making devices that run in the + # context of the PipeWire server. + { name = libpipewire-module-spa-device-factory } + ... +] +``` +The matching libraries are: +``` +$ ls -1 /usr/lib64/pipewire-0.3/libpipewire-module* +... +/usr/lib64/pipewire-0.3/libpipewire-module-metadata.so +/usr/lib64/pipewire-0.3/libpipewire-module-profiler.so +/usr/lib64/pipewire-0.3/libpipewire-module-protocol-native.so +/usr/lib64/pipewire-0.3/libpipewire-module-spa-device-factory.so +... +``` + +A module's entry point is the `pipewire__module_init` function, see \ref +PIPEWIRE_SYMBOL_MODULE_INIT. + +\code +int pipewire__module_init(struct pw_impl_module *module, const char *args).` +\endcode + +See the \ref page_module_example_sink and \ref page_module_example_source +modules for a general oveview of how modules look like. + +List of known modules: + +- \subpage page_module_access +- \subpage page_module_adapter +- \subpage page_module_avb +- \subpage page_module_client_device +- \subpage page_module_client_node +- \subpage page_module_combine_stream +- \subpage page_module_echo_cancel +- \subpage page_module_example_sink +- \subpage page_module_example_source +- \subpage page_module_fallback_sink +- \subpage page_module_filter_chain +- \subpage page_module_link_factory +- \subpage page_module_loopback +- \subpage page_module_metadata +- \subpage page_module_pipe_tunnel +- \subpage page_module_portal +- \subpage page_module_profiler +- \subpage page_module_protocol_native +- \subpage page_module_protocol_pulse +- \subpage page_module_protocol_simple +- \subpage page_module_pulse_tunnel +- \subpage page_module_raop_sink +- \subpage page_module_raop_discover +- \subpage page_module_roc_sink +- \subpage page_module_roc_source +- \subpage page_module_rtp_sink +- \subpage page_module_rtp_source +- \subpage page_module_rt +- \subpage page_module_session_manager +- \subpage page_module_x11_bell +- \subpage page_module_zeroconf_discover + +*/ diff --git a/doc/pipewire-objects-design.dox b/doc/pipewire-objects-design.dox new file mode 100644 index 0000000..f67b6b6 --- /dev/null +++ b/doc/pipewire-objects-design.dox @@ -0,0 +1,347 @@ +/** \page page_objects_design Objects Design + +This document is a design reference on the various objects that exist +in the PipeWire media and session management graphs. Explaining what these +objects are, how they are meant to be used, and how they relate to other +kinds of objects and concepts that exist in subsystems or other libraries. + + +# The Media Graph + +The media graph represents and enables the media flow inside the PipeWire +daemon and between the daemon and its clients. It consists of nodes, ports +and links. + +``` ++------------+ +------------+ +| | | | +| +--------+ Link +--------+ | +| Node | Port |--------| Port | Node | +| +--------+ +--------+ | +| | | | ++------------+ +------------+ +``` + +## Node + +A **node** is a media processing element. It consumes and/or produces buffers +that contain data, such as audio or video. + +A node may operate entirely inside the PipeWire daemon or it may be operating +in a client process. In the second case, media is transferred to/from that +client using the PipeWire protocol. + +In an analogy to GStreamer, a _node_ is similar (but not equal) to a +GStreamer _element_. + +## Port + +A **port** is attached on a **node** and provides an interface for input +or output of media on the node. A node may have multiple ports. + +A port always has a direction, input or output: + +- Input: it allows media input into the node (in other terms, it is a _sink_) +- Output: it outputs media out of the node (in other terms, it is a _source_) + +In an analogy to GStreamer, a _port_ is similar (but not equal) to a +GStreamer _pad_. + +## Link + +A **link** connects two ports of opposite direction, making media flow from +the output port to the input port. + + +# The Session Management Graph + +The session management graph is a virtual, higher level representation of the +media flow. It is created entirely by the session manager and it can affect +the routing on the media graph only through the session manager's actions. + +The session management graph is useful to abstract the complexity of the +actual media flow both for the target user and for the policy management +codebase. + +``` ++---------------------+ +----------------------+ +| | | | +| +----------------+ Endpoint Link +----------------+ | +| Endpoint |Endpoint Stream |-----------------|Endpoint Stream | Endpoint | +| +----------------+ +----------------+ | +| | | | ++---------------------+ +----------------------+ +``` + +## Endpoint + +An **endpoint** is a session management object that provides a representation +of user conceivable places where media can be routed to/from. + +Examples of endpoints associated with hardware on a desktop-like system: + +- Laptop speakers. +- USB webcam. +- Bluetooth headset microphone. +- Line out stereo jack port. + +Examples of endpoints associated with hardware in a car: + +- Speakers amplifier. +- Front right seat microphone array. +- Rear left seat headphones. +- Bluetooth phone voice gateway. +- Hardware FM radio device. + +Examples of endpoints associated with software: + +- Desktop screen capture source. +- Media player application. +- Camera application. + +In most cases an endpoint maps to a node on the media graph, but this is not +always the case. An endpoint may be backed by several nodes or no nodes at all. +Different endpoints may also be sharing nodes in some cases. + +An endpoint that does not map to any node may be useful to represent hardware +that the session manager needs to be able to control, but there is no way +to route media to/from that hardware through the PipeWire media graph. For +example, in a car we may have a CD player device that is directly wired to the +speakers amplifier and therefore audio flows between them without passing +through the controlling CPU. However, it is useful for the session manager to +be able to represent the *CD player endpoint* and the _endpoint link_ between +it and the amplifier, so that it can apply audio policy that takes into account +whether the CD player is playing or not. + +### Target + +An **endpoint** may be grouping together targets that can be reached by +following the same route and they are mutually exclusive with each other. + +For example, the speakers and the headphones jack on a laptop are usually +mutually exclusive by hardware design (hardware mutes the speakers when the +headphones are enabled) and they share the same ALSA PCM device, so audio still +follows the same route to reach both. + +In this case, a session manager may choose to group these two targets into the +same endpoint, using a parameter on the _endpoint_ object to allow the user +to choose the target (if the hardware allows configuring this at all). + +## Endpoint Stream + +An **endpoint stream** is attached to an **endpoint** and represents a logical +path that can be taken to reach this endpoint, often associated with +a _use case_. + +For example, the "Speakers amplifier" endpoint in a car might have the +following streams: + +- _Music_: A path to play music; + the implementation will output this to all speakers, using the volume + that has been configured for the "Music" use case. +- _Voice_: A path to play a voice message; such as a navigation message or + feedback from a voice assistant, the implementation will output this + to the front speakers only. Lowering the volume of the music (if any) + on these speakers at the same time. +- _Emergency_: A path to play an emergency situation sound (a beep, + or equivalent); the implementation will output this on all speakers. + Increasing the volume to a factory defined value if necessary (to ensure + that it is audible) while muting audio from all other streams at the + same time. + +In another example, a microphone that can be used for activating a voice +assistant might have the following streams: + +- _Capture_: A path to capture directly from the microphone; this can be used + by an application that listens for the assistant's wake-word in order + to activate the full voice recognition engine. +- _CaptureDelayed_: A path to capture with a constant delay (meaning that + starting capturing now will actually capture something that was spoken + a little earlier); this can be used by the full voice recognition engine, + allowing it to start after the wake-word has been spoken while capturing + audio that also includes the wake-word. + +Endpoint streams may be mutually exclusive or they may used simultaneously, +depending on the implementation. + +Endpoint streams may be implemented in many ways: + +- By plugging additional nodes in the media graph that link to the device node + (ex. a simple buffering node linked to an alsa source node could implement + the _CaptureDelayed_ stream in the above microphone example). +- By using a different device node (ex. different ALSA device on the same card) + that has a special meaning for the hardware. +- By triggering switches on the hardware (ex. modify ALSA controls on the + same device). + +## Endpoint Link + +An **endpoint link** connects two streams from two different endpoints, creating +a logical representation of media flow between the endpoints. + +An **endpoint link** may be implemented by creating one or more _links_ in the +underlying media graph, or it may be implemented by configuring hardware +resources to enable media flow, in case the flow does not pass through the +media graph. + +### Constructing + +Constructing an **endpoint link** is done by asking the _endpoint stream_ +objects to prepare it. First, the source stream is asked to provide linking +information. When the information is retrieved, the sink stream is asked to +use this information to prepare and to provide its own linking information. +When this is done, the session manager is asked to create the link using the +provided information. + +This mechanism allows stream implementations: + +- To prepare for linking, adjusting hardware paths if necessary. +- To check for stream linking compatibility; not all streams can be connected + to all others (ex. streams with media flow in the hardware cannot be linked + to streams that are backed by nodes in the media graph). +- To provide implementation specific information for linking; in the standard + case this is going to be a list of _ports_ to be linked in the media graph, + but in a hardware-flow case it can be any kind of hardware-specific detail. + + +# Other Related Objects + +## Device + +A **device** represents a handle to an underlying API that is used to create +higher level objects, such as nodes, or other devices. + +Well-known devices include: + +| Device API | Description | +| :--- | :--- | +| alsa.pcm.device | A handle to an ALSA card (ex. `hw:0`, `hw:1`, etc). | +| alsa.seq.device | A handle to an ALSA Midi device. | +| v4l2.device | A handle to a V4L2 device (`/dev/video0`, `/dev/video1`, etc..). | +| jack.device | A JACK client, allowing PipeWire to slave to JACK for audio input/output. | + +A device may have a _profile_, which allows the user to choose between +multiple configurations that the device may be capable of having, or to simply +turn the device _off_, which means that the handle is closed and not used +by PipeWire. + +## Session + +The **session** represents the session manager and can be used to expose +global properties or methods that affect the session management. + +### Default Endpoints + +The session is responsible for book-keeping the default device endpoints (one +for each kind of device) that is to be used to link new clients when +simulating a PulseAudio-like behavior, where the user can choose from the UI +device preferences. + +For example, a system may have both "Speakers" and "HDMI" endpoints on the +"Audio Output" category and the user may be offered to make a choice within +the UI to select which endpoint they want to use by default for audio output. +This preference is meant to be stored in the session object. + +### Multiple Sessions + +It is not currently defined whether it is allowed to have multiple sessions +or not and how the system should behave if this happens. + + +# Mappings To Underlying Subsystem Objects + +## ALSA UCM + +This is a ***proposal*** + +| ALSA / UCM | PipeWire | +| :--- | :--- | +| ALSA card | device | +| UCM verb | device profile | +| UCM device | endpoint (+ target, grouping conflicting devices into the same endpoint) | +| UCM modifier | endpoint stream | +| PCM stream | node | + +In UCM mode, an ALSA card is represented as a PipeWire device, with the +available UCM verbs listed as profiles of the device. + +Activating a profile (ie. a verb) will create the necessary nodes for the +available PCM streams and at the same time it will also create one endpoint +for each UCM device. Optionally conflicting UCM devices can be grouped in +the same endpoint, listing the conflicting options as targets of the endpoint. + +The available UCM modifiers for each UCM device will be added as streams, plus +one "default" stream for accessing the device with no modifiers. + +## ALSA Fallback + +| ALSA | PipeWire | +| :--- | :--- | +| card | device | +| PCM stream | node + endpoint | + +In the case where UCM (or another similar mechanism) is not available, +ALSA cards are represented as PipeWire devices with only two profiles on/off. + +When the on profile is activated, a node and an associated endpoint are created +for every available PCM stream. + +Endpoints in this case have only one "default" stream, unless they are extended +by the session manager to have software-backed streams. + +## V4L2 + +***FIXME*** + +| V4L2 | PipeWire | +| :--- | :--- | +| device | device + node | + + +# Relationship To Other API's + +## PulseAudio + +### Mapping PipeWire Objects For Access By PulseAudio Clients + +| PipeWire | PulseAudio | +| :--- | :--- | +| device | card | +| device profile | card profile | +| endpoint (associated with a device) | sink / source | +| endpoint (associated with a client) | sink-input / source-output | +| endpoint target | port | +| endpoint stream | N/A, PA clients will be limited to the default stream | + +### Mapping PulseAudio Clients To PipeWire + +| PulseAudio | PipeWire | +| :--- | :--- | +| stream | client + node + endpoint (no targets, 1 default stream) | + +## Jack + +Note: This section is about JACK clients connecting to PipeWire through the +JACK compatibility library. The scenario where PipeWire connects to another +JACK server as a client is out of scope here. + +### Mapping PipeWire Objects For Access By JACK Clients + +| PipeWire | JACK | +| :--- | :--- | +| node | client | +| port | port | +| device | N/A | +| endpoint | N/A | + +### Mapping JACK Clients To PipeWire + +| JACK | PipeWire | +| :--- | :--- | +| client | client + node | +| port | port | + +JACK clients do not create endpoints. A session manager should be JACK aware +in order to anticipate direct node linking. + +*/ diff --git a/doc/pipewire-portal.dox b/doc/pipewire-portal.dox new file mode 100644 index 0000000..721d981 --- /dev/null +++ b/doc/pipewire-portal.dox @@ -0,0 +1,215 @@ +/** \page page_portal Portal Access Control + +This document explains how clients from the portal are handled. + +The portal is a DBus service that exposes interfaces to +request access to the PipeWire daemon to perform a certain set of +functions. The PipeWire daemon runs outside the sandbox, the portal is a way +for clients inside the sandbox to connect to and use PipeWire. + +The PipeWire socket is not exposed in the sandbox. Instead, The portal +connects to PipeWire on behalf of the client, informing PipeWire that this +client is a portal-managed client. PipeWire can detect and enforce +extra permission checks on the portal managed clients. + +Once such portal is the [camera +portal](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.portal.Camera) +that provides a PipeWire session to stream video from a camera. + + +# Use Cases + +## New Portal Managed Clients Need Device Permissions Configured + +When a new client is detected, the available objects need to be +scanned and permissions configured for each of them. + +Only the devices belonging to the media_roles given by the +portal are considered. + +## New Devices Need To Be Made Visible To Portal Managed Clients + +Newly created objects are made visible to a client when the client +is allowed to interact with it. + +Only the devices belonging to the media_roles given by the +portal are considered. + +## Permissions For A Device Need To Be Revoked + +The session manager listens to changes in the permissions of devices +and will remove the client permissions accordingly. + +Usually this is implemented by listening to the permission store +DBus object. The desktop environment might provide a configuration panel +where these permissions can be managed. + + +# Design + +## The Portal + +A sandboxed client cannot connect to PipeWire directly. Instead, it connects +to the sandbox side of the portal which then connects the PipeWire daemon to +configure the session. The portal then hands the file descriptor of the +PipeWire connection to the client and the client can use this file descriptor +to interface with the PipeWire session directly. + +When the portal connects, it will set the following properties on the +client object: + +- `"pipewire.access.portal.is_portal" = true` for the connection of the + portal itself (as opposed to a client managed by the portal). +- `"pipewire.access.portal.app_id"` the [application ID](https://docs.flatpak.org/en/latest/conventions.html#application-ids) of the client. +- `"pipewire.access.portal.media_roles"` media roles of the client. + Currently only `"Camera"` is defined. + +Before returning the connection to a client, the portal configures +minimal permissions on the client. No objects are initially visible. It is +the task of the \ref page_session_manager to make the objects in the graph +visible, depending on the client's `media_roles` (see also \ref +PW_KEY_MEDIA_ROLE). + +## The PipeWire Portal Module + +The PipeWire daemon uses the \ref page_module_portal to find the PID of the +processes that owns the DBus name `org.freedesktop.portal.Desktop` +(see the [XDG Desktop Portal](https://github.com/flatpak/xdg-desktop-portal)). + +Client connections from this PID are tagged as \ref PW_KEY_ACCESS +`"portal"` (see \ref page_module_access). It will also set ALL permissions for +this client so that it can resume. + +\dot +digraph pw { + compound=true; + node [shape="box"]; + rankdir="TB"; + + dbus [label="org.freedesktop.portal.Desktop"]; + + portal_access [label="PipeWire (mod: Portal Access)"]; + portal [label="xdg-desktop-portal"]; + + dbus -> portal_access [arrowhead="dot"]; + dbus -> portal [arrowhead="dot"]; + + portal_access -> portal [label="pipewire.access = portal"]; + + { rank="same"; portal_access; portal} +} +\enddot + +## The Client + +A client can ask the portal for a connection to the PipeWire daemon. + +\dot +digraph pw { + compound=true; + node [shape="box"]; + rankdir="LR"; + + pw [label="PipeWire"]; + portal [label="xdg-desktop-portal"]; + client [label="client"]; + + client -> portal; + portal -> pw [label="portal.is_portal=true", arrowhead="none"] + + {rank="min"; pw}; + {rank="max"; client}; +} +\enddot + +The portal maintains an (unrestricted) connection to the PipeWire daemon with +`"pipewire.access.portal.is_portal" = true` to identify the nodes the client +needs access to. It then creates a new restricted connection for the client, +tagged with additional information. + +\dot +digraph pw { + compound=true; + node [shape="box"]; + rankdir="LR"; + + pw [label="PipeWire"]; + portal [label="xdg-desktop-portal"]; + client [label="client"]; + + client -> portal [arrowhead="none"]; + portal -> pw [label="portal.is_portal=true", arrowhead="none"] + portal -> pw [label="portal.app_id = $appid"] + + {rank="min"; pw}; + {rank="max"; client}; +} +\enddot + +The file descriptor for this restricted connection is passed back to the +client which can now make use of the resources it has been permitted to +access. + +\dot +digraph pw { + compound=true; + node [shape="box"]; + rankdir="LR"; + + pw [label="PipeWire"]; + portal [label="xdg-desktop-portal"]; + client [label="client"]; + + portal -> pw [label="portal.is_portal=true", arrowhead="none"] + + pw->client [label="restricted connection"]; + + {rank="min"; pw}; + {rank="max"; client}; +} +\enddot + +## The Session Manager + +The session manager listens for new clients to appear. It will use the +\ref PW_KEY_ACCESS property to find portal connections. For client connections +from the portal the session manager checks the requested `media_roles` and +enables or disables access to the respective PipeWire objects. +It might have to consult a database to decide what is allowed, for example the +[org.freedesktop.impl.portal.PermissionStore](https://flatpak.github.io/xdg-desktop-portal/portal-docs.html#gdbus-org.freedesktop.impl.portal.PermissionStore). + +\dot +strict digraph pw { + compound=true; + node [shape="box"]; + rankdir="LR"; + + portal [label="xdg-desktop-portal"]; + client [label="client"]; + + + subgraph { + rankdir="TB"; + permissions [label="PermissionStore"]; + + sm->permissions; + + sm [label="Session Manager"]; + pw [label="PipeWire"]; + sm -> pw [headlabel="allow $media.roles"]; + pw -> sm; + portal -> pw [label="portal.app_id = $appid"]; + } + + client -> portal [arrowhead="none"]; + + {rank="min"; sm, pw}; + {rank="max"; client}; +} +\enddot + +In the case of the [XDG Desktop +Portal](https://github.com/flatpak/xdg-desktop-portal), the portal itself +queries the PermissionStore directly. + +*/ diff --git a/doc/pipewire-session-manager.dox b/doc/pipewire-session-manager.dox new file mode 100644 index 0000000..d21ba9a --- /dev/null +++ b/doc/pipewire-session-manager.dox @@ -0,0 +1,46 @@ +/** \page page_session_manager PipeWire Session Manager + +The \ref page_daemon is primarily a framework that allows devices and +applications to exchange data. + +It provides the mechanism to do so but the policy deciding which components +can talk to each other and when is controlled by the session manager. As +outlined in \ref page_objects_design, PipeWire provides a media graph +consisting of devices, nodes and ports. The session manager is the one that +decides on the links between those elements. + +Two prominent session managers currently exist: + +- [PipeWire Media Session](https://gitlab.freedesktop.org/pipewire/media-session), the +example session manager. +- [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber), a +modular session manager based on GObject. +[Documentation](https://pipewire.pages.freedesktop.org/wireplumber/) + +This page describes some of the requirements for session managers in general. + + +# Client Management + +PipeWire provides a \ref page_access "permission system" to limit client's +access to resources but only \ref page_module_access "basic permission +handling". The session manager is expected to decide whether clients may +access specific resources. + + +# Device Management + +PipeWire's responsibility is to open devices, however the decision on which +devices should be opened is the job of a session manager, including the +configuration of those devices. + + +# Endpoint Grouping + +An endpoint is, effectively, a group of nodes that are a logical unit that can +consume or produce media data. For example, a Bluetooth speaker may present as +several nodes but is only one logical unit to stream audio to. + +See \ref page_objects_design for details on Endpoints. + +*/ diff --git a/doc/pipewire-tools.dox.in b/doc/pipewire-tools.dox.in new file mode 100644 index 0000000..e0bf116 --- /dev/null +++ b/doc/pipewire-tools.dox.in @@ -0,0 +1,7 @@ +/** \page page_tools PipeWire Tools + +Manual pages: + +@man_subpages@ + +*/ diff --git a/doc/pipewire.dox b/doc/pipewire.dox new file mode 100644 index 0000000..03b9c6f --- /dev/null +++ b/doc/pipewire.dox @@ -0,0 +1,26 @@ +/** \page page_pipewire PipeWire Design + +# Internals + +- \subpage page_design +- \subpage page_audio +- \subpage page_access +- \subpage page_portal +- \subpage page_midi +- \subpage page_objects_design +- \subpage page_library +- \subpage page_dma_buf + + +# Components + +- \subpage page_daemon +- \subpage page_tools +- \subpage page_session_manager + + +# Backends + +- \subpage page_pulseaudio + +*/ diff --git a/doc/pulseaudio.dox b/doc/pulseaudio.dox new file mode 100644 index 0000000..2ac19a9 --- /dev/null +++ b/doc/pulseaudio.dox @@ -0,0 +1,69 @@ +/** \page page_pulseaudio PulseAudio Compatibility + +# Internals - Mapping Between ALSA and Streams + +This explains the mapping between alsa cards and streams and session manager +objects. + +## ALSA Cards + +An ALSA card is exposed as a PipeWire device. + +## Streams + +Each ALSA PCM is opened and a node is created for each PCM stream. + + +# Session Manager + +## ALSA UCM + +The mapping of the PipeWire object hierarchy to the ALSA object hierarchy is the following: + +One PipeWire device is created for every ALSA card. + +- For each UCM verb, a node is created for the associated PCM devices. +- For each UCM verb, an endpoint is created. + +In a first step: For each available combination of UCM device and modifier, +a stream is created. Streams are marked with compatible other streams. + +Streams with the same modifier and mutually exclusive devices are grouped +into one stream and the UCM devices are exposed on the endpoint as destinations. + +## ALSA Fallback + +Each PCM stream (node) becomes an endpoint. The endpoint references the +ALSA device ID. + +Each endpoint has one stream (for now) called HiFi Playback / HiFi Capture. + +More streams can be created depending on the format of the node. + +## ALSA Pulse UCM + +Using the ALSA backend of PulseAudio we can create the following streams. + +## ALSA Pulse Fallback + +The pulse ALSA backend will use the mixer controls and some probing to +create the following nodes and endpoints. + + +# PulseAudio + +PulseAudio uses the session manager API to construct cards with profiles +and sink/source with ports. + +If an endpoint references a device, a card object is created for the device. + +Each endpoint becomes a sink/source. + +Each Stream in the endpoint becomes a profile on the PulseAudio card. Because +only one profile is selected on the device, only one stream is visible on +the endpoint. This clashes with the notion that multiple streams can be +active at the same time but is a PulseAudio limitation. + +Each Endpoint destination becomes a port on the sink/source. + +*/ diff --git a/doc/spa-buffer.dox b/doc/spa-buffer.dox new file mode 100644 index 0000000..ddd0935 --- /dev/null +++ b/doc/spa-buffer.dox @@ -0,0 +1,71 @@ +/** \page page_spa_buffer SPA Buffers + +> What is the array of `spa_data` in `spa_buffer`? + +A \ref spa_buffer "SPA Buffer" contains metadata and data. There can be many metadata items (headers, color info, cursor position, etc) in the buffer. The metadata items are stored in the metas array. In the same way, the buffer can contain multiple data blocks in the datas array. Each data block is, for example, a video plane or an audio channel. There are `n_datas` of those blocks. + +> What is the `void*` data pointer in `spa_data`? + +The data information either has a file descriptor or a data pointer. The type of the `spa_data` tells you what to expect. For a file descriptor, the data pointer can optionally be set when the FD is mapped into memory. Otherwise the user has to mmap the data themselves. + +Also associated with each `spa_data` is a chunk, which is read/write and contains the valid region in the `spa_data` (offset, size, stride and some flags). + +The reason why is this set up like this is that the metadata memory, the data and chunks can be directly transported in shared memory while the buffer structure can be negotiated separately (describing the shared memory). This way buffers can be shared but no process can destroy the structure of the buffers. + + + * The buffer skeleton is placed in memory like below and can + * be accessed as a regular structure. + * + * +==============================+ + * | struct spa_buffer | + * | uint32_t n_metas | number of metas + * | uint32_t n_datas | number of datas + * +-| struct spa_meta *metas | pointer to array of metas + * +|-| struct spa_data *datas | pointer to array of datas + * || +------------------------------+ + * |+>| struct spa_meta | + * | | uint32_t type | metadata + * | | uint32_t size | size of metadata + * +|--| void *data | pointer to metadata + * || | ... <n_metas> | more spa_meta follow + * || +------------------------------+ + * |+->| struct spa_data | + * | | uint32_t type | memory type + * | | uint32_t flags | + * | | int fd | fd of shared memory block + * | | uint32_t mapoffset | offset in shared memory of data + * | | uint32_t maxsize | size of data block + * | +-| void *data | pointer to data + * |+|-| struct spa_chunk *chunk | pointer to chunk + * ||| | ... <n_datas> | more spa_data follow + * ||| +==============================+ + * VVV + * + * metadata, chunk and memory can either be placed right + * after the skeleton (inlined) or in a separate piece of memory. + * + * vvv + * ||| +==============================+ + * +-->| meta data memory | metadata memory, 8 byte aligned + * || | ... <n_metas> | + * || +------------------------------+ + * +->| struct spa_chunk | memory for n_datas chunks + * | | uint32_t offset | + * | | uint32_t size | + * | | int32_t stride | + * | | int32_t dummy | + * | | ... <n_datas> chunks | + * | +------------------------------+ + * +>| data | memory for n_datas data, aligned + * | ... <n_datas> blocks | according to alignments + * +==============================+ + +Taken from [here](https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/11f95fe11e07192cec19fddb4fafc708e023e49c/spa/include/spa/buffer/alloc.h). + + + +\addtogroup spa_buffer + +See: \ref page_spa_buffer + +*/ diff --git a/doc/spa-design.dox b/doc/spa-design.dox new file mode 100644 index 0000000..403e27d --- /dev/null +++ b/doc/spa-design.dox @@ -0,0 +1,35 @@ +/** \page page_spa_design SPA Design + +# Conventions + +## Types + +Types are generally divided into two categories: + +- String types: They identify interfaces and highlevel object types. +- Integer types: These are enumerations used in the parts where high + performance/ease of use/low space overhead is needed. + +The SPA type is system is statis and very simple but still allows you +to make and introspect complex object type hierarchies. + +See the type system docs for more info. + +## Error Codes + +SPA uses negative integers as errno style error codes. Functions that return an +int result code generated an error when < 0. `spa_strerror()` can be used to +get a string representation of the error code. + +SPA also has a way to encode asynchronous results. This is done by setting a +high bit (bit 30, the `ASYNC_BIT`) in the result code and a sequence number +in the lower bits. This result is normally identified as a positive success +result code and the sequence number can later be matched to the completion +event. + +## Useful Macros + +SPA comes with some useful macros defined in `<spa/utils/defs.h>` and a +number of utility functions, see \ref spa_utils. + +*/ diff --git a/doc/spa-index.dox b/doc/spa-index.dox new file mode 100644 index 0000000..f7f5ffe --- /dev/null +++ b/doc/spa-index.dox @@ -0,0 +1,88 @@ +/** \page page_spa SPA (Simple Plugin API) + +\ref api_spa (Simple Plugin API) is an extensible API to implement all kinds of +plugins. + +It is inspired by many other plugin APIs, mostly LV2 and +GStreamer. SPA provides two parts: + +- A header-only API with no external dependencies. +- A set of support libraries ("plugins") for commonly used functionality. + +The usual approach is that PipeWire and PipeWire clients can use the +header-only functions to interact with the plugins. Those plugins are +usually loaded at runtime (through `dlopen(3)`). + + +# Motivation + +SPA was designed with the following goals in mind: + +- No dependencies, SPA is shipped as a set of header files that have no dependencies except for the standard C library. +- Very efficient both in space and in time. +- Very configurable and usable in many different environments. All aspects + of the plugin environment can be configured and changed, like logging, + poll loops, system calls, etc. +- Consistent API. +- Extensible; new API can be added with minimal effort, existing API can be updated and versioned. + +The original user of SPA is PipeWire, which uses SPA to implement the +low-level multimedia processing plugins, device detection, mainloops, CPU +detection, logging, among other things. SPA however can be used outside +of PipeWire with minimal problems. + + +# The SPA Header-Only API + +A very simple example on how SPA headers work are the \ref spa_utils, a set +of utilities commonly required by C projects. SPA functions use the `spa_` +namespace and are easy to identify. + +\code +/* cc $(pkg-config --cflags libspa-0.2) -o spa-test spa-test.c */ + +#include <stdint.h> +#include <spa/utils/string.h> + +int main(int argc, char **argv) { + uint32_t val; + + if (spa_atoi32(argv[1], &val, 16)) + printf("argv[1] is hex %#x\n", val); + else + printf("argv[1] is not a hex number\n"); + + return 0; +} +\endcode + + +# SPA Plugins + +SPA plugins are shared libraries (`.so` files) that can be loaded at +runtime. Each library provides one or more "factories", each of which may +implement several "interfaces". Code that uses SPA plugins then uses those +interfaces (through SPA header files) to interact with the plugin. + +For example, the PipeWire daemon can load the normal `printf`-based logger +or a systemd journal-based logger. Both of those provide the \ref spa_log +interface and once instantiated, PipeWire no longer has to differentiate +between the two logging facilities. + +Please see \ref page_spa_plugins for the details on how to use SPA plugins. + + +# Further details + +- \ref api_spa +- \subpage page_spa_design +- \subpage page_spa_plugins +- \subpage page_spa_pod +- \subpage page_spa_buffer + + +\addtogroup api_spa + +See: \ref page_spa, \ref page_spa_design + +*/ diff --git a/doc/spa-plugins.dox b/doc/spa-plugins.dox new file mode 100644 index 0000000..af14d5e --- /dev/null +++ b/doc/spa-plugins.dox @@ -0,0 +1,360 @@ +/** \page page_spa_plugins SPA Plugins + +\ref spa_handle "SPA plugins" are dynamically loadable objects that contain objects and interfaces that +can be introspected and used at runtime in any application. This document +introduces the basic concepts of SPA plugins. It first covers using the API +and then talks about implementing new plugins. + + +# Outline + +To use a plugin, the following steps are required: + +- **Load** the shared library. +- **Enumerate** the available factories. +- **Enumerate** the interfaces in each factory. +- **Instantiate** the desired interface. +- **Use** the interface-specific functions. + +In pseudo-code, loading a logger interface looks like this: + +\code{.py} +handle = dlopen("$SPA_PLUGIN_DIR/support/libspa-support.so") +factory_enumeration_func = dlsym(handle, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME) +spa_log *logger = NULL + +while True: + factory = get_next_factory(factory_enumeration_func): + if factory != SPA_NAME_SUPPORT_LOG: # <spa/utils/name.h> + continue + + interface_info = get_next_interface_info(factory) + if info->type != SPA_TYPE_INTERFACE_Log: # </spa/support/log.h> + continue + + interface = spa_load_interface(handle, interface_info->type) + logger = (struct spa_log *)interface + break + +spa_log_error(log, "This is an error message\n") +\endcode + +SPA does not specify where plugins need to live, although plugins are +normally installed in `/usr/lib64/spa-0.2/` or equivalent. Plugins and API +are versioned and many versions can live on the same system. + +\note The directory the SPA plugins reside in is available through + `pkg-config --variable plugindir libspa-0.2` + +The `spa-inspect` tool provides a CLI interface to inspect SPA plugins: + +\verbatim +$ export SPA_PLUGIN_DIR=$(pkg-config --variable plugindir libspa-0.2) +$ spa-inspect ${SPA_PLUGIN_DIR}/support/libspa-support.so +... +factory version: 1 +factory name: 'support.cpu' +factory info: + none +factory interfaces: + interface: 'Spa:Pointer:Interface:CPU' +factory instance: + interface: 'Spa:Pointer:Interface:CPU' +skipping unknown interface +factory version: 1 +factory name: 'support.loop' +factory info: + none +factory interfaces: + interface: 'Spa:Pointer:Interface:Loop' + interface: 'Spa:Pointer:Interface:LoopControl' + interface: 'Spa:Pointer:Interface:LoopUtils' +... +\endverbatim + + +# Open A Plugin + +A plugin is opened with a platform specific API. In this example we use +`dlopen()` as the method used on Linux. + +A plugin always consists of two parts, the vendor path and then the .so file. + +As an example we will load the "support/libspa-support.so" plugin. You will +usually use some mapping between functionality and plugin path as we'll see +later, instead of hardcoding the plugin name. + +To `dlopen` a plugin we then need to prefix the plugin path like this: + +\code{.c} +#define SPA_PLUGIN_DIR /usr/lib64/spa-0.2/" +void *hnd = dlopen(SPA_PLUGIN_DIR"/support/libspa-support.so", RTLD_NOW); +\endcode + +The environment variable `SPA_PLUGIN_DIR` and `pkg-config` variable +`plugindir` are usually used to find the location of the plugins. You will +have to do some more work to construct the shared object path. + +The plugin must have exactly one public symbol, called +`spa_handle_factory_enum`, which is defined with the macro +`SPA_HANDLE_FACTORY_ENUM_FUNC_NAME` to get some compile time checks and avoid +typos in the symbol name. We can get the symbol like so: + +\code{.c} +spa_handle_factory_enum_func_t enum_func; +enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)); +\endcode + +If this symbol is not available, the library is not a valid SPA plugin. + + +# Enumerating Factories + +With the `enum_func` we can now enumerate all the factories in the plugin: + +\code{.c} +uint32_t i; +const struct spa_handle_factory *factory = NULL; +for (i = 0;;) { + if (enum_func(&factory, &i) <= 0) + break; + // check name and version, introspect interfaces, + // do something with the factory. +} +\endcode + +A factory has a version, a name, some properties and a couple of functions +that we can check and use. The main use of a factory is to create an +actual new object from it. + +We can enumerate the interfaces that we will find on this new object with +the `spa_handle_factory_enum_interface_info()` method. Interface types +are simple strings that uniquely define the interface (see also the type +system). + +The name of the factory is a well-known name that describes the functionality +of the objects created from the factory. `<spa/utils/names.h>` contains +definitions for common functionality, for example: + +\code{.c} +#define SPA_NAME_SUPPORT_CPU "support.cpu" // A CPU interface +#define SPA_NAME_SUPPORT_LOG "support.log" // A Log interface +#define SPA_NAME_SUPPORT_DBUS "support.dbus" // A DBUS interface +\endcode + +Usually the name will be mapped to a specific plugin. This way an +alternative compatible implementation can be made in a different library. + + +# Making A Handle + +Once we have a suitable factory, we need to allocate memory for the object +it can create. SPA usually does not allocate memory itself but relies on +the application and the stack for storage. + +First get the size of the required memory: + +\code{.c} +struct spa_dict *extra_params = NULL; +size_t size = spa_handle_factory_get_size(factory, extra_params); +\endcode + +Sometimes the memory can depend on the extra parameters given in +`_get_size()`. Next we need to allocate the memory and initialize the object +in it: + +\code{.c} +handle = calloc(1, size); +spa_handle_factory_init(factory, handle, + NULL, // info + NULL, // support + 0 // n_support + ); +\endcode + +The info parameter should contain the same extra properties given in +`spa_handle_factory_get_size()`. + +The support parameter is an array of `struct spa_support` items. They +contain a string type and a pointer to extra support objects. This can +be a logging API or a main loop API for example. Some plugins require +certain support libraries to function. + + +# Retrieving An Interface + +When a SPA handle is made, you can retrieve any of the interfaces that +it provides: + +\code{.c} +void *iface; +spa_handle_get_interface(handle, SPA_NAME_SUPPORT_LOG, &iface); +\endcode + +If this method succeeds, you can cast the `iface` variable to +`struct spa_log *` and start using the log interface methods. + +\code{.c} +struct spa_log *log = iface; +spa_log_warn(log, "Hello World!\n"); +\endcode + + +# Clearing An Object + +After you are done with a handle you can clear it with +`spa_handle_clear()` and you can unload the library with `dlclose()`. + + +# SPA Interfaces + +We briefly talked about retrieving an interface from a plugin in the +previous section. Now we will explore what an interface actually is +and how to use it. + +When you retrieve an interface from a handle, you get a reference to +a small structure that contains the type (string) of the interface, +a version and a structure with a set of methods (and data) that are +the implementation of the interface. Calling a method on the interface +will just call the appropriate method in the implementation. + +Interfaces are defined in a header file (for example see +`<spa/support/log.h>` for the logger API). It is a self contained +definition that you can just use in your application after you `dlopen()` +the plugin. + +Some interfaces also provide extra fields in the interface, like the +log interface above that has the log level as a read/write parameter. + +See \ref spa_interface for some implementation details on interfaces. + + +# SPA Events + +Some interfaces will also allow you to register a callback (a hook or +listener) to be notified of events. This is usually when something +changed internally in the interface and it wants to notify the registered +listeners about this. + +For example, the `struct spa_node` interface has a method to register such +an event handler like this: + +\code{.c} +static void node_info(void *data, const struct spa_node_info *info) +{ + printf("got node info!\n"); +} + +static struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .info = node_info, +}; + +struct spa_hook listener; +spa_zero(listener); +spa_node_add_listener(node, &listener, &node_event, my_data); +\endcode + +You make a structure with pointers to the events you are interested in +and then use `spa_node_add_listener()` to register a listener. The +`struct spa_hook` is used by the interface to keep track of registered +event listeners. + +Whenever the node information is changed, your `node_info` method will +be called with `my_data` as the first data field. The events are usually +also triggered when the listener is added, to enumerate the current +state of the object. + +Events have a `version` field, set to `SPA_VERSION_NODE_EVENTS` in the +above example. It should contain the version of the event structure +you compiled with. When new events are added later, the version field +will be checked and the new signal will be ignored for older versions. + +You can remove your listener with: + +\code{.c} +spa_hook_remove(&listener); +\endcode + + +# API Results + +Some interfaces provide API that gives you a list or enumeration of +objects/values. To avoid allocation overhead and ownership problems, +SPA uses events to push results to the application. This makes it +possible for the plugin to temporarily create complex objects on the +stack and push this to the application without allocation or ownership +problems. The application can look at the pushed result and keep/copy +only what it wants to keep. + +## Synchronous Results + +Here is an example of enumerating parameters on a node interface. + +First install a listener for the result: + +\code{.c} +static void node_result(void *data, int seq, int res, + uint32_t type, const void *result) +{ + const struct spa_result_node_params *r = + (const struct spa_result_node_params *) result; + printf("got param:\n"); + spa_debug_pod(0, NULL, r->param); +} + +struct spa_hook listener = { 0 }; +static const struct spa_node_events node_events = { + SPA_VERSION_NODE_EVENTS, + .result = node_result, +}; + +spa_node_add_listener(node, &listener, &node_events, node); +\endcode + +Then perform the `enum_param` method: + +\code{.c} +int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL); +\endcode + +This triggers the result event handler with a 0 sequence number for each +supported format. After this completes, remove the listener again: + +\code{.c} +spa_hook_remove(&listener); +\endcode + +## Asynchronous Results + +Asynchronous results are pushed to the application in the same way as +synchronous results, they are just pushed later. You can check that +a result is asynchronous by the return value of the enum function: + +\code{.c} +int res = spa_node_enum_params(node, 0, SPA_PARAM_EnumFormat, 0, MAXINT, NULL); + +if (SPA_RESULT_IS_ASYNC(res)) { + // result will be received later + ... +} +\endcode + +In the case of async results, the result callback will be called with the +sequence number of the async result code, which can be obtained with: + +\code{.c} +expected_seq = SPA_RESULT_ASYNC_SEQ(res); +\endcode + +# Implementing A New Plugin + +***FIXME*** + + + +\addtogroup spa_handle + +See: \ref page_spa_plugins + +*/ diff --git a/doc/spa-pod.dox b/doc/spa-pod.dox new file mode 100644 index 0000000..0c88bb7 --- /dev/null +++ b/doc/spa-pod.dox @@ -0,0 +1,530 @@ +/** \page page_spa_pod SPA POD + +\ref spa_pod (plain old data) is a sort of data container. It is comparable to +DBus Variant or LV2 Atom. + +A POD can express nested structures of objects (with properties), vectors, +arrays, sequences and various primitives types. All information in the POD +is laid out sequentially in memory and can be written directly to +storage or exchanged between processes or threads without additional +marshalling. + +Each POD is made of a 32 bits size followed by a 32 bits type field, +followed by the POD contents. This makes it possible to skip over unknown +POD types. The POD start is always aligned to 8 bytes. + +POD's can be efficiently constructed and parsed in real-time threads without +requiring memory allocations. + +POD's use the SPA type system for the basic types and containers. See +the SPA types for more info. + + +# Types + +POD's can contain a number of basic SPA types: + +- `SPA_TYPE_None`: No value or a NULL pointer. +- `SPA_TYPE_Bool`: A boolean value. +- `SPA_TYPE_Id`: An enumerated value. +- `SPA_TYPE_Int`, `SPA_TYPE_Long`, `SPA_TYPE_Float`, `SPA_TYPE_Double`: + various numeral types, 32 and 64 bits. +- `SPA_TYPE_String`: A string. +- `SPA_TYPE_Bytes`: A byte array. +- `SPA_TYPE_Rectangle`: A rectangle with width and height. +- `SPA_TYPE_Fraction`: A fraction with numerator and denominator. +- `SPA_TYPE_Bitmap`: An array of bits. + +POD's can be grouped together in these container types: + +- `SPA_TYPE_Array`: An array of equal sized objects. +- `SPA_TYPE_Struct`: A collection of types and objects. +- `SPA_TYPE_Object`: An object with properties. +- `SPA_TYPE_Sequence`: A timed sequence of POD's. + +POD's can also contain some extra types: + +- `SPA_TYPE_Pointer`: A typed pointer in memory. +- `SPA_TYPE_Fd`: A file descriptor. +- `SPA_TYPE_Choice`: A choice of values. +- `SPA_TYPE_Pod`: A generic type for the POD itself. + + +# Constructing A POD + +A POD is usually constructed with a `struct spa_pod_builder`. The builder +needs to be initialized with a memory region to write into. It is +also possible to dynamically grow the memory as needed. + +The most common way to construct a POD is on the stack. This does +not require any memory allocations. The size of the POD can be +estimated pretty easily and if the buffer is not large enough, an +appropriate error will be generated. + +The code fragment below initializes a POD builder to write into +the stack allocated buffer. + +\code{.c} +uint8_t buffer[4096]; +struct spa_pod_builder b; +spa_pod_builder_init(&b, buffer, sizeof(buffer)); +\endcode + +Next we need to write some object into the builder. Let's write +a simple struct with an Int and Float in it. Structs are comparable +to JSON arrays. + +\code{.c} +struct spa_pod_frame f; +spa_pod_builder_push_struct(&b, &f); +\endcode + +First we open the struct container, the `struct spa_pod_frame` keeps +track of the container context. Next we add some values to +the container like this: + +\code{.c} +spa_pod_builder_int(&b, 5); +spa_pod_builder_float(&b, 3.1415f); +\endcode + +Then we close the container by popping the frame again: + +\code{.c} +struct spa_pod *pod; +pod = spa_pod_builder_pop(&b, &f); +\endcode + +`spa_pod_builder_pop()` returns a reference to the object we completed +on the stack. + +## Using varargs Builder + +We can also use the following construct to make POD objects: + +\code{.c} +spa_pod_builder_push_struct(&b, &f); +spa_pod_builder_add(&b, + SPA_POD_Int(5), + SPA_POD_Float(3.1415f)); +pod = spa_pod_builder_pop(&b, &f); +\endcode + +Or even shorter: + +\code{.c} +pod = spa_pod_builder_add_struct(&b, + SPA_POD_Int(5), + SPA_POD_Float(3.1415f)); +\endcode + +It's not possible to use the varargs builder to make a sequence or +array, use the normal builder methods for that. + +## Making Objects + +POD objects are containers for properties and are comparable to JSON +objects. + +Start by pushing an object: + +\code{.c} +spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); +\endcode + +An object requires an object type (`SPA_TYPE_OBJECT_Props`) and a context +ID (`SPA_PARAM_Props`). The object type defines the properties that can be +added to the object and their meaning. The SPA type system allows you to +make this connection (See the type system). + +Next we can push some properties in the object: + +\code{.c} +spa_pod_builder_prop(&b, SPA_PROP_device, 0); +spa_pod_builder_string(&b, "hw:0"); +spa_pod_builder_prop(&b, SPA_PROP_frequency, 0); +spa_pod_builder_float(&b, 440.0); +\endcode + +As can be seen, we always need to push a prop (with key and flags) +and then the associated value. For performance reasons it is a good +idea to always push (and parse) the object keys in ascending order. + +Don't forget to pop the result when the object is finished: + +\code{.c} +pod = spa_pod_builder_pop(&b, &f); +\endcode + +There is a shortcut for making objects: + +\code{.c} +pod = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_device, SPA_POD_String("hw:0"), + SPA_PROP_frequency, SPA_POD_Float(440.0f)); +\endcode + +## Choice Values + +It is possible to express ranges or enumerations of possible +values for properties (and to some extend structs). This is achieved +with choice values. + +Choice values are really just a choice type and an array of choice values +(of the same type). Depending on the choice type, the array values are +interpreted in different ways: + +- `SPA_CHOICE_None`: No choice, first value is current. +- `SPA_CHOICE_Range`: Range: default, min, max. +- `SPA_CHOICE_Step`: Range with step: default, min, max, step. +- `SPA_CHOICE_Enum`: Enum: default, alternative,... +- `SPA_CHOICE_Flags`: Bitmask of flags. + +Let's illustrate this with a props object that specifies a range of +possible values for the frequency: + +\code{.c} +struct spa_pod_frame f2; + +spa_pod_builder_push_object(&b, &f, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props); +spa_pod_builder_prop(&b, SPA_PROP_frequency, 0); +spa_pod_builder_push_choice(&b, &f2, SPA_CHOICE_Range, 0); +spa_pod_builder_float(&b, 440.0); // default +spa_pod_builder_float(&b, 110.0); // min +spa_pod_builder_float(&b, 880.0); // min +pod = spa_pod_builder_pop(&b, &f2); +pod = spa_pod_builder_pop(&b, &f); +\endcode + +As you can see, first push the choice as a range, then the values. A range +choice expects at least three values, the default value, minimum and maximum +values. There is a shortcut for this as well using varargs: + +\code{.c} +pod = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_frequency, SPA_POD_CHOICE_RANGE_Float(440.0f, 110.0f, 880.0f)); +\endcode + +## Choice Examples + +This is a description of a possible `SPA_TYPE_OBJECT_Format` as used when +enumerating allowed formats (`SPA_PARAM_EnumFormat`) in SPA objects: + +\code{.c} +pod = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + // specify the media type and subtype + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + // audio/raw properties + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id( + SPA_AUDIO_FORMAT_S16, // default + SPA_AUDIO_FORMAT_S16, // alternative1 + SPA_AUDIO_FORMAT_S32, // alternative2 + SPA_AUDIO_FORMAT_f32 // alternative3 + ), + SPA_FORMAT_AUDIO_rate, SPA_POD_CHOICE_RANGE_Int( + 44100, // default + 8000, // min + 192000 // max + ), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2)); +\endcode + +## Fixate + +We can remove all choice values from the object with the +`spa_pod_object_fixate()` method. This modifies the pod in-place and sets all +choice properties to `SPA_CHOICE_None`, forcing the default value as the +only available value in the choice. + +Running fixate on our previous example would result in an object equivalent +to: + +\code{.c} +pod = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + // specify the media type and subtype + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + // audio/raw properties + SPA_FORMAT_AUDIO_format, SPA_POD_Id(SPA_AUDIO_FORMAT_S16), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(44100), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(2)); +\endcode + + +# Parsing A POD + +Parsing a POD usually consists of: + +- Validating if raw bytes + size can contain a valid POD. +- Inspecting the type of a POD. +- Looping over the items in an object or struct. +- Getting data out of POD's. + +## Validating Bytes + +Use `spa_pod_from_data()` to check if maxsize of bytes in data contain +a POD at the size bytes starting at offset. This function checks that +the POD size will fit and not overflow. + +\code{.c} +struct spa_pod *pod; +pod = spa_pod_from_data(data, maxsize, offset, size); +\endcode + +## Checking The Type Of POD + +Use one of `spa_pod_is_bool()`, `spa_pod_is_int()`, etc to check +for the type of the pod. For simple (non-container) types, +`spa_pod_get_bool()`, `spa_pod_get_int()` etc can be used to +extract the value of the pod. + +`spa_pod_is_object_type()` can be used to check if the POD contains +an object of the expected type. + +## Struct Fields + +To iterate over the fields of a struct use: + +\code{.c} +struct spa_pod *pod, *obj; +SPA_POD_STRUCT_FOREACH(obj, pod) { + printf("field type:%d\n", pod->type); +} +\endcode + +For parsing structs it is usually much easier to use the parser +below. + +## Object Properties + +To iterate over the properties in an object you can do: + +\code{.c} +struct spa_pod_prop *prop; +struct spa_pod_object *obj = (struct spa_pod_object*)pod; +SPA_POD_OBJECT_FOREACH(pod, prop) { + printf("prop key:%d\n", prop->key); +} +\endcode + +There is a function to retrieve the property for a certain key +in the object. If the properties of the object are in ascending +order, you can start searching from the previous key. + +\code{.c} +struct spa_pod_prop *prop; +prop = spa_pod_find_prop(obj, NULL, SPA_FORMAT_AUDIO_format); + // .. use first prop +prop = spa_pod_find_prop(obj, prop, SPA_FORMAT_AUDIO_rate); + // .. use next prop +\endcode + +## Parser + +Similar to the builder, there is a parser object as well. + +If the fields in a struct are known, it is much easier to use the +parser. Similarly, if the object type (and thus its keys) are known, +the parser is easier. + +First initialize a `struct spa_pod_parser`: + +\code{.c} +struct spa_pod_parser p; +spa_pod_parser_pod(&p, obj); +\endcode + +You can then enter containers such as objects or structs with a push +operation: + +\code{.c} +struct spa_pod_frame f; +spa_pod_parser_push_struct(&p, &f); +\endcode + +You need to store the context in a `struct spa_pod_frame` to be able +to exit the container again later. + +You can then parse each field. The parser takes care of moving to the +next field. + +\code{.c} +uint32_t id, val; +spa_pod_parser_get_id(&p, &id); +spa_pod_parser_get_int(&p, &val); +... +\endcode + +And finally exit the container again: + +\code{.c} +spa_pod_parser_pop(&p, &f); +\endcode + +## Parser With Variable Arguments + +In most cases, parsing objects is easier with the variable argument +functions. The parse function look like the mirror image of the builder +functions. + +To parse a struct: + +\code{.c} +spa_pod_parser_get_struct(&p, + SPA_POD_Id(&id), + SPA_POD_Int(&val)); +\endcode + +To parse properties in an object: + +\code{.c} +uint32_t type, subtype, format, rate, channels; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(&type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(&format), + SPA_FORMAT_AUDIO_rate, SPA_POD_Int(&rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(&channels)); +\endcode + +When parsing objects it is possible to have optional fields. You can +make a field optional be parsing it with the `SPA_POD_OPT_` prefix +for the type. + +In the next example, the rate and channels fields are optional +and when they are not present, the variables will not be changed. + +\code{.c} +uint32_t type, subtype, format, rate = 0, channels = 0; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(&type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Id(&format), + SPA_FORMAT_AUDIO_rate, SPA_POD_OPT_Int(&rate), + SPA_FORMAT_AUDIO_channels, SPA_POD_OPT_Int(&channels)); +\endcode + +It is not possible to parse a sequence or array with the parser. +Use the iterator for this. + +## Choice Values + +The parser will handle choice values as long as they are of type +`none`. It will then parse the single value from the choice. When +dealing with other choice values, it's possible to parse the +property values into a `struct spa_pod` and then inspect the choice +manually, if needed. + +Here is an example of parsing the format values as a POD: + +\code{.c} +uint32_t type, subtype; +struct spa_pod *format; +spa_pod_parser_get_object(&p, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(&type), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(&subtype), + SPA_FORMAT_AUDIO_format, SPA_POD_Pod(&format)); +\endcode + +`spa_pod_get_values()` is a useful function. It returns a +`struct spa_pod*` with and array of values. For normal POD's +and choice none values, it simply returns the POD and one value. +For other choice values it returns the choice type and an array +of values: + +\code{.c} +struct spa_pod *value; +uint32_t n_vals, choice; + +value = spa_pod_get_values(pod, &n_vals, &choice); + +switch (choice) { +case SPA_CHOICE_None: + // one single value + break; +case SPA_CHOICE_Range: + // array of values of type of pod, cast to right type + // to iterate. + uint32_t *v = SPA_POD_BODY(values); + if (n_vals < 3) + break; + printf("default value: %u\n", v[0]); + printf("min value: %u\n", v[1]); + printf("max value: %u\n", v[2]); + break; + + // ... +default: + break; +} +\endcode + + +# Filter + +Given two POD objects of the same type (object, struct, ..) one can +run a filter and generate a new POD that only contains values that +are compatible with both input POD's. + +This is, for example, used to find a compatible format between two ports. + +As an example we can run a filter on two simple POD's: + +\code{.c} +pod = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id( + SPA_AUDIO_FORMAT_S16, // default + SPA_AUDIO_FORMAT_S16, // alternative1 + SPA_AUDIO_FORMAT_S32, // alternative2 + SPA_AUDIO_FORMAT_f32 // alternative3 + )); + +filter = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id( + SPA_AUDIO_FORMAT_S16, // default + SPA_AUDIO_FORMAT_S16, // alternative1 + SPA_AUDIO_FORMAT_f64 // alternative2 + )); + +struct spa_pod *result; +if (spa_pod_filter(&b, &result, pod, filter) < 0) + goto exit_error; +\endcode + +Filter will contain a POD equivalent to: + +\code{.c} +result = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_format, SPA_AUDIO_FORMAT_S16); +\endcode + +# POD Layout + +Each POD has a 32 bits size field, followed by a 32 bits type field. The size +field specifies the size following the type field. + +Each POD is aligned to an 8 byte boundary. + + +\addtogroup spa_pod + +See: \ref page_spa_pod + +*/ diff --git a/doc/tutorial.dox b/doc/tutorial.dox new file mode 100644 index 0000000..bc53725 --- /dev/null +++ b/doc/tutorial.dox @@ -0,0 +1,21 @@ +/** \page page_tutorial Tutorial + +Welcome to the PipeWire tutorial. The goal is to learn +PipeWire API step-by-step with simple short examples. + +- \subpage page_tutorial1 +- \subpage page_tutorial2 +- \subpage page_tutorial3 +- \subpage page_tutorial4 +- \subpage page_tutorial5 +- \subpage page_tutorial6 + + +# More Example Programs + +- \ref audio-src.c "": \snippet{doc} audio-src.c title +- \ref audio-dsp-filter.c "": \snippet{doc} audio-dsp-filter.c title +- \ref video-play.c "": \snippet{doc} video-play.c title +- \subpage page_examples + +*/ diff --git a/doc/tutorial1.c b/doc/tutorial1.c new file mode 100644 index 0000000..050aa88 --- /dev/null +++ b/doc/tutorial1.c @@ -0,0 +1,19 @@ +/* + [title] + \ref page_tutorial1 + [title] + */ +/* [code] */ +#include <pipewire/pipewire.h> + +int main(int argc, char *argv[]) +{ + pw_init(&argc, &argv); + + fprintf(stdout, "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + pw_get_headers_version(), + pw_get_library_version()); + return 0; +} +/* [code] */ diff --git a/doc/tutorial1.dox b/doc/tutorial1.dox new file mode 100644 index 0000000..6e88cce --- /dev/null +++ b/doc/tutorial1.dox @@ -0,0 +1,47 @@ +/** \page page_tutorial1 Tutorial - Part 1: Getting Started + + +\ref page_tutorial "Index" | \ref page_tutorial2 + +In this tutorial we show the basics of a simple PipeWire application. +Use this tutorial to get started and help you set up your development +environment. + + +# Initialization + +Let get started with the simplest application. + +\snippet tutorial1.c code + +Before you can use any PipeWire functions, you need to call `pw_init()`. + + +# Compilation + +PipeWire provides a pkg-config file named `libpipewire-0.3` (note: the version +suffix may change with future releases of PipeWire). +To compile the simple test application, copy it into a test1.c file and +use pkg-config to provide the required dependencies: + + gcc -Wall test1.c -o test1 $(pkg-config --cflags --libs libpipewire-0.3) + +then run it with: + + # ./test1 + Compiled with libpipewire 0.3.5 + Linked with libpipewire 0.3.5 + # + +Use your build system's pkg-config support to integrate it into your project. +For example, a minimal [meson.build](https://mesonbuild.com/) entry would look +like this: + + project('test1', ['c']) + pipewire_dep = dependency('libpipewire-0.3') + executable('test1', 'test1.c', + dependencies: [pipewire_dep]) + +\ref page_tutorial "Index" | \ref page_tutorial2 + +*/ diff --git a/doc/tutorial2.c b/doc/tutorial2.c new file mode 100644 index 0000000..66647d7 --- /dev/null +++ b/doc/tutorial2.c @@ -0,0 +1,56 @@ +/* + [title] + \ref page_tutorial2 + [title] + */ +/* [code] */ +#include <pipewire/pipewire.h> + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + printf("object: id:%u type:%s/%d\n", id, type, version); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +int main(int argc, char *argv[]) +{ + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook registry_listener; + + pw_init(&argc, &argv); + + loop = pw_main_loop_new(NULL /* properties */); + context = pw_context_new(pw_main_loop_get_loop(loop), + NULL /* properties */, + 0 /* user_data size */); + + core = pw_context_connect(context, + NULL /* properties */, + 0 /* user_data size */); + + registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, + 0 /* user_data size */); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, + ®istry_events, NULL); + + pw_main_loop_run(loop); + + pw_proxy_destroy((struct pw_proxy*)registry); + pw_core_disconnect(core); + pw_context_destroy(context); + pw_main_loop_destroy(loop); + + return 0; +} +/* [code] */ diff --git a/doc/tutorial2.dox b/doc/tutorial2.dox new file mode 100644 index 0000000..1688aed --- /dev/null +++ b/doc/tutorial2.dox @@ -0,0 +1,129 @@ +/** \page page_tutorial2 Tutorial - Part 2: Enumerating Objects + +\ref page_tutorial1 | \ref page_tutorial "Index" | \ref page_tutorial3 + +In this tutorial we show how to connect to a PipeWire daemon and +enumerate the objects that it has. + +Let take a look at the following application to start. + +\snippet tutorial2.c code + +To compile the simple test application, copy it into a tutorial2.c file and +use: + + gcc -Wall tutorial2.c -o tutorial2 $(pkg-config --cflags --libs libpipewire-0.3) + +Let's break this down: + +First we need to initialize the PipeWire library with `pw_init()` as we +saw in the previous tutorial. This will load and configure the right +modules and setup logging and other tasks. + +\code{.c} + ... + pw_init(&argc, &argv); + ... +\endcode + +Next we need to create one of the `struct pw_loop` wrappers. PipeWire +ships with 2 types of mainloop implementations. We will use the +`struct pw_main_loop` implementation, we will see later how we can +use the `struct pw_thread_loop` implementation as well. + +The mainloop is an abstraction of a big poll loop, waiting for events +to occur and things to do. Most of the PipeWire work will actually +be performed in the context of this loop and so we need to make one +first. + +We then need to make a new context object with the loop. This context +object will manage the resources for us and will make it possible for +us to connect to a PipeWire daemon: + +\code{.c} + struct pw_main_loop *loop; + struct pw_context *context; + + loop = pw_main_loop_new(NULL /* properties */); + context = pw_context_new(pw_main_loop_get_loop(loop), + NULL /* properties */, + 0 /* user_data size */); +\endcode + +It is possible to give extra properties when making the mainloop or +context to tweak its features and functionality. It is also possible +to add extra data to the allocated objects for your user data. It will +stay alive for as long as the object is alive. We will use this +feature later. + +A real implementation would also need to check if the allocation +succeeded and do some error handling, but we leave that out to make +the code easier to read. + +With the context we can now connect to the PipeWire daemon: + +\code{.c} + struct pw_core *core; + core = pw_context_connect(context, + NULL /* properties */, + 0 /* user_data size */); +\endcode + +This creates a socket between the client and the server and makes +a proxy object (with ID 0) for the core. Don't forget to check the +result here, a NULL value means that the connection failed. + +At this point we can send messages to the server and receive events. +For now we're not going to handle events on this core proxy but +we're going to handle them on the registry object. + + +\code{.c} + struct pw_registry *registry; + struct spa_hook registry_listener; + + registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, + 0 /* user_data size */); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, + ®istry_events, NULL); +\endcode + +From the core we get the registry proxy object and when we use +`pw_registry_add_listener()` to listen for events. We need a +small `struct spa_hook` to keep track of the listener and a +reference to the `struct pw_registry_events` that contains the +events we want to listen to. + +This is how we define the event handler and the function to +handle the events: + +\code{.c} +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + printf("object: id:%u type:%s/%d\n", id, type, version); +} +\endcode + +Now that everything is set up we can start the mainloop and let +the communication between client and server continue: + +\code{.c} + pw_main_loop_run(loop); +\endcode + +Since we don't call `pw_main_loop_quit()` anywhere, this loop will +continue forever. In the next tutorial we'll see how we can nicely +exit our application after we received all server objects. + +\ref page_tutorial1 | \ref page_tutorial "Index" | \ref page_tutorial3 + +*/ diff --git a/doc/tutorial3.c b/doc/tutorial3.c new file mode 100644 index 0000000..17c0ee4 --- /dev/null +++ b/doc/tutorial3.c @@ -0,0 +1,89 @@ +/* + [title] + \ref page_tutorial3 + [title] + */ +/* [code] */ +#include <pipewire/pipewire.h> + +/* [roundtrip] */ +struct roundtrip_data { + int pending; + struct pw_main_loop *loop; +}; + +static void on_core_done(void *data, uint32_t id, int seq) +{ + struct roundtrip_data *d = data; + + if (id == PW_ID_CORE && seq == d->pending) + pw_main_loop_quit(d->loop); +} + +static void roundtrip(struct pw_core *core, struct pw_main_loop *loop) +{ + static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .done = on_core_done, + }; + + struct roundtrip_data d = { .loop = loop }; + struct spa_hook core_listener; + + pw_core_add_listener(core, &core_listener, &core_events, &d); + + d.pending = pw_core_sync(core, PW_ID_CORE, 0); + + pw_main_loop_run(loop); + + spa_hook_remove(&core_listener); +} +/* [roundtrip] */ + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + printf("object: id:%u type:%s/%d\n", id, type, version); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +int main(int argc, char *argv[]) +{ + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + struct spa_hook registry_listener; + + pw_init(&argc, &argv); + + loop = pw_main_loop_new(NULL /* properties */); + context = pw_context_new(pw_main_loop_get_loop(loop), + NULL /* properties */, + 0 /* user_data size */); + + core = pw_context_connect(context, + NULL /* properties */, + 0 /* user_data size */); + + registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, + 0 /* user_data size */); + + pw_registry_add_listener(registry, ®istry_listener, + ®istry_events, NULL); + + roundtrip(core, loop); + + pw_proxy_destroy((struct pw_proxy*)registry); + pw_core_disconnect(core); + pw_context_destroy(context); + pw_main_loop_destroy(loop); + + return 0; +} +/* [code] */ diff --git a/doc/tutorial3.dox b/doc/tutorial3.dox new file mode 100644 index 0000000..776ea14 --- /dev/null +++ b/doc/tutorial3.dox @@ -0,0 +1,119 @@ +/** \page page_tutorial3 Tutorial - Part 3: Forcing A Roundtrip + +\ref page_tutorial2 | \ref page_tutorial "Index" | \ref page_tutorial4 + +In this tutorial we show how to force a roundtrip to the server +to make sure an action completed. + +We'll change our example from \ref page_tutorial2 "Tutorial 2" slightly +and add the extra code to implement the roundtrip. + +Let's take the following small method first: + +\snippet tutorial3.c roundtrip + +Let's take a look at what this method does. + +\code{.c} + struct spa_hook core_listener; + + pw_core_add_listener(core, &core_listener, &core_events, &d); +\endcode + +First of all we add a listener for the events of the core +object. We are only interested in the `done` event in this +tutorial. This is the event handler: + +\code{.c} +static void on_core_done(void *data, uint32_t id, int seq) +{ + struct roundtrip_data *d = data; + + if (id == PW_ID_CORE && seq == d->pending) + pw_main_loop_quit(d->loop); +} +\endcode + +When the done event is received for an object with id `PW_ID_CORE` and +a certain sequence number `seq`, this function will call `pw_main_loop_quit()`. + +Next we do: + +\code{.c} + d.pending = pw_core_sync(core, PW_ID_CORE, 0); +\endcode + +This triggers the `sync` method on the core object with id +`PW_ID_CORE` and sequence number 0. + +Because this is a method on a proxy object, it will be executed +asynchronously and the return value will reflect this. PipeWire +uses the return values of the underlying SPA (Simple Plugin API) +helper objects (See also \ref page_spa_design ). + +Because all messages on the PipeWire server are handled sequentially, +the sync method will be executed after all previous methods are +completed. The PipeWire server will emit a `done` event with the +same ID and the return value of the original `pw_core_sync()` +method in the sequence number. + +We then run the mainloop to send the messages to the server and +receive the events: + +\code{.c} + pw_main_loop_run(loop); +\endcode + +When we get the done event, we can compare it to the sync method +and then we know that we did a complete roundtrip and there are no +more pending methods on the server. We can quit the mainloop and +remove the listener: + +\code{.c} + spa_hook_remove(&core_listener); +\endcode + +If we add this roundtrip method to our code and call it instead of the +`pw_main_loop_run()` we will exit the program after all previous methods +are finished. This means that the `pw_core_get_registry()` call +completed and thus that we also received all events for the globals +on the server. + +\snippet tutorial3.c code + +To compile the simple test application, copy it into a tutorial3.c file and +use: + + gcc -Wall tutorial3.c -o tutorial3 $(pkg-config --cflags --libs libpipewire-0.3) + +Now that our program completes, we can take a look at how we can destroy +the objects we created. Let's destroy each of them in reverse order that we +created them: + +\code{.c} + pw_proxy_destroy((struct pw_proxy*)registry); +\endcode + +The registry is a proxy and can be destroyed with the generic proxy destroy +method. After destroying the object, you should not use it anymore. It is +an error to destroy an object more than once. + +We can disconnect from the server with: + +\code{.c} + pw_core_disconnect(core); +\endcode + +This will also destroy the core proxy object and will remove the proxies +that might have been created on this connection. + +We can finally destroy our context and mainloop to conclude this tutorial: + +\code{.c} + pw_context_destroy(context); + pw_main_loop_destroy(loop); +\endcode + +\ref page_tutorial2 | \ref page_tutorial "Index" | \ref page_tutorial4 + +*/ diff --git a/doc/tutorial4.c b/doc/tutorial4.c new file mode 100644 index 0000000..ff0cb27 --- /dev/null +++ b/doc/tutorial4.c @@ -0,0 +1,112 @@ +/* + [title] + \ref page_tutorial4 + [title] + */ +/* [code] */ +#include <math.h> + +#include <spa/param/audio/format-utils.h> + +#include <pipewire/pipewire.h> + +#define M_PI_M2 ( M_PI + M_PI ) + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_VOLUME 0.7 + +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + double accumulator; +}; + +/* [on_process] */ +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + int i, c, n_frames, stride; + int16_t *dst, val; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if ((dst = buf->datas[0].data) == NULL) + return; + + stride = sizeof(int16_t) * DEFAULT_CHANNELS; + n_frames = buf->datas[0].maxsize / stride; + + for (i = 0; i < n_frames; i++) { + data->accumulator += M_PI_M2 * 440 / DEFAULT_RATE; + if (data->accumulator >= M_PI_M2) + data->accumulator -= M_PI_M2; + + val = sin(data->accumulator) * DEFAULT_VOLUME * 16767.f; + for (c = 0; c < DEFAULT_CHANNELS; c++) + *dst++ = val; + } + + buf->datas[0].chunk->offset = 0; + buf->datas[0].chunk->stride = stride; + buf->datas[0].chunk->size = n_frames * stride; + + pw_stream_queue_buffer(data->stream, b); +} +/* [on_process] */ + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, +}; + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + pw_init(&argc, &argv); + + data.loop = pw_main_loop_new(NULL); + + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-src", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL), + &stream_events, + &data); + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .channels = DEFAULT_CHANNELS, + .rate = DEFAULT_RATE )); + + pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + + pw_main_loop_run(data.loop); + + pw_stream_destroy(data.stream); + pw_main_loop_destroy(data.loop); + + return 0; +} +/* [code] */ diff --git a/doc/tutorial4.dox b/doc/tutorial4.dox new file mode 100644 index 0000000..b8d1707 --- /dev/null +++ b/doc/tutorial4.dox @@ -0,0 +1,158 @@ +/** \page page_tutorial4 Tutorial - Part 4: Playing A Tone + +\ref page_tutorial3 | \ref page_tutorial "Index" | \ref page_tutorial5 + +In this tutorial we show how to use a stream to play a tone. + +Let's take a look at the code before we break it down: + +\snippet tutorial4.c code + +Save as tutorial4.c and compile with: + + gcc -Wall tutorial4.c -o tutorial4 -lm $(pkg-config --cflags --libs libpipewire-0.3) + +We start with the usual boilerplate, `pw_init()` and a `pw_main_loop_new()`. +We're going to store our objects in a structure so that we can pass them +around in callbacks later. + +\code{.c} +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + double accumulator; +}; + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + + pw_init(&argc, &argv); + + data.loop = pw_main_loop_new(NULL); +\endcode + +Next we create a stream object. It takes the mainloop as first argument and +a stream name as the second. Next we provide some properties for the stream +and a callback + data. + +\code{.c} + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "audio-src", + pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Music", + NULL), + &stream_events, + &data); +\endcode + +We are using `pw_stream_new_simple()` but there is also a `pw_stream_new()` that +takes an existing `struct pw_core` as the first argument and that requires you +to add the event handle manually, for more control. The `pw_stream_new_simple()` +is, as the name implies, easier to use because it creates a `struct pw_context` +and `struct pw_core` automatically. + +In the properties we need to give as much information about the stream as we +can so that the session manager can make good decisions about how and where +to route this stream. There are three important properties to configure: + +- `PW_KEY_MEDIA_TYPE`: The media type; like Audio, Video, MIDI. +- `PW_KEY_MEDIA_CATEGORY`: The category; like Playback, Capture, Duplex, Monitor. +- `PW_KEY_MEDIA_ROLE`: The media role; like Movie, Music, Camera, Screen, + Communication, Game, Notification, DSP, Production, Accessibility, Test. + +The properties are owned by the stream and freed when the stream is destroyed +later. + +This is the event structure that we use to listen for events: + +\code{.c} +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .process = on_process, +}; +\endcode + +We are for the moment only interested now in the `process` event. This event +is called whenever we need to produce more data. We'll see how that function +is implemented but first we need to setup the format of the stream: + +\code{.c} + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 + + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, + &SPA_AUDIO_INFO_RAW_INIT( + .format = SPA_AUDIO_FORMAT_S16, + .channels = DEFAULT_CHANNELS, + .rate = DEFAULT_RATE )); +\endcode + +This is using a `struct spa_pod_builder` to make a `struct spa_pod *` object +in the buffer array on the stack. The parameter is of type `SPA_PARAM_EnumFormat` +which means that it enumerates the possible formats for this stream. We have +only one, a Signed 16 bit stereo format at 44.1KHz. + +We use `spa_format_audio_raw_build()` which is a helper function to make the param +with the builder. See \ref page_spa_pod for more information about how to +make these POD objects. + +Now we're ready to connect the stream and run the main loop: + +\code{.c} + pw_stream_connect(data.stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, 1); + + pw_main_loop_run(data.loop); +\endcode + +To connect we specify that we have a `PW_DIRECTION_OUTPUT` stream. The third argument +is always `PW_ID_ANY`. Next we set some flags: + +- `PW_STREAM_FLAG_AUTOCONNECT`: Automatically connect this stream. This instructs + the session manager to link us to some consumer. +- `PW_STREAM_FLAG_MAP_BUFFERS`: mmap the buffers for us so we can access the + memory. If you don't set these flags you have either work with the fd or mmap + yourself. +- `PW_STREAM_FLAG_RT_PROCESS`: Run the process function in the realtime thread. + Only use this if the process function only uses functions that are realtime + safe, this means no allocation or file access or any locking. + +And last we pass the extra parameters for our stream. Here we only have the +allowed formats (`SPA_PARAM_EnumFormat`). + +Running the mainloop will then start processing and will result in our +`process` callback to be called. Let's have a look at that function now. + +The main program flow of the process function is: + +- `pw_stream_dequeue_buffer()` to obtain a buffer to write into. +- Get pointers in buffer memory to write to. +- Write data into buffer. +- Adjust buffer with number of written bytes, offset, stride. +- `pw_stream_queue_buffer()` to queue the buffer for playback. + +\snippet tutorial4.c on_process + +Check out the docs for \ref page_spa_buffer for more information +about how to work with buffers. + +Try to change the number of channels, samplerate or format; the stream +will automatically convert to the format on the server. + + +\ref page_tutorial3 | \ref page_tutorial "Index" | \ref page_tutorial5 + +*/ diff --git a/doc/tutorial5.c b/doc/tutorial5.c new file mode 100644 index 0000000..e49da91 --- /dev/null +++ b/doc/tutorial5.c @@ -0,0 +1,141 @@ +/* + [title] + \ref page_tutorial5 + [title] + */ +/* [code] */ +#include <spa/param/video/format-utils.h> +#include <spa/debug/types.h> +#include <spa/param/video/type-info.h> + +#include <pipewire/pipewire.h> + +struct data { + struct pw_main_loop *loop; + struct pw_stream *stream; + + struct spa_video_info format; +}; + +/* [on_process] */ +static void on_process(void *userdata) +{ + struct data *data = userdata; + struct pw_buffer *b; + struct spa_buffer *buf; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if (buf->datas[0].data == NULL) + return; + + /** copy frame data to screen */ + printf("got a frame of size %d\n", buf->datas[0].chunk->size); + + pw_stream_queue_buffer(data->stream, b); +} +/* [on_process] */ + +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct data *data = userdata; + + if (param == NULL || id != SPA_PARAM_Format) + return; + + if (spa_format_parse(param, + &data->format.media_type, + &data->format.media_subtype) < 0) + return; + + if (data->format.media_type != SPA_MEDIA_TYPE_video || + data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + if (spa_format_video_raw_parse(param, &data->format.info.raw) < 0) + return; + + printf("got video format:\n"); + printf(" format: %d (%s)\n", data->format.info.raw.format, + spa_debug_type_find_name(spa_type_video_format, + data->format.info.raw.format)); + printf(" size: %dx%d\n", data->format.info.raw.size.width, + data->format.info.raw.size.height); + printf(" framerate: %d/%d\n", data->format.info.raw.framerate.num, + data->format.info.raw.framerate.denom); + + /** prepare to render video of this size */ +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process, +}; + +int main(int argc, char *argv[]) +{ + struct data data = { 0, }; + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + struct pw_properties *props; + + pw_init(&argc, &argv); + + data.loop = pw_main_loop_new(NULL); + + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + NULL); + if (argc > 1) + pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]); + + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "video-capture", + props, + &stream_events, + &data); + + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_YUY2, + SPA_VIDEO_FORMAT_I420), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(4096, 4096)), + SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(25, 1), + &SPA_FRACTION(0, 1), + &SPA_FRACTION(1000, 1))); + + pw_stream_connect(data.stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + + pw_main_loop_run(data.loop); + + pw_stream_destroy(data.stream); + pw_main_loop_destroy(data.loop); + + return 0; +} +/* [code] */ diff --git a/doc/tutorial5.dox b/doc/tutorial5.dox new file mode 100644 index 0000000..e73c1cf --- /dev/null +++ b/doc/tutorial5.dox @@ -0,0 +1,223 @@ +/** \page page_tutorial5 Tutorial - Part 5: Capturing Video Frames + +\ref page_tutorial4 | \ref page_tutorial "Index" | \ref page_tutorial6 + +In this tutorial we show how to use a stream to capture a +stream of video frames. + +Even though we are now working with a different media type and +we are capturing instead of playback, you will see that this +example is very similar to \ref page_tutorial4. + +Let's take a look at the code before we break it down: + +\snippet tutorial5.c code + +Save as tutorial5.c and compile with: + + gcc -Wall tutorial5.c -o tutorial5 -lm $(pkg-config --cflags --libs libpipewire-0.3) + +Most of the application is structured like \ref page_tutorial4. + +We create a stream object with different properties to make it a Camera +Video Capture stream. + +\code{.c} + props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + NULL); + if (argc > 1) + pw_properties_set(props, PW_KEY_TARGET_OBJECT, argv[1]); + + data.stream = pw_stream_new_simple( + pw_main_loop_get_loop(data.loop), + "video-capture", + props, + &stream_events, + &data); +\endcode + +We also optionally allow the user to pass the name of the target node where the session +manager is supposed to connect the node. The user may also give the value of the +unique target node serial (`PW_KEY_OBJECT_SERIAL`) as the value. + +In addition to the `process` event, we are also going to listen to a new event, +`param_changed`: + +\code{.c} +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process, +}; +\endcode + +Because we capture a stream of a wide range of different +video formats and resolutions, we have to describe our accepted formats in +a different way: + +\code{.c} + const struct spa_pod *params[1]; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + params[0] = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_YUY2, + SPA_VIDEO_FORMAT_I420), + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(4096, 4096)), + SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(25, 1), + &SPA_FRACTION(0, 1), + &SPA_FRACTION(1000, 1))); +\endcode + +This is using a `struct spa_pod_builder` to make a `struct spa_pod *` object +in the buffer array on the stack. The parameter is of type `SPA_PARAM_EnumFormat` +which means that it enumerates the possible formats for this stream. + +In this example we use the builder to create some `CHOICE` entries for +the format properties. + +We have an enumeration of formats, we need to first give the amount of enumerations +that follow, then the default (preferred) value, followed by alternatives in order +of preference: + +\code{.c} + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, + SPA_VIDEO_FORMAT_RGB, /* default */ + SPA_VIDEO_FORMAT_RGB, /* alternative 1 */ + SPA_VIDEO_FORMAT_RGBA, /* alternative 2 */ + SPA_VIDEO_FORMAT_RGBx, /* .. etc.. */ + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_YUY2, + SPA_VIDEO_FORMAT_I420), +\endcode + +We also have a `RANGE` of values for the size. We need to give a default (preferred) +size and then a min and max value: + +\code{.c} + SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), /* default */ + &SPA_RECTANGLE(1, 1), /* min */ + &SPA_RECTANGLE(4096, 4096)), /* max */ +\endcode + +We have something similar for the framerate. + +Note that there are other video parameters that we don't specify here. This +means that we don't have any restrictions for their values. + +See \ref page_spa_pod for more information about how to make these +POD objects. + +Now we're ready to connect the stream and run the main loop: + +\code{.c} + pw_stream_connect(data.stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS, + params, 1); + + pw_main_loop_run(data.loop); +\endcode + +To connect we specify that we have a `PW_DIRECTION_INPUT` stream. The third +argument is always `PW_ID_ANY`. + +We're setting the `PW_STREAM_FLAG_AUTOCONNECT` flag to make an automatic +connection to a suitable camera and `PW_STREAM_FLAG_MAP_BUFFERS` to let the +stream mmap the data for us. + +And last we pass the extra parameters for our stream. Here we only have the +allowed formats (`SPA_PARAM_EnumFormat`). + +Running the mainloop will start the connection and negotiation process. +First our `param_changed` event will be called with the format that was +negotiated between our stream and the camera. This is always something that +is compatible with what we enumerated in the EnumFormat param when we +connected. + +Let's take a look at how we can parse the format in the `param_changed` +event: + +\code{.c} +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct data *data = userdata; + + if (param == NULL || id != SPA_PARAM_Format) + return; +\endcode + +First check if there is a param. A NULL param means that it is cleared. The ID +of the param tells you what param it is. We are only interested in Format +param (`SPA_PARAM_Format`). + +We can parse the media type and subtype as below and ensure that it is +of the right type. In our example this will always be true but when your +EnumFormat contains different media types or subtypes, this is how you can +parse them: + +\code{.c} + if (spa_format_parse(param, + &data->format.media_type, + &data->format.media_subtype) < 0) + return; + + if (data->format.media_type != SPA_MEDIA_TYPE_video || + data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; +\endcode + +For the `video/raw` media type/subtype there is a utility function to +parse out the values into a `struct spa_video_info`. This makes it easier +to deal with. + +\code{.c} + if (spa_format_video_raw_parse(param, &data->format.info.raw) < 0) + return; + + printf("got video format:\n"); + printf(" format: %d (%s)\n", data->format.info.raw.format, + spa_debug_type_find_name(spa_type_video_format, + data->format.info.raw.format)); + printf(" size: %dx%d\n", data->format.info.raw.size.width, + data->format.info.raw.size.height); + printf(" framerate: %d/%d\n", data->format.info.raw.framerate.num, + data->format.info.raw.framerate.denom); + + /** prepare to render video of this size */ +} +\endcode + +In this example we dump the video size and parameters but in a real playback +or capture application you might want to set up the screen or encoder to +deal with the format. + +After negotiation, the process function is called for each new frame. Check out +\ref page_tutorial4 for another example. + +\snippet tutorial5.c on_process + +In a real playback application, one would do something with the data, like +copy it to the screen or encode it into a file. + +\ref page_tutorial4 | \ref page_tutorial "Index" | \ref page_tutorial6 + +*/ diff --git a/doc/tutorial6.c b/doc/tutorial6.c new file mode 100644 index 0000000..45d4485 --- /dev/null +++ b/doc/tutorial6.c @@ -0,0 +1,97 @@ +/* + [title] + \ref page_tutorial6 + [title] + */ +/* [code] */ +#include <pipewire/pipewire.h> + +struct data { + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct pw_client *client; + struct spa_hook client_listener; +}; + +/* [client_info] */ +static void client_info(void *object, const struct pw_client_info *info) +{ + struct data *data = object; + const struct spa_dict_item *item; + + printf("client: id:%u\n", info->id); + printf("\tprops:\n"); + spa_dict_for_each(item, info->props) + printf("\t\t%s: \"%s\"\n", item->key, item->value); + + pw_main_loop_quit(data->loop); +} + +static const struct pw_client_events client_events = { + PW_VERSION_CLIENT_EVENTS, + .info = client_info, +}; +/* [client_info] */ + +/* [registry_event_global] */ +static void registry_event_global(void *_data, uint32_t id, + uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) +{ + struct data *data = _data; + if (data->client != NULL) + return; + + if (strcmp(type, PW_TYPE_INTERFACE_Client) == 0) { + data->client = pw_registry_bind(data->registry, + id, type, PW_VERSION_CLIENT, 0); + pw_client_add_listener(data->client, + &data->client_listener, + &client_events, data); + } +} +/* [registry_event_global] */ + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +int main(int argc, char *argv[]) +{ + struct data data; + + spa_zero(data); + + pw_init(&argc, &argv); + + data.loop = pw_main_loop_new(NULL /* properties */ ); + data.context = pw_context_new(pw_main_loop_get_loop(data.loop), + NULL /* properties */ , + 0 /* user_data size */ ); + + data.core = pw_context_connect(data.context, NULL /* properties */ , + 0 /* user_data size */ ); + + data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, + 0 /* user_data size */ ); + + pw_registry_add_listener(data.registry, &data.registry_listener, + ®istry_events, &data); + + pw_main_loop_run(data.loop); + + pw_proxy_destroy((struct pw_proxy *)data.client); + pw_proxy_destroy((struct pw_proxy *)data.registry); + pw_core_disconnect(data.core); + pw_context_destroy(data.context); + pw_main_loop_destroy(data.loop); + + return 0; +} +/* [code] */ diff --git a/doc/tutorial6.dox b/doc/tutorial6.dox new file mode 100644 index 0000000..0cee850 --- /dev/null +++ b/doc/tutorial6.dox @@ -0,0 +1,69 @@ +/** \page page_tutorial6 Tutorial - Part 6: Binding Objects + +\ref page_tutorial5 | \ref page_tutorial "Index" + +In this tutorial we show how to bind to an object so that we can +receive events and call methods on the object. + +Let take a look at the following application to start. + +\snippet tutorial6.c code + +To compile the simple test application, copy it into a tutorial6.c file and +use: + + gcc -Wall tutorial6.c -o tutorial6 $(pkg-config --cflags --libs libpipewire-0.3) + +Most of this is the same as \ref page_tutorial2 where we simply +enumerated all objects on the server. Instead of just printing the object +id and some other properties, in this example we also bind to the object. + +We use the `pw_registry_bind()` method on our registry object like this: + +\snippet tutorial6.c registry_event_global + +We bind to the first client object that we see. This gives us a pointer +to a `struct pw_proxy` that we can also cast to a `struct pw_client`. + +On the proxy we can call methods and listen for events. PipeWire will +automatically serialize the method calls and events between client and +server for us. + +We can now listen for events by adding a listener. We're going to +listen to the info event on the client object that is emitted right +after we bind to it or when it changes. This is not very different +from the registry listener we added before: + +\snippet tutorial6.c client_info + +\code{.c} +static void registry_event_global(void *_data, uint32_t id, + uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) +{ + /* ... */ + pw_client_add_listener(data->client, + &data->client_listener, + &client_events, data); + /* ... */ +} +\endcode + +We're also quitting the mainloop after we get the info to nicely stop +our tutorial application. + +When we stop the application, don't forget to destroy all proxies that +you created. Otherwise, they will be leaked: + +\code{.c} + /* ... */ + pw_proxy_destroy((struct pw_proxy *)data.client); + /* ... */ + + return 0; +} +\endcode + +\ref page_tutorial5 | \ref page_tutorial "Index" + +*/ |