H.264 编码原理与 RTP/RTSP 传输流程详解
H.264 编码原理与 RTP/RTSP 传输流程详解
- 第一部分:H.264 编码结构
- H.264 简介
- 出现背景
- 编码结构层次
- 帧 (I、P、B 帧)
- NALU (Network Abstraction Layer Unit)(重点)
- VCL 层(视频编码层)
- NAL 层(网络抽象层)
- NALU 头部字段(F、NRI、Type)与起始码
- 数据流封装
- Annex-B 格式
- AVCC 格式 (mp4/flv 内常见)
- 第二部分:RTP 封包
- RTP 协议简介
- RTP header 基本字段(序列号、时间戳、payload type)
- H.264 与 RTP 的结合
- H.264 与 RTP 的封装过程:
- H.264 RTP 流的典型结构:
- FFMpeg编码NALU
- 单 NALU 模式(一个 RTP 包放一个 NALU)
- 分片模式 FU-A(大 NALU 被切片)
- 去掉起始码
- RTP 打包流程示例
- 输入:H.264 NALU
- 输出:RTP 包(头 + 负载)
- wireshark 看rtp包 判断包类型
- 第三部分:RTSP 发送
- RTSP 协议与 H.264/RTP 实战速查
- 1) RTSP 协议简介
- 2) 常见命令(Methods)
- 3) RTSP 会话建立流程(典型)
- 4) 客户端请求 → 服务器响应(示例)
- 5) SDP 关键字段(H.264 例)
- 6) 数据发送过程(H.264 → RTP)
- 7) RTP over TCP(interleaved)说明
- 8) RTSP × H.264/RTP 示例(要点)
- 9) 附:强制关键帧(FFmpeg/libx264 小贴士)
- 10) 术语速记
- 参考抓包操作
第一部分:H.264 编码结构
H.264 简介
H.264(也称为 AVC,Advanced Video Coding)是目前应用最广泛的视频压缩标准之一。无论是在线视频、监控摄像头,还是视频会议和直播,大多数场景都离不开 H.264。相比早期的 MPEG-2、MPEG-4 ASP 等标准,H.264 在保持视频质量的同时显著降低了码率,通常能在同等画质下节省 30%~50% 的带宽。这一特性使它成为流媒体传输和存储领域的事实标准。
出现背景
在互联网早期,视频的传输和存储成本非常高。MPEG-2 主要用于 DVD 和电视广播,码率通常在 2Mbps~8Mbps,对网络环境要求过高。而随着互联网视频和实时通信的兴起,人们迫切需要一种压缩效率更高、延迟更低的编码方式。H.264 正是在这一背景下由 ITU-T VCEG 和 ISO/IEC MPEG 联合制定,于 2003 年发布。
它的出现解决了两个关键问题:
-
带宽不足:在有限的网络带宽下依然能够传输清晰视频。
-
存储成本高:降低码率意味着同样大小的硬盘可以存更多视频内容。
因此,H.264 一经推出,就迅速成为视频应用的首选标准。
编码结构层次
H.264 的编码结构是分层次的,从宏观到微观,大体可以分为三个层面:
- 帧级(Frame):视频由一帧帧静态图像组成。
- 片级(Slice):每一帧可以被划分为若干个 slice,方便分割和传输。
- 宏块(Macroblock):在片内部,视频进一步划分为 16×16 的像素块,是编码的基本单位。

这套层次化的设计,让 H.264 能够灵活地在不同应用场景下平衡效率与质量。
帧 (I、P、B 帧)
在 H.264 中,最重要的就是对不同帧类型的划分。不同类型的帧承担不同的编码和解码职责,通过它们的配合来降低冗余、提升压缩率。
I 帧(Intra-coded frame)
也叫关键帧,它是独立编码的帧,不依赖于其他帧。I 帧保存了完整的画面信息,相当于一张完整的图片。它的作用主要有两个:
提供解码起点,播放器只要拿到 I 帧就能正确显示画面;
在画质上起到“锚点”作用,保证视频解码的参考质量。
不过,I 帧的数据量最大,因为没有利用到时间上的冗余。
P 帧(Predicted frame)
依赖之前的 I 帧或 P 帧,通过运动预测和补偿来只保存变化部分。比如视频中一个人说话,背景几乎不变,那么 P 帧只需要记录嘴巴的变化信息即可。这大大减少了码率。
特点:压缩率较高,解码时必须依赖参考帧。
B 帧(Bi-predictive frame)
同时参考前后的 I/P 帧来进行双向预测。B 帧的压缩效率最高,但解码复杂度也最大,需要解码器缓存更多帧才能正确还原。
特点:能进一步降低码率,但会增加延迟,因此在实时性要求高的场景(如视频会议)通常较少使用
NALU (Network Abstraction Layer Unit)(重点)
无论是存储还是网络传输,H264 原始码流是由一个接一个 NALU(NAL Unit) 组成,它的功能分为两层,VCL(Video Coding Layer)视频编码层和 NAL(Network Abstraction Layer)网络提取层。
VCL 层(视频编码层)
VCL:包括核心压缩引擎和块、宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码;
NAL 层(网络抽象层)
NAL:负责将 VCL 产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别;
注意 VCL层不需要研究,我们网络传输需要重点看NAL。
NALU其实就是NAL Unit,也就是说nalu其实就是nal组成的,而h264码流就是这样一个一个nalu组成

sps和pps是码流信息描述,在整个文件中可以只有一组sps和pps
从上图我们可以知道,一张图片可以有多个NALU; 对解码器来说,需要先收到SPS和PPS进行初始化,否则解码器无法解出正常的帧数据。
发I帧之前,至少要发送一次SPS和PPS,因此如果在实际应用中遇到H264无法解码的时候,检查SPS和PPS是否有接收到并正常初始化。
NALU 头部字段(F、NRI、Type)与起始码
起始码作为nalu的起始在每个NALU前加上00 00 00 01 或00 00 01

orbidden_zero_bit (F,占1bit)
在 H.264 规范中规定了这⼀位必须为 0 。
nal_ref_idc (NRI,占2bit)
NAL重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU,而不影响图像的回放 。 如果当前NALU是属于参考帧的片,或是序列参数集,或是图像参数集这些重要的单位时,本句法元素必需大于0。
nal_unit_type(Type,占5bit):
这个NALU单元的类型,1~12由H.264使用,24~31由H.264以外的应用使用。
NALU类型的详细解析表 :
NAL单元类型参考表
| nal_unit_type | 描述 |
|---|---|
| 0 | 未指定 |
| 1 | 一个非IDR图像的编码条带(P帧/B帧) |
| 2 | 编码条带数据分割块A |
| 3 | 编码条带数据分割块B |
| 4 | 编码条带数据分割块C |
| 5 | IDR图像的编码条带(IDR帧) |
| 6 | 辅助增强信息(SEI) |
| 7 | 序列参数集(SPS) |
| 8 | 图像参数集(PPS) |
| 9 | 访问单元分隔符 |
| 10 | 序列结尾 |
| 11 | 流结尾 |
| 12 | 填充数据 |
| 13 | 序列参数集扩展 |
| 14-18 | 保留 |
| 19 | 未分割的辅助编码图像的编码条带 |
| 20-23 | 保留 |
| 24-31 | 未指定 |
常见NALU类型对照表
| 十六进制 | 二进制 | 类型 | 重要性 | 类型值 |
|---|---|---|---|---|
| 0x67 | 0 11 00111 | SPS | 非常重要 | type = 7 |
| 0x68 | 0 11 01000 | PPS | 非常重要 | type = 8 |
| 0x65 | 0 11 00101 | IDR帧 | 关键帧 非常重要 | type = 5 |
| 0x61 | 0 11 00001 | I帧 | 非常重要 | type = 1 |
| 0x41 | 0 10 00001 | P帧 | 重要 | type = 1 |
| 0x01 | 0 00 00001 | B帧 | 不重要 | type = 1 |
| 0x06 | 0 00 00110 | SEI | 不重要 | type = 6 |
我们只需要记住类型为1 5678 即p i帧 和sei sps pps
在 十六进制中 ,开始符开始之后,第二个十六进制数就是nal类型
我们以一个h264文件看一下

在这里 起始码为00 00 01 47的二进制位 0 100 0111 这就是sps

在这里发现是65 就是关键帧 。学会这个很重要
数据流封装
在 H.264 编码过程中,视频数据从原始的 YUV 格式转换为压缩后的 ES(Elementary Stream) 格式。为了便于传输和存储,这些压缩后的数据需要进行封装。封装的主要任务是将编码后的数据组织成适合传输、存储的格式。在 H.264 中,常见的数据流封装格式包括 Annex-B 格式 和 AVCC 格式。这两种格式的选择主要取决于视频数据的应用场景。
Annex-B 格式
Annex-B 格式 是 H.264 编码流的一种原始封装格式,广泛用于实时视频流的传输(如 RTP、RTSP)。它通过 起始码(Start Code) 标记每个 NALU(Network Abstraction Layer Unit) 的边界,便于视频数据的同步和传输。
格式结构
起始码(Start Code):每个 NALU 前都有一个 起始码,通常是 0x000001 或 0x00000001,用于标识 NALU 的开始位置。
NALU 数据:紧随其后的部分即为 NALU 的内容,包含了视频数据和头信息(例如 SPS、PPS、slice 等)。
优点
同步性强:通过起始码标记 NALU 边界,解码器可以很容易地解析数据流。
简单易用:结构简单,容易理解和实现,适用于实时传输。
缺点
冗余数据:每个 NALU 前都需要添加起始码,增加了传输的开销。
不适合存储:由于每个 NALU 都有一个起始码,这使得其在存储时效率较低。
应用场景
实时流媒体传输,例如使用 RTP 或 RTSP 协议传输视频数据时,通常采用 Annex-B 格式。
AVCC 格式 (mp4/flv 内常见)
AVCC 格式 是另一种常见的封装格式,主要用于 文件存储,如 MP4、FLV 等容器格式。在 AVCC 格式 中,每个 NALU 前会有一个 4 字节的长度字段,用来表示该 NALU 的大小。这种格式适合存储大量视频数据,并且具有较低的传输开销。
第二部分:RTP 封包
RTP 协议简介
RTP(Real-time Transport Protocol) 是一种专门用于 实时数据传输 的协议,广泛应用于 语音、视频、音频 等流媒体的网络传输。RTP 是一个端到端协议,通常与 RTCP(Real-time Transport Control Protocol) 配合使用,后者负责反馈传输状态和控制信息。
RTP 主要目标是确保在 低延迟 和 实时性要求高的应用(如 视频会议、直播流、VoIP)中,数据可以顺利地传输,并尽可能减少丢包和时延。
RTP 的特点:
-
端到端传输:直接在源和接收端之间传输数据。
-
不保证可靠性:RTP 不会对丢失的数据进行重传,它通常用于 实时传输,丢包是可以容忍的。
-
带有时间戳:为每个数据包添加时间戳,确保数据按时间顺序播放。
-
支持多媒体数据:能够传输音频、视频以及其他数据流。
RTP 协议通过将媒体数据分成小的数据包(packet)进行传输,每个数据包都有特定的头部和载荷部分,用于标识包的类型和传输信息
RTP header 基本字段(序列号、时间戳、payload type)
| 字段 | 大小 (bit) | 说明 |
|---|---|---|
| 版本号 (Version) | 2 | 表示 RTP 的版本号,当前为 2 |
| 填充标志 (Padding) | 1 | 如果设置为 1,则表示 RTP 包包含填充字节 |
| 扩展标志 (Extension) | 1 | 如果设置为 1,则表示有扩展头部 |
| CSRC 数量 (CC) | 4 | 指示 CSRC(贡献源)的数量 |
| 标志位 (Marker) | 1 | 标记位,通常用于标记帧的开始(如 I 帧) |
| 负载类型 (Payload Type) | 7 | 指示 RTP 包的负载类型,通常用于指示编码格式(如 H.264、G.711) |
| 序列号 (Sequence Number) | 16 | 序列号,标识 RTP 包的顺序,接收端可以根据序列号检测丢包 |
| 时间戳 (Timestamp) | 32 | 标识数据包的采样时间,帮助接收端同步数据流 |
| 同步源标识符 (SSRC) | 32 | 唯一标识 RTP 会话中的同步源 |
| 贡献源标识符 (CSRC) | 0 或 32 × N | 如果使用了 CC,此字段用于标识数据源 |
主要字段解析:
版本号 (Version):指示 RTP 协议的版本,当前为 2。
负载类型 (Payload Type):指示 RTP 数据包中包含的数据类型。对于 H.264 视频流,Payload Type 通常会设置为某个特定的值,例如 96(根据 IETF RFC 3551 的建议)。接收端可以根据这个字段识别数据流的编码格式。
序列号 (Sequence Number):RTP 数据包的序列号是一个 递增的数字,帮助接收端检测丢包和恢复数据流顺序。如果某个包丢失,接收端会通过序列号检查缺失包的位置。
时间戳 (Timestamp):表示 RTP 包的采样时间点,通常对应于视频帧的显示时间。时间戳对于视频流的同步至关重要,确保帧按正确的顺序和时间间隔播放。
标志位 (Marker):标记位常用于指示特殊的时间点或数据包。对于视频流,标志位常常用于标识 I 帧(关键帧),让接收端知道何时需要进行新的解码序列。
H.264 与 RTP 的结合
H.264 视频流与 RTP 的结合,通常用于实时视频流的传输(如视频会议、网络直播等),它需要将 H.264 编码数据 按照 RTP 协议 打包,以便网络传输。H.264 编码视频数据通常以 NALU(Network Abstraction Layer Unit) 的形式存在,而 RTP 负责将这些 NALU 封装并在网络中传输。
H.264 与 RTP 的封装过程:
-
NALU 生成:
- H.264 编码器将原始视频帧(YUV 格式)压缩为 H.264 视频流。这个流由多个 NALU 组成,其中每个 NALU 代表视频编码过程中的一个片段(如一个 slice)。
- 每个 NALU 内部包含了压缩后的图像数据或编码参数(如 SPS、PPS 等)。
-
RTP 封装:
- RTP 协议负责将这些 NALU 数据分割成多个数据包,确保每个数据包带有相应的 序列号 和 时间戳,以便接收端能够按顺序重组视频帧。
- 为了适应 RTP 协议的传输要求,H.264 数据流可能会进行 分片(特别是当 NALU 较大时)。这种分片过程可以使用 FU-A(Fragmentation Unit A)等方法进行处理。
-
RTP 与 H.264 的兼容性:
- RTP Payload Type:RTP 中的 Payload Type 字段用来标识 H.264 编码数据流。通常,Payload Type 为 96(根据 IETF RFC 3551),表示负载类型为 H.264 视频流。
- NALU 封装:RTP 封装的 H.264 视频流 会根据 Annex-B 格式 或 AVCC 格式 来处理 NALU,这取决于传输协议。一般来说,Annex-B 格式 更常见于流媒体传输。
-
RTP 与 H.264 数据流同步:
- 时间戳:RTP 包中的 时间戳 用于指示该包的播放时间,这样接收端可以按照正确的时间顺序播放视频。
- 序列号:RTP 包中的 序列号 用于确保数据包按顺序进行解码和播放,防止数据包丢失或乱序。
H.264 RTP 流的典型结构:
- RTP 包 = RTP header + NALU Payload
- RTP Header:包含 序列号、时间戳、Payload Type 等信息。
- NALU Payload:即 H.264 编码数据,比如 slice 或 SPS/PPS 等。
FFMpeg编码NALU
我们在对NALU组包时 也需要知道,怎么把YUV变成NALU
[ 输入 NV12 YUV 数据 ]
│
▼
sws_scale 转换
(NV12 → YUV420P)
│
▼
avcodec_send_frame()
(送入编码器)
│
▼
┌───────────────────────┐
│ x264 编码器逻辑 │
│ - GOP/参考帧 │
│ - I/P/B 帧决策 │
│ - 生成 NALU (VCL) │
│ - 插入 SPS/PPS/SEI │
└───────────────────────┘
│
▼
avcodec_receive_packet()
(得到 AVPacket)
│
▼
- 解析 NALU 起始码
- 保存 SPS/PPS
│
▼
[ 输出 H.264 Annex-B 码流 ]
这里的转换是很重要的 ,我们在FFMpeg进行编码的时候需要把YUV格式 转换为YUV420进行从AVFRame----->AVPacket,当我们送入YUV 编码器会把YUV编码为IBP帧,并且组成nalu单元,输出ES码流存入AVPAcket,那么一个AVPacket存多少nalu呢 提供测试demo
// C++11 仅用:-std=c++11
// 链接:avcodec avutil (若不做像素转换,无需 swscale)
// Windows: 链接 avcodec.lib avutil.lib,并把对应 DLL 放到 exe 目录。
// Linux/macOS: g++ test.cpp -std=c++11 -lavcodec -lavutil -o test
#define _CRT_SECURE_NO_WARNINGS#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <iostream>
#include <vector>extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h> // av_opt_set 在这里
}// ---------- NAL 类型到文本 ----------
static const char* h264_nal_type(int t) {switch (t) {case 1: return "Non-IDR slice (P/B)";case 5: return "IDR slice (I)";case 6: return "SEI";case 7: return "SPS";case 8: return "PPS";case 9: return "AUD";default: return "Other";}
}// ---------- Annex-B 起始码检测 ----------
static inline int startcode_len(const uint8_t* p, const uint8_t* end) {if (p + 3 <= end && p[0] == 0 && p[1] == 0 && p[2] == 1) return 3;if (p + 4 <= end && p[0] == 0 && p[1] == 0 && p[2] == 0 && p[3] == 1) return 4;return 0;
}// 解析并打印 NAL(优先按 Annex-B;不匹配则尝试 AVCC 4-byte length)
static void print_nalus_from_packet(const AVPacket* pkt) {const uint8_t* p = pkt->data;const uint8_t* end = pkt->data + pkt->size;// 尝试 Annex-Bconst uint8_t* s = p;int sc = 0;while (s < end && (sc = startcode_len(s, end)) == 0) ++s;if (sc) {const uint8_t* cur = s + sc;while (cur < end) {const uint8_t* q = cur;int sc2 = 0;while (q < end && (sc2 = startcode_len(q, end)) == 0) ++q;const uint8_t* nal = cur;const uint8_t* nal_end = (sc2 ? q : end);if (nal < nal_end) {uint8_t nal_type = nal[0] & 0x1F;std::cout << " NAL type=" << (int)nal_type<< " (" << h264_nal_type((int)nal_type) << ")\n";}if (!sc2) break;cur = q + sc2;}return;}// 尝试 AVCC(4 字节长度前缀,大端)const uint8_t* cur = p;while (cur + 4 <= end) {uint32_t len = ((uint32_t)cur[0] << 24) | ((uint32_t)cur[1] << 16) |((uint32_t)cur[2] << 8) | (uint32_t)cur[3];cur += 4;if (len == 0 || cur + len > end) break;uint8_t nal_type = cur[0] & 0x1F;std::cout << " NAL type=" << (int)nal_type<< " (" << h264_nal_type((int)nal_type) << ")\n";cur += len;}
}// 把连续平面 YUV420P(紧密存储)拷到 AVFrame(考虑 linesize)
static bool copy_frame_yuv420p(FILE* f, AVFrame* frame, int w, int h) {const int y_size = w * h;const int uv_w = w / 2;const int uv_h = h / 2;const int uv_size = uv_w * uv_h;std::vector<uint8_t> y(y_size), u(uv_size), v(uv_size);if (fread(&y[0], 1, y_size, f) != (size_t)y_size) return false;if (fread(&u[0], 1, uv_size, f) != (size_t)uv_size) return false;if (fread(&v[0], 1, uv_size, f) != (size_t)uv_size) return false;if (av_frame_make_writable(frame) < 0) return false;// Yfor (int i = 0; i < h; ++i) {memcpy(frame->data[0] + i * frame->linesize[0],&y[0] + i * w, w);}// Ufor (int i = 0; i < uv_h; ++i) {memcpy(frame->data[1] + i * frame->linesize[1],&u[0] + i * uv_w, uv_w);}// Vfor (int i = 0; i < uv_h; ++i) {memcpy(frame->data[2] + i * frame->linesize[2],&v[0] + i * uv_w, uv_w);}return true;
}int main(int argc, char** argv) {// -------- 参数 --------std::string in_path = "output.yuv"; // 输入 YUV420P 文件int W = 320;int H = 240;int FPS = 25;int max_frames = 100; // 读取的最多帧数(可按需改)if (argc >= 6) {in_path = argv[1];W = std::atoi(argv[2]);H = std::atoi(argv[3]);FPS = std::atoi(argv[4]);max_frames = std::atoi(argv[5]);}else {std::cout << "Usage: " << (argc > 0 ? argv[0] : "app")<< " <in.yuv> <W> <H> <FPS> <max_frames>\n"<< "Default: out.yuv 320 240 25 100\n";}FILE* fin = std::fopen(in_path.c_str(), "rb");if (!fin) {std::cerr << "open " << in_path << " failed\n";return 1;}// -------- 找编码器 --------const AVCodec* codec = avcodec_find_encoder_by_name("libx264");if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!codec) {std::cerr << "no h264 encoder available\n";std::fclose(fin);return 1;}// -------- 创建编码器上下文 --------AVCodecContext* ctx = avcodec_alloc_context3(codec);if (!ctx) {std::cerr << "avcodec_alloc_context3 failed\n";std::fclose(fin);return 1;}ctx->width = W;ctx->height = H;ctx->pix_fmt = AV_PIX_FMT_YUV420P;ctx->gop_size = FPS;ctx->max_b_frames = 0;// 用 C++11 结构体赋值ctx->time_base = { 1, FPS };ctx->framerate = { FPS, 1 };// 低延迟/可观测配置(不同编码器可能忽略)av_opt_set(ctx->priv_data, "preset", "veryfast", 0);av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);av_opt_set(ctx->priv_data, "repeat_headers", "1", 0); // 每个 IDR 前 SPS/PPSif (avcodec_open2(ctx, codec, NULL) < 0) {std::cerr << "avcodec_open2 failed\n";std::fclose(fin);avcodec_free_context(&ctx);return 1;}// -------- 准备帧 --------AVFrame* frame = av_frame_alloc();if (!frame) {std::cerr << "av_frame_alloc failed\n";std::fclose(fin);avcodec_free_context(&ctx);return 1;}frame->format = ctx->pix_fmt;frame->width = ctx->width;frame->height = ctx->height;if (av_frame_get_buffer(frame, 32) < 0) {std::cerr << "av_frame_get_buffer failed\n";std::fclose(fin);av_frame_free(&frame);avcodec_free_context(&ctx);return 1;}// -------- 读帧并编码 --------int64_t pts = 0;int frame_cnt = 0;while (frame_cnt < max_frames) {if (!copy_frame_yuv420p(fin, frame, W, H)) break;frame->pts = pts++;++frame_cnt;if (avcodec_send_frame(ctx, frame) < 0) {std::cerr << "avcodec_send_frame failed\n";break;}AVPacket* pkt = av_packet_alloc();if (!pkt) { std::cerr << "av_packet_alloc failed\n"; break; }while (1) {int r = avcodec_receive_packet(ctx, pkt);if (r == AVERROR(EAGAIN) || r == AVERROR_EOF) break;if (r < 0) {std::cerr << "avcodec_receive_packet error\n";break;}std::cout << "AVPacket pts=" << pkt->pts<< " size=" << pkt->size << "\n";print_nalus_from_packet(pkt);av_packet_unref(pkt);}av_packet_free(&pkt);}// -------- flush --------avcodec_send_frame(ctx, NULL);{AVPacket* pkt = av_packet_alloc();if (pkt) {while (1) {int r = avcodec_receive_packet(ctx, pkt);if (r == AVERROR(EAGAIN) || r == AVERROR_EOF) break;if (r < 0) break;std::cout << "FLUSH AVPacket pts=" << pkt->pts<< " size=" << pkt->size << "\n";print_nalus_from_packet(pkt);av_packet_unref(pkt);}av_packet_free(&pkt);}}// -------- 清理 --------std::fclose(fin);av_frame_free(&frame);avcodec_free_context(&ctx);return 0;
}


