BUG 国标点播结束后 BYE 未完整发送,重复点播产生孤儿 RTP

Author: kassolCreated Aug 14, 2026Updated Aug 14, 2026

环境

  • WVP:2.7.4,commit 642a9fce82cd22246be28a233c046d696a88f283,2026-07-19
  • 系统:Linux / ARM64
  • 部署:WVP、ZLMediaKit、Redis 容器化部署
  • 收流:ZLMediaKit 单端口 RTP
  • 接入:GB28181 设备及下级平台

现象

  1. 无人观看关闭流后,WVP 和 ZLMediaKit 的媒体列表已无该流,但设备仍持续发送 RTP。
  2. ZLMediaKit 中持续存在 UdpSession,服务器入站带宽持续被占用。
  3. 设备继续推流时,WVP 反复输出“推流鉴权-拒绝”,但残留 SIP Dialog 没有收到匹配的 BYE。
  4. 同一通道再次点播会建立新 Dialog,残留 RTP 可随点播次数叠加。
  5. stop 接口可返回成功,但未必发送 BYE,或只结束最后一个 Dialog。
  6. 重启 WVP 或 ZLMediaKit 不能根治,远端仍会向原 RTP 端口发送。

复现

  1. 对一个 GB28181 通道发起点播,等待 INVITE / 200 OK / ACK 完成。
  2. 退出播放,等待 ZLMediaKit 触发 on_stream_none_reader 并注销媒体源。
  3. 重复一次上述操作,形成两个持续发送 RTP 的旧 Dialog。
  4. 再次点播该通道并调用 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 日志时序:

  1. ZLMediaKit 输出 [ZLM HOOK]流无人观看
  2. WVP 返回关闭流,日志中输出 流无人观看是否触发关闭:true
  3. ZLMediaKit 注销媒体源,WVP 输出 [ZLM HOOK] 流注销
  4. 旧 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 处理:

如果前者先执行,后者无法再取得会话信息,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.stopInviteInfo == 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。

期望

  1. MediaDepartureEvent 不应在发送 BYE 前删除终止 Dialog 所需的会话信息。
  2. 同一 stream 的多个 Dialog 不应互相覆盖或误删,stop 应能终止对应的所有残留 Dialog。
  3. Redis 仍保留完整事务时,应能按 Call-ID 或 SSRC 进行清理,不需要停止 WVP。
  4. BYE 构造或发送失败时,不应先删除可用于重试的事务。
  5. stop 找不到会话时不应返回正常停止结果。

关联

  • #2211:无人观看关闭后未发送 BYE,设备继续推流并出现推流鉴权拒绝。
  • #2135:无人观看时设备仍持续推流,重复点播后带宽叠加,stop 只减少最新一路。
  • #2200:无人观看时流未按预期自动断开。
  • #2201:回放退出后流仍存在并持续占用带宽。
  • PR #2166:提到 PlayServiceImplInviteStreamServiceImpl 的异步 MediaDepartureEvent 处理存在时序问题。

Source: 648540858/wvp-GB28181-pro