ZLMediaKit / RTSP / H.264 播放音视频踩坑记录 - SDP、SPS、PPS 完整详解
适用场景:RTSP 信令交互、addStreamProxy 拉流、WebRTC、H264 裸流、花屏、首帧黑屏、无法解码,绝大多数问题根源就是 SDP / SPS/PPS。
1、SDP 是什么
SDP:Session Description Protocol,会话描述协议,文本格式。
RTSP 中,客户端发送DESCRIBE请求,服务端返回 SDP;
SDP 告诉播放器:这个流有什么轨道(视频 / 音频)、编码格式、分辨率、采样率、RTP 端口、传输协议、SPS/PPS 等信息。
SDP 只是描述元数据,不携带音视频帧数据,纯文本。
RTSP 交互片段:
VLC/ZLM(PlayerProxy) →
DESCRIBE rtsp://xxx/vs0 RTSP/1.0设备 RTSP 服务端回复,Body 就是SDP 文本。
SDP 典型片段 (H.264 视频)
v=0 o=- 1600000001 1600000001 IN IP4 127.0.0.1 s=MediaServer t=0 0 m=video 0 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop‑parameter‑sets=Z0IAKeKQFAe2AtwEBAaQeJEA8AAIB+BA1KGA,g48IAA==重点字段:
m=video:视频媒体轨道rtpmap:96 H264/90000:PT=96,H264 编码,时间戳频率 90000fmtp:96:H264 私有参数packetization-mode=1:模式 1,FU‑A 分片模式(RTSP/RTP 最常用)profile‑level‑id:H264 profile + levelsprop‑parameter‑sets=base64(SPS),base64(PPS)→SPS、PPS 做 base64 编码写在这里
关键点:
sprop‑parameter‑sets就是把 SPS、PPS 放到 SDP 里面,播放器拿到 SDP 就提前拿到解码参数,不需要等 I 帧。
2、SPS / PPS 是什么(H.264)
H.264 码流由 NALU(NAL 单元)组成。
SPS、PPS 属于参数集 NALU,不是图像帧,是解码器初始化必须的元数据。
| 名称 | 全称 | 作用 | NALU 类型 |
|---|---|---|---|
| SPS | Sequence Parameter Set 序列参数集 | 序列全局信息:分辨率、profile、level、帧率、参考帧数量 | NALU type=7 |
| PPS | Picture Parameter Set 图像参数集 | 图像相关:熵编码模式、片组参数,依附 SPS | NALU type=8 |
⚠️解码器没有 SPS/PPS,绝对不能解码 I 帧 (IDR 帧)。
现象:收到 I 帧,但是画面黑屏、花屏、绿屏,没有报错但是出不来图像。
NALU 类型简单记忆
7:SPS
8:PPS
5:IDR‑I 帧(关键帧)
1:普通 P 帧、非 I 帧
两种携带 SPS/PPS 的方式(RTSP/RTP 场景)
方式 1:SDP 内携带 sprop‑parameter‑sets(最理想,RTSP 标准)
SDP 的fmtp字段把 SPS、PPS 做 base64 编码写入。
播放器收到 DESCRIBE 应答,解析 SDP 就拿到 SPS/PPS,解码器直接初始化。
ZLMediaKit addStreamProxy 拉流,RTSP 服务端输出 SDP 带 sprop,体验最好,首帧秒出。
方式 2:SDP 不带 sprop‑parameter‑sets,SPS/PPS 随码流在 IDR 前面发送
SDP 里没有 sprop‑parameter‑sets;SPS+PPS NALU 放在每一个 IDR (I 帧) 前面,作为码流一部分随 RTP 包传输。
播放器必须等到第一个 IDR 帧到来,解析前面 SPS/PPS,才能开始解码。
现象:VLC 打开流要等 1~2 秒才出画面;seek、断流重连容易黑屏。
很多嵌入式设备 RTSP 服务端容易踩坑:SDP 不填 sprop‑parameter‑sets,只在码流里面输出 SPS/PPS。
裸 H264 AnnexB 格式(文件 / 本地码流)
起始码0x00 00 01分割 NALU:
00 00 01 67 ... (SPS NALU) 00 00 01 68 ... (PPS NALU) 00 00 01 65 ... (IDR I帧)RTP 传输 RTSP 时,AnnexB 起始码会被去掉,用 RTP 封装;SPS/PPS 可以放到 SDP (sprop) 或者穿插在 RTP 负载中。
3、sprop‑parameter‑sets 解析示例
sprop‑parameter‑sets=Z0IAKeKQFAe2AtwEBAaQeJEA8AAIB+BA1KGA,g48IAA==逗号分隔:
逗号前 base64 串 →SPS 原始二进制
逗号后 base64 串 →PPS 原始二进制
解码流程:
base64 解码 → 得到 SPS NALU 原始字节(带 NALU 头 0x67)、PPS NALU 字节(0x68)。
注意:sprop 里面的 SPS/PPS不带 AnnexB 起始码 00 00 01,只是纯 NALU 负载。
常见故障现象与 SPS/PPS 关系
| 现象 | 根因 |
|---|---|
| VLC 打开 RTSP 流,音频正常,视频黑屏,过 1‑2 秒出图 | SDP 没有 sprop‑parameter‑sets,要等码流中 SPS/PPS+IDR 到来 |
| 首帧花屏、马赛克,后面正常 | 拿到 SPS/PPS 之前收到 I 帧,解码器参数错误 |
| ZLMediaKit WebRTC 播放失败,报错 no sps/pps | WebRTC 强制要求 SDP 携带 sprop‑parameter‑sets;仅码流携带 SPS/PPS 不行 |
| 网络抖动断流恢复后黑屏 | 断流丢失 SPS/PPS,后续 I 帧没有配套参数集 |
WebRTC 硬性要求:SDP 必须携带 sprop‑parameter‑sets,只在 RTP 流里送 SPS/PPS 是无法播放 WebRTC。这是 ZLM 非常高频坑。
4、RTSP 完整交互看 SDP/SPS/PPS 时序(摄像机设备 A <-> ZLMediaKit addStreamProxy)
ZLM PlayerProxy(RTSP 客户端)发送 DESCRIBE
A 设备 RTSP 服务端回复 SDP。
✅理想:SDP fmtp 携带
sprop‑parameter‑sets,ZLM 直接解析 SPS/PPS。❌缺陷:SDP 无 sprop,ZLM 只能等待 RTP 码流中出现 SPS/PPS NALU。
ZLM 发送 SETUP,建立 RTP 通道(rtsp_type=0 TCP interleaved)
ZLM 发送 PLAY
A 设备输出 RTP 包:SPS/PPS + IDR‑I + P 帧
如果 SDP 不带 sprop,ZLM 内部会缓存,等待码流中 SPS/PPS 到来,才生成完整 MediaSource 对外分发。此时 VLC 连接 ZLM,也要等待 IDR 帧。
5、A 端设备 RTSP 服务端输出建议方法
A 端(设备端)RTSP 服务端:
SDP 的 fmtp 必须填充
sprop‑parameter‑sets(SPS,PPS base64),不要只放在码流里。packetization‑mode=1(FU‑A 分片,大 I 帧分片传输,RTSP over TCP 标准)。IDR 帧前面,码流中依然重复输出 SPS+PPS(双重保险,网络丢包容错)。
ZLMediaKit addStreamProxy 相关参数影响 SDP 处理
directProxy=1(默认):透传源 SDP,源 SDP 不带 sprop,则 ZLM 对外 SDP 也不带 sprop;WebRTC 无法播放。directProxy=0:ZLM 解封装,从 RTP 码流提取 SPS/PPS,重新生成 SDP,补全 sprop‑parameter‑sets,兼容性提升,消耗少量 CPU。
curl 可以追加参数
&directProxy=0
curl"http://127.0.0.1:80/index/api/addStreamProxy?secret=$SECRET&vhost=__defaultVhost__&app=live&stream=ak3918&url=rtsp%3A%2F%2F127.0.0.1%3A8554%2Fvs0&rtsp_type=0&directProxy=0"6、快速排错手段
tcpdump/wireshark 抓 RTSP 报文,查看 DESCRIBE 应答 SDP,看
sprop‑parameter‑sets是否存在。ZLMediaKit 日志搜索
sps、pps、h264,观察是否成功解析到参数集。如果 SDP 没有 sprop,打开 directProxy=0,ZLM 会从 RTP 码流自动提取 SPS/PPS 重建 SDP。
7、C 代码:原始 SPS/PPS NALU 生成 sprop‑parameter‑sets 字符串
说明:
输入:去掉 AnnexB 起始码 (0x000001) 的纯 NALU 二进制(SPS NAL、PPS NAL,包含 NALU 头字节 0x67 / 0x68)
输出:
sprop‑parameter‑sets=xxxx,yyyy字符串,可直接填入 SDP 的 fmtp 字段依赖:需要实现 base64 编码函数;嵌入式可使用开源 base64 实现。
注意:不要带入 00 00 01 起始码,sprop 标准只接受 NALU 净荷。
#include<stdio.h>#include<string.h>#include<stdlib.h>/** * @brief base64编码函数,输入二进制,输出base64字符串(外部释放内存) */char*base64_encode(constunsignedchar*data,size_tlen);/** * @brief 生成SDP fmtp中的 sprop‑parameter‑sets 字段字符串 * @param sps_buf SPS NALU(无000001起始码,含nal头0x67) * @param sps_len SPS字节长度 * @param pps_buf PPS NALU(无000001起始码,含nal头0x68) * @param pps_len PPS字节长度 * @return 动态分配字符串,示例:"sprop‑parameter‑sets=Z0IA...,g48IAA=="; 返回NULL失败,调用者free释放 */char*h264_gen_sprop_parameter_sets(constuint8_t*sps_buf,size_tsps_len,constuint8_t*pps_buf,size_tpps_len){if(!sps_buf||sps_len==0||!pps_buf||pps_len==0){returnNULL;}// SPS base64char*sps_b64=base64_encode(sps_buf,sps_len);if(!sps_b64)returnNULL;// PPS base64char*pps_b64=base64_encode(pps_buf,pps_len);if(!pps_b64){free(sps_b64);returnNULL;}// sprop‑parameter‑sets=sps_b64,pps_b64size_tout_len=strlen("sprop‑parameter‑sets=")+strlen(sps_b64)+1+strlen(pps_b64)+1;char*out_str=(char*)malloc(out_len);if(!out_str){free(sps_b64);free(pps_b64);returnNULL;}snprintf(out_str,out_len,"sprop‑parameter‑sets=%s,%s",sps_b64,pps_b64);free(sps_b64);free(pps_b64);returnout_str;}/* --------------使用示例-------------- */voiddemo_usage(void){// 示例:sps_nal、pps_nal 为去除00 00 01起始码的NALU原始数据uint8_tsps_nal[]={0x67,0x42,0x00,0x1E,0x8D,0x8D,0x40,0xA0};uint8_tpps_nal[]={0x68,0xCE,0x3C,0x80};char*sprop_str=h264_gen_sprop_parameter_sets(sps_nal,sizeof(sps_nal),pps_nal,sizeof(pps_nal));if(sprop_str){printf("%sn",sprop_str);/* * 拼接进SDP fmtp行: * a=fmtp:96 packetization-mode=1;profile-level-id=42001E;%srn */free(sprop_str);}}配套:从 AnnexB 码流提取 SPS/PPS NALU(去除起始码)
从 H264 AnnexB 流 (带
00 00 01) 中剥离起始码,拿到纯 NALU,作为上面函数输入。重点:传给
h264_gen_sprop_parameter_sets的不能包含 0x000001 起始码。
/** * @brief 简单查找AnnexB起始码 0x00 00 01 */intfind_annexb_startcode(constuint8_t*buf,size_tlen,size_t*pos){for(size_ti=0;i+3<=len;i++){if(buf[i]==0&&buf[i+1]==0&&buf[i+2]==1){*pos=i+3;// 跳过3字节起始码,返回NALU有效载荷起始偏移return1;}}return0;}profile‑level‑id 生成补充
SDP fmtp 还需要profile‑level‑id=XXYYZZ:
取自 SPS NALU 的第 1、2、3 字节:
byte0:profile_idc
byte1:constraint_set_flags
byte2:level_idc
拼接为十六进制 6 位字符串,例如42001E。
集成进 RTSP SDP 输出示例片段
a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop‑parameter‑sets=Z0IA...,g48IAA==重要注意事项
sprop‑parameter‑sets 格式:
sps_base64,pps_base64,逗号分隔,无空格。输入必须是 NALU 本体,剔除 AnnexB 起始码 00 00 01,否则 base64 之后参数集错误,解码器无法初始化。
编码输出的字符串直接填入 SDP,不要额外 urlencode;SDP 本身不需要 URL 编码,URL 编码只用于 HTTP API。
H.265 HEVC:需要同时处理 VPS+SPS+PPS,对应字段:
sprop‑vps‑sets;sprop‑sps‑sets;sprop‑pps‑sets。
补充 H.265 (HEVC)
H265 对应:SPS (type=33), PPS (type=34), VPS (type=32);
SDP 字段为sprop‑vps‑sets,sprop‑sps‑sets,sprop‑pps‑sets。
wireshark 常用过滤表达式:
# 过滤RTSP DESCRIBE应答,查看SDP rtsp && rtsp.method == "DESCRIBE" # 过滤H264 SPS NALU h264.nal_unit_type == 7 # 过滤H264 PPS NALU h264.nal_unit_type == 8