mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-07-03 00:06:38 -04:00
module-rtp: Add documentation about module internals and improve comments
* Add .dox file that describes the internal design of the RTP module. * Add details to the sink module documentation about the meaning of source.ip and how it interacts with local.ifname . * Rename "constant delay mode" to "constant latency mode" comments in the code to match the documentation. * Minor comment fixes.
This commit is contained in:
parent
2f94a49962
commit
c4a9023215
7 changed files with 428 additions and 5 deletions
|
|
@ -16,6 +16,7 @@
|
|||
- \subpage page_latency
|
||||
- \subpage page_tag
|
||||
- \subpage page_native_protocol
|
||||
- \subpage page_rtp_module_internals
|
||||
|
||||
|
||||
# Components
|
||||
|
|
|
|||
335
doc/dox/internals/rtp-module-internals.dox
Normal file
335
doc/dox/internals/rtp-module-internals.dox
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
/** \page page_rtp_module_internals RTP sink and source module internals
|
||||
|
||||
This document explains the architecture of PipeWire's RTP module.
|
||||
|
||||
\tableofcontents
|
||||
|
||||
# Introduction {#rtp-module-internals-introduction}
|
||||
|
||||
The "RTP module" actually refers to a set of three modules which share source code:
|
||||
|
||||
- \ref page_module_rtp_sink "RTP sink module" : Creates an RTP sink node and
|
||||
exposes it to the graph. This sink node places PCM audio into an internal ring
|
||||
buffer. This ring buffer is the source for the data of outgoing packets. The
|
||||
RTP timestamps may be synchronized against PTP time, depending on what buffer
|
||||
mode is used. This module also has a special "separate PTP sender" mode, where
|
||||
the actual send portion is done by an internal mini graph that runs on a special
|
||||
PTP based graph driver.
|
||||
- \ref page_module_rtp_source "RTP source module" : Creates an RTP source node
|
||||
and exposes it to the graph. This source node receives RTP packets and places
|
||||
their PCM data into an internal ring buffer. The node's process callback reads
|
||||
from that ring buffer and outputs that data to the graph. Depending on what mode
|
||||
is used, the position that the ring buffer is read from may be synchronized
|
||||
against a PTP time source.
|
||||
- \ref page_module_rtp_sap "SAP module" : Announces SAP sessions via multicast,
|
||||
and also listens for SAP sessions. If it discovers another SAP session, it
|
||||
instantiates the RTP source module, which in turn creates and exposes its RTP
|
||||
source node. See RFC 2974 for more about SAP.
|
||||
|
||||
For notes about the configuration, see the individual module documentation.
|
||||
|
||||
# RTP stream details {#rtp-module-internals-stream-details}
|
||||
|
||||
The core of the RTP sink and source modules is the `rtp_stream`. This is built around
|
||||
a \ref pw_stream "PipeWire stream". This stream can operate in the `PW_DIRECTION_INPUT`
|
||||
direction (used by the RTP sink module) or in the `PW_DIRECTION_OUTPUT` direction
|
||||
(used by the RTP source module).
|
||||
|
||||
The `rtp_stream` is implemented in `stream.c` and `stream.h`. `stream.c` includes
|
||||
`audio.c`, `midi.c`, `opus.c`. These handle media subtype specific setups,
|
||||
teardowns, and data processing:
|
||||
|
||||
- `audio.c` corresponds to `SPA_MEDIA_SUBTYPE_raw` and handles PCM audio.
|
||||
- `midi.c` corresponds to `SPA_MEDIA_SUBTYPE_control` and handles MIDI.
|
||||
- `opus.c` is similar to `audio.c`, but corresponds to `SPA_MEDIA_SUBTYPE_opus`,
|
||||
and encodes PCM audio to Opus prior to sending out RTP packets and decodes
|
||||
Opus encoded audio from incoming RTP packets.
|
||||
|
||||
The process callback in `rtp_stream` is set by these sources depending
|
||||
on the media subtype. Other, `rtp_stream` specific callbacks like a flush timeout
|
||||
handler are also set by these sources, since they are media subtype specific.
|
||||
|
||||
The RTP sink and source modules are configured via properties, represented by
|
||||
`pw_properties`. Both support "stream.props" values inside their properties. These
|
||||
values in turn are child `pw_properties` instances that are passed directly to
|
||||
their `rtp_stream` instances. The modules also copy some of the values of their
|
||||
own properties into that child `pw_properties` instance. The exact list of values
|
||||
that are copied over depends on the module. But, this means that some values can
|
||||
be set directly in the module properties, or inside the stream.props properties.
|
||||
One example of this would be `sess.ts-direct`.
|
||||
|
||||
\note This document refers to this as "copying to the stream properties". Actually,
|
||||
a value is copied from the module's properties to the stream properties if and only
|
||||
if that value is not already set in the stream properties. If it is, the already
|
||||
existing value takes priority.
|
||||
|
||||
`audio.c` is by far the most complex of the media subtype handlers. All three
|
||||
handlers have some notion of the direct timestamp and constant latency modes, but
|
||||
`audio.c` is (currently) the only one with the fully reworked implementation that
|
||||
this document describes (the `impl->actual_max_buffer_size` modulo scheme,
|
||||
`impl->ts_align`, device delay compensation, and the exact over/underrun thresholds).
|
||||
`midi.c` and `opus.c` still carry their own, simpler direct-vs-constant-latency
|
||||
handling and a `TODO` to converge on the `audio.c` approach. `audio.c` also features
|
||||
the separate PTP sender mode, which the other two do not have at all.
|
||||
|
||||
## Ring buffer and wrap-around behavior {#rtp-module-internals-ring-buffer-behavior}
|
||||
|
||||
The `rtp_stream` sets up a fixed-size ring buffer. Its size is derived from the
|
||||
`sess.buffer-size` property, in bytes. Note that this is a *stream* property: it
|
||||
is read by `rtp_stream_new()` from the properties it is handed, and - unlike e.g.
|
||||
`sess.ts-direct` - neither the sink nor the source module copies it over from its
|
||||
own properties, so in practice it can only be set inside `stream.props`.
|
||||
|
||||
The `sess.buffer-size` value is not used verbatim. `rtp_stream_new()` derives two
|
||||
quantities from it:
|
||||
|
||||
- `impl->buffer_size` is `sess.buffer-size` rounded *up* to the next power of two
|
||||
(via `SPA_ROUND_UP_POW2_32()`), and is the size of the actual allocation (that is,
|
||||
of `impl->buffer`). It is a power of two because the `midi.c` and `opus.c` handlers
|
||||
wrap their indices with a bit mask (`impl->buffer_mask`, and `impl->buffer_mask2`
|
||||
against the half-sized `impl->buffer_size2`) rather than a modulo, and masking only
|
||||
wraps correctly for power-of-two sizes. `impl->buffer_size` is generally *not* an
|
||||
integer multiple of the stride.
|
||||
- `impl->actual_max_buffer_size` is `impl->buffer_size` rounded *down* to an integer
|
||||
multiple of the stride (via `SPA_ROUND_DOWN()`). This is used by `audio.c`, which
|
||||
- unlike `midi.c` and `opus.c` - wraps via a modulo against this value. `audio.c`
|
||||
was reworked to do this to fix the stride-alignment problem described below;
|
||||
`midi.c` and `opus.c` still use the mask scheme and carry a `TODO` to converge on
|
||||
it.
|
||||
|
||||
The actual, allocated buffer is present as `impl->buffer`. This is the pure data
|
||||
storage buffer, without any read or write index.
|
||||
|
||||
\note `impl->buffer` and `impl->target_buffer` are not to be confused. The former
|
||||
is the actual buffer, while the latter is the session latency, converted to RTP
|
||||
samples. Furthermore, `sess.buffer-size` and the session latency must be picked such
|
||||
that `impl->target_buffer` worth of samples fits within the buffer. Since
|
||||
`impl->target_buffer` is in samples while `impl->actual_max_buffer_size` is in bytes,
|
||||
this means `impl->target_buffer * stride` must not exceed
|
||||
`impl->actual_max_buffer_size` (equivalently, `impl->target_buffer` must not exceed
|
||||
`impl->actual_max_buffer_size / stride`).
|
||||
|
||||
The stride value depends on the media subtype, and is set internally by `rtp_stream_new()`.
|
||||
|
||||
The buffer contents are always interleaved when the number of channels is greater
|
||||
than 1 and the data is raw audio (so, this does not apply to MIDI for example).
|
||||
The stride value specifies the unit size inside the buffer that contains audio
|
||||
data for all channels, played at the exact same time. In the PCM case, the stride
|
||||
is (num_channels * bytes_per_pcm_sample).
|
||||
|
||||
\note It is important to keep in mind that the way the read and write index are
|
||||
handled in this ring buffer deviates somewhat from standard ring buffer usage
|
||||
in typical producer-consumer schemes, especially in the direct timestamp mode
|
||||
(more on that further below).
|
||||
|
||||
The read and write index logic is handled by `impl->ring`. Both read and write
|
||||
indices increase monotonically (as free-running values) unless they are
|
||||
resynchronized. Because they are free-running rather than being wrapped at the
|
||||
buffer boundary, the fill level is simply their difference, and that is what removes
|
||||
the usual ambiguity about whether the ring buffer is empty or full. When accessing
|
||||
the actual buffer contents, an index is first turned into a byte offset (see below),
|
||||
and that offset is then reduced to the buffer bounds - in `audio.c` by taking it
|
||||
modulo `impl->actual_max_buffer_size`, and in `midi.c` and `opus.c` by masking it
|
||||
with `impl->buffer_mask` / `impl->buffer_mask2`. Reducing modulo
|
||||
`impl->actual_max_buffer_size` (rather than the raw `impl->buffer_size`) is essential
|
||||
for the buffer modes to work properly (explained further below).
|
||||
|
||||
The read and write indices are given in RTP sample units. To access data in the
|
||||
buffer, the indices are multiplied by the stride to get a byte offset. This also
|
||||
means that the buffer size (which is given in bytes) must be an integer multiple
|
||||
of the stride size - otherwise, the read and write indices may refer to places in
|
||||
the buffer that cannot contain a full data set for all channels. For example, if
|
||||
the stride is 6, and the buffer size is 100, then when the read index is 16, the
|
||||
byte offset would be 16*6 = 96 - but there, only 4 bytes could be read, not 6.
|
||||
For this reason, the buffer size is internally rounded down to the nearest
|
||||
integer multiple of the stride size, as mentioned above.
|
||||
|
||||
In the RTP sink module, the `rtp_stream` appends data to the ring buffer at its
|
||||
write index, except for when a resynchronization happens - the write index is then
|
||||
reset to match the `spa_io_clock.position` value (scaled to RTP sample units).
|
||||
One resynchronization always happens at startup. The RTP timestamps of outgoing
|
||||
packets are derived from the ring buffer's read index.
|
||||
|
||||
In the RTP source module, `rtp_stream` reads data from the ring buffer depending
|
||||
on the buffer mode. More on that further below.
|
||||
|
||||
## Threading model and data processing {#rtp-module-internals-threading-model}
|
||||
|
||||
Most of the code in `stream.c` runs in the stream's main loop, while most of the
|
||||
code in the media subtype handlers (`audio.c` etc.) runs in the stream's data loop.
|
||||
|
||||
`stream_start()` is called by `on_stream_state_changed()`when the stream's state
|
||||
changes to `PW_STREAM_STATE_STREAMING`. At that stage, the stream's data loop is
|
||||
running, but the stream's PipeWire graph node is not yet attached to the data loop,
|
||||
so no data processing takes place at this time. The attachment happens after
|
||||
`on_stream_state_changed()` finished. This means that while `stream_start()` is
|
||||
run from the main loop, it is safe to set internal states that are accessed and
|
||||
modified by other functions that run in the data loop.
|
||||
|
||||
Similarly, `stream_stop()` is called by `on_stream_state_changed()`when the stream's
|
||||
state changes to `PW_STREAM_STATE_PAUSED`. (It is not called however if the
|
||||
`node.always-process` in the stream.props properties in the RTP source module
|
||||
is set to true.) At that stage, the stream's graph node has already been detached
|
||||
from the data loop. It therefore is safe for `stream_stop()` to touch internal
|
||||
states that normally would be accessed by functions that run in the data loop.
|
||||
|
||||
The media subtype handlers each have an init function, like `rtp_audio_init()`.
|
||||
This is one of the functions from these handlers that runs in the main loop, since
|
||||
these init functions are called by `rtp_stream_new()`. The other functions are:
|
||||
|
||||
- `stop_timer()` (called by `stream_start()`)
|
||||
- `resend_packets()` (RAOP specific - not used by the RTP sink or source modules)
|
||||
- `deinit()` (called by `rtp_stream_destroy()`)
|
||||
|
||||
Everything else in the media subtype handlers runs in the data loop, with the
|
||||
exception of `ptp_sender_process()` in `audio.c`, which runs under the separate
|
||||
PTP sender's own driver and may have a separate data loop.
|
||||
|
||||
`audio.c` has two extra specialties:
|
||||
|
||||
1. It aggregates the contents of the ring buffer such that it can split it up into
|
||||
RTP packets with the specified packet time (see `rtp.ptime` in the module
|
||||
and stream properties). Depending on how full the ring buffer is, it may decide
|
||||
to send out some of its contents within the current graph cycle, and may use
|
||||
a timer (which runs in the data loop) to schedule the output of the remaining
|
||||
data later, to not risk an xrun by blocking the data loop in the current graph
|
||||
cycle for too long.
|
||||
2. The separate PTP sender mode is driven by its own driver. More on that
|
||||
mode is documented further below.
|
||||
|
||||
# Buffer modes {#rtp-module-internals-buffer-modes}
|
||||
|
||||
\note Read the buffer modes documentation in \ref page_module_rtp_source first
|
||||
if not already done.
|
||||
|
||||
Also, this section specifically describes how the buffer modes in `audio.c` are
|
||||
handled. `midi.c` and `opus.c` do branch on `impl->direct_timestamp` too, but with
|
||||
their own, simpler handling (and aligning those with what `audio.c` does is an
|
||||
open `TODO`); the detailed behavior described here is `audio.c` specific.
|
||||
|
||||
The buffer mode only has a minor influence on the RTP sink module. In the constant
|
||||
latency mode, `impl->ts_align` is used in resynchronization cases to avoid a
|
||||
discontinuity in the outgoing RTP timestamps. In the direct timestamp mode,
|
||||
`impl->ts_align` is not used.
|
||||
|
||||
The rest of the buffer mode documentation is about the behavior on the receiving
|
||||
side, that is, how the RTP source module uses the `rtp_stream`.
|
||||
|
||||
In both modes, received data is inserted into the ring buffer according to the
|
||||
RTP timestamp. This timestamp is first shifted into the future by the value of
|
||||
`impl->target_buffer`. Then, the ring buffer's write index is advanced. It is
|
||||
expected by the code that the sender produces continuous timestamps; that is,
|
||||
`rtp_timestamp_of_packet_2 = rtp_timestamp_of_packet_1 + rtp_samples_per_packet`.
|
||||
In certain cases, resynchronization may take place; the read and write indices
|
||||
are then reset; the read index is set to the timestamp of the next incoming RTP
|
||||
packet, while the write index is set to that packet timestamp + `impl->target_buffer`;
|
||||
that is, the write index is set to be ahead of the read index by the session
|
||||
latency in samples.
|
||||
|
||||
The write index is advanced in `rtp_audio_receive()`, the read index is advanced
|
||||
in `rtp_audio_process_playback()`.
|
||||
|
||||
## Constant latency mode {#rtp-module-internals-constant-latency-mode}
|
||||
|
||||
As mentioned in the RTP source module documentation, this is the default mode,
|
||||
where the fill level is kept at a steady value, which is `impl->target_buffer`.
|
||||
If the fill level is above or below this, a DLL is used to compute an error rate,
|
||||
which then is fed into the ASRC of the `pw_stream` the `rtp_stream` is based on.
|
||||
The estimated amount of samples that are "in-flight" (that is, samples that
|
||||
already were sent out but not yet received or which arrived right after the
|
||||
last graph cycle) are also factored into this computation. This establishes a
|
||||
control loop that resamples the audio data as needed to maintain the fill level
|
||||
at `impl->target_buffer`. Should the difference between the target and the
|
||||
actual fill level exceed a threshold, the ring buffer indices are resynchronized.
|
||||
|
||||
More concretely, the thresholds work as follows. An *underrun* is detected when
|
||||
fewer samples are available than the current graph cycle needs (`avail < wanted`);
|
||||
the missing samples are filled with silence and the sync state is dropped.
|
||||
An *overrun* on the read side is detected when the fill level exceeds
|
||||
`SPA_MIN(target_buffer * 8, impl->buffer_size / stride)`; the excess is dropped
|
||||
by advancing the read index so that only `target_buffer` worth of data remains
|
||||
(a soft correction, not a full resync). Here `target_buffer` is the
|
||||
device-delay-adjusted target (see below), i.e. `impl->target_buffer` minus the
|
||||
device delay - the two coincide only when the device delay is zero. On the write
|
||||
side (`rtp_audio_receive()`), a fill level exceeding the ring capacity
|
||||
`impl->buffer_size / stride` sets `impl->have_sync` to false, forcing a full resync.
|
||||
|
||||
\note The factor of 8 in `target_buffer * 8` is an arbitrarily / empirically
|
||||
chosen headroom multiplier: it sets how far the fill level may run above the target
|
||||
before the buffered data is treated as stale. It is *not* a unit conversion - in
|
||||
particular, it is unrelated to the eight bits in a byte, despite the superficial
|
||||
resemblance. The `impl->buffer_size / stride` term merely caps this bound at the
|
||||
physical ring capacity, in samples.
|
||||
|
||||
If the device delay (specified by the `pw_time.delay` value) is nonzero, then it
|
||||
is subtracted from `impl->target_buffer`, and the result is then used as the target
|
||||
fill level instead of `impl->target_buffer` directly.
|
||||
|
||||
## Direct timestamp mode {#rtp-module-internals-direct-timestamp-mode}
|
||||
|
||||
Since this mode requires that the graph drivers of sender and receiver are somehow
|
||||
synchronized, it implies that, if the sender's and the receiver's
|
||||
\ref spa_io_clock::position values are sampled at the exact same moment, they
|
||||
are identical. In practice, they usually deviate a bit. This deviation is the
|
||||
time sync error, and the time synchronization mechanism that is used tries to
|
||||
keep this sync error as minimal as possible.
|
||||
|
||||
The aforementioned incoming RTP timestamp shift by `impl->target_buffer` plays
|
||||
a crucial role here, since it makes sure the transport delay (which is what
|
||||
the session latency specifies in this mode) is accounted for.
|
||||
|
||||
This mode is called "direct timestamp" mode since, unlike in the constant latency
|
||||
mode, the `rtp_audio_process_playback()` function directly reads from the ring
|
||||
buffer at an index that is derived from \ref spa_io_clock::position , even if this
|
||||
position jumps around. There is some logic to detect underruns and substitute
|
||||
missing data with silence, but discontinuities otherwise have no lasting effect.
|
||||
The driver must ensure that the \ref spa_io_clock::position value increases steadily
|
||||
(except in major discontinuity cases); clock drift compensation is done by the
|
||||
driver by adjusting the graph invocation timings. See \ref page_driver for more.
|
||||
|
||||
In this mode, the `rtp_stream` DLL is not used.
|
||||
|
||||
# Separate PTP sender {#rtp-module-internals-separate-ptp-sender}
|
||||
|
||||
This section covers the *internals* of the separate PTP sender. Its user-facing
|
||||
behavior - what it is for, how it is activated via `aes67.driver-group`, and its
|
||||
benefits and trade-offs - is documented in \ref page_module_rtp_sink .
|
||||
|
||||
Only the `audio.c` media subtype handler supports this mode. When it is enabled,
|
||||
`rtp_audio_init()` in `audio.c` creates an internal `pw_filter` node that is kept
|
||||
isolated from the graph and is driven by the driver from the `aes67.driver-group`
|
||||
node group.
|
||||
|
||||
When this separate PTP sender is active, `rtp_audio_process_capture()` behaves
|
||||
differently. Rather than computing a drift itself, it stores the sink driver's
|
||||
timing information (`impl->sink_nsec`, `impl->sink_next_nsec`,
|
||||
`impl->sink_resamp_delay`, `impl->sink_quantum`) for the sender to use. From that
|
||||
information, `ptp_sender_process()` estimates the current total delay and computes
|
||||
the error between it and the target. That error is fed into a separate dedicated DLL
|
||||
(`impl->ptp_dll`), which outputs a rate. That rate (`impl->ptp_corr`) is then applied
|
||||
as the ASRC's rate at the start of `rtp_audio_process_capture()`. The ASRC then
|
||||
produces larger or smaller amounts of data, filling the ring buffer to a larger or
|
||||
smaller degree, thus forming a control loop that keeps the fill level at a certain
|
||||
target (see below), similar to what the constant latency mode does.
|
||||
|
||||
During the refilling state, no packets are sent out. The refilling state ends once
|
||||
the estimated total delay reaches `impl->target_buffer` (which is also what the
|
||||
control loop mentioned above targets). That estimated total delay is the sum of
|
||||
the current ring buffer fill level, the delay of the ASRC, and the estimated
|
||||
amount of samples that are "in-flight" (that is, samples that already were sent
|
||||
out but not yet received or which arrived right after the last graph cycle).
|
||||
|
||||
Additionally, the sender contains code for checking for too severe deviations
|
||||
between the send progress and the current PTP time. The tolerance range is
|
||||
2x the quantum size. If the deviation goes beyond that, a resynchronization
|
||||
(and consequently, another refilling) is performed. This catches cases where
|
||||
the separate sender is starved of data (that is, the main graph is lagging
|
||||
behind), and also cases when PTP discontinuities occur.
|
||||
|
||||
A similar check exists for the node wake up times. The filter node is scheduled
|
||||
by its own driver, independently of the sink node, so their wake ups are not
|
||||
inherently aligned. It is therefore important to check that the filter wakes
|
||||
up within the bounds of the sink node's wake up times (with some tolerance);
|
||||
if it does not, a resynchronization is performed.
|
||||
|
||||
*/
|
||||
|
|
@ -73,6 +73,7 @@ extra_docs = [
|
|||
'dox/internals/protocol.dox',
|
||||
'dox/internals/pulseaudio.dox',
|
||||
'dox/internals/dma-buf.dox',
|
||||
'dox/internals/rtp-module-internals.dox',
|
||||
'dox/tutorial/index.dox',
|
||||
'dox/tutorial/tutorial1.dox',
|
||||
'dox/tutorial/tutorial2.dox',
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@
|
|||
* The `rtp-sink` module creates a PipeWire sink that sends audio
|
||||
* RTP packets.
|
||||
*
|
||||
* For the internal design of the shared RTP stream implementation (ring buffer,
|
||||
* buffer modes, threading model, and the separate PTP sender mechanism), see
|
||||
* \ref page_rtp_module_internals .
|
||||
*
|
||||
* ## Module Name
|
||||
*
|
||||
* `libpipewire-module-rtp-sink`
|
||||
|
|
@ -76,6 +80,41 @@
|
|||
* - `aes67.driver-group = <string>`: for AES67 streams, can be specified in order to allow
|
||||
* the sink to be driven by a different node than the PTP driver.
|
||||
*
|
||||
* ### Additional information about `source.ip` and `local.ifname`
|
||||
*
|
||||
* The default (ANY, 0.0.0.0 or ::) lets the kernel choose the local egress interface
|
||||
* (and, from it, the source address) based on the route to `destination.ip`.
|
||||
* Setting a concrete `source.ip` address instead of ANY alters how the source-address
|
||||
* field in the outgoing packets is populated, and interacts with routing.
|
||||
*
|
||||
* In the unicast case, `source.ip` binds the socket to that local IP, setting the
|
||||
* source-address field that will appear in outgoing packets. Egress is still determined
|
||||
* by the kernel's routing lookup for the destination rather than by this address, thoug
|
||||
* source-based policy routing (if configured in the OS) can factor it into the lookup.
|
||||
*
|
||||
* \important In the multicast case, do not rely on `source.ip` to choose the outgoing
|
||||
* interface. The sockets API makes no guarantee that the source address selects multicast
|
||||
* egress, and what happens is not portable across address families (it differs between
|
||||
* IPv4 and IPv6) or operating systems. For example, the Linux kernel implicitly uses a
|
||||
* bound IPv4 source to pin the egress device for legacy compatibility, but does not do
|
||||
* so for IPv6. To control which interface multicast packets leave on, set `local.ifname`.
|
||||
*
|
||||
* (No corresponding `source.port` property exists, because the kernel
|
||||
* automatically picks an ephemeral local egress port during bind.)
|
||||
*
|
||||
* Should `local.ifname` be set, egress is strictly forced out of that named interface
|
||||
* via SO_BINDTODEVICE. If `source.ip` is left at ANY, the kernel auto-selects a source
|
||||
* address belonging to that interface, and uses that source address as the value of the
|
||||
* source-address field in the outgoing packets.
|
||||
*
|
||||
* These two properties can be combined. `local.ifname` chooses the physical interface,
|
||||
* while `source.ip` fixates the exact value of the source-address field in outgoing packets.
|
||||
* Setting them inconsistently (a `source.ip` that belongs to a different interface
|
||||
* than `local.ifname`) is not rejected at setup, but it is almost always a
|
||||
* misconfiguration. The packet then leaves via the `local.ifname` device carrying a
|
||||
* source address from another interface, which is a common cause of reverse-path
|
||||
* filtering (rp_filter) drops at the receiver or an intermediate hop.
|
||||
*
|
||||
* ## General options
|
||||
*
|
||||
* Options with well-known behavior:
|
||||
|
|
@ -169,6 +208,50 @@
|
|||
* pw-cli c 56 User '{ extra="{ \"command.id\" : \"clear-receivers\" }" }'
|
||||
* \endcode
|
||||
*
|
||||
* ## Separate PTP sender
|
||||
*
|
||||
* For AES67-style streams, the sink can be driven by a graph driver that is
|
||||
* separate from the main graph, decoupling RTP transmission timing from whatever
|
||||
* drives the rest of the graph. This is the "separate PTP sender".
|
||||
*
|
||||
* This feature is only available on the sink (sending) side; receivers cannot use
|
||||
* it. It is activated by setting `aes67.driver-group` to a non-empty string. The
|
||||
* value may be given either directly in the module's properties (in which case the
|
||||
* module copies it into `stream.props`) or in `stream.props` directly.
|
||||
*
|
||||
* `aes67.driver-group` is the name of a node group. The graph driver that shall be
|
||||
* used for sending out RTP packets and generating RTP timestamps must have its node
|
||||
* group set to that same name. It is called the "PTP sender" because that driver
|
||||
* typically synchronizes itself using PTP, but any time-synchronization method works
|
||||
* as long as the driver keeps \ref spa_io_clock::position synchronized.
|
||||
*
|
||||
* The benefits of decoupling the main graph from the synchronized driver are:
|
||||
*
|
||||
* 1. Any discontinuities and resynchronizations in the time-sync protocol do not
|
||||
* affect the entire graph, just the separate sender.
|
||||
* 2. Local audio sinks running in parallel to the RTP sink do not have to rate-match
|
||||
* to follow the synchronized graph driver, so their local output is left unaltered
|
||||
* (rate matching would otherwise be done with an ASRC or a tweakable PLL).
|
||||
* 3. Graph clock rate changes (for example, playing audio at a rate that does not
|
||||
* match the current one) no longer affect the synchronized driver's time sync.
|
||||
* 4. Linking/unlinking the RTP sink does not trigger a graph driver renegotiation,
|
||||
* which otherwise can cause subtle bugs if not handled carefully.
|
||||
*
|
||||
* The main downsides are:
|
||||
*
|
||||
* 1. Increased complexity, and thus more places where something can go wrong. In
|
||||
* particular, the fill-level-based control loop can suffer from over/underruns,
|
||||
* making it an additional potential source of audible dropouts.
|
||||
* 2. Increased latency. Since the control loop keeps the fill level at the target,
|
||||
* the separate PTP sender adds roughly `sess.latency.msec` minus one quantum of
|
||||
* latency (the last quantum's worth of data is already being used to produce the
|
||||
* current graph cycle).
|
||||
* 3. It only benefits the sender. The receiver still has to use the synchronized
|
||||
* graph driver for its entire graph.
|
||||
*
|
||||
* The internal mechanism (the dedicated DLL, the refilling state machine, and the
|
||||
* clock-drift computation) is described in \ref page_rtp_module_internals .
|
||||
*
|
||||
* \since 0.3.60
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
* source.ip and source.port and format parameters matches that of the sender that
|
||||
* is announced via SAP.
|
||||
*
|
||||
* For the internal design of the shared RTP stream implementation (ring buffer,
|
||||
* buffer modes, and threading model), see \ref page_rtp_module_internals .
|
||||
*
|
||||
* ## Module Name
|
||||
*
|
||||
* `libpipewire-module-rtp-source`
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ static void rtp_audio_process_playback(void *data)
|
|||
spa_ringbuffer_read_update(&impl->ring, timestamp);
|
||||
}
|
||||
} else {
|
||||
/* In the constant delay mode, it is assumed that the ring buffer fill
|
||||
* level matches impl->target_buffer. If not, check for over- and
|
||||
/* In the constant latency mode, it is assumed that the ring buffer
|
||||
* fill level matches impl->target_buffer. If not, check for over- and
|
||||
* underruns. Adjust the DLL as needed. If the over/underruns are too
|
||||
* severe, resynchronize. */
|
||||
|
||||
|
|
@ -387,7 +387,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len,
|
|||
write, expected_write);
|
||||
}
|
||||
|
||||
/* Write overrun only makes sense in constant delay mode. See the
|
||||
/* Write overrun only makes sense in constant latency mode. See the
|
||||
* RTP source module documentation and the rtp_audio_process_playback()
|
||||
* code for an explanation why. */
|
||||
if (!impl->direct_timestamp && (filled + samples > impl->buffer_size / stride)) {
|
||||
|
|
@ -425,7 +425,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len,
|
|||
* to be done differently to account for the wrap-around.
|
||||
*
|
||||
* (Note that this write index update is only important if
|
||||
* the constant delay mode is active, or if no spa_io_position
|
||||
* the constant latency mode is active, or if no spa_io_position
|
||||
* was not provided yet. See the rtp_audio_process_playback()
|
||||
* code for more about this.) */
|
||||
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ struct impl {
|
|||
*
|
||||
* Also, since GCC __atomic built-ins (which the SPA macros use) are
|
||||
* designed to work with integral scalar or pointer type that is 1,
|
||||
* 2, 4, or 8 bytes in length, impl->internal_state is of type uint33_t.
|
||||
* 2, 4, or 8 bytes in length, impl->internal_state is of type uint32_t.
|
||||
* This guarantee a correct size for the built-ins. The accessors take
|
||||
* care of casting from/to rtp_stream_internal_state . The relevant
|
||||
* GCC manual page for this is:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue