大家好,我是第一次贡献者,如果格式有误,请见谅。
我正在为一个汽车爱好者团体搭建一个 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(在其他浏览器和手机上也有观察到)
步骤
- 两个账户(其中一个在无痕模式窗口中)开启摄像头加入同一个支持 LiveKit 的房间。
- 在一个窗口中,点击一个会离开房间页面的链接,使通话移到小部件中。
- 在该窗口中,其他参与者的视频块是黑屏。另一个窗口可以正常看到双方。
我观察到的现象
- 在黑屏窗口中,其他人的
<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 房间的情况相符。
已确认的浏览器控制台变通方法
- 当视频块黑屏时,执行
Discourse.__container__.lookup("service:live-webrtc").setWatching(1, true)可以使视频恢复。 - 在切换前安装此包装器,可以在每次页面 → 小部件切换时保持视频块正常显示:
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(() => {})),因此被拒绝的“观看”更新会静默失败。