Skip to content

fix(player): recover from silent playback stalls on live RTP channels - #799

Closed
gdwengf wants to merge 3 commits into
stackia:mainfrom
gdwengf:fix/silent-stall-recovery
Closed

gdwengf wants to merge 3 commits into
stackia:mainfrom
gdwengf:fix/silent-stall-recovery

Conversation

@gdwengf

@gdwengf gdwengf commented Oct 21, 2026

Copy link
Copy Markdown

现象

手机浏览器(Android/鸿蒙 Chromium)播放直播台,约 1~2 分钟后画面定格,且不恢复,必须手动切台才回来。

同一时刻的客观状态:

  • 服务端完全正常:/status/sse 里该客户端队列 0~80 KB(上限 4.7 MB)、currentBandwidth 稳定 ~1.1 MB/s、slow=0、连接不掉;
  • 换 VLC 播同一条 URL,连续 20 分钟一次不断;
  • 更换隧道(WireGuard/OpenVPN)、换局域网直连,现象一致;
  • 浏览器端没有任何 error 事件,/player 也不显示错误页。

根因

video-player.tsx 的重试只挂在 handlePlayerError(PlayerError)上:MSE 报错、加载失败才会走到 scheduleRetryReload。

而这类静默停读(媒体时钟不再前进、但网络层的读取还在继续)不产生任何 PlayerError ⇒ MAX_RETRIES 一次都不会触发,只能等用户手动切台。

改动

1. playback-engine/mse/playback-controller.ts:把静默停读接入现有重试链路

新增播放进度看门狗,每秒检查 video.currentTime,并有三条硬约束:

  • 起播不干预:只有本次会话连续前进 ≥ 8 秒媒体时间后才进入监控。起播、切台、重试后 currentTime 归零,永远进不了监控 —— 原有起播流程(含 MP2 软解与视频对齐时的 1/16 减速)一字未改;
  • 只认屏幕上的那个 <video>:无缝切台用 A/B 双 slot,旧元素仍留在 DOM 里但已离屏,其时钟必然冻结,不能拿它判定;
  • 判定后先关自己再重试:suspend() 释放当前连接(否则冻结的连接还占着服务端 maxclients 名额,越重连越慢甚至 503),再抛 MEDIA_MSE_ERROR 走原有重试路径发新请求。

2. components/player/video-player.tsx:MAX_RETRIES 3 → 100

原版播放稳定 30 秒会重置 retry baseline,所以这个数只约束真正无望的流,不会无限重试坏源。

为什么不在 C 端做转码

这些台是 H.264 + MP2 音频,浏览器 MSE 不认 MP2,前端已经用 minimp3 WASM 软解(pcm-audio-player.ts),音频不是"没解",所以不是解码器缺失问题。

实际验证过在服务端把音频转 AAC(-c:v copy -c:a aac):仍然会卡,而 rtp2httpd 本身是零拷贝转发、不解析也不改写负载。所以问题定位在客户端播放管线的停读,不在转发层。

验证

  • 部署同一份前端为静态资源、反代到现有实例(未改动 rtp2httpd 本体)后复现与修复对照;
  • 修复后在 OpenWrt 24.10.4 与另一套实例上,手机(移动网络经隧道回家)+ 局域网各跑多轮:定格不再需要手动切台,2 秒内自愈并重新出画;
  • 起播、切台、EPG、台单均无变化(起播阶段看门狗不工作)。

说明

改动只碰播放器前端两个文件,未改 C 代码、未改配置格式、未引入依赖。

The playback watchdog only ever fires on a PlayerError. When a browser stops
consuming frames without raising any event (common on mobile with these
MP2-audio / 1080i multicast channels), nothing retries and the user has to
switch channels by hand to get the picture back.

Detect that case in the MSE controller and turn it into the error the stock
player already retries on, so the existing retry path issues a new request.

- watchdog requires >= 8 s of continuous playback in the current session before
  it judges anything, so startup / channel switching / post-rebuild (currentTime
  back to ~0) never enters monitoring: the stock startup path is untouched
- only the on-screen <video> is judged: with dual A/B slots the detached slot
  keeps a frozen clock and must not raise errors for the playing one
- 1 s without progress => suspend() first (release our own connection so the
  retry does not stack up against maxclients) then MEDIA_MSE_ERROR
- MAX_RETRIES 3 -> 100 (the stock player resets the baseline after 30 s of
  stable playback, so this only bounds a truly hopeless stream)
…lace

Reloading segments inside the wedged slot keeps the same video element, worker
and audio context, and recovers slowly. Load into the hidden A/B slot and
hard-switch when it is ready — the same path as tapping a channel — which also
releases the frozen connection instead of stacking a retry on top of it.

The MSE watchdog now asks the page to do this, falling back to the retry path
when nothing handles the event.
A silent freeze is not the only failure mode: sometimes loading never produces
video at all and the page sits on 'buffering' indefinitely.

Track when the visible slot started loading and, if no video has been produced
after 12 s, take the same recovery path. The clock is (re)started while the slot
is off screen, so the hidden slot of a seamless switch never trips it, and the
normal startup path (a few seconds) is untouched.
@gdwengf

gdwengf commented Oct 22, 2026

Copy link
Copy Markdown
Author

已被新方案取代:判据改为「没收到数据」+ 0.2s 预建 / 0.8s 切换 + 静默恢复,详见后续 PR。

@gdwengf gdwengf closed this Oct 22, 2026
@gdwengf
gdwengf deleted the fix/silent-stall-recovery branch October 22, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant