PipeWire 1.7.0
Loading...
Searching...
No Matches
RTP sink

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 RTP sink and source module internals .

Module Name

libpipewire-module-rtp-sink

Module Options

Options specific to the behavior of this module

  • source.ip =<str>: source IP address, default "0.0.0.0"
  • destination.ip =<str>: destination IP address, default "224.0.0.56"
  • destination.port =<int>: destination port, default random between 46000 and 47024
  • local.ifname = <str>: interface name to use
  • net.mtu = <int>: MTU to use, default 1280
  • net.ttl = <int>: TTL to use, default 1
  • net.loop = <bool>: loopback multicast, default false
  • sess.min-ptime = <float>: minimum packet time in milliseconds, default 2
  • sess.max-ptime = <float>: maximum packet time in milliseconds, default 20
  • sess.name = <str>: a session name
  • rtp.ptime = <float>: size of the packets in milliseconds, default up to MTU but between sess.min-ptime and sess.max-ptime
  • rtp.framecount = <int>: number of samples per packet, default up to MTU but between sess.min-ptime and sess.max-ptime
  • sess.latency.msec = <float>: target node latency in milliseconds, default as rtp.ptime
  • sess.ts-offset = <int>: an offset to apply to the timestamp, default -1 = random offset
  • sess.ts-refclk = <string>: the name of a reference clock
  • sess.media = <string>: the media type audio|midi|opus, default audio
  • sess.ts-direct = <bool>: use direct timestamp mode, default false.
    Note
    RTP sources that use direct timestamp mode expect the associated RTP sink to use direct timestamp mode as well. See the sess.ts-direct documentation in RTP source for more.
  • stream.props = {}: properties to be passed to the stream
  • 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:

Example configuration

# ~/.config/pipewire/pipewire.conf.d/my-rtp-sink.conf
context.modules = [
{ name = libpipewire-module-rtp-sink
args = {
#local.ifname = "eth0"
#source.ip = "0.0.0.0"
#destination.ip = "224.0.0.56"
#destination.port = 46000
#net.mtu = 1280
#net.ttl = 1
#net.loop = false
#sess.min-ptime = 2
#sess.max-ptime = 20
#sess.name = "PipeWire RTP stream"
#sess.media = "audio"
#audio.format = "S16BE"
#audio.rate = 48000
#audio.channels = 2
#audio.position = [ FL FR ]
stream.props = {
node.name = "rtp-sink"
}
}
}
]

Adding and removing receivers through commands

The following commands can be sent to the RTP sink node via pw_node_send_command():

  • add-receiver : Adds a receiver to the sink's list. If the given IP address <-> port combination was already added, the command is logged, but otherwise ignored. Arguments:
  • destination.ip : IP address to send data to. Can be a uni- or multicast address, but must be a valid address.
  • destination.port : Port to send data to. Must be valid.
  • local.ifname, source.ip, net.ttl, net.dscp, net.loop : These are all optional, and work just like in the RTP sink module's properties.
  • remove-receiver : Removes a receiver from the sink's list. The receiver is identified by the given IP address. A port can optionally be specified as well. If it isn't, then the first receiver with that IP address is removed. If no matching receiver is in the sink's list, this command does nothing. Arguments:
    • destination.ip : IP address to send data to. Can be a uni- or multicast address, but must be a valid address.
    • destination.port : Port to send data to. This is optional. But, if it is set, it must be a valid port number.
  • clear-receivers : Removes all receivers from the sink's list. If the list is empty, this does nothing. This command has no arguments.

If the RTP sink module is created with the destination.ip and destination.port properties set, it behaves as if add-receiver were called right after the module was initialized. This means that if none of these commands are used, the module behaves just as it did prior to this patch. Note that the remove-receivers command can remove this initial receiver as well.

If no receivers are added, the module continues to work normally. Adding and removing receivers mid-operation is supported.

Example pw-cli calls (56 is the ID of the RTP sink node):

pw-cli c 56 User '{ extra="{ \"command.id\" : \"add-receiver\" , \"destination.ip\" : \"10.42.0.1\", \"destination.port\" : 55001 }" }'
pw-cli c 56 User '{ extra="{ \"command.id\" : \"remove-receiver\", \"destination.ip\" : \"10.42.0.1\" }" }'
pw-cli c 56 User '{ extra="{ \"command.id\" : \"clear-receivers\" }" }'

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 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 RTP sink and source module internals .

Since
0.3.60