RFC 2508

Compressing IP/UDP/RTP Headers for Low-Speed Serial Links

๐Ÿ“… published February 1999, from ietf

okay bestie, this RFC is from February 1999 and it's a Standards Track doc (so it's a real protocol spec, not just vibes ๐Ÿ’…). It was written by S. Casner and V. Jacobson, both at Cisco Systems, and it's published under the Network Working Group. It doesn't list anything it obsoletes or updates in its header. ๐Ÿฆ‹

Status of This Memo โœจ

ok so this doc specifies an Internet standards track protocol for the Internet community, and it's basically asking everyone to talk about it and send over suggestions for making it better ๐Ÿซถ. If you want to know where this protocol stands officially, check the current edition of the "Internet Official Protocol Standards" (STD 1). And the distribution of this memo is unlimited, so share it with everyone ๐Ÿ’–

Copyright (C) The Internet Society (1999). All Rights Reserved.

Abstract ๐ŸŒธ

this doc explains a way to shrink the headers of IP/UDP/RTP datagrams so they don't eat up so much space on low-speed serial links ๐Ÿ˜ญ. In lots of cases, all three headers can get squished down to just 2-4 bytes. iconic ๐Ÿ‘‘

comments are wanted! send them to the working group mailing list at rem-conf@es.net and/or to the author(s) ๐Ÿงš

the key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. ๐Ÿ”ฅ


1. Introduction ๐Ÿ“

ok so the Real-time Transport Protocol got published as an RFC [1], and ever since then people have been super into using RTP as one step toward getting different network audio/video apps to work together ๐ŸŒˆ. BUT there's a concern: the 12-byte RTP header is kind of huge for a 20-byte payload when you're running over slow lines like dial-up modems at 14.4 or 28.8 kb/s ๐Ÿ’€. (Some apps already running in that environment use their own protocol with a header of just a few bytes, but it has less functionality than RTP. ๐Ÿ™ˆ)

header size can be cut down with compression, which worked really well for TCP [2] โœจ. Here, the compression could be applied to the RTP header by itself on an end-to-end basis, or to the whole combo of IP, UDP and RTP headers on a link-by-link basis ๐Ÿฆ‹. Compressing the 40 bytes of combined headers together gets you way more gain than compressing just the 12 bytes of RTP header, because the result ends up about the same size (2-4 bytes) either way ๐Ÿคฏ. Link-by-link also performs better since the delay and loss rate are lower ๐Ÿซถ. So the method in this doc is for combined compression of IP, UDP and RTP headers on a link-by-link basis. main character energy ๐Ÿ‘‘

this doc defines a compression scheme that can be used with IPv4, IPv6, or packets encapsulated with more than one IP header, though the main focus is IPv4 ๐ŸŽ€. The IP/UDP/RTP compression here is meant to fit inside the bigger compression framework specified in [3], which works for both IPv6 and IPv4. That framework splits transports above IP into two classes: TCP and non-TCP. This spec creates IP/UDP/RTP as a third class pulled out of the non-TCP class ๐Ÿ’….

2. Assumptions and Tradeoffs ๐Ÿ’–

the goal here is to get the IP/UDP/RTP headers down to two bytes for most packets when no UDP checksums are being sent, or four bytes when checksums are included ๐ŸŒธ. The big motivation is sending audio and video over 14.4 and 28.8 dialup modems ๐Ÿ“ž. These links tend to be full-duplex, and the protocol takes advantage of that, though it can still work on simplex links with reduced performance ๐Ÿงš. This compression scheme does best on local links with a low round-trip-time โœจ.

this spec doesn't deal with segmentation and preemption of large packets to cut down the delay on the slow link that small real-time packets experience. The one exception is that Section 4 points out some interactions between segmentation and compression that can happen ๐Ÿ‘€. Segmentation schemes can be defined separately and used together with the compression here ๐Ÿซถ.

worth noting that implementation simplicity is a big deal when you're judging a compression scheme ๐Ÿ”ฅ. Communications servers might need to support compression across as many as 100 dial-up modem lines on a single processor ๐Ÿ˜ญ. So it can make sense to simplify the design and give up some generality, or to build a flexible design that's general but can be subsetted for simplicity ๐Ÿ“. Higher compression gain could come from sending more complex models of the changing header fields from the compressor to the decompressor, but that complexity is considered unnecessary here ๐Ÿ™…โ€โ™€๏ธ. The next sections talk through some of the tradeoffs listed above ๐Ÿ’œ.

2.1. Simplex vs. Full Duplex ๐ŸŒˆ

if nothing else is a constraint, a compression scheme that works over simplex links is the one you'd pick over one that doesn't ๐Ÿ’…. BUT running over a simplex link means you need periodic refreshes with an uncompressed packet header to restore compression state if an error happens ๐Ÿ˜ญ. If an explicit error signal can be sent back instead, recovery gets way faster, and the overhead in the no-error case goes down too ๐Ÿฆ‹. To get those perf wins, this spec includes an explicit error indication sent on the reverse path โœจ.

on a simplex link, you could use a periodic refresh instead ๐Ÿงš. Whenever the decompressor detects an error in a particular packet stream, it would just throw away all the packets in that stream until it gets an uncompressed header for that stream, and then it picks decompression back up ๐Ÿ™ˆ. The penalty is the potentially large number of packets that get discarded ๐Ÿ’€. The periodic refresh method described in Section 3.3 of [3] applies to IP/UDP/RTP compression on simplex links or on links with high delay, and it also applies to other non-TCP packet streams ๐ŸŒธ.

2.2. Segmentation and Layering ๐ŸŽ€

the delay from the time it takes to send a large packet over the slow link isn't a problem for one-way audio, for example, because the receiver can adapt to the variance in delay ๐Ÿซถ. But for interactive conversations, keeping the end-to-end delay low is critical ๐Ÿ”ฅ. Segmenting large, non-real-time packets so small real-time packets can go out between the segments can cut down the delay ๐Ÿ’–.