片的介绍如上图。
通过上面的介绍,YUV在通过编码器之后拿到了NALU ,NALU被封装在AVPacket里面。

由于FFMpeg已经帮我们进行了i帧分片,但是我们mtu最大是1500 ,我们可以设置不分片 如下图

不分片一个i帧很大,分3片也超过1500(MTU),我们可以设置
av_opt_set(ctx->priv_data, "x264-params", "slice-max-size=1500", 0);
1500还是偏大的,加上rtp头还是不合适的

单 NALU 模式(一个 RTP 包放一个 NALU)
当我们编码时,如果做了nalu分片,那么可以直接打包发送,但是大部分情况我们都没有分包那么精准,所以我们都是采用分片模式组包
分片模式 FU-A(大 NALU 被切片)
分片流程
去掉起始码
接口流程 输入AVPacket,对AVPacket中的NALU 去头 并存入pair<const uint8_t*, size_t> > 也可以使用结构体管理。
分离思路:
[00 00 00 01] 67 xx xx xx
[00 00 00 01] 68 yy yy
[00 00 01] 65 zz zz zz zz
找到开始码的起始位和结束位就可以进行分离
// ---------- 只分离:从 Annex-B 里切出裸 NALU(不含起始码) ----------
static void splitAnnexB(const uint8_t* data, size_t len,std::vector<std::pair<const uint8_t*, size_t> >& out)
{out.clear();if (!data || len < 4) return;size_t i = 0;size_t nal_start = (size_t)-1;// 找到第一个起始码,记录“起始码之后”作为 NAL 开头while (i + 3 < len) {if (i + 4 <= len && isStartCode4(data + i)) { i += 4; nal_start = i; break; }if (isStartCode3(data + i)) { i += 3; nal_start = i; break; }++i;}if (nal_start == (size_t)-1) { // 兜底:未找到起始码,整体作为一个片段out.push_back(std::make_pair(data, len));return;}// 继续扫描,遇到下一个起始码就切出前一个 NALUwhile (i + 3 < len) {bool sc4 = (i + 4 <= len) && isStartCode4(data + i);bool sc3 = isStartCode3(data + i);if (sc4 || sc3) {size_t nal_end = i;if (nal_end > nal_start) {out.push_back(std::make_pair(data + nal_start, nal_end - nal_start));}i += sc4 ? 4 : 3; // 跳过起始码nal_start = i; // 新 NAL 开始}else {++i;}}// 收尾:文件末尾/包末尾if (nal_start < len) {out.push_back(std::make_pair(data + nal_start, len - nal_start));}// 清理零长度(保险)std::vector<std::pair<const uint8_t*, size_t> >::iterator it = out.begin();while (it != out.end()) {if (it->second == 0) it = out.erase(it);else ++it;}
}
RTP 打包流程示例
输入:H.264 NALU


