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 StreamClientStateStreamClientState是一个状态管理类用于保存 RTSP 会话的完整生命周期状态包括MediaSession* session媒体会话对象MediaSubsessionIterator* iter子流迭代器MediaSubsession* subsession当前处理的子流double duration流的持续时间2.2 ourRTSPClient继承自RTSPClient添加了StreamClientState成员变量。它的主要职责是发送 RTSP 命令DESCRIBE、SETUP、PLAY、TEARDOWN管理客户端与服务器之间的连接状态2.3 DummySinkDummySink继承自MediaSink是实际接收 RTP 数据的地方。当数据到达时会触发afterGettingFrame()回调开发者可以在此处理接收到的音视频数据。三、回调函数详解3.1 continueAfterDESCRIBE处理 RTSP DESCRIBE 返回的 SDP创建MediaSession并开始逐个 SETUP 每个媒体子流为后续 RTP 数据接收做准备。SDP 示例// SDP版本一般为0 v0 // 会话发起者 o- 0 0 IN IP4 0.0.0.0 // 会话名称 sStreamed by ZLMediaKit // 连接地址 cIN IP4 0.0.0.0 // 时间范围0 0 表示永久有效 t0 0 // 播放范围 arange:npt0-8.2 // 聚合控制 acontrol:* // 视频媒体描述 mvideo 0 RTP/AVP 96 // 格式参数 afmtp:96 packetization-mode1; profile-level-id640029; sprop-parameter-setsZ2QAKawsaoHgCJWbgoCCoAAAAMAgAAAGUIA,aO4xshsA // Payload映射 artpmap:96 H264/90000 // Track URL acontrol:trackID0 // 音频媒体描述 maudio 0 RTP/AVP 97 afmtp:97 streamtype5;profile-level-id1;modeAAC-hbr;sizelength13;indexlength3;indexdeltalength3;config1588 artpmap:97 mpeg4-generic/8000/1 acontrol:trackID1如何判断视频流/音频流的 trackID通过 SDP 中的m行可以区分mvideo 0 RTP/AVP 96→ 视频流trackID0maudio 0 RTP/AVP 97→ 音频流trackID1关键代码解释scs.sessionMediaSession::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创建 MediaSubsession3.2 setupNextSubsession依次为 RTSP 会话中的每个子流发送 SETUP 请求全部完成后再发送 PLAY 请求开始播放。重要设置使用 TCP 还是 UDP 传输数据也是在该函数中配置。核心代码// next()的实现逻辑是返回当前节点指针移到下一个节点scs.subsessionscs.iter-next();// 所有子流都setup完成后发送PLAY命令if(scs.session-absStartTime()!NULL){// 回放流在SDP中附带arange:clock...表示播放时长rtsp_client-sendPlayCommand(*scs.session,continueAfterPLAY,scs.session-absStartTime(),scs.session-absEndTime());}else{scs.durationscs.session-playEndTime()-scs.session-playStartTime();rtsp_client-sendPlayCommand(*scs.session,continueAfterPLAY);}3.3 continueAfterSETUP这个函数主要做了四件事检查 SETUP 是否成功为这个 subsession 创建数据接收器MediaSink准备接收 RTP 数据但真正的数据要等 PLAY 才开始继续 setup 下一个 subsession核心代码// 创建数据接收器scs.subsession-sinkDummySink::createNew(env,*scs.subsession,rtspClient-url());if(scs.subsession-sinkNULL){env*rtspClientFailed to create a data sink for the \*scs.subsession\ subsession: env.getResultMsg()\n;break;}// 保存RTSPClient指针供回调使用scs.subsession-miscPtrrtspClient;// 开始准备接收数据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 会话。// 设置一个定时器在流的预期持续时间结束时触发处理// 前提是该流没有通过 RTCP 的 BYE 报文主动通知结束// 这一步是可选的。RTSP 流结束有两种方式服务器发送 RTCP BYE客户端自己用定时器关闭流这个定时器的作用是在预计播放时间结束后自动关闭流。通常摄像头实时流没有 duration不设置录像回放流有 duration可能会设置3.5 subsessionAfterPlaying当某个媒体子流播放结束时进行清理并检查是否需要关闭整个 RTSP 会话。参数中的clientData是用户自定义数据在创建 sink 时传入这里实际是MediaSubsession。该函数会在如下情况下被调用点播流播放完网络或者服务器断开3.6 shutdownStream安全的关闭 RTSP 流关闭所有 subsession → 发送 TEARDOWN → 释放 RTSPClient → 必要时退出程序。voidshutdownStream(RTSPClient*rtspClient,intexitCode){UsageEnvironmentenvrtspClient-envir();StreamClientStatescs((ourRTSPClient*)rtspClient)-scs;if(scs.session!NULL){Boolean someSubsessionsWereActiveFalse;MediaSubsessionIteratoriter(*scs.session);MediaSubsession*subsession;while((subsessioniter.next())!NULL){// 遍历所有子流关闭sinkif(subsession-sink!NULL){Medium::close(subsession-sink);subsession-sinkNULL;if(subsession-rtcpInstance()!NULL){// 取消RTCP BYE回调避免访问已释放对象subsession-rtcpInstance()-setByeHandler(NULL,NULL);}someSubsessionsWereActiveTrue;}}if(someSubsessionsWereActive){// 发送TEARDOWN不处理响应rtspClient-sendTeardownCommand(*scs.session,NULL);}}env*rtspClientClosing the stream.\n;Medium::close(rtspClient);if(--rtspClientCount0){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 时必须注意的问题。参考资料LIVE555中RTSP客户端接收媒体流分析及测试代码Live555学习之建立RTSP连接的过程雷霄骅的live555专栏live555_ffmpeg项目