Skip to content

Protocol Reference

Technical details of the bilbycast-rist implementation, covering wire formats, state machines, and design decisions.

Spec Title Status
RFC 3550 RTP: A Transport Protocol for Real-Time Applications Implemented
RFC 4585 Extended RTP Profile for RTCP-Based Feedback (AVPF) Implemented (Generic NACK)
TR-06-1:2020 RIST Simple Profile Implemented
TR-06-2:2024 RIST Main Profile Types stubbed
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • V = 2 (always)
  • PT = 33 (MPEG-2 TS, per SMPTE 2022-1)
  • Timestamp = NTP-aligned, 90 kHz clock (NTP_seconds × 90000, truncated to 32 bits)
  • Payload = 1316 bytes (7 × 188-byte TS packets, standard for broadcast)

The RTP retransmit flag is carried in the SSRC LSB (LSB=1 marks a retransmission), the authoritative source for the retransmits_received stat.

Sent by the sender every 100 ms. Always RC=0 (no report blocks) per TR-06-1. The NTP and RTP timestamps use the same wall-clock source; the receiver uses this mapping to compute output timing.

Sent by the receiver every 100 ms. RC=1 with one report block, or RC=0 for keepalive.

Sent with every compound RTCP. One chunk, one CNAME item, 32-bit aligned.

Two formats, both as Transport-layer Feedback (RTPFB):

Bitmask NACK (FMT=1, RFC 4585) — [PID(16), BLP(16)]*. PID identifies one lost packet; BLP is a bitmask covering packets PID+1 through PID+16.

Range NACK (FMT=15, TR-06-1) — [start(16), extra(16)]*. Identifies a contiguous range: start through start+extra (inclusive).

bilbycast-rist also parses librist’s default APP-based range NACK (PT=204, “RIST” subtype 0), verified against librist 0.2.11.

  • Parser cap — the APP range-NACK parser stops at MAX_RANGE_NACK_ENTRIES = 512 entries, so a hostile or oversized buffer can’t make the entry Vec scale with its input. The ceiling sits above both librist’s own 200-entry-per-packet writer limit and the 509 entries that physically fit a 2048-byte RTCP datagram, so no NACK that can reach the parser intact is ever truncated.
  • Work budget is per received datagram, not per sub-packet — a per-NACK cap is no bound at all, because a compound packs as many 16-byte minimal NACKs as fit and each arrives with fresh counters. One recv_from therefore buys at most one retransmit ring’s worth of retransmits — retransmit_buffer_capacity rounded up to a power of two, then clamped to 512–8192 — and four times that many sequence-number lookups, shared by every NACK in the compound. Below that 8192 ceiling honest ARQ can’t be under-served: serving more retransmits than the ring holds is provably duplicate work, since by then every packet still buffered has already gone back out. A ring configured larger than 8192 packets is the one case where the budget itself binds.
  • Media SSRC 0 is accepted on purpose — older bilbycast receivers built NACKs as sender_ssrc.unwrap_or(0) before they had seen an SR, so a NACK naming 0 is still honoured. That means a blind off-path attacker naming 0 can force retransmits out of a sender that has never received RTCP. The cost is bounded by the per-datagram budget above and the egress is aimed only at the operator’s own configured remote_rtp_addr — self-inflicted cost, not a reflector — but Simple Profile is unauthenticated and offers no way to close it. Current receivers name the SSRC seen on inbound RTP headers, so the accept can be dropped once no supported peer emits 0.

Optional mechanism for measuring round-trip time (TR-06-1 Section 5.2.6).

  • Request (subtype=2): SSRC, "RIST", timestamp_MSW, timestamp_LSW
  • Response (subtype=3): SSRC, "RIST", timestamp_MSW, timestamp_LSW, processing_delay_us

All RTCP is sent as compound packets (RFC 3550 Section 6.1). First packet must be SR or RR. Parsing is lenient — unknown sub-packets (e.g. XR PT=77, librist’s RRT extension) are tolerated and preserved rather than aborting the compound.

  • Sender compound: [SR, SDES, RTT_echo_request?]
  • Receiver compound: [RR, SDES, NACK?, RTT_echo_request?]
  • RTT Echo Response compound: [RR(empty), RTT_echo_response]

O(1) gap detection using a flat ring buffer indexed by seq % 4096.

  1. Gap detection: when packet N arrives and expected was M (M < N), mark M..N-1 as missing
  2. NACK timing: first NACK after base_delay (20 ms); subsequent retries at RTT intervals if RTT is known
  3. Max retries: after max_nack_retries (default 10) attempts, give up on the packet
  4. Recovery: when an out-of-order or retransmitted packet arrives, deactivate its NACK slot

A fast NACK pump (10 ms tick, RTT-scaled retry delay) drives retransmission requests, and a receiver-side reorder / jitter buffer handles out-of-order delivery.

O(1) insert and lookup using a flat ring buffer indexed by seq % capacity.

  • Default capacity: 2048 packets (power of two for fast modulo via bitmask)
  • Each slot stores Bytes (reference-counted, zero-copy on retransmit)
  • Stale detection: slot stores the sequence number; lookup verifies it matches

TCP-style EWMA (RFC 6298):

  • SRTT = 7/8 × SRTT + 1/8 × sample
  • RTTVAR = 3/4 × RTTVAR + 1/4 × |SRTT - sample|
  • NACK delay = SRTT / 2

Deduplicates packets arriving on multiple network paths:

  • Maintains a window of recently seen sequence numbers
  • First arrival wins; duplicates are dropped
  • Handles wraparound at the 16-bit sequence boundary

It is a rist-protocol primitive and is not reachable from RistSocket — RistSocketConfig has no bonding field and nothing in rist-transport constructs BondingConfig / ReceiverBonding. A transport consumer that wants a hitless merge runs one itself over RistDelivered.rtp_seq, the wire RTP sequence number shared across 2022-7 redundant legs.

RTP timestamps are NTP-aligned at 90 kHz:

rtp_timestamp = (NTP_time_in_microseconds * 90 / 1000) as u32

Where NTP time = Unix time + 2,208,988,800 seconds. This makes the SR’s NTP-to-RTP mapping exact, which is required for correct output timing in receivers. Both RTP data packets and RTCP SR packets derive timestamps from the same wall-clock epoch captured at sender start (monotonic Instant paired with SystemTime for NTP conversion).

RIST Simple Profile uses adjacent UDP port pairs:

  • RTP data: even port P
  • RTCP control: port P + 1

Both sender and receiver bind dual-port channels. The port must be even; odd ports are rejected with ChannelError::OddPort.

Sockets are created with:

  • SO_REUSEADDR — allows quick restart after crash
  • 32 MB receive buffer — prevents kernel drops at high bitrates
  • 32 MB send buffer — prevents send blocking during retransmit bursts
  • Non-blocking mode — for tokio async I/O

The sender and receiver tasks own all mutable state. No Mutex or RwLock on the data path:

  • Sender task: sequence counter, RTCP state, retransmit buffer, RTT estimator
  • Receiver task: RTCP state, NACK scheduler, RTT estimator
  • Communication: mpsc channels for application data in/out; a shared RistConnStats handle (Arc<AtomicU64>) exposes counters lock-free

The tokio::select! loop handles all I/O multiplexing without blocking.