Olá a todos, sou um contribuinte de primeira viagem, então peço desculpas se não estou formatando isso da maneira adequada.
Estou configurando um fórum Discourse para um grupo de entusiastas de automóveis e eles queriam usar videochamadas ao vivo via LiveKit para salas mais amplas/apresentações.
Durante os testes, encontramos consistentemente blocos pretos nos streams de vídeo, mas apenas ao usar a integração com o LiveKit. Usei o Claude Code para investigar e encontrei o que parece ser o problema e uma solução. O relatório gerado pelo Claude segue abaixo.
Ficarei feliz em criar um PR com a correção proposta, se isso for útil.
Resumo
Em uma sala LiveKit (SFU), ao navegar da página da sala para o widget de chamada flutuante, o espectador fica com blocos pretos para todos os outros participantes. Os demais participantes ainda conseguem ver o espectador normalmente. Voltar para a página da sala, ou sair e entrar novamente enquanto estiver no widget, restaura o vídeo. Isso acontece sempre, porque o desmonte da página da sala envia watching=false depois que o widget já enviou watching=true.
Versão
- Discourse no commit
1f09a33bffd908d6e38ef4b51697e8390e2a4c7f(branchlatest, outubro de 2026), plugin Live conforme empacotado - LiveKit Cloud como servidor de mídia (
live_livekit_room_policy = per_room) - Firefox 159 no Linux (também observado em outros navegadores e celulares)
Passos
- Duas contas (uma em uma janela anônima) entram na mesma sala habilitada para LiveKit com as câmeras ligadas.
- Em uma das janelas, clique em um link que sai da página da sala, fazendo a chamada migrar para o widget.
- Nessa janela, o bloco do outro participante fica preto. A outra janela vê ambos normalmente.
O que observei
- Na janela com a tela preta, a tag
<video>da outra pessoa possui um stream, não está pausada e temreadyState4. Mas sua track está comreadyState: "live",muted: true, resolução8×8egetVideoPlaybackQuality().totalVideoFramesé 0. Nenhum mídia chegou a ela. - Na aba de rede, durante a troca de uma página para o widget: três chamadas
POST /live/rooms/<id>/state, comwatching= true, true, false nessa ordem.
Causa (a partir da leitura do código no commit acima)
room-page.gjs,willDestroy:next(() => liveWebrtc.setWatching(roomId, false, { keepVideo }))call-widget.gjs: odidInsertdo widget chamawatchWidgetRoom, que executanext(() => setWatching(roomId, true))live-webrtc.js,setWatching: em salas não-mesh, chamasetVideoSubscriptionsEnabled(watching), o que cancela a inscrição nas tracks de câmera, tela e áudio da tela do espectador (livekit-session.js,#applyDesiredSubscription)
O false adiado da página chega depois do true do widget, fazendo com que o espectador acabe no estado “não assistindo” enquanto o widget está na tela. A transição do widget para a página da sala funciona porque o stopWatchingWidgetRoom do widget é executado sincronamente na destruição e o true da página é adiado, então o true vence. Uma nova entrada no widget funciona porque não há uma página antiga.
Salas peer-to-peer não cancelam inscrições de ninguém (o ramo SFU é ignorado), o que estaria de acordo com o problema ser específico para salas LiveKit.
Workaround confirmado, no console do navegador
- Com o bloco preto,
Discourse.__container__.lookup("service:live-webrtc").setWatching(1, true)restaurou o vídeo. - Este wrapper, instalado antes da troca, manteve o bloco ativo em todas as transições página → widget:
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);
};
Sugestão de correção
O desmonte da página da sala não deve desativar o status “assistindo” quando o widget está prestes a mostrar a mesma sala. Por exemplo, derivar o estado “assistindo” de whether a página ou o widget está montado, em vez de deixar cada um enviar seu próprio conjunto/limpeza. Dessa forma, a ordem não importa.
Relacionado, pode ser separado
Um POST .../state na mesma sessão foi respondido com 429. Falhas nessas chamadas são ignoradas (.catch(() => {})), então uma recusa da atualização “watching” falha silenciosamente.