live555源码分析--client流程分析2
本文深入分析 live555 客户端的核心源码,包括关键类的作用、回调函数的执行流程,以及实际项目中遇到的问题与解决方案。
一、整体架构概览
live555 client 的代码结构非常清晰,几个核心类之间的关系如下:
ourRTSPClient (控制层:RTSP协议)
↓
StreamClientState (状态层:记录流生命周期)
↓
DummySink (数据层:接收RTP帧)
- ourRTSPClient :继承自
RTSPClient,在其基础上增加StreamClientState,主要作用是发送 RTSP 命令。 - StreamClientState :保存一个 RTSP 流从
DESCRIBE → SETUP → PLAY → TEARDOWN的所有状态。 - DummySink:接收 RTP 数据帧。
二、核心类详解
2.1 StreamClientState
StreamClientState 是一个状态管理类,用于保存 RTSP 会话的完整生命周期状态,包括:
MediaSession* session:媒体会话对象MediaSubsessionIterator* iter:子流迭代器MediaSubsession* subsession:当前处理的子流double duration:流的持续时间
2.2 ourRTSPClient
继承自 RTSPClient,添加了 StreamClientState 成员变量。它的主要职责是:
- 发送 RTSP 命令(DESCRIBE、SETUP、PLAY、TEARDOWN)
- 管理客户端与服务器之间的连接状态
2.3 DummySink
DummySink 继承自 MediaSink,是实际接收 RTP 数据的地方。当数据到达时,会触发 afterGettingFrame() 回调,开发者可以在此处理接收到的音视频数据。
三、回调函数详解
3.1 continueAfterDESCRIBE
处理 RTSP DESCRIBE 返回的 SDP,创建 MediaSession,并开始逐个 SETUP 每个媒体子流,为后续 RTP 数据接收做准备。
SDP 示例:
text
// SDP版本一般为0
v=0
// 会话发起者
o=- 0 0 IN IP4 0.0.0.0
// 会话名称
s=Streamed by ZLMediaKit
// 连接地址
c=IN IP4 0.0.0.0
// 时间范围,0 0 表示永久有效
t=0 0
// 播放范围
a=range:npt=0-8.2
// 聚合控制
a=control:*
// 视频媒体描述
m=video 0 RTP/AVP 96
// 格式参数
a=fmtp:96 packetization-mode=1; profile-level-id=640029; sprop-parameter-sets=Z2QAKawsaoHgCJ+WbgoCCoAAAAMAgAAAGUIA,aO4xshsA
// Payload映射
a=rtpmap:96 H264/90000
// Track URL
a=control:trackID=0
// 音频媒体描述
m=audio 0 RTP/AVP 97
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
a=rtpmap:97 mpeg4-generic/8000/1
a=control:trackID=1
如何判断视频流/音频流的 trackID:
通过 SDP 中的 m= 行可以区分:
m=video 0 RTP/AVP 96→ 视频流,trackID=0m=audio 0 RTP/AVP 97→ 音频流,trackID=1
关键代码解释:
cpp
scs.session = MediaSession::createNew(env, sdpDescription);
根据 SDP 描述解析并创建一个 MediaSession(媒体会话),其中包含多个 MediaSubsession(媒体子流,如 video/audio),用于后续逐个 SETUP 和接收 RTP 数据。
MediaSession 与 MediaSubsession 的关系:
MediaSession = 一个RTSP流会话
一个RTSP URL 对应一个 MediaSession
MediaSubsession = 一个具体的媒体轨道
在 SDP 中由 m= 行定义
因此:
一个 MediaSession
│
├── MediaSubsession(video)
├── MediaSubsession(audio)
└── MediaSubsession(metadata)
createNew() 主要做了三件事:
- 解析 SDP
- 创建 MediaSession
- 创建 MediaSubsession
3.2 setupNextSubsession
依次为 RTSP 会话中的每个子流发送 SETUP 请求,全部完成后再发送 PLAY 请求开始播放。
重要:设置使用 TCP 还是 UDP 传输数据也是在该函数中配置。
核心代码:
cpp
// next()的实现逻辑是返回当前节点,指针移到下一个节点
scs.subsession = scs.iter->next();
// 所有子流都setup完成后,发送PLAY命令
if (scs.session->absStartTime() != NULL) {
// 回放流在SDP中附带"a=range:clock=..."表示播放时长
rtsp_client->sendPlayCommand(*scs.session, continueAfterPLAY,
scs.session->absStartTime(),
scs.session->absEndTime());
} else {
scs.duration = scs.session->playEndTime() - scs.session->playStartTime();
rtsp_client->sendPlayCommand(*scs.session, continueAfterPLAY);
}
3.3 continueAfterSETUP
这个函数主要做了四件事:
- 检查 SETUP 是否成功
- 为这个 subsession 创建数据接收器
MediaSink - 准备接收 RTP 数据(但真正的数据要等 PLAY 才开始)
- 继续 setup 下一个 subsession
核心代码:
cpp
// 创建数据接收器
scs.subsession->sink = DummySink::createNew(env, *scs.subsession, rtspClient->url());
if (scs.subsession->sink == NULL) {
env << *rtspClient << "Failed to create a data sink for the \""
<< *scs.subsession << "\" subsession: " << env.getResultMsg() << "\n";
break;
}
// 保存RTSPClient指针,供回调使用
scs.subsession->miscPtr = rtspClient;
// 开始准备接收数据(PLAY后才开始)
scs.subsession->sink->startPlaying(*(scs.subsession->readSource()),
subsessionAfterPlaying, scs.subsession);
// 设置收到RTCP BYE的回调函数
if (scs.subsession->rtcpInstance() != NULL) {
scs.subsession->rtcpInstance()->setByeWithReasonHandler(
subsessionByeHandler, scs.subsession);
}
3.4 continueAfterPLAY
处理 RTSP PLAY 命令返回的结果。如果成功则开始播放流,如果失败则调用 shutdownStream 关闭流。
关于设置定时器断开流:
如果流有 duration(比如录像回放),则会根据流的持续时间设置一个定时器,在流结束时自动关闭 RTSP 会话。
cpp
// 设置一个定时器,在流的预期持续时间结束时触发处理
// (前提是该流没有通过 RTCP 的 "BYE" 报文主动通知结束)
// 这一步是可选的。
RTSP 流结束有两种方式:
- 服务器发送 RTCP BYE
- 客户端自己用定时器关闭流
这个定时器的作用是:在预计播放时间结束后自动关闭流。通常:
- 摄像头实时流(没有 duration)不设置
- 录像回放流(有 duration)可能会设置
3.5 subsessionAfterPlaying
当某个媒体子流播放结束时进行清理,并检查是否需要关闭整个 RTSP 会话。
参数中的 clientData 是用户自定义数据,在创建 sink 时传入,这里实际是 MediaSubsession。
该函数会在如下情况下被调用:
- 点播流播放完
- 网络或者服务器断开
3.6 shutdownStream
安全的关闭 RTSP 流:关闭所有 subsession → 发送 TEARDOWN → 释放 RTSPClient → 必要时退出程序。
cpp
void shutdownStream(RTSPClient* rtspClient, int exitCode) {
UsageEnvironment& env = rtspClient->envir();
StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs;
if (scs.session != NULL) {
Boolean someSubsessionsWereActive = False;
MediaSubsessionIterator iter(*scs.session);
MediaSubsession* subsession;
while ((subsession = iter.next()) != NULL) {
// 遍历所有子流,关闭sink
if (subsession->sink != NULL) {
Medium::close(subsession->sink);
subsession->sink = NULL;
if (subsession->rtcpInstance() != NULL) {
// 取消RTCP BYE回调,避免访问已释放对象
subsession->rtcpInstance()->setByeHandler(NULL, NULL);
}
someSubsessionsWereActive = True;
}
}
if (someSubsessionsWereActive) {
// 发送TEARDOWN,不处理响应
rtspClient->sendTeardownCommand(*scs.session, NULL);
}
}
env << *rtspClient << "Closing the stream.\n";
Medium::close(rtspClient);
if (--rtspClientCount == 0) {
exit(exitCode);
}
}
五、使用过程中遇到的问题
5.1 RTCPInstance error: Hit limit when reading incoming packet over TCP

原因:未知
解决方法 :将 maxRTCPPacketSize 增大,项目中增大了 10 倍。
5.2 使用 ffmpeg 拉流和 live555 拉流的区别
IO 模型不同:
- live555:事件驱动,当有数据时通过回调函数将数据传给用户
- ffmpeg :阻塞 IO,用户需要调用
av_read_frame等待数据的到来
| 特性 | 事件驱动 (live555) | 阻塞 IO (ffmpeg) |
|---|---|---|
| 优点 | 线程开销小,一个线程管理多连接;高并发能力强;延迟可控 | 代码简单;容易调试 |
| 缺点 | 代码复杂,调试困难;回调函数不能长时间阻塞,否则整个事件循环会卡住 | 线程数量多,100路RTSP需要100个线程;延迟可能更高;10000线程时系统可能崩溃 |
关键注意:live555 的事件循环是单线程模式,如果回调函数执行太久会导致整个事件循环卡住,这是使用 live555 时必须注意的问题。