截取了两包AVFrame ,第一包AVframe,封装了 6个nalu 其中有SPS,PPS ,SEI以及和一个关键帧。
第二包AVFrame,封装了3nalu都是p帧。
这就是这两个AVFrame 里面NALU的情况。
输出:RTP 包(头 + 负载)
输出RTP包时,需要对NALU分片,以及填充rtp头 ,fu-a头。
±-----------±-----------±-----------±-----------+
| RTP Header (12 Bytes) | FU Indicator | FU Header |
±-----------±-----------±-----------±-----------+
| Fragmented NALU data … |
±-------------------------------------------------+
FU-a头有两个头FU Indicator 和FU Header。
RTP Header 只知道“我在传输第 N 个包”
FU Indicator 告诉你“这个包是FU-A”
FU Header 告诉你“这个分片是第一个/中间/最后一个”
一般FU-A的Indicator都为7C
| 分片 | FU Header | 二进制 | 含义 |
|---|---|---|---|
| 第1包 | 0x85 | 1000 0101 | S=1,E=0,表示“首片”,Type=5 |
| 第2包 | 0x05 | 0000 0101 | S=0,E=0,表示“中片”,Type=5 |
| 第3包 | 0x45 | 0100 0101 | S=0,E=1,表示“尾片”,Type=5 |
接下来进行组包rtp:
- 读取NALU头
| 位域 | bit7 | bit6-bit5 | bit4-bit3-bit2-bit1-bit0 |
|---|---|---|---|
| 名称 | F | NRI | NUT |
| 大小 | 1 bit | 2 bit | 5 bit |
nalh = nal[0] 含义:原始 NALU 的 1 字节头,里面同时包含了 F / NRI / NUT 三个字段的所有位。
作用:作为源字节,用来提取下面三个字段。NUT = (nalh & 0x1F) 含义:NAL Unit Type(低 5 位),范围 0–31。 作用:指明 NALU
的“内容/用途”,解码与打包策略主要看它。常见值:1:非 IDR 片(P/B slice)
5:IDR 片(关键帧 slice)
6:SEI
7:SPS
8:PPS
9:AUD
24:STAP-A(聚合包)
28:FU-A(分片单元)
NRI = (nalh & 0x60) 含义:nal_ref_idc(位 5–6,共 2 位),代表参考重要性,取值 0–3(在字节里就是
0、0x20、0x40、0x60)。 作用:指示这个 NALU 是否会被参考/重要程度,通常:0:非参考(如 SEI/AUD 多为 0)
1–3:有参考价值,数值越大“越重要”(如 SPS/PPS 常见为 3) 在你的代码里保留 未右移 的形式(仍占据
bit5–6),便于直接和别的“低 5 位类型值”按位或,组成新的字节(见下文 FU-A)。Fbit = (nalh & 0x80) 含义:forbidden_zero_bit(位 7,最高位)。标准要求必须为 0。 作用:一旦为
1,表示该 NALU/比特流出现错误或被标记为非法;接收端可以据此丢弃该 NALU。实际流里几乎总是 0。
if (!nal || nal_len == 0 || max_payload == 0) return; // 基本健壮性校验const uint8_t nalh = nal[0]; // 原始 1B NAL 头(F|NRI|Type)const uint8_t NUT = (nalh & 0x1F); // 原始 Type(低5位)const uint8_t NRI = (nalh & 0x60); // 原始 NRI(重要性)const uint8_t Fbit = (nalh & 0x80); // 原始 F 位(违规标记,通常为 0)
- 如果需要分包填写FU-A
图 4 表示FU-A的RTP荷载格式。FU-A由1字节的分片单元指示(如图5),1字节的分片单元头(如图6),和分片单元荷载组成。

