Hi all, first time contributor, so sorry if I’m not formatting this the proper way.
I’m setting up a Discourse forum for an automotive enthusiast group and they wanted to use Live video chats via LiveKit for larger rooms/presentations.
During testing we consistently ran into black tiles for video streams, but only when using the LiveKit integration. I used Claude Code to investigate and found what looks like the issue and a fix, the Claude generated report follows.
I’m happy to make a PR with a proposed fix if that would be helpful.
Summary
In a LiveKit (SFU) room, going from the room page to the floating call widget leaves the viewer with black tiles for everyone else. The other participants still see the viewer fine. Going back to the room page, or leaving and rejoining while in the widget, brings the video back. It happens every time, because the room page’s teardown sends watching=false after the widget has sent watching=true.
Version
- Discourse at commit
1f09a33bffd908d6e38ef4b51697e8390e2a4c7f(thelatestbranch, 2026-10), Live plugin as bundled - LiveKit Cloud as the media server (
live_livekit_room_policy = per_room) - Firefox 159 on Linux (also observed on other browsers and phones)
Steps
- Two accounts (one in a private window) join the same LiveKit-enabled room with cameras on.
- In one window, click a link that leaves the room page, so the call moves to the widget.
- In that window the other participant’s tile is black. The other window sees both fine.
What I saw
- In the black window, the other person’s
<video>has a stream, is not paused and hasreadyState4. But its track isreadyState: "live",muted: true,8×8, andgetVideoPlaybackQuality().totalVideoFramesis 0. No media ever arrived on it. - Network tab, one page → widget switch: three
POST /live/rooms/<id>/statecalls, withwatching= true, true, false in that order.
Cause (from reading the code at the commit above)
room-page.gjs,willDestroy:next(() => liveWebrtc.setWatching(roomId, false, { keepVideo }))call-widget.gjs: the widget’sdidInsertcallswatchWidgetRoom, which runsnext(() => setWatching(roomId, true))live-webrtc.js,setWatching: on non-mesh rooms it callssetVideoSubscriptionsEnabled(watching), which unsubscribes the viewer’s camera, screen and screen-audio tracks (livekit-session.js,#applyDesiredSubscription)
The page’s deferred false lands after the widget’s true, so the viewer ends up in the not-watching state while the widget is on screen. Going widget → room page works because the widget’s stopWatchingWidgetRoom runs synchronously on destroy and the page’s true is deferred, so true wins. A fresh join in the widget works because there is no old page.
Peer-to-peer rooms don’t unsubscribe anyone (the SFU branch is skipped), which would match this being specific to LiveKit rooms.
Confirmed workaround, in the browser console
- With the tile black,
Discourse.__container__.lookup("service:live-webrtc").setWatching(1, true)brought the video back. - This wrapper, installed before switching, kept the tile up on every page → widget switch:
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);
};
Suggested fix
The room page’s teardown shouldn’t turn watching off when the widget is about to show the same room. For example, derive the watching state from whether the page or the widget is mounted, instead of letting each send its own set/clear. That way ordering can’t matter.
Related, may be separate
One POST .../state in the same session was answered with 429. Failures on these calls are ignored (.catch(() => {})), so a refused “watching” update fails silently.