this spec only deals with compression and assumes that segmentation, if it's included, gets handled as a separate layer ๐Ÿงš. It would be a bad move to mash segmentation and compression together in a way where the compression couldn't be used by itself in situations where segmentation isn't necessary or isn't practical ๐Ÿ™…โ€โ™€๏ธ. Similarly, it's best to avoid requiring a reservation protocol ๐Ÿฆ‹. The compression scheme can run locally on the two ends of a link, independent of any other mechanisms, except that the link layer has to provide some packet type codes, a packet length indication, and good error detection ๐Ÿ’….

BUT separately compressing the IP/UDP and RTP layers loses too much of the compression gain you could get by treating them together ๐Ÿ˜ญ. Crossing these protocol layer boundaries is fine here because the same function is being applied across all the layers โœจ.

3. The Compression Algorithm ๐ŸŒˆ

the compression algorithm in this doc is heavily based on the design of TCP/IP header compression as described in RFC 1144 [2] ๐Ÿ‘‘. Check that RFC for more info on the underlying motivations and general principles of header compression ๐Ÿ“š.

3.1. The basic idea ๐Ÿ’œ

in TCP header compression, the first factor-of-two reduction in data rate comes from noticing that half of the bytes in the IP and TCP headers stay constant for the whole life of the connection ๐Ÿคฏ. After the uncompressed header is sent once, those fields can be left out of the compressed headers that follow ๐Ÿซถ. The rest of the compression comes from differential coding on the changing fields to shrink them, and from getting rid of the changing fields entirely in common cases by calculating the changes from the length of the packet ๐Ÿงš. That length is indicated by the link-level protocol ๐ŸŽ€.

for RTP header compression, some of the same techniques can be used ๐ŸŒธ. BUT the big win comes from noticing that even though several fields change in every packet, the difference from packet to packet is often constant, so the second-order difference is zero ๐Ÿ’–. By keeping both the uncompressed header and the first-order differences in the session state shared between the compressor and decompressor, all that has to be communicated is an indication that the second-order difference was zero ๐Ÿฆ‹. In that case, the decompressor can rebuild the original header with no loss of information, just by adding the first-order differences to the saved uncompressed header as each compressed packet comes in ๐Ÿ”ฅ.

just like TCP/IP header compression keeps shared state for multiple simultaneous TCP connections, this IP/UDP/RTP compression SHOULD maintain state for multiple session contexts ๐ŸŒˆ. A session context is defined by the combo of the IP source and destination addresses, the UDP source and destination ports, and the RTP SSRC field ๐ŸŽ€. A compressor implementation might use a hash function on these fields to index a table of stored session contexts ๐Ÿงš. The compressed packet carries a small integer, called the session context identifier or CID, which says which session context that packet should be read against ๐Ÿ’…. The decompressor can use the CID to index its table of stored session contexts directly โœจ.

because RTP compression is lossless, it can be applied to any UDP traffic that benefits from it ๐Ÿ’–. Most likely the only packets that'll benefit are RTP packets, but it's totally acceptable to use heuristics to figure out whether a packet is RTP, because no harm is done if the heuristic gets it wrong ๐Ÿ™ˆ. The catch is this means running the compression algorithm on all UDP packets, or at least the ones with even port numbers (see section 3.4) ๐Ÿซถ.

most compressor implementations will need to keep a "negative cache" of packet streams that have failed to compress as RTP packets after some number of attempts, so they don't keep trying ๐Ÿ™…โ€โ™€๏ธ. Failing to compress means some fields in the potential RTP header that are expected to stay constant most of the time, like the payload type field, keep changing ๐Ÿ’€. Even if the other such fields stay constant, a packet stream with a constantly changing SSRC field SHOULD be entered in the negative cache so it doesn't eat up all the available session contexts ๐Ÿ”ฅ. The negative cache is indexed by the source and destination IP address and UDP port pairs, but NOT the RTP SSRC field, since the SSRC might be changing ๐ŸŒธ. When RTP compression fails, the IP and UDP headers MAY still be compressed ๐Ÿฆ‹.

โš ๏ธ heads up, this one's serious: fragmented IP packets that aren't initial fragments, and packets that aren't long enough to contain a complete UDP header, MUST NOT be sent as FULL_HEADER packets ๐Ÿšซ. Also, packets that don't additionally contain at least 12 bytes of UDP data MUST NOT be used to establish RTP context ๐Ÿ’”. If a packet like that is sent as a FULL_HEADER packet, it MAY be followed by COMPRESSED_UDP packets, but it MUST NOT be followed by COMPRESSED_RTP packets ๐Ÿ˜ญ.

3.2. Header Compression for RTP Data Packets โœจ

