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

@ -16,6 +16,7 @@
- \subpage page_latency
- \subpage page_tag
- \subpage page_native_protocol
- \subpage page_rtp_module_internals
# Components

View 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.
*/

View file

@ -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',

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: