HEVC NAL Unit Headers and RTP Fragmentation Units in RTP Streaming
HEVC NAL unit header (2 bytes)
HEVC encodes each NAL unit with a 16‑bit header laid out as:
- forbidden_zero_bit: 1 bit (must be 0)
- nal_unit_type: 6 bits
- nuh_layer_id: 6 bits
- nuh_temporal_id_plus1: 3 bits
Bit packing across the two bytes (b7 is MSB):
- byte0: b7=fzb, b6..b1=type, b0=layer_id[5]
- byte1: b7..b3=layer_id[4:0], b2..b0=TID+1
Common single-layer, single-temporal-stream values: fzb=0, layer_id=0, nuh_temporal_id_plus1=1.
C helpers too extract fields:
static inline uint8_t hevc_forbidden_bit(const uint8_t *n)
{
return (n[0] >> 7) & 0x01;
}
static inline uint8_t hevc_nal_type(const uint8_t *n)
{
return (n[0] >> 1) & 0x3F; // 0..63
}
static inline uint8_t hevc_layer_id(const uint8_t *n)
{
return ((n[0] & 0x01) << 5) | ((n[1] & 0xF8) >> 3); // 6 bits
}
static inline uint8_t hevc_tid_plus1(const uint8_t *n)
{
return n[1] & 0x07; // 1..7
}
Selected HEVC NAL unit types
Only a subset is most relevant to RTP packetization:
enum HevcNalType {
HEVC_NAL_TRAIL_N = 0,
HEVC_NAL_TRAIL_R = 1,
HEVC_NAL_TSA_N = 2,
HEVC_NAL_TSA_R = 3,
HEVC_NAL_STSA_N = 4,
HEVC_NAL_STSA_R = 5,
HEVC_NAL_RADL_N = 6,
HEVC_NAL_RADL_R = 7,
HEVC_NAL_RASL_N = 8,
HEVC_NAL_RASL_R = 9,
// IRAP picture types
HEVC_NAL_BLA_W_LP = 16,
HEVC_NAL_BLA_W_DLP= 17,
HEVC_NAL_BLA_N_LP = 18,
HEVC_NAL_IDR_W_DLP= 19,
HEVC_NAL_IDR_N_LP = 20,
HEVC_NAL_CRA = 21,
// parameter and SEI sets
HEVC_NAL_VPS = 32,
HEVC_NAL_SPS = 33,
HEVC_NAL_PPS = 34,
HEVC_NAL_AUD = 35,
HEVC_NAL_EOS = 36,
HEVC_NAL_EOB = 37,
HEVC_NAL_FD = 38,
HEVC_NAL_SEI_PREFIX = 39,
HEVC_NAL_SEI_SUFFIX = 40,
// RTP aggregation/fragmentation
HEVC_NAL_AP = 48, // Aggregation Packet
HEVC_NAL_FU = 49 // Fragmentation Unit
};
RTP packetizasion: single NAL vs. FU (fragmentation)
For RTP per RFC 7798:
- If the NAL unit size fits the payload budget, transmit it as a Single NAL Unit packet (payload starts with the NAL header).
- If it exceeds the payload budget, split it into Fragmentation Units (FU):
- Payload Header: 2 bytes, same layout as the HEVC NAL header, but nal_unit_type=49 (FU). F, LayerId, and TID should be copied from the original NAL header for multilayer/temporla streams. In simple single-layer cases they are typically F=0, LayerId=0, TID+1=1.
- FU Header: 1 byte with fields S|E|FuType (S=start bit, E=end bit, FuType=original NAL unit type).
- FU payload: the original NAL unit payload excluding its 2‑byte HEVC NAL header.
Bit layout of the FU header:
- bit7: S (1 for the first fragment)
- bit6: E (1 for the last fragment)
- bits5..0: FuType (oriignal nal_unit_type)
Building the FU payload header
Given original NAL header fields F, Lid, TIDp1 and the fixed FU type (49), the two payload header bytes for a FU are constructed as:
static inline void hevc_make_fu_payload_header(uint8_t fzb,
uint8_t layer_id,
uint8_t tid_plus1,
uint8_t out[2])
{
const uint8_t fu_type = 49; // HEVC_NAL_FU
out[0] = (fzb & 0x1) << 7;
out[0] |= (fu_type & 0x3F) << 1;
out[0] |= (layer_id >> 5) & 0x01; // layer_id[5]
out[1] = ((layer_id & 0x1F) << 3) | (tid_plus1 & 0x07);
}
FU packetization implementation (C)
The following function fragments an oversized HEVC NAL unit into RTP FU packets. It copies F/LayerId/TID from the original NAL header (recommended for generality). The RTP sender callback must attach the RTP header and set the marker bit on the last packet of the access unit as appropriate.
typedef struct {
int mtu; // Maximum network MTU including RTP header
int max_payload; // Budget for RTP payload bytes (no RTP header)
void (*rtp_send)(const uint8_t *payload, int size, int marker);
} RtpHevcOut;
static void rtp_send_hevc_nal(RtpHevcOut *s,
const uint8_t *nalu,
int nalu_size,
int is_last_in_au)
{
if (nalu_size <= 0) return;
const int nal_hdr_bytes = 2; // HEVC NAL header length
const int fu_overhead = 2 + 1; // payload header (2) + FU header (1)
const int room = s->max_payload - fu_overhead;
uint8_t fzb = hevc_forbidden_bit(nalu);
uint8_t ntype = hevc_nal_type(nalu);
uint8_t lid = hevc_layer_id(nalu);
uint8_t tidp1 = hevc_tid_plus1(nalu);
// If it fits, send as a single NAL
if (nalu_size <= s->max_payload) {
s->rtp_send(nalu, nalu_size, is_last_in_au);
return;
}
// Otherwise, fragment using FU (Type=49)
uint8_t hdr[2];
hevc_make_fu_payload_header(fzb, lid, tidp1, hdr);
uint8_t fu_hdr = 0;
fu_hdr |= (1u << 7); // S=1 for the first fragment
fu_hdr |= (ntype & 0x3F); // FuType = original nal_unit_type
const uint8_t *payload = nalu + nal_hdr_bytes;
int payload_left = nalu_size - nal_hdr_bytes;
// Emit middle fragments (S cleared, E=0)
while (payload_left > room) {
// build packet: [payload header (2)] [FU header (1)] [payload chunk]
uint8_t pkt[3 + 1500]; // temporary buffer; ensure > fu_overhead + room as needed
int copy = room;
pkt[0] = hdr[0];
pkt[1] = hdr[1];
pkt[2] = fu_hdr; // S=1 for first, cleared below for the rest
memcpy(pkt + fu_overhead, payload, copy);
s->rtp_send(pkt, fu_overhead + copy, 0);
// Advance
payload += copy;
payload_left -= copy;
fu_hdr &= ~(1u << 7); // clear S after first packet
}
// Last fragment: set E=1
fu_hdr |= (1u << 6);
{
uint8_t pkt[3 + 1500];
pkt[0] = hdr[0];
pkt[1] = hdr[1];
pkt[2] = fu_hdr;
memcpy(pkt + fu_overhead, payload, payload_left);
s->rtp_send(pkt, fu_overhead + payload_left, is_last_in_au);
}
}
Notes:
- room = max_payload − 3 accounts for the 2‑byte HEVC payload header and 1‑byte FU header.
- The FU payload omits the original 2‑byte HEVC NAL header.
- For simple single-layer streams, payload header fields can be set to F=0, LayerId=0, TID+1=1 if you don't need to preserve the original values.
Minimal single‑NAL fast path
When the NAL unit fits the payload budget, transmit it unchanged as the RTP payload:
static inline void rtp_send_hevc_single(RtpHevcOut *s,
const uint8_t *nalu,
int nalu_size,
int is_last_in_au)
{
s->rtp_send(nalu, nalu_size, is_last_in_au);
}
Putting it together
A typical sender chooses per NAL unit between single‑NAL and FU modes:
static void rtp_send_hevc_access_unit(RtpHevcOut *s,
const uint8_t *data,
int size)
{
// This example assumes data points at exactly one NALU. If you have an AU
// with multiple NALUs, iterate and set is_last_in_au only for the last.
const int is_last = 1;
if (size <= s->max_payload) {
rtp_send_hevc_single(s, data, size, is_last);
} else {
rtp_send_hevc_nal(s, data, size, is_last);
}
}
Reference
- RTP Payload Format for High Efficiency Video Coding (HEVC) — RFC 7798
- FFmpeg rtpenc_hevc.c (implementation of HEVC RTP packetization)