live555源码分析--client流程分析2

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 成员变量。它的主要职责是:

  1. 发送 RTSP 命令(DESCRIBE、SETUP、PLAY、TEARDOWN)
  2. 管理客户端与服务器之间的连接状态

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=0
  • m=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() 主要做了三件事:

  1. 解析 SDP
  2. 创建 MediaSession
  3. 创建 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

这个函数主要做了四件事:

  1. 检查 SETUP 是否成功
  2. 为这个 subsession 创建数据接收器 MediaSink
  3. 准备接收 RTP 数据(但真正的数据要等 PLAY 才开始)
  4. 继续 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

该函数会在如下情况下被调用:

  1. 点播流播放完
  2. 网络或者服务器断开

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 时必须注意的问题。


参考资料

相关推荐
南山有乔木7892 小时前
音频文件怎么从MP3转换成WAV?音频处理、剪辑导入都适用的教程
音视频
AI服务老曹2 小时前
统一安防底座:基于 GB28181 与 RTSP 的边缘计算 AI 视频管理平台架构演进(附 Docker 部署与源码交付机制)
人工智能·音视频·边缘计算
fangcaojushi3 小时前
文创图影 视频生成完整流程
音视频
DogDaoDao3 小时前
深入解析 libaom:AV1 开源编解码库技术分析
google·开源·音视频·视频编解码·hevc·av1·libaom
开开心心就好4 小时前
解决图片无页码添加功能的实用工具
javascript·python·安全·智能手机·pdf·音视频·1024程序员节
EasyCVR14 小时前
国标GB28181视频监控平台EasyCVR行业解决方案深度解读——雪亮工程、智慧城市与智慧交通
人工智能·音视频·智慧城市
“码”力全开16 小时前
打破芯片与协议壁垒:基于 Docker + 边缘计算的 GB28181/RTSP 视频智能管理平台架构设计与源码交付方案
docker·音视频·边缘计算
AI服务老曹1 天前
解密企业级视频中台:基于 GB28181/RTSP 统一接入与边缘计算的 AI 视频管理平台(附 Docker 部署与源码交付方案)
人工智能·音视频·边缘计算
shandianchengzi1 天前
【记录】LosslessCut|Linux下配置开源无损剪辑软件 LosslessCut AppImage 命令行启动和设置图标
linux·运维·服务器·音视频·视频·剪辑