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 编码,时间戳频率 90000 -
fmtp:96:H264 私有参数-
packetization-mode=1:模式 1,FU‑A 分片模式(RTSP/RTP 最常用) -
profile‑level‑id:H264 profile + level -
sprop‑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(const unsigned char *data, size_t len);
/**
* @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(const uint8_t *sps_buf, size_t sps_len,
const uint8_t *pps_buf, size_t pps_len)
{
if(!sps_buf || sps_len == 0 || !pps_buf || pps_len == 0){
return NULL;
}
// SPS base64
char *sps_b64 = base64_encode(sps_buf, sps_len);
if(!sps_b64) return NULL;
// PPS base64
char *pps_b64 = base64_encode(pps_buf, pps_len);
if(!pps_b64){
free(sps_b64);
return NULL;
}
// sprop‑parameter‑sets=sps_b64,pps_b64
size_t out_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);
return NULL;
}
snprintf(out_str, out_len, "sprop‑parameter‑sets=%s,%s", sps_b64, pps_b64);
free(sps_b64);
free(pps_b64);
return out_str;
}
/* --------------使用示例-------------- */
void demo_usage(void)
{
// 示例:sps_nal、pps_nal 为去除00 00 01起始码的NALU原始数据
uint8_t sps_nal[] = {0x67,0x42,0x00,0x1E,0x8D,0x8D,0x40,0xA0};
uint8_t pps_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
*/
int find_annexb_startcode(const uint8_t *buf, size_t len, size_t *pos)
{
for(size_t i = 0; i + 3 <= len; i++){
if(buf[i]==0 && buf[i+1]==0 && buf[i+2]==1){
*pos = i + 3; // 跳过3字节起始码,返回NALU有效载荷起始偏移
return 1;
}
}
return 0;
}
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
3116

被折叠的 条评论
为什么被折叠?



