Live (LiveKit 房间):从房间页面切换到通话小部件会导致取消订阅所有人的视频(黑屏)- 附带修复建议

大家好,我是第一次贡献者,如果格式有误,请见谅。

我正在为一个汽车爱好者团体搭建一个 Discourse 论坛,他们希望在大房间/演示中使用通过 LiveKit 实现的视频聊天功能。

在测试期间,我们一致遇到了视频流黑屏的问题,但这仅在使用 LiveKit 集成时发生。我使用 Claude Code 进行了调查,发现这似乎是问题所在以及一种修复方法,以下是 Claude 生成的报告。

如果有用,我很乐意提交一个包含建议修复方案的 PR。

摘要

在 LiveKit (SFU) 房间中,从房间页面切换到浮动通话小部件时,观众看到其他所有人的视频都是黑屏。其他参与者仍然可以正常看到观众。返回房间页面,或者在小部件中离开并重新加入,视频就会恢复。这种情况每次都发生,因为房间页面的销毁逻辑会在小部件发送 watching=true 之后发送 watching=false。

版本

  • Discourse 提交哈希 1f09a33bffd908d6e38ef4b51697e8390e2a4c7f(latest 分支,2026-10),使用捆绑的 Live 插件
  • LiveKit Cloud 作为媒体服务器(live_livekit_room_policy = per_room)
  • Linux 上的 Firefox 159(在其他浏览器和手机上也有观察到)

步骤

  1. 两个账户(其中一个在无痕模式窗口中)开启摄像头加入同一个支持 LiveKit 的房间。
  2. 在一个窗口中,点击一个会离开房间页面的链接,使通话移到小部件中。
  3. 在该窗口中,其他参与者的视频块是黑屏。另一个窗口可以正常看到双方。

我观察到的现象

  • 在黑屏窗口中,其他人的 <video> 有流,未暂停,且 readyState 为 4。但其轨道的 readyState: "live",muted: true,尺寸为 8×8,且 getVideoPlaybackQuality().totalVideoFrames 为 0。媒体从未到达该轨道。
  • 在网络选项卡中,进行一次页面 → 小部件切换时:有三个 POST /live/rooms/<id>/state 调用,watching 的顺序依次为 true, true, false。

原因(基于上述提交中的代码阅读)

  • room-page.gjs, willDestroy: next(() => liveWebrtc.setWatching(roomId, false, { keepVideo }))
  • call-widget.gjs: 小部件的 didInsert 调用 watchWidgetRoom,它运行 next(() => setWatching(roomId, true))
  • live-webrtc.js, setWatching: 对于非 mesh 房间,它调用 setVideoSubscriptionsEnabled(watching),这会取消订阅观众的摄像头、屏幕和屏幕音频轨道(livekit-session.js, #applyDesiredSubscription)

页面的延迟 false 会在小部件的 true 之后到达,导致观众在小部件显示时最终处于非观看状态。小部件 → 房间页面的切换能正常工作,因为小部件的 stopWatchingWidgetRoom 在销毁时同步运行,而页面的 true 是延迟的,因此 true 获胜。在小部件中重新加入能正常工作,因为不存在旧的页面。

点对点房间不会取消订阅任何人(跳过了 SFU 分支),这与该问题特定于 LiveKit 房间的情况相符。

已确认的浏览器控制台变通方法

  1. 当视频块黑屏时,执行 Discourse.__container__.lookup("service:live-webrtc").setWatching(1, true) 可以使视频恢复。
  2. 在切换前安装此包装器,可以在每次页面 → 小部件切换时保持视频块正常显示:
const svc = Discourse.__container__.lookup("service:live-webrtc");
const orig = svc.setWatching.bind(svc);
svc.setWatching = (roomId, watching, options = {}) => {
  if (watching) return orig(roomId, true, options);
  setTimeout(() => {
    const shown = document.querySelector(".live-call-widget, .live-room-page");
    orig(roomId, !!shown, options);
  }, 500);
};

建议的修复方案

当小部件即将显示同一个房间时,房间页面的销毁逻辑不应关闭观看状态。例如,可以通过判断页面或小部件是否挂载来派生观看状态,而不是让每个组件发送自己的 set/clear 命令。这样顺序就不会产生影响了。

相关,可能是独立的问题

同一会话中的一个 POST .../state 请求被返回了 429 状态码。这些调用的失败被忽略了(.catch(() => {})),因此被拒绝的“观看”更新会静默失败。