网络摄像机首次播放音视频延时出图像踩坑记录

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 交互片段:

  1. VLC/ZLM(PlayerProxy) → DESCRIBE rtsp://xxx/vs0 RTSP/1.0

  2. 设备 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 类型
SPSSequence Parameter Set 序列参数集序列全局信息:分辨率、profile、level、帧率、参考帧数量NALU type=7
PPSPicture Parameter Set 图像参数集图像相关:熵编码模式、片组参数,依附 SPSNALU 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==

逗号分隔:

  1. 逗号前 base64 串 → SPS 原始二进制

  2. 逗号后 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/ppsWebRTC 强制要求 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)

  1. ZLM PlayerProxy(RTSP 客户端)发送 DESCRIBE

  2. A 设备 RTSP 服务端回复 SDP。

    • ✅理想:SDP fmtp 携带sprop‑parameter‑sets,ZLM 直接解析 SPS/PPS。

    • ❌缺陷:SDP 无 sprop,ZLM 只能等待 RTP 码流中出现 SPS/PPS NALU。

  3. ZLM 发送 SETUP,建立 RTP 通道(rtsp_type=0 TCP interleaved)

  4. ZLM 发送 PLAY

  5. A 设备输出 RTP 包:SPS/PPS + IDR‑I + P 帧

如果 SDP 不带 sprop,ZLM 内部会缓存,等待码流中 SPS/PPS 到来,才生成完整 MediaSource 对外分发。此时 VLC 连接 ZLM,也要等待 IDR 帧。

5、A 端设备 RTSP 服务端输出建议方法

A 端(设备端)RTSP 服务端:

  1. SDP 的 fmtp 必须填充 sprop‑parameter‑sets(SPS,PPS base64),不要只放在码流里。

  2. packetization‑mode=1(FU‑A 分片,大 I 帧分片传输,RTSP over TCP 标准)。

  3. 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、快速排错手段

  1. tcpdump/wireshark 抓 RTSP 报文,查看 DESCRIBE 应答 SDP,看sprop‑parameter‑sets是否存在。

  2. ZLMediaKit 日志搜索 spsppsh264,观察是否成功解析到参数集。

  3. 如果 SDP 没有 sprop,打开 directProxy=0,ZLM 会从 RTP 码流自动提取 SPS/PPS 重建 SDP。

7、C 代码:原始 SPS/PPS NALU 生成 sprop‑parameter‑sets 字符串

说明:

  1. 输入:去掉 AnnexB 起始码 (0x000001) 的纯 NALU 二进制(SPS NAL、PPS NAL,包含 NALU 头字节 0x67 / 0x68)

  2. 输出:sprop‑parameter‑sets=xxxx,yyyy 字符串,可直接填入 SDP 的 fmtp 字段

  3. 依赖:需要实现 base64 编码函数;嵌入式可使用开源 base64 实现。

  4. 注意:不要带入 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==

重要注意事项

  1. sprop‑parameter‑sets 格式:sps_base64,pps_base64逗号分隔,无空格

  2. 输入必须是 NALU 本体,剔除 AnnexB 起始码 00 00 01,否则 base64 之后参数集错误,解码器无法初始化。

  3. 编码输出的字符串直接填入 SDP,不要额外 urlencode;SDP 本身不需要 URL 编码,URL 编码只用于 HTTP API。

  4. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RFCEO

你的鼓励是我最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值