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:
Carlos Rafael Giani 2026-06-30 10:12:50 +02:00
parent 2f94a49962
commit c4a9023215
7 changed files with 428 additions and 5 deletions

View file

@ -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
*/

View file

@ -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`

View file

@ -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.) */

View file

@ -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: