BUG 国标点播结束后 BYE 未完整发送,重复点播产生孤儿 RTP
环境
- WVP:2.7.4,commit
642a9fce82cd22246be28a233c046d696a88f283,2026-07-19 - 系统:Linux / ARM64
- 部署:WVP、ZLMediaKit、Redis 容器化部署
- 收流:ZLMediaKit 单端口 RTP
- 接入:GB28181 设备及下级平台
现象
- 无人观看关闭流后,WVP 和 ZLMediaKit 的媒体列表已无该流,但设备仍持续发送 RTP。
- ZLMediaKit 中持续存在
UdpSession,服务器入站带宽持续被占用。 - 设备继续推流时,WVP 反复输出“推流鉴权-拒绝”,但残留 SIP Dialog 没有收到匹配的 BYE。
- 同一通道再次点播会建立新 Dialog,残留 RTP 可随点播次数叠加。
- stop 接口可返回成功,但未必发送 BYE,或只结束最后一个 Dialog。
- 重启 WVP 或 ZLMediaKit 不能根治,远端仍会向原 RTP 端口发送。
复现
- 对一个 GB28181 通道发起点播,等待 INVITE / 200 OK / ACK 完成。
- 退出播放,等待 ZLMediaKit 触发
on_stream_none_reader并注销媒体源。 - 重复一次上述操作,形成两个持续发送 RTP 的旧 Dialog。
- 再次点播该通道并调用 stop,同时抓取 SIP 和统计 RTP。
结果:
- 前两次点播均建立了独立的 INVITE Dialog 和 SSRC。
- stop 返回成功,抓包中可以看到 BYE,但只对应当前记录的 Dialog。
- 前两个已被覆盖的 Dialog 没有收到匹配 BYE,仍持续发送 RTP。
- ZLMediaKit 媒体列表为 0,但仍有 2 个对应
RtpSession。 - Redis Call-ID Hash 中仍有这 2 个 Dialog 的完整事务,stream 索引已不存在。
日志与现场数据
WVP / ZLMediaKit 日志时序:
- ZLMediaKit 输出
[ZLM HOOK]流无人观看。 - WVP 返回关闭流,日志中输出
流无人观看是否触发关闭:true。 - ZLMediaKit 注销媒体源,WVP 输出
[ZLM HOOK] 流注销。 - 旧 RTP 再次到达时,WVP 持续输出
[ZLM HOOK]推流鉴权-拒绝。
抓包中能看到 stop 对当前 Dialog 发送的 BYE,但没有两个残留 Call-ID 对应的 BYE。
现场查询到 getMediaList=0,但仍有 3 个 RtpSession。连续两次查询时 session id 均已变化,说明发送端在被拒绝后持续重连。
Redis 中还有 2 个孤儿 RTP 对应的完整 SsrcTransaction,包含 Call-ID、From-tag 和 To-tag,但对应的 stream 索引已不存在;另 1 个孤儿 RTP 已找不到任何 SIP 事务。
原因
现象与当前 master 中的以下路径一致。下面的链接指向 commit 768d740949570f3447773aa85985a5ab0c6f750f,2026-08-06。
1. 流离开事件存在异步清理竞态
MediaDepartureEvent 同时由两个 @Async listener 处理:
InviteStreamServiceImpl直接删除InviteInfo,不发送 BYE。PlayServiceImpl只有再次查到InviteInfo才会调用 stop 发送 BYE。
如果前者先执行,后者无法再取得会话信息,SIP Dialog 将保留。
2. 同一 stream 只保留一个事务索引
SipInviteSessionManager.put 使用 app + stream 作为单值索引。同一通道第二次点播会覆盖第一个 Dialog 的 stream 索引,但 Call-ID Hash 中仍保留多个事务。
stop 不传 Call-ID 时,SIPCommander.streamByeCmd 只能通过 stream 取到一个事务。
3. 删除旧 Call-ID 可能删掉新 Dialog 的 stream 索引
SipInviteSessionManager.removeByCallId 删除 Call-ID 时,会无条件删除该事务的 app + stream 字段。如果 stream 已指向新 Dialog,新 Dialog 的索引也会被删除。
4. stop 在 InviteInfo 丢失时直接返回
PlayServiceImpl.stop 在 InviteInfo == null 时不再查询 Call-ID Hash,因此 Redis 中即使仍有完整 Dialog 事务,现有 stop 也无法终止它。
另外,SIPCommander.streamByeCmd 在构造和发送 BYE 之前删除事务,如果发送失败,无法再使用原事务重试。
运维清理结果
对 26 个孤儿 RTP Dialog 进行过一次性清理:
BYE 200 OK : 22
BYE 481 : 1
timeout : 3
ZLM UdpSession : 26 -> 1
实际停止 RTP : 25外部清理程序必须先停止 WVP,使用 WVP 原 SIP 端口发送 BYE。剩余 1 路已返回 200 OK,但下级平台仍持续发送 RTP,并在 ZLMediaKit 拒绝后重新建立 session。
对当前的 3 路孤儿 RTP 检查 Redis,2 路仍保留完整 Dialog 信息,但现有 stop 无法访问;1 路已无原 Dialog 信息,无法构造匹配的 BYE。
期望
MediaDepartureEvent不应在发送 BYE 前删除终止 Dialog 所需的会话信息。- 同一 stream 的多个 Dialog 不应互相覆盖或误删,stop 应能终止对应的所有残留 Dialog。
- Redis 仍保留完整事务时,应能按 Call-ID 或 SSRC 进行清理,不需要停止 WVP。
- BYE 构造或发送失败时,不应先删除可用于重试的事务。
- stop 找不到会话时不应返回正常停止结果。
关联
- #2211:无人观看关闭后未发送 BYE,设备继续推流并出现推流鉴权拒绝。
- #2135:无人观看时设备仍持续推流,重复点播后带宽叠加,stop 只减少最新一路。
- #2200:无人观看时流未按预期自动断开。
- #2201:回放退出后流仍存在并持续占用带宽。
- PR #2166:提到
PlayServiceImpl与InviteStreamServiceImpl的异步MediaDepartureEvent处理存在时序问题。
Source: 648540858/wvp-GB28181-pro