okay bestie, in the IPv4 header only the total length, packet ID, and header check-sum fields normally change ๐Ÿ’…. The total length is redundant with the length the link layer already gives you. This scheme has to depend on the link layer for good error detection (like PPP's CRC [4]), so the header checksum can be elided too ๐Ÿซถ. That leaves just the packet ID, which wouldn't need to be sent if there's no IP fragmentation. BUT to keep compression lossless, changes in the packet ID still get transmitted ๐Ÿ˜ญ. The packet ID usually goes up by one or a small number per packet. (Some systems increment the ID with the bytes swapped, which gives slightly less compression, rip ๐Ÿ™ˆ.) In the IPv6 base header there's no packet ID and no header checksum, so only the payload length field changes ๐Ÿฆ‹.

In the UDP header, the length field is redundant with the IP total length field and the length the link layer indicates ๐ŸŒธ. The UDP check-sum field will be a constant zero if the source elects not to generate UDP checksums. Otherwise the checksum must be communicated intact so the compression stays lossless. Keeping end-to-end error detection for apps that need it is a super important principle ๐Ÿ‘‘๐Ÿ’–.

In the RTP header, the SSRC identifier stays constant within a given context, since it's literally part of what identifies that context โœจ. For most packets, only the sequence number and the timestamp change from packet to packet ๐Ÿ’œ. If packets aren't lost or misordered upstream from the compressor, the sequence number goes up by one for each packet. For audio packets of constant duration, the timestamp goes up by the number of sample periods carried in each packet ๐ŸŽ€. For video, the timestamp changes on the first packet of each frame, then stays constant for any extra packets in that frame ๐ŸŽฌ. If each video frame fits in only one packet, but the frames are generated at a constant rate, the change in timestamp from frame to frame is also constant ๐Ÿ“.

Here's the big brain part ๐Ÿง ๐Ÿ”ฅ: in each of these cases, the second-order difference of the sequence number and timestamp fields is zero. So the next packet header can be built from the previous one by adding the first-order differences for these fields. Those differences are stored in the session context, along with the previous uncompressed header ๐Ÿ’…. When the second-order difference isn't zero, the change is usually way smaller than the full number of bits in the field. So you can shrink things by encoding the new first-order difference and sending that, instead of the absolute value ๐Ÿ˜ญโœจ.

The M bit gets set on the first packet of an audio talkspurt and on the last packet of a video frame ๐ŸŽค. If it were treated as a constant field, so every change required sending the full RTP header, the compression would drop a lot ๐Ÿ’€. So one bit in the compressed header carries the M bit explicitly ๐Ÿซถ.

If the packets are flowing through an RTP mixer (most commonly for audio), the CSRC list and CC count also change ๐ŸŽง. But the CSRC list usually stays the same during a talkspurt or even longer, so it only needs to be sent when it changes ๐ŸŒˆ.


3.3. The protocol ๐Ÿงš

The compression protocol must keep a collection of shared information in a consistent state between the compressor and decompressor ๐Ÿค. There's a separate session context for each IP/UDP/RTP packet stream. Each one is defined by a particular combo of the IP source and destination addresses, the UDP source and destination ports, and the RTP SSRC field ๐Ÿ’œ. The number of session contexts to be maintained MAY be negotiated between the compressor and decompressor ๐ŸŒธ. Each context is identified by an 8- or 16-bit identifier, depending on how many contexts were negotiated, so the maximum number is 65536 ๐Ÿ‘‘. Both uncompressed and compressed packets MUST carry the context ID and a 4-bit sequence number, which is used to detect packet loss between the compressor and decompressor ๐Ÿ”. Each context has its own separate sequence number space, so a single packet loss only needs to invalidate one context ๐Ÿ™Œ.

The shared information in each context has these items:

  • The full IP, UDP and RTP headers, possibly including a CSRC list, for the last packet sent by the compressor or reconstructed by the decompressor ๐Ÿ“ฆ.
  • The first-order difference for the IPv4 ID field. It's initialized to 1 whenever an uncompressed IP header for this context is received, and updated each time a delta IPv4 ID field is received in a compressed packet ๐Ÿฆ‹.
  • The first-order difference for the RTP timestamp field. It's initialized to 0 whenever an uncompressed packet for this context is received, and updated each time a delta RTP timestamp field is received in a compressed packet โฐ.
  • The last value of the 4-bit sequence number, which is used to detect packet loss between the compressor and decompressor ๐Ÿ”ข.
  • The current generation number for non-differential coding of UDP packets with IPv6 (see [3]). For IPv4, the generation number may be set to zero if the COMPRESSED_NON_TCP packet type (defined below) is never used ๐ŸŒˆ.
  • A context-specific delta encoding table (see section 3.3.4) may optionally be negotiated for each context ๐Ÿญ.

To communicate packets in the various uncompressed and compressed forms, this protocol depends on the link layer being able to indicate four new packet formats, on top of the normal IPv4 and IPv6 packet formats ๐ŸŽ€:

  • FULL_HEADER: communicates the uncompressed IP header plus any following headers and data, to set up the uncompressed header state in the decompressor for a particular context ๐Ÿ—๏ธ. The FULL_HEADER packet also carries the 8- or 16-bit session context identifier and the 4-bit sequence number, so the compressor and decompressor stay in sync ๐Ÿซถ. The format is shown in section 3.3.1.
  • COMPRESSED_UDP: communicates the IP and UDP headers compressed to 6 or fewer bytes (often 2 if UDP checksums are disabled), followed by any subsequent headers (possibly RTP) in uncompressed form, plus data ๐Ÿ—œ๏ธ. This packet type is used when there are differences in the usually constant fields of the (potential) RTP header โœจ. The RTP header can include a changed SSRC value, so this packet may redefine the session context ๐Ÿ”„. The format is shown in section 3.3.3.
  • COMPRESSED_RTP: indicates that the RTP header is compressed along with the IP and UDP headers ๐Ÿ’ซ. This header might still be just two bytes, or bigger if differences need to be communicated ๐Ÿ“. This packet type is used when the second-order difference (at least in the usually constant fields) is zero ๐Ÿงฎ. It includes delta encodings for the fields that changed by something other than the expected amount, so the first-order differences get set up after an uncompressed RTP header is sent, and again whenever they change ๐Ÿ”. The format is shown in section 3.3.2.
  • CONTEXT_STATE: a special packet sent from the decompressor to the compressor, listing context IDs for which synchronization has been lost or might have been lost ๐Ÿšจ. It's only sent across the point-to-point link, so it doesn't need an IP header ๐Ÿ›ฐ๏ธ. The format is shown in section 3.3.5.

When this compression scheme is used with IPv6 as part of the general header compression framework specified in [3], another packet type MAY be used ๐Ÿงƒ:

  • COMPRESSED_NON_TCP: communicates the compressed IP and UDP headers as defined in [3], without differential encoding ๐ŸงŠ. If it were used for IPv4, it would need one or two extra bytes compared to the COMPRESSED_UDP form above, in order to carry the IPv4 ID field ๐Ÿ™ƒ. For IPv6 there's no ID field, and this non-differential compression is more resilient to packet loss ๐Ÿ›ก๏ธโœจ.

Assignments of numeric codes for these packet formats in the Point-to-Point Protocol [4] are to be made by the Internet Assigned Numbers Authority ๐Ÿ”ข๐Ÿ’œ.

3.3.1. FULL_HEADER (uncompressed) packet format โœจ

okay so this is the FULL_HEADER packet, the uncompressed one that sends everything in full ๐Ÿ’… the definition here is meant to be consistent with the one in [3], and all the juicy design reasoning lives over there ๐Ÿงš

the FULL_HEADER packet looks just like the original packet it came from ๐ŸŽ€ for IPv4 that's usually an IP header, then a UDP header, then the UDP payload, which might be an RTP header plus its payload. it can also carry IP-encapsulated packets, so you'd get two IP headers, then UDP and maybe RTP. with IPv6 the packet can be some mix of IPv6 and IPv4 headers ๐Ÿฆ‹ each header tells you what comes next through the type field of the header before it, as usual โœจ

now the twist ๐Ÿ‘€ a FULL_HEADER packet has to also carry the compression context ID and the 4-bit sequence number, and it has to do that without making the header bigger ๐Ÿ™ˆ so those values get tucked into the length fields of the IP and UDP headers, since the real length can be figured out from the link layer anyway. you need two 16-bit length fields, and they come from the first two headers in the packet. for an IPv4/UDP packet, the first one is the total length field of the IPv4 header and the second is the length field of the UDP header. for an IPv4 encapsulated packet, the first comes from the total length field of the first IP header and the second comes from the total length field of the second IP header ๐Ÿ’–

as specified in Sections 5.3.2 of [3], where the context ID (CID) and 4-bit sequence number sit depends on whether you picked 8-bit or 16-bit context IDs ๐Ÿ“ (16 bits wide, with the most-significant bit is to the left):

        For 8-bit context ID:

        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |0|1| Generation|      CID      |  First length field
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |            0          |  seq  |  Second length field
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

        For 16-bit context ID:

        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |1|1| Generation|   0   |  seq  |  First length field
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |              CID              |  Second length field
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

the first bit in the first length field tells you the length of the CID ๐Ÿ‘‘ the CID length MUST either be constant for all contexts, or two extra distinct packet types MUST be provided to separately indicate COMPRESSED_UDP and COMPRESSED_RTP packet formats with 8- and 16-bit CIDs. the second bit in the first length field is 1 to say the 4-bit sequence number is there, which is always the case for this IP/UDP/RTP compression scheme ๐Ÿซถ

the generation field is used with IPv6 for COMPRESSED_NON_TCP packets, as described in [3] โœจ if you're an IPv4-only implementation that doesn't use COMPRESSED_NON_TCP packets, the compressor SHOULD set the generation value to zero. for things to stay consistent between IPv4 and IPv6, the decompressor stores the generation value in the context when it arrives, and the most recent value gets sent back in the CONTEXT_STATE packet ๐ŸŒˆ

when a FULL_HEADER packet shows up, the decompressor stores the complete set of headers into the context picked by the context ID ๐Ÿงš the 4-bit sequence number gets stored there too, which resynchronizes the decompressor to the compressor. main character energy ๐Ÿ’…

when COMPRESSED_NON_TCP packets are in use, the 4-bit sequence number goes into the "Data Field" of that packet, and the D bit gets set as described in Section 6 of [3] ๐Ÿฆ‹ when a COMPRESSED_NON_TCP packet is received, its generation number is compared to the value stored in the context. if they don't match, the context isn't up to date and it MUST be refreshed by a FULL_HEADER packet ๐Ÿ˜ญ if the generation does match, then the compressed IP and UDP header information, the 4-bit sequence number, and the (potential) RTP header all get stored into the saved context ๐ŸŽ€

the context's memory usage depends on how many encapsulating headers the FULL_HEADER packet includes ๐Ÿ“ and the compressor and decompressor MAY negotiate a maximum header size ๐Ÿ’œ

3.3.2. COMPRESSED_RTP packet format

okay so when the second-order difference of the RTP header is zero from packet to packet, the decompressor can rebuild the packet all by itself ๐Ÿงš it just adds the stored first-order differences to the stored uncompressed header from the previous packet ๐Ÿ’… the only things that need to be sent are the session context identifier and a small sequence number (this one is NOT the RTP sequence number, bestie) so the two ends stay in sync and can notice when packets go missing ๐Ÿซถ

if the second-order difference isn't zero for some fields, only the new first-order difference for those fields gets sent, using a compact encoding โœจ the decompressor adds those new values to the matching fields in its uncompressed header and also saves them in its session context, so they keep getting added on every later packet where the second-order difference is zero ๐Ÿฆ‹ whenever the first-order difference changes, it gets sent and saved again.

in practice, the only fields where it's actually useful to store the first-order difference are the IPv4 ID field and the RTP timestamp ๐ŸŒธ the RTP sequence number usually goes up by 1, so that's the default. if it changes by something other than 1, the difference has to be sent, but it does NOT become the expected difference for the next packet. the expected first-order difference stays fixed at 1, so if the next packet is in order, nothing extra needs to be sent for it ๐Ÿ‘‘

for the RTP timestamp: when a FULL_HEADER, COMPRESSED_NON_TCP or COMPRESSED_UDP packet gets sent to refresh the RTP state, the stored first-order difference is set to zero ๐Ÿ“ if the timestamp is the same on the next packet (like when it's the same video frame), the second-order difference is zero, iykyk. otherwise, the difference between the two timestamps gets sent as the new first-order difference, to be added to the timestamp in the uncompressed header the decompressor has stored, and it's also saved as the first-order difference in that context. every time the first-order difference changes after that, the new one gets sent and used to update the context ๐Ÿ’œ

same vibe for the IPv4 ID field ๐ŸŽ€ it usually goes up by one, so its first-order difference starts at one whenever the state is refreshed by a FULL_HEADER packet, or whenever a COMPRESSED_NON_TCP packet gets sent (because that one carries the ID field uncompressed). after that, any change to the first-order difference gets sent and stored in the context. it's giving consistency, periodt ๐Ÿ’…

a bit mask shows which fields changed by something other than the expected difference ๐ŸŒˆ on top of the small link sequence number, these are the items that can be conditionally sent in the compressed IP/UDP/RTP header:

  • I = IPv4 packet ID (always 0 if no IPv4 header)
  • U = UDP checksum
  • M = RTP marker bit
  • S = RTP sequence number
  • T = RTP timestamp
  • L = RTP CSRC count and list

if you need 4 bits for the link sequence number to get a decent chance of spotting loss, there aren't enough bits left to give each item its own bit AND still squeeze everything into one byte next to the context ID ๐Ÿ˜ญ kind of a tight squeeze, ngl.

the U bit doesn't need to be sent explicitly to say whether a UDP checksum is there, because a source usually puts checksums on every packet of a session or on none of them ๐Ÿ™ˆ when the session state gets set up with an uncompressed header and the checksum is nonzero, an unencoded 16-bit checksum gets inserted into the compressed header of every later packet, until another uncompressed packet changes that setting.

of the items left over, the L bit for the CSRC count and list is probably the one used least often ๐Ÿ’€ instead of giving it a whole bit in the mask, an unusual combination of the other bits gets used to signal it. this combination is called MSTI. when the bits for the IP packet ID, RTP marker bit, RTP sequence number and RTP timestamp are ALL set, that's a special case meaning an extended form of the compressed RTP header follows โœจ that header has an extra byte holding the real values of those four bits plus the CC count, and then the CSRC list (as long as the CC count says) gets included exactly as it looks in the uncompressed RTP header. main character energy for sure ๐Ÿ‘‘

the other RTP header fields (version, P bit, X bit, payload type and SSRC identifier) are assumed to stay pretty steady ๐Ÿซถ the SSRC identifier is defined to be constant for a given context, since it's one of the things that picks the context in the first place. if any of the other fields change, the uncompressed RTP header MUST be sent as described in Section 3.3.3, no excuses ๐Ÿ”ฅ

here's the compressed IP/UDP/RTP header ๐Ÿฆ‹ dotted lines mean the field may or may not be there. the most significant bit is numbered 0, and multi-byte fields go out in network byte order (most significant byte first). the delta fields are usually a single byte like shown, but they can be two or three bytes depending on the delta value, as explained in Section 3.3.4.

          0   1   2   3   4   5   6   7
        +...............................+
        :   msb of session context ID   :  (if 16-bit CID)
        +-------------------------------+
        |   lsb of session context ID   |
        +---+---+---+---+---+---+---+---+
        | M | S | T | I | link sequence |
        +---+---+---+---+---+---+---+---+
        :                               :
        +         UDP checksum          +  (if nonzero in context)
        :                               :
        +...............................+
        :                               :
        +        "RANDOM" fields        +  (if encapsulated)
        :                               :
        +...............................+
        : M'| S'| T'| I'|      CC       :  (if MSTI = 1111)
        +...............................+
        :         delta IPv4 ID         :  (if I or I' = 1)
        +...............................+
        :      delta RTP sequence       :  (if S or S' = 1)
        +...............................+
        :      delta RTP timestamp      :  (if T or T' = 1)
        +...............................+
        :                               :
        :           CSRC list           :  (if MSTI = 1111
        :                               :   and CC nonzero)
        :                               :
        +...............................+
        :                               :
        :      RTP header extension     :  (if X set in context)
        :                               :
        :                               :
        +-------------------------------+
        |                               |
        |            RTP data           |
        /                               /
        /                               /
        |                               |
        +-------------------------------+
        :            padding            :  (if P set in context)
        +...............................+

when the context set up by the FULL_HEADER packet has more than one IPv4 header, the IP ID fields of the encapsulating headers MUST be sent as absolute values, as described in [3] ๐ŸŒธ those fields are called "RANDOM" fields. they go into the COMPRESSED_RTP packet in the same order they show up in the original headers, right after the UDP checksum if there is one, or right after the MSTI byte if there isn't, just like the diagram shows. the IP ID of a UDP header only gets sent differentially if an IPv4 packet comes right before that UDP header. then it can be sent with no bits at all if the second difference is zero, or as a delta IPv4 ID field if it isn't. and if there's no IPv4 header right before the UDP header, the I bit MUST be 0 and no delta IPv4 ID field will be there ๐Ÿ˜ญ๐Ÿ’–


3.3.3. COMPRESSED_UDP packet format

if any of the RTP header fields that normally stay constant (like the payload type field) change, an uncompressed RTP header MUST be sent ๐Ÿ“ if the IP and UDP headers don't need updating too, that RTP header MAY be carried in a COMPRESSED_UDP packet instead of a FULL_HEADER packet โœจ the COMPRESSED_UDP packet looks like COMPRESSED_RTP, EXCEPT the M, S and T bits are always 0 and their delta fields never show up:

          0   1   2   3   4   5   6   7
        +...............................+
        :   msb of session context ID   :  (if 16-bit CID)
        +-------------------------------+
        |   lsb of session context ID   |
        +---+---+---+---+---+---+---+---+
        | 0 | 0 | 0 | I | link sequence |
        +---+---+---+---+---+---+---+---+
        :                               :
        +         UDP checksum          +  (if nonzero in context)
        :                               :
        +...............................+
        :                               :
        +        "RANDOM" fields        +  (if encapsulated)
        :                               :
        +...............................+
        :         delta IPv4 ID         :  (if I = 1)
        +-------------------------------+
        |           UDP data            |
        :   (uncompressed RTP header)   :

note that this is a form of IP/UDP header compression that's different from the COMPRESSED_NON_TCP packet type defined in [3] ๐Ÿฆ‹ the goal is to hit the two-byte target when UDP checksums are turned off, which IPv4 allows ๐ŸŽ€ the protocol in [3] doesn't do differential coding for UDP packets, so with IPv4, two bytes of IP ID plus two bytes of UDP checksum (if it's nonzero) would ALWAYS get sent on top of the two-byte compression prefix ๐Ÿ˜ญ for IPv6, the COMPRESSED_NON_TCP packet type MAY be used instead ๐Ÿ’œ

3.3.4. Encoding of differences ๐Ÿ’–

okay so the delta fields in COMPRESSED_RTP and COMPRESSED_UDP packets get squished down with a variable-length mapping, so the more common values take up less room ๐Ÿฆ‹ there's a default encoding below, but it is RECOMMENDED that implementations use a table-driven delta encoder and decoder. that way each session can negotiate its own table if it makes sense, maybe even an optimal Huffman encoding ๐Ÿ’… encodings that read the bit stream in order (the default table and Huffman are examples of this) keep the table size reasonable, and they may run faster than a version that has to explicitly test ranges of values โœจ

the default delta encoding is in the table below. it was designed to handle the small changes that can show up in the IP ID and the RTP sequence number when packets get lost upstream of the compressor ๐Ÿซถ it still manages to fit most audio and video deltas in two bytes ๐ŸŽ€ the left column is the decimal value being encoded, and the right column is the bytes in hex, in the order they get sent (network byte order). only the first and last value of each contiguous range is shown, with ellipses in between:

      Decimal  Hex

       -16384  C0 00 00
            :  :
         -129  C0 3F 7F
         -128  80 00
            :  :
           -1  80 7F
            0  00
            :  :
          127  7F
          128  80 80
            :  :
        16383  BF FF
        16384  C0 40 00
            :  :
      4194303  FF FF FF

for positive values, a change of zero through 127 gets written directly as one byte ๐ŸŒธ if the two most significant bits of that byte are 10 or 11, that's a signal that it's extended to a two- or three-byte value, respectively. the six least significant bits of the first byte get combined, in decreasing order of significance, with the next one or two bytes to make a 14- or 22-bit value ๐Ÿงš

negative deltas can happen when packets get misordered, or with the intentionally out-of-order RTP timestamps on MPEG video [5]. those events are less likely, so a smaller range of negative values gets encoded using parts of the positive side of the table that would otherwise go unused ๐Ÿ™ˆ

a change in the RTP timestamp value less than -16384 or greater than 4194303 forces the RTP header to be sent uncompressed, using a FULL_HEADER, COMPRESSED_NON_TCP or COMPRESSED_UDP packet type. no cap, that's a big deal ๐Ÿ˜ญ

the IP ID and RTP sequence number fields are only 16 bits, so negative deltas for those fields SHOULD be masked to 16 bits and then encoded (as large positive 16-bit numbers) ๐Ÿ’œ

3.3.5. Error Recovery ๐ŸŒˆ

so this part is about what happens when things go sideways. whenever the 4-bit sequence number for a particular context increments by anything other than 1, the decompressor MUST invalidate that context and send a CONTEXT_STATE packet back to the compressor saying the context has been invalidated ๐Ÿ’€ the exception is when the sequence number was set by a FULL_HEADER or COMPRESSED_NON_TCP packet. all packets for the invalid context MUST be discarded until a FULL_HEADER or COMPRESSED_NON_TCP packet comes in for that context to re-establish consistent state (unless the "twice" algorithm is used, which we'll get to later in this section) ๐Ÿ”ฅ since several compressed packets might show up in the meantime, the decompressor SHOULD NOT retransmit the CONTEXT_STATE packet for every single compressed packet it gets. instead it SHOULD limit how often it retransmits, so it doesn't flood the reverse channel ๐Ÿ“

when an error happens on the link, the link layer usually throws out the damaged packet (if there was one), but it might give some indication that something went wrong ๐Ÿฅน then it can take a while before another packet for the same context arrives, and the decompressor has to toss that one too once it notices it's out of sequence. that means at least two packets get lost ๐Ÿ˜ฉ to recover faster when the link does give an explicit error indication, the decompressor MAY optionally send an advisory CONTEXT_STATE packet listing the last valid sequence number and generation number for one or more recently active contexts (not necessarily all of them) ๐Ÿฆ‹ for a given context, if the compressor hasn't sent a compressed packet with a higher sequence number, and the generation number matches the current generation, no corrective action is required ๐Ÿ‘‘ otherwise, the compressor MAY choose to mark the context invalid so the next packet goes out as a FULL_HEADER or COMPRESSED_NON_TCP packet (a FULL_HEADER is required if the generation doesn't match). heads up though: if the link round-trip time is large compared to the spacing between packets, there could be several packets from multiple contexts in flight across the link. that makes it more likely the sequence numbers have already moved on by the time the CONTEXT_STATE packet reaches the compressor ๐Ÿ™ƒ the result could be that some contexts get invalidated for no reason, which wastes extra bandwidth ๐Ÿ’…

the CONTEXT_STATE packet is shown in the diagrams below. the first byte is a type code, so the CONTEXT_STATE packet type can be shared by multiple compression schemes within the general compression framework specified in [3]. what comes after depends on the compression scheme. for the IP/UDP/RTP compression scheme specified here, the rest of the CONTEXT_STATE packet is a list of blocks, so the state for multiple contexts can be indicated, and it's preceded by a one-byte count of the number of blocks ๐ŸŽ€

there are two type code values for the IP/UDP/RTP compression scheme. the value 1 means 8-bit session context IDs are being used:

         0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
       | 1 = IP/UDP/RTP with 8-bit CID |
       +---+---+---+---+---+---+---+---+
       |         context count         |
       +---+---+---+---+---+---+---+---+
       +---+---+---+---+---+---+---+---+
       |       session context ID      |
       +---+---+---+---+---+---+---+---+
       | I | 0 | 0 | 0 |    sequence   |
       +---+---+---+---+---+---+---+---+
       | 0 | 0 |       generation      |
       +---+---+---+---+---+---+---+---+
                      ...
       +---+---+---+---+---+---+---+---+
       |       session context ID      |
       +---+---+---+---+---+---+---+---+
       | I | 0 | 0 | 0 |    sequence   |
       +---+---+---+---+---+---+---+---+
       | 0 | 0 |       generation      |
       +---+---+---+---+---+---+---+---+

the value 2 means 16-bit session context IDs are being used. the session context ID gets sent in network byte order (most significant byte first) โœจ

         0   1   2   3   4   5   6   7
       +---+---+---+---+---+---+---+---+
       | 2 = IP/UDP/RTP with 16-bit CID|
       +---+---+---+---+---+---+---+---+
       |         context count         |
       +---+---+---+---+---+---+---+---+
       +---+---+---+---+---+---+---+---+
       |                               |
       +       session context ID      +
       |                               |
       +---+---+---+---+---+---+---+---+
       | I | 0 | 0 | 0 |    sequence   |
       +---+---+---+---+---+---+---+---+
       | 0 | 0 |       generation      |
       +---+---+---+---+---+---+---+---+
                      ...
       +---+---+---+---+---+---+---+---+
       |                               |
       +       session context ID      +
       |                               |
       +---+---+---+---+---+---+---+---+
       | I | 0 | 0 | 0 |    sequence   |
       +---+---+---+---+---+---+---+---+
       | 0 | 0 |       generation      |
       +---+---+---+---+---+---+---+---+

the bit labeled "I" gets set to one for contexts that have been marked invalid and need a FULL_HEADER or COMPRESSED_NON_TCP packet to be transmitted ๐Ÿšจ if the I bit is zero, the context state is advisory. the I bit is set to zero to indicate advisory context state that MAY be sent following a link error indication ๐ŸŒธ

since the CONTEXT_STATE packet itself can get lost, retransmitting one or more blocks is allowed. it's expected that retransmission will only be triggered by receiving another packet, but if the line is near idle, retransmission MAY be triggered by a relatively long timer (on the order of 1 second) โญ

if a CONTEXT_STATE block for a given context gets retransmitted, it may cross paths with the FULL_HEADER or COMPRESSED_NON_TCP packet meant to refresh that context. in that case, the compressor MAY choose to ignore the error indication ๐Ÿ™ˆ

when UDP checksums are being transmitted, the decompressor MAY attempt to use the "twice" algorithm described in section 10.1 of [3]. the idea here is that the delta gets applied more than once, on the assumption that the delta may have been the same on the missing packet(s) and the one that arrived afterwards. success is indicated by a checksum match. for the scheme defined here, the difference in the 4-bit sequence number tells you how many times the delta must be applied ๐Ÿงฎ heads up, though: there's a nontrivial risk of an incorrect positive indication. it may be advisable to request a FULL_HEADER or COMPRESSED_NON_TCP packet even if the "twice" algorithm succeeds ๐Ÿ˜ฌ

some errors might not get detected, for example if 16 packets are lost in a row and the link level doesn't give any error indication. in that case, the decompressor will generate packets that aren't valid. if UDP checksums are being transmitted, the receiver will probably notice the invalid packets and discard them, but the receiver has no way to signal the decompressor ๐Ÿฅบ that's why it is RECOMMENDED that the decompressor verify the UDP checksum periodically, perhaps once every 16 packets. if an error is detected, the decompressor would invalidate the context and signal the compressor with a CONTEXT_STATE packet ๐Ÿซถ

3.4. Compression of RTCP Control Packets

okay bestie, so RTP has this convention where the data rides on an even port number and the matching RTCP packets sit on the next odd one up ๐Ÿฆ‹. That means you could build separate compression schemes for RTP and RTCP! For RTCP, compression could hit not just the header but also the "data", aka the contents of the different packet types. The numbers in Sender Report (SR) and Receiver Report (RR) packets wouldn't compress well ๐Ÿ˜ญ, but the text in Source Description (SDES) packets could get squished down to a bit mask showing which items were there but compressed out (for timing on the SDES NOTE item, and so the end system can measure the average RTCP packet size for the interval calculation) ๐ŸŽ€.

However, this scheme does NOT compress RTCP headers or "data" at all, for a few reasons ๐Ÿ’… (though compression SHOULD still be applied to the IP and UDP headers). The RTP spec suggests scaling the RTCP interval so the aggregate RTCP bandwidth used by all participants in a session is no more than 5% of the session bandwidth, so there's not much to gain from RTCP compression ๐Ÿคทโ€โ™€๏ธ. Compressing out the SDES items would need a way bigger pile of shared state stored for each context ID. And to allow compression when SDES info for several sources gets sent through an RTP "mixer", you'd have to keep a separate RTCP session context for every SSRC identifier. In a session with more than 255 participants, that causes perfect thrashing of the context cache, even when only one participant is sending data ๐Ÿ’€.

Even though RTCP isn't compressed, the fraction of total bandwidth taken up by RTCP packets on the compressed link stays no more than 5% in most cases ๐ŸŒธ, assuming the RTCP packets are sent as COMPRESSED_UDP packets. Since uncompressed RTCP traffic eats no more than 5% of the total session bandwidth, a typical RTCP packet length of 90 bytes means RTCP will be no more than 5% of the compressed bandwidth if the RTP data payload is at least 108 bytes. If the RTP data payload is smaller, the fraction goes up, but it's still under 7% for a payload size of 37 bytes. For big data payloads, the compressed RTCP fraction is actually smaller than the uncompressed one (for example, 4% at 1000 bytes) ๐Ÿ“ˆโœจ.


3.5. Compression of non-RTP UDP Packets

as we covered earlier, the COMPRESSED_UDP packet MAY be used to compress UDP packets that don't carry RTP ๐Ÿงš. Whatever data follows the UDP header is unlikely to have constant values in the bits that line up with the usually constant fields in the RTP header. The SSRC field in particular would probably keep changing. So you have to keep track of these non-RTP UDP packet streams, otherwise they'd use up all the context slots as the "SSRC field" changes (since that field is part of what identifies a particular RTP context) ๐Ÿ˜ตโ€๐Ÿ’ซ. Each of those streams can get its own context, but the encoder sets a flag in the context saying the changing SSRC field should be ignored, and COMPRESSED_UDP packets should always be sent instead of COMPRESSED_RTP packets. Main character energy for the UDP streams ๐Ÿ‘‘.


4. Interaction With Segmentation โœจ

okay so a segmentation scheme can team up with RTP header compression to let small, real-time packets cut in line in front of big, presumably non-real-time packets, so the delay goes down ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ. It's assumed the large packets bypass the compressor and decompressor, because interleaving would change the sequencing of packets at the decompressor and make it look like there were errors ๐Ÿ™ˆ. Header compression should be less important for large packets, since the overhead ratio is smaller anyway.

If some packets from an RTP session context get picked for segmentation (maybe based on size) and some don't, re-ordering is possible ๐Ÿ”€. That lowers compression efficiency, because the large packets would look like lost packets in the sequence space. But it shouldn't cause bigger problems, since the RTP sequence numbers should still be reconstructed correctly, and that lets the application fix the ordering ๐Ÿซถ.

Link errors that the segmentation scheme spots using its own sequencing info MAY be reported to the compressor with an advisory CONTEXT_STATE message, the same way link errors spotted by the link layer itself are ๐Ÿ“ฃ.

The context ID byte goes first in the COMPRESSED_RTP header, so this byte MAY be shared with the segmentation layer if that sharing is feasible and has been negotiated ๐Ÿ’œ. Since the compressor can assign context ID values however it likes, the value can be set to match the context identifier from the segmentation layer. Neat, right? ๐ŸŽ€


5. Negotiating Compression

IP/UDP/RTP compression over a particular link depends on the link-layer protocol ๐Ÿ”—. Negotiation is expected to be defined separately for PPP [4], for example. The following items MAY be negotiated:

  • The size of the context ID.
  • The maximum size of the stack of headers in the context.
  • A context-specific table for decoding of delta values.

6. Acknowledgments

a bunch of people helped design this compression scheme and related problems, and they deserve all the flowers ๐ŸŒท๐Ÿซถ. Scott Petrack kicked off the discussion of RTP header compression in the AVT working group at Los Angeles in March, 1996. Carsten Bormann developed an overall architecture for compression combined with traffic control across a low-speed link, and made several specific contributions to the scheme described here ๐Ÿ‘. David Oran independently developed a note based on similar ideas, and suggested using the PPP Multilink protocol for segmentation ๐Ÿง . Mikael Degermark gave advice on integrating this compression scheme with the IPv6 compression framework ๐Ÿ’–.


7. References:

[1] Schulzrinne, H., Casner, S., Frederick, R. and V. Jacobson, "RTP:
    A Transport Protocol for real-time applications", RFC 1889,
    January 1996.

[2] Jacobson, V., "TCP/IP Compression for Low-Speed Serial Links",
    RFC 1144, February 1990.

[3] Degermark, M., Nordgren, B. and S. Pink, "Header Compression for
    IPv6", RFC 2507, February 1999.

[4] Simpson, W., "The Point-to-Point Protocol (PPP)", STD 51, RFC
    1661, July 1994.

[5] Hoffman, D., Fernando, G., Goyal, V. and M. Civanlar, "RTP
    Payload Format for MPEG1/MPEG2 Video", RFC 2250, January 1998.

8. Security Considerations

okay this section is SERIOUS but also kind of iconic ๐Ÿ”ฅ๐Ÿ’€. Encryption wipes out the redundancy this compression scheme tries to exploit, so there's some temptation to skip encryption just to get things working over a low-bandwidth link ๐Ÿ˜ฌ. But if encrypting the data and not the headers is good enough for you, RTP does specify an alternative encryption method where only the RTP payload gets encrypted and the headers stay in the clear. That way compression can still be applied ๐Ÿ”โœจ.

A malfunctioning or malicious compressor could make the decompressor rebuild packets that don't match the originals but still have valid IP, UDP and RTP headers, and maybe even valid UDP check-sums ๐Ÿšจ. Corruption like that may be caught with end-to-end authentication and integrity mechanisms, which compression won't affect. Constant portions of authentication headers will be compressed as described in [3] ๐Ÿงฉ.

Nothing authenticates the CONTEXT_STATE control packet sent by this protocol ๐Ÿ™…โ€โ™€๏ธ. An attacker with access to the link between the decompressor and compressor could inject false CONTEXT_STATE packets and reduce compression efficiency, which would probably cause congestion on the link. But honestly, an attacker with link access could mess with the traffic in lots of other ways too ๐Ÿคทโ€โ™€๏ธ.

There's a potential denial-of-service threat with compression techniques that put a non-uniform computational load on the receiver ๐Ÿ˜ฑ. An attacker can inject pathological datagrams into the stream that are complex to decompress, overloading the receiver and slowing down processing of other streams. Good news, though: this compression doesn't show any significant non-uniformity ๐Ÿ™Œ.

A security review of this protocol found no additional security considerations ๐ŸŽ‰.


9. Authors' Addresses

Stephen L. Casner
Cisco Systems, Inc.
170 West Tasman Drive
San Jose, CA 95134-1706
United States

EMail: casner@cisco.com


Van Jacobson
Cisco Systems, Inc.
170 West Tasman Drive
San Jose, CA 95134-1706
United States

EMail: van@cisco.com

the legal bits stay exactly as written, no vibes-edits here ๐Ÿ“œ๐Ÿ’…:

Copyright (C) The Internet Society (1999).  All Rights Reserved.

This document and translations of it may be copied and furnished to
others, and derivative works that comment on or otherwise explain it
or assist in its implementation may be prepared, copied, published
and distributed, in whole or in part, without restriction of any
kind, provided that the above copyright notice and this paragraph are
included on all such copies and derivative works.  However, this
document itself may not be modified in any way, such as by removing
the copyright notice or references to the Internet Society or other
Internet organizations, except as needed for the purpose of
developing Internet standards in which case the procedures for
copyrights defined in the Internet Standards process must be
followed, or as required to translate it into languages other than
English.

The limited permissions granted above are perpetual and will not be
revoked by the Internet Society or its successors or assigns.

This document and the information contained herein is provided on an
"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING
TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION
HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
๐Ÿ“ฆ where this page comes from