图中左边(图5)—— FU Indicator(分片单元指示符) 这一字节在每个 FU-A 包开头都会出现。 位段 名称 说明 F (bit 0) forbidden_zero_bit 固定为 0(若为 1 表示该NALU有错误) NRI (bit 1–2) nal_ref_idc 表示 NALU 的重要程度(0~3,数值越大越重要) Type (bit 3–7) nal_unit_type 在 FU-A 中固定为 28(即 11100₂)表示这是“分片单元” 这个字节 =原始NALU头的前3位(F,NRI) + 新Type(28)。 它告诉接收端:这不是完整NALU,而是一个FU-A分片。
图中右边(图6)—— FU Header(分片单元头) 这是 FU-A结构中的第二个字节,用来说明**当前分片的位置(首片/中间/尾片)**以及原始NALU的类型。 位段 名称 说明 S (bit 0) Start =1 表示本片是 NALU 的第一个分片 E (bit 1) End =1 表示本片是NALU 的最后一个分片 R (bit 2) Reserved 保留位,固定为 0 Type (bit3–7) nal_unit_type 原始 NALU 的类型(例如 1=非IDR,5=IDR,7=SPS,8=PPS 等)
const uint8_t FU_IND = (uint8_t)(Fbit | NRI | 28); // FU-Indicator: F|NRI|Type=28
对于有效NALU的 FU_IND为 124(7C)。

对于FU_ Header
| 分片 | FU Header | 二进制 | 含义 |
|---|---|---|---|
| 第1包 | 0x85 | 1000 0101 | S=1,E=0,表示“首片”,Type=5 |
| 第2包 | 0x05 | 0000 0101 | S=0,E=0,表示“中片”,Type=5 |
| 第3包 | 0x45 | 0100 0101 | S=0,E=1,表示“尾片”,Type=5 |
| 分片 | FU Header | 二进制 | 含义 |
|---|---|---|---|
| 第1包 | 0x81 | 1000 0001 | S=1, E=0,首片,Type=1(非IDR:P/B) |
| 第2包 | 0x01 | 0000 0001 | S=0, E=0,中片,Type=1 |
| 第3包 | 0x41 | 0100 0001 | S=0, E=1,尾片,Type=1 |
FU-Header字段的Type就是NALU的NUT
FU-Header=SER|NUT(NALU)
其中如果是最后一片RTP的头 M=1。
总结 这里对写入 RTP包的首数据常见做了总结
| 十六进制 | 二进制(8位) | 低5位(Type,二进制) | 类型 | 值(十进制) |
|---|---|---|---|---|
| 7C | 0111 1100 | 11100 | 分片 FU-A | 28 |
| 67 | 0110 0111 | 00111 | 不分片 SPS | 7 |
| 68 | 0110 1000 | 01000 | 不分片 PPS | 8 |
wireshark 看rtp包 判断包类型

