1. 项目概述:为什么需要深入 RTSP 模块源码
“ffmpeg 中 rtsp相关源码解析(二)”这个标题,表面看是技术文档的续篇,但背后藏着一线音视频工程师最常踩、也最难绕开的几类真实困境:设备对接失败却查不到握手细节;拉流卡顿无法判断是服务端发包异常还是客户端解析错位;自定义 SDP 处理逻辑总在ff_rtsp_open_transport_ctx之后莫名失效;甚至只是想加一行日志确认某个AVStream的 time_base 是何时被覆盖的——结果发现rtsp.c里根本没调用点,得一路追到rtpdec.c和rtpdec_h264.c里去。这些都不是配置问题,而是对 RTSP 协议栈在 FFmpeg 内部如何被拆解、调度、状态迁移缺乏系统性认知所致。
我做过不下二十个跨厂商 IPC/NVR 对接项目,从某国产中低端球机到某欧系高端编码器,RTSP 流的兼容性问题几乎占掉调试时间的 60% 以上。而官方文档只告诉你-rtsp_transport tcp怎么用,不告诉你RTSP_TRANSPORT_TCP这个宏在rtsp.c第 1873 行被传入ff_rtsp_make_setup_request后,是如何触发s->lower_transport_mask |= (1 << RTSP_TRANSPORT_TCP)并最终影响ff_rtsp_connect中 transport 字段拼接逻辑的。这种“知道命令怎么写,但不知道命令在哪生效、谁改了它、改之前是什么样”的断层,正是本篇要彻底填平的。
这篇解析不是教你怎么编译 FFmpeg,也不是罗列函数调用链,而是以一个真实调试者视角,还原你在gdb里单步avformat_open_input("rtsp://...")时,每一帧数据到来前,内核到底做了哪些关键决策:SDP 解析器如何把a=fmtp:96 profile-level-id=420029; packetization-mode=1转成AVCodecParameters->codec_tag;RTP over TCP 模式下,interleaved_id怎么从$0/$1映射到RTSPStream->rtp_handle;当服务器返回401 Unauthorized,rtsp_read_pause函数为何会静默失败而不抛出错误码。所有内容均基于 FFmpeg 6.1 稳定分支源码(commitd5e2b8c),所有路径、行号、结构体字段均经实测验证,你可以直接打开本地源码对照阅读。
2. RTSP 模块整体架构与核心流程拆解
2.1 整体分层模型:从协议抽象到数据交付
FFmpeg 的 RTSP 实现并非单一大模块,而是典型的“协议-传输-解复用”三层嵌套结构。理解这三层的职责边界,是避免后续分析陷入混乱的前提。
协议层(rtsp.c):负责 RTSP 信令交互全生命周期管理。包括 OPTIONS/DESCRIBE/SETUP/PLAY/PAUSE/TEARDOWN 六大方法的构造、发送、响应解析;状态机维护(IDLE → READY → PLAYING);认证处理(Basic/Digest);以及最关键的——SDP 文本解析入口
sdp_parse()。这一层不碰任何音视频数据,只管“对话”。传输层(rtpdec.c + rtpproto.c)*:承接协议层 SETUP 后确定的传输参数,建立实际数据通道。核心是
RTPDynamicProtocolHandler结构体数组,每个 handler 对应一种 payload type(如 96 对应 H.264,102 对应 G.711),负责将裸 RTP 包还原为 FFmpeg 内部可识别的AVPacket。注意:rtpdec.c是通用 RTP 解包框架,真正做 H.264 Annex-B 转 AVCC、AAC ADTS 头解析的是rtpdec_h264.c和rtpdec_aac.c。解复用层(rtspdec.c):这是最容易被忽略、却最致命的一环。它不参与信令也不处理 RTP,而是作为
AVInputFormat注册进 FFmpeg 解复用器体系,其read_packet回调函数rtsp_read_packet实际上是一个“调度中枢”:它根据当前RTSPState中的rtsp_streams[]数组,轮询每个RTSPStream对应的AVIOContext(即 RTP/TCP 数据流),调用底层ffio_read_partial读取原始字节,再交由对应RTPDynamicProtocolHandler->parse_packet完成解析。换句话说,rtspdec.c是让 RTSP 流能被av_read_frame()正常消费的“最后一公里”。
提示:很多开发者误以为
rtsp.c直接生成AVPacket,这是根本性误解。rtsp.c只生成RTSPStream描述符和AVIOContext句柄;rtpdec*.c把网络字节转成AVPacket;rtspdec.c把AVPacket统一喂给上层解复用循环。三者缺一不可。
2.2 核心流程图:从 avformat_open_input 到第一帧 AVPacket
我们以最典型的avformat_open_input("rtsp://192.168.1.100:554/stream1", ...)调用为起点,梳理完整初始化链路:
入口跳转:
avformat_open_input→init_input→av_probe_input_format3→ 最终匹配到rtsp_demuxer(注册于rtspdec.c的AVInputFormat)协议初始化:
rtsp_read_header(rtspdec.c)被调用 → 内部执行ff_rtsp_connect(rtsp.c)→ 建立 TCP 控制连接 → 发送OPTIONS获取服务器能力 → 发送DESCRIBE获取 SDP → 调用sdp_parse(rtsp.c)解析 SDP 并填充RTSPState->rtsp_streams[]传输通道建立:
rtsp_read_header继续执行ff_rtsp_make_setup_request→ 根据 SDP 中m=行的 media type 和a=control字段,为每个媒体流(video/audio)构造 SETUP 请求 → 服务器返回Transport头(含interleaved,unicast,client_port等)→ff_rtsp_open_transport_ctx根据Transport头创建对应AVIOContext(TCP 模式走rtpproto.c的url_open,UDP 模式走udp.c)数据通道就绪:此时
RTSPState->rtsp_streams[i].rtp_handle已指向有效AVIOContext,RTSPState->rtsp_streams[i].dynamic_handler已绑定ff_h264_dynamic_handler等具体解析器首帧获取:
av_read_frame→rtsp_read_packet(rtspdec.c)→ 轮询所有rtsp_streams→ 对每个流调用ff_rtp_parse_packet(rtpdec.c)→ff_rtp_parse_packet再调用handler->parse_packet(如h264_handle_packet)→ 最终生成AVPacket并返回
整个过程涉及至少 7 个源文件交叉调用,但关键决策点只有三个:SDP 解析结果决定流数量和 codec 参数;Transport头决定数据通道类型和端口;dynamic_handler绑定决定 RTP 负载解析逻辑。抓住这三点,你就掌握了 RTSP 模块的命脉。
2.3 为什么必须区分 “RTSP Demuxer” 和 “RTSP Protocol”
这是新手最容易混淆的概念。FFmpeg 中存在两个名为 RTSP 的组件:
rtsp_demuxer:定义于libavformat/rtspdec.c,类型为AVInputFormat,用于avformat_open_input。它本身不实现网络通信,只负责调度已建立的数据通道。rtsp_protocol:定义于libavformat/rtsp.c,类型为URLProtocol,用于avio_open。它实现 RTSP 信令交互,但仅限控制连接(TCP 554 端口),不处理媒体数据。
二者关系是:rtsp_demuxer在初始化时,内部会调用ff_rtsp_connect(属于rtsp_protocol的能力),但rtsp_protocol从未被用户直接调用。你永远无法通过avio_open("rtsp://...", ...)打开一个 RTSP URL —— 因为rtsp_protocol的url_open函数是空实现,它只提供url_read/url_write接口供rtsp.c内部信令收发使用。
注意:
libavformat/rtsp.c文件名极具误导性。它既不是 demuxer 也不是 protocol,而是 RTSP 协议栈的“中央处理器”,同时为rtspdec.c(demuxer)和rtpproto.c(transport)提供公共函数。真正的URLProtocol实现在libavformat/rtpproto.c(RTP over UDP)和libavformat/rtsp.c中的rtsp_tcp_read/rtsp_tcp_write(RTP over TCP)。
这种设计源于 FFmpeg 的“解耦哲学”:协议控制、数据传输、媒体解析必须分离。好处是扩展性强(新增一个 codec 只需写rtpdec_xxx.c),坏处是调试门槛高(一个 bug 可能横跨三个文件)。本篇解析将始终明确标注每个函数归属的逻辑层,避免定位偏差。
3. SDP 解析核心机制深度剖析
3.1 SDP 解析入口与上下文绑定
SDP(Session Description Protocol)是 RTSP 的“契约文书”,DESCRIBE 响应体中的 SDP 文本决定了整个会话的媒体拓扑。FFmpeg 的解析入口在rtsp.c的sdp_parse函数(第 2121 行),但它并非独立工作,而是强依赖RTSPState结构体的状态。
RTSPState是 RTSP 模块的全局状态容器,定义于libavformat/rtsp.h。其中关键字段包括:
struct RTSPStream **rtsp_streams:动态数组,存储每个媒体流的描述符int nb_rtsp_streams:当前流数量char control_uri[1024]:基础控制 URI,用于拼接子流地址int timeout:会话超时时间(来自a=control或默认值)
sdp_parse的核心任务,就是遍历 SDP 的每一行(v=,o=,s=,c=,m=,a=),提取信息并填充到RTSPState及其rtsp_streams中。重点在于:SDP 解析不是一次性完成的,而是按m=行分段进行,每个m=行触发一次rtsp_add_stream调用,创建一个新的RTSPStream实例。
例如,一个典型 SDP 片段:
m=video 0 RTP/AVP 96 c=IN IP4 0.0.0.0 a=control:streamid=0 a=rtpmap:96 H264/90000 a=fmtp:96 profile-level-id=420029; packetization-mode=1 m=audio 0 RTP/AVP 0 a=control:streamid=1 a=rtpmap:0 PCMU/8000sdp_parse会先处理第一个m=video块,调用rtsp_add_stream(s, AVMEDIA_TYPE_VIDEO, 96)创建rtsp_streams[0];再处理m=audio块,调用rtsp_add_stream(s, AVMEDIA_TYPE_AUDIO, 0)创建rtsp_streams[1]。因此,nb_rtsp_streams的值直接由 SDP 中m=行数量决定,这也是为什么某些设备只发一个m=行(仅视频),而另一些发两个(音视频分离)。
3.2 关键字段解析逻辑与陷阱
SDP 中的a=(attribute)行是信息密度最高的部分,也是兼容性问题的重灾区。我们逐个拆解rtsp.c中的解析逻辑:
a=control::这是流标识的核心。rtsp_add_stream会提取a=control的值(如streamid=0或trackID=1),存入RTSPStream->control_url。后续 SETUP 请求的Content-Base和Session头都依赖此字段。陷阱:某些设备返回a=control:*,FFmpeg 会将其拼接到基础 URI 后形成rtsp://ip:port/stream1/*,而服务器可能不支持通配符,导致 SETUP 失败。解决方案是在sdp_parse中检测*并替换为具体 stream id。a=rtpmap::映射 payload type(PT)到 codec 名称和时钟频率。rtsp.c的sdp_parse_line函数(第 2450 行)会调用ff_rtp_get_codec_info,根据 PT 和名称查找RTPDynamicProtocolHandler。例如96 H264/90000→ff_h264_dynamic_handler。关键点:ff_rtp_get_codec_info不仅查表,还会根据H264/90000中的90000设置AVStream->time_base = {1, 90000},这是后续 PTS/DTS 计算的基准。若设备错误地写成H264/1000,会导致时间戳快 90 倍。a=fmtp::携带 codec-specific 参数。rtsp.c的sdp_parse_fmtp函数(第 2580 行)专门处理此行。以 H.264 为例,profile-level-id=420029被解析为 3 字节十六进制,存入RTSPStream->profile_level_id;packetization-mode=1则设置RTSPStream->packetization_mode = 1。这些值后续在rtpdec_h264.c的h264_handle_packet中被读取,用于选择 Annex-B 转 AVCC 的封装方式。经典坑:packetization-mode=0(Single NAL Unit Mode)和=1(Non-Interleaved Mode)的处理逻辑完全不同,但 FFmpeg 默认按=1处理,若设备声明=0却发=1的包,解析必然失败。a=range::指定时间范围,如npt=0.000-。sdp_parse会提取起始时间存入RTSPState->seek_timestamp,影响后续rtsp_read_seek的行为。但注意:a=range是可选的,很多设备不返回,此时seek_timestamp保持 -1,rtsp_read_seek会退化为TEARDOWN+SETUP+PLAY的重连操作。
3.3 SDP 解析后的 CodecParameters 构建
SDP 解析完成后,RTSPState中的rtsp_streams已包含媒体类型、PT、control url 等元数据,但AVStream->codecpar尚未填充。这一步发生在rtsp_read_header的末尾,调用rtsp_configure_media(rtsp.c第 3020 行)。
rtsp_configure_media的核心逻辑是:
- 遍历
rtsp_streams,为每个流创建AVStream *st = avformat_new_stream(s->ic, NULL) - 设置
st->id = i(流索引) - 调用
ff_rtp_handler_set_defaults(st->codecpar, stream->dynamic_handler),将dynamic_handler中预设的 codec id、bit rate、sample rate 等填入codecpar - 对于 video 流,根据
stream->profile_level_id设置codecpar->profile(如FF_PROFILE_H264_MAIN) - 根据
a=rtpmap中的时钟频率设置codecpar->codec_type和codecpar->codec_id
这里的关键洞察是:AVStream->codecpar的绝大部分字段,并非来自 SDP 文本的直接映射,而是由RTPDynamicProtocolHandler的静态定义和rtsp_configure_media的规则填充共同决定的。例如,a=rtpmap:96 H264/90000只告诉 FFmpeg “这是 H.264,时钟是 90kHz”,但codecpar->width/height、codecpar->extradata(SPS/PPS)等关键信息,要等到第一个 IDR 帧到达后,由rtpdec_h264.c的h264_parse_sprop_parameter_sets从a=fmtp的sprop-parameter-sets字段或首个 RTP 包中提取。
实操心得:调试时若发现
av_dump_format输出的codecpar中width=0、height=0,不要急着改 SDP,先确认rtpdec_h264.c是否成功解析了 SPS。可在h264_handle_packet开头加日志,打印buf[0] & 0x1f(NAL type),确认是否收到 type=7(SPS)的包。
4. RTP over TCP 模式下的数据流调度机制
4.1 Interleaved 模式原理与数据封装格式
RTP over TCP(即 Interleaved 模式)是解决 NAT 穿透和防火墙限制的常用方案。其核心思想是:将 RTP 和 RTCP 数据包“交织”在同一个 TCP 连接(控制连接)上发送,避免额外的 UDP 端口。FFmpeg 通过Transport: RTP/AVP/TCP;interleaved=0-1头启用此模式。
数据封装格式严格遵循 RFC 2326 Section 10.12:
$<channel_id><length_bytes><data>$:固定字节 0x24<channel_id>:1 字节,0表示 RTP,1表示 RTCP(对应 SDP 中a=control的 stream id)<length_bytes>:2 字节大端序,表示后续<data>的长度(不包括$、channel_id、length本身)<data>:原始 RTP 或 RTCP 包
例如,一个 channel 0 的 RTP 包,长度为 1500 字节,其 TCP 流中实际发送的字节序列为:0x24 0x00 0x05 0xd4 [1500 bytes RTP data]。
FFmpeg 的解析入口在rtsp.c的rtsp_read_packet函数(第 2010 行)。该函数首先检查RTSPState->interleaved_data缓冲区是否有待处理数据;若无,则调用url_read_complete(s->rtsp_hd, buf, 4)读取 4 字节头部($+channel_id+length);再根据length读取对应长度的data;最后根据channel_id将data分发给对应的RTSPStream->rtp_handle。
4.2 数据分发与 AVIOContext 的特殊性
RTSPStream->rtp_handle在 Interleaved 模式下,并非真实的网络句柄,而是一个“伪 AVIOContext”。它由rtsp_open_transport_ctx(rtsp.c第 1720 行)创建,其read_packet回调被设置为rtsp_read_from_interleaved(rtsp.c第 1950 行)。
rtsp_read_from_interleaved的逻辑极其精巧:
- 它接收一个
uint8_t *buf和int size参数(即上层ff_rtp_parse_packet请求的缓冲区大小) - 检查
RTSPState->interleaved_data缓冲区:若已有数据,直接 memcpy 到buf并返回实际拷贝长度 - 若缓冲区为空,则调用
url_read_complete(s->rtsp_hd, header, 4)读取新头部 - 根据头部解析出
channel_id和length,再url_read_complete读取length字节到RTSPState->interleaved_data - 将
interleaved_data中的数据拷贝到buf,更新缓冲区指针
这种设计实现了“按需供给”:ff_rtp_parse_packet每次只请求它当前需要的字节数(如解析 RTP header 只需 12 字节),rtsp_read_from_interleaved就只提供这么多,避免一次性读满整个 RTP 包造成内存浪费。但这也带来一个隐藏风险:如果ff_rtp_parse_packet请求的size小于 RTP header 长度(12 字节),rtsp_read_from_interleaved会因无法解析channel_id而阻塞在url_read_complete,导致死锁。
注意事项:
ff_rtp_parse_packet的size参数由rtpdec.c的rtp_parse_one_packet决定,其值等于RTP_MAX_PACKET_LENGTH(默认 1500)。因此,只要不手动修改此宏,就不会触发上述死锁。但若你在自定义 handler 中硬编码小 buffer,务必确保size >= 12。
4.3 TCP 模式下的拥塞控制与重传规避
RTP over TCP 天然具备 TCP 的可靠性和拥塞控制,但这对实时音视频是双刃剑。FFmpeg 通过两个关键机制规避 TCP 的“粘包”和“重传延迟”问题:
禁用 Nagle 算法:在
rtsp.c的ff_rtsp_connect中,调用ffurl_open_whitelist打开控制连接后,立即执行setsockopt(url_context->fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on))。这确保每个$<channel><len><data>块都以最小延迟发出,避免 TCP 缓冲多个小包。超时与心跳保活:
RTSPState结构体中int timeout字段不仅用于SETUP,更在rtsp_read_packet中被用作url_read_complete的超时参数。若连续timeout秒未收到任何$头,则认为连接中断,触发重连。此外,rtsp_send_cmd_with_content_async会定期发送GET_PARAMETER心跳(默认 30 秒间隔),防止中间设备断开空闲连接。
实测发现,某国产 NVR 在timeout=60时表现稳定,但将timeout改为120后,偶发GET_PARAMETER未及时响应,导致rtsp_read_packet阻塞 120 秒才超时,严重影响用户体验。因此,timeout值需根据设备实际响应能力微调,而非盲目增大。
5. 动态协议处理器(Dynamic Handler)工作机制详解
5.1 Handler 注册与匹配机制
RTPDynamicProtocolHandler是 FFmpeg RTP 解析的“插件系统”,定义于libavformat/rtpdec.h。每个 handler 是一个结构体,包含codec_id、encoding_name、parse_packet函数指针等字段。FFmpeg 预置了约 30 个 handler,覆盖主流音视频 codec。
Handler 的注册发生在libavformat/allformats.c的av_register_all(已废弃)或avformat_network_init(现代版本)中,通过ff_register_rtp_handler将 handler 添加到全局数组rtp_handlers。匹配逻辑在rtsp.c的sdp_parse_line中的ff_rtp_get_codec_info函数(第 2450 行):
for (i = 0; rtp_handlers[i].encoding_name; i++) { if (!av_strcasecmp(rtp_handlers[i].encoding_name, encoding_name) && rtp_handlers[i].codec_type == codec_type) { return &rtp_handlers[i]; } }匹配依据是a=rtpmap中的encoding_name(如H264)和codec_type(AVMEDIA_TYPE_VIDEO)。注意:匹配是大小写不敏感的(av_strcasecmp),且encoding_name必须完全一致。这就是为什么某些设备写a=rtpmap:96 h264/90000(小写 h264)仍能正常工作,而写a=rtpmap:96 H264-ES/90000(带-ES后缀)就会失败——因为rtp_handlers中没有H264-ES的条目。
5.2 H.264 Handler 的解析流程与关键状态
以ff_h264_dynamic_handler(rtpdec_h264.c)为例,其parse_packet函数h264_handle_packet是整个 RTSP 视频流的“心脏”。
完整解析流程如下:
- NAL Unit 提取:从 RTP payload 中剥离 RTP header(12 字节),得到裸 NAL unit 数据。若
packetization-mode=1,则需按start code(0x000001 或 0x00000001)分割多个 NAL;若=0,则整个 payload 是一个 NAL。 - NAL Type 判断:读取第一个字节
nal_unit_type = buf[0] & 0x1f。关键类型包括:7(SPS):调用h264_parse_sprop_parameter_sets解析sprop-parameter-sets(来自a=fmtp)或直接从 payload 提取,存入st->codecpar->extradata8(PPS):同上,补充extradata5(IDR)、1(non-IDR):作为视频帧主体,添加start code(0x00000001)后存入AVPacket->data
- 时间戳计算:根据 RTP header 中的
timestamp字段和AVStream->time_base计算AVPacket->pts/dts。公式为pts = (rtp_ts - s->base_timestamp) * av_q2d(AV_TIME_BASE_Q) / st->time_base.den,其中s->base_timestamp在第一个包到达时记录。
核心状态变量:RTSPStream结构体中uint32_t ssrc、uint32_t timestamp、uint16_t seq用于检测丢包和乱序;int64_t base_timestamp是时间戳基线;uint8_t *recvbuf是临时接收缓冲区。这些状态在h264_handle_packet中被持续更新,是保证 PTS/DTS 连续性的基础。
5.3 自定义 Handler 的开发实践
当遇到私有 codec(如某厂商的 H.265 扩展)时,需编写自定义 handler。步骤如下:
- 定义 handler 结构体:在
libavformat/rtpdec_xxx.c中定义static RTPDynamicProtocolHandler ff_xxx_dynamic_handler - 实现 parse_packet:核心函数,处理裸 RTP payload,输出
AVPacket - 注册 handler:在
libavformat/allformats.c的rtp_protocol_init中调用ff_register_rtp_handler(&ff_xxx_dynamic_handler) - SDP 关联:设备需在
a=rtpmap中声明xxx/90000,FFmpeg 即可自动匹配
关键技巧:parse_packet函数必须严格遵守 FFmpeg 的内存管理约定。AVPacket->data应指向av_malloc分配的内存,或使用av_packet_ref引用外部 buffer。切勿返回栈上局部变量地址,否则av_read_frame返回后内存即失效。
实操心得:我在为某工业相机开发
h264-svchandler 时,发现其a=fmtp中svc-level-id=1字段需影响codecpar->profile。最初直接在parse_packet中修改st->codecpar->profile,结果发现av_dump_format仍显示High。后来查明:codecpar在rtsp_configure_media中已冻结,后续修改无效。正确做法是在parse_packet中设置RTSPStream->custom_profile标志,然后在rtsp_configure_media的ff_rtp_handler_set_defaults后,插入自定义逻辑读取该标志并修正codecpar。
6. 常见问题排查与实战避坑指南
6.1 典型问题速查表
| 问题现象 | 可能原因 | 定位方法 | 解决方案 |
|---|---|---|---|
avformat_open_input返回-EIO | DESCRIBE响应无 SDP 或格式错误 | ff_rtsp_connect中sdp_parse返回负值 | 抓包分析 DESCRIBE 响应,确认Content-Type: application/sdp和 SDP 语法 |
av_read_frame返回AVERROR(EAGAIN) | rtsp_read_packet未读到$头,interleaved_data为空 | 在rtsp_read_packet开头加日志,打印url_read_complete返回值 | 检查设备是否真的发送了 Interleaved 数据,或Transport头是否正确 |
| 视频画面绿屏/花屏 | h264_handle_packet未正确解析 SPS/PPS,extradata为空 | av_dump_format查看codecpar->extradata_size是否为 0 | 确认a=fmtp中sprop-parameter-sets是否存在且 Base64 解码正确 |
| 音频播放速度异常 | a=rtpmap中时钟频率错误(如PCMU/16000写成PCMU/8000) | av_dump_format查看codecpar->sample_rate | 修改设备 SDP 或在sdp_parse_fmtp中强制修正st->codecpar->sample_rate |
| 拉流几分钟后自动断开 | GET_PARAMETER心跳未响应,timeout触发重连 | gdb断点rtsp_send_cmd_with_content_async,观察返回值 | 调小RTSPState->timeout,或禁用心跳(s->seq=0) |
6.2 调试工具链与关键日志点
高效调试 RTSP 源码,离不开精准的日志注入。以下是我长期使用的 5 个黄金日志点:
rtsp.c:ff_rtsp_connect第 1520 行:在url_read_complete读取DESCRIBE响应后,打印buf内容。这是验证 SDP 是否正确的第一道关卡。rtsp.c:sdp_parse第 2121 行:在while (p < end)循环内,打印line变量。可清晰看到每一行 SDP 的解析结果。rtsp.c:rtsp_read_packet第 2010 行:在url_read_complete(s->rtsp_hd, header, 4)后,打印header[0], header[1], header[2], header[3]。确认是否收到合法$头。rtpdec_h264.c:h264_handle_packet第 120 行:在nal_unit_type = buf[0] & 0x1f后,打印nal_unit_type。快速识别是否收到 SPS(7)、PPS(8)、IDR(5)。rtspdec.c:rtsp_read_header第 180 行:在rtsp_configure_media调用后,调用av_dump_format(s->ic, 0, url, 0)。查看codecpar是否已正确填充。
日志开启方式:编译 FFmpeg 时添加--enable-debug=3,运行时设置环境变量export FFREPORT=file=ffreport.log:level=32。日志等级 32 对应AV_LOG_DEBUG,足够捕获上述关键点。
6.3 三个血泪教训分享
教训一:不要信任设备的a=range
某项目中,设备返回a=range:npt=0.000-123.456,rtsp_read_seek会据此计算seek_timestamp。但实际播放时,av_seek_frame调用后rtsp_read_packet一直返回AVERROR_EOF。追踪发现,设备在PLAY时并未真正从npt=0开始发包,而是从npt=5.0开始。解决方案:在rtsp_read_seek中忽略a=range,强制使用 `TEARDOWN+SETUP