首包 41 ,则是nalu整个 并且为bp帧
void H264RtpServer::sendNal_(const uint8_t* nalu, size_t nalu_len, uint32_t ts90k, bool marker) {if (!nalu || nalu_len == 0) return; // 校验int fd = cfd_; // 读取当前客户端 fdif (fd < 0) return; // 无连接则返回const size_t MAX_PAYLOAD = 1400; // 单个 RTP 载荷上限(不含 12B 头)std::lock_guard<std::mutex> lk(send_mtx_); // 发送保护(互斥)if (nalu_len <= MAX_PAYLOAD) { // ---- Single NAL ----auto rtp = make_rtp_header(marker, seq_++, ts90k, ssrc_); // 12B RTP 固定头(M=marker)uint16_t rtp_len = uint16_t(12 + nalu_len); // RTP 总长度std::vector<uint8_t> pkt; // interleaved 块缓冲pkt.reserve(4 + rtp_len); // 预留空间:4B + RTPpkt.push_back('$'); pkt.push_back(0); // '$' + channel=0(RTP)pkt.push_back(uint8_t(rtp_len >> 8)); // 长度高位(大端)pkt.push_back(uint8_t(rtp_len & 0xFF)); // 长度低位pkt.insert(pkt.end(), rtp.begin(), rtp.end()); // RTP 头pkt.insert(pkt.end(), nalu, nalu + nalu_len); // 载荷=完整 NALU(void)send_all(fd, pkt.data(), pkt.size()); // 发送整个 interleaved 块} else { // ---- FU-A 分片 ----const uint8_t h = nalu[0]; // 原始 NAL 头const uint8_t F = (h & 0x80); // F 位const uint8_t NRI = (h & 0x60); // NRI(重要性)const uint8_t NUT = (h & 0x1F); // 原始类型(低 5 位)const uint8_t FU_INDICATOR = F | NRI | 28; // FU-Indicator: F|NRI|Type=28size_t remain = nalu_len - 1; // 剩余 RBSP 字节数(去掉 1B 头)const uint8_t* p = nalu + 1; // 分片起点bool start = true; // 首片标记while (remain > 0) { // 循环切片size_t chunk = std::min(remain, MAX_PAYLOAD - 2); // 本片大小(扣除 2B FU 头)bool end = (chunk == remain); // 是否最后一片(决定 E 位/M 位)uint8_t fu_header = (start ? 0x80 : 0x00) // FU-Header.S(bit7)| (end ? 0x40 : 0x00) // FU-Header.E(bit6)| (NUT & 0x1F); // FU-Header.NUT(低5位)auto rtp = make_rtp_header(marker && end, seq_++, ts90k, ssrc_); // 仅尾片且帧末置 Muint16_t rtp_len = uint16_t(12 + 2 + chunk); // RTP 总长度(12+FU头2+数据)std::vector<uint8_t> pkt; // interleaved 块pkt.reserve(4 + rtp_len); // 预留pkt.push_back('$'); pkt.push_back(0); // '$'+ch=0pkt.push_back(uint8_t(rtp_len >> 8)); // 长度高位pkt.push_back(uint8_t(rtp_len & 0xFF)); // 长度低位pkt.insert(pkt.end(), rtp.begin(), rtp.end()); // RTP 头pkt.push_back(FU_INDICATOR); // 1B FU-Indicatorpkt.push_back(fu_header); // 1B FU-Headerpkt.insert(pkt.end(), p, p + chunk); // 分片载荷if (!send_all(fd, pkt.data(), pkt.size())) break; // 发送失败则中断p += chunk; remain -= chunk; start = false; // 前进到下一片}}
}
第三部分:RTSP 发送
RTSP 协议与 H.264/RTP 实战速查
面向工程实现与抓包调试的简明笔记。涵盖常见 RTSP 命令、会话时序、SDP 字段、RTP over TCP(interleaved)封装,以及 H.264/RTP 负载化要点。
1) RTSP 协议简介
- 用途:控制媒体会话(播放/暂停/位置跳转等),本身不承载媒体数据。
- 传输层:通常走 TCP 554(也可其他端口)。媒体数据常用 RTP/UDP;也可 RTP over TCP(interleaved)。
- 与 HTTP 的区别:都是基于文本的请求/响应协议,但 RTSP 是面向会话与媒体控制。
2) 常见命令(Methods)
| 方法 | 作用简述 | 常见关键头 |
|---|---|---|
| OPTIONS | 询问服务器支持的 RTSP 方法 | CSeq |
| DESCRIBE | 请求 SDP 描述,了解媒体编码/时钟等参数 | CSeq、Accept: application/sdp |
| SETUP | 建立传输通道(UDP 或 RTP/AVP/TCP interleaved) | CSeq、Transport、(成功后响应里会返回 Session) |
| PLAY | 开始发送媒体流 | CSeq、Session、(响应可含 RTP-Info 初始 seq/rtptime) |
| PAUSE | 暂停播放 | CSeq、Session |
| TEARDOWN | 结束会话 | CSeq、Session |
| GET_PARAMETER | 保活/心跳(可选) | CSeq、Session |
最小必需头:CSeq(序号)、Session(从 SETUP 响应获取,后续请求需带)。
3) RTSP 会话建立流程(典型)
sequenceDiagramautonumberparticipant C as Clientparticipant S as ServerC->>S: OPTIONS rtsp://host/pathS-->>C: 200 OK (Public: ...)C->>S: DESCRIBE rtsp://host/path (Accept: application/sdp)S-->>C: 200 OK + SDPC->>S: SETUP rtsp://host/path/track1 (Transport: RTP/AVP/TCP;interleaved=0-1)S-->>C: 200 OK (Transport: ...;ssrc=..., Session: ...)C->>S: PLAY rtsp://host/path (Session: ...)S-->>C: 200 OK (RTP-Info: seq=..., rtptime=...)Note over C,S: S 开始以 interleaved 模式在 TCP 流中发送:<br/>"$" + ch + len + RTP
若是 RTP/UDP,SETUP 阶段会协商客户端/服务器的 UDP 端口;PLAY 后媒体数据走 UDP。
4) 客户端请求 → 服务器响应(示例)
OPTIONS
OPTIONS rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 1--
RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE
DESCRIBE
DESCRIBE rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 2
Accept: application/sdp--
RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: <len>v=0
o=- 0 0 IN IP4 10.0.0.1
s=H264
t=0 0
a=control:*
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;sprop-parameter-sets=<b64-sps>,<b64-pps>
a=control:track1
SETUP(RTP over TCP)
SETUP rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 3
Transport: RTP/AVP/TCP;unicast;interleaved=0-1--
RTSP/1.0 200 OK
CSeq: 3
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=12345678
Session: 12345678
PLAY
PLAY rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 4
Session: 12345678--
RTSP/1.0 200 OK
CSeq: 4
RTP-Info: url=rtsp://10.0.0.1/track1;seq=1000;rtptime=0
Session: 12345678
5) SDP 关键字段(H.264 例)
m=video 0 RTP/AVP 96:媒体类型=video,控制端口占位 0(TCP 模式不在 SDP 暴露端口),PT=96(动态负载)。a=rtpmap:96 H264/90000:将 PT=96 映射为 H.264,时钟 90 kHz(RTP 时间戳单位)。a=fmtp:96 packetization-mode=1; sprop-parameter-sets=...:packetization-mode:0=单 NAL;1=Single NAL/STAP-A/FU-A;2=非常用(STAP-B/MTAP)。sprop-parameter-sets:SPS/PPS 的 Base64,用于解码器初始化。
a=control:track1:该媒体的 RTSP 控制 URL 后缀。
SPS/PPS Base64:直接对 Annex‑B 中去掉起始码的 SPS/PPS 字节做 Base64。
6) 数据发送过程(H.264 → RTP)
- 编码器输出 Annex‑B 字节流:
00 00 00 01+ NALU(SPS/PPS/SEI/IDR/P 等)。 - 将起始码剥离,逐个 NALU 做 RTP 负载化:
- Single NAL Unit:小 NALU(如 SPS/PPS/小 P 帧片段)直接一个 RTP 包携带。
- STAP‑A (type=24):把多个小 NALU 合并到一个 RTP 包(很少用)。
- FU‑A (type=28):大 NALU 分片:载荷 =
FU-Indicator(1B)+FU-Header(1B)+片数据。FU-Header:S=首片(bit7),E=尾片(bit6),Type=原始 NAL 类型(低5位)。
- RTP 头(12 B):
V=2,PT=96(由 SDP 指定),seq递增,ts(90 kHz 时钟,通常按帧步进),ssrc唯一。 - Marker(M) 位:通常置于“一帧的最后一个 RTP 包”。
- RTP over TCP(interleaved):
'$'(0x24) + channel(1B) + length(2B-BE) + RTP(12B+payload)。
7) RTP over TCP(interleaved)说明
- 动机:穿越 NAT/防火墙,复用 RTSP 的 TCP 连接承载 RTP/RTCP。
- 帧格式:
'$' 1B | Channel 1B | Length 2B (大端,=后续RTP字节数) | RTP(12B+payload)
- 通道:常见
0=RTP、1=RTCP(也可协商其他)。 - 示例(十六进制):
24 00 04 B0 ...0x24=‘$’,0x00=RTP 通道,0x04B0=1200(接下来 1200 字节是 RTP)。
8) RTSP × H.264/RTP 示例(要点)
- SPS/PPS 发送:常在关键帧前发送(或复写)以便随时解码入会。
- FU‑A 分片:大于 MTU 的 IDR/P 片会被拆分;
- FU-Indicator =
F|NRI|28; - FU-Header =
S/E 标志 + 原始 NAL Type; - 只在帧最后一片置
Marker=1。
- FU-Indicator =
- Wireshark 过滤:
rtsp、tcp.port==554、rtp。 - 识别 H.264 NAL 类型:
- 单包:载荷首字节
F|NRI|Type,Type=7(SPS),8(PPS),5(IDR),1(P/B)等; - FU‑A:载荷首两字节:
[0]=FU-Indicator(type=28)、[1]=FU-Header(S/E/Type)。
- 单包:载荷首字节
9) 附:强制关键帧(FFmpeg/libx264 小贴士)
- 固定 GOP:
ctx->gop_size = FPS;(例如 25 → 每 1s 关键帧)。 - x264 参数(降低不确定性):
av_opt_set(ctx->priv_data, "x264-params", "keyint=25:min-keyint=25:scenecut=0", 0); - 驱动 I 帧:部分编码器支持将
AVFrame->pict_type=AV_PICTURE_TYPE_I作为“请求”,但是否立即生效取决于编码器;更可靠还是上面的 keyint/min-keyint/scenecut。
10) 术语速记
- CSeq:RTSP 会话内请求序号。
- Session:SETUP 响应分配的会话 ID,后续请求要带。
- SSRC:RTP 同步源 ID,用于区分与同步流。
- PT:RTP 负载类型;H.264 常用 96(动态,SDP 中声明)。
- RTP 时间戳:单位 90 kHz;逐帧递增(例如 25 fps → 每帧 +3600)。
参考抓包操作
- 过滤:
tcp.port == 554看 RTSP 文本;rtp看媒体包。 - RTP over TCP:展开
RTSP流中的Interleaved Data,RTP 解码同样可用。 - 典型问题定位:
- 看
SDP是否含packetization-mode=1、sprop-parameter-sets; - 检查
Marker是否出现在每帧最后一个包; - FU‑A 的
S/E是否成对出现,从S=1到E=1闭合。
- 看
