diff --git a/internal/engine/jitsi/jitsi.go b/internal/engine/jitsi/jitsi.go index a30d3fb..31c6d70 100644 --- a/internal/engine/jitsi/jitsi.go +++ b/internal/engine/jitsi/jitsi.go @@ -293,10 +293,11 @@ func (s *Session) Connect(ctx context.Context) error { s.jSess.Store(jSess) logger.Infof("jitsi: MUC joined %s/%s; waiting for peer …", s.host, s.room) - s.wg.Add(3) + s.wg.Add(4) go s.sendLoop() go s.recvLoop() go s.waitForJingle() + go s.bridgeKeepalive() return nil } @@ -673,6 +674,36 @@ func (s *Session) rtcpKeepalive(pc *webrtc.PeerConnection) { } } +// bridgeKeepalive sends a lightweight colibri-ws message every 10 seconds so +// JVB updates its endpoint lastActivity timestamp. Without this, JVB expires +// the endpoint after its inactivity timeout (~30-60s) when the ICE/DTLS path +// is routed through a TURN relay whose allocation silently dies. +func (s *Session) bridgeKeepalive() { + defer s.wg.Done() + const interval = 10 * time.Second + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-s.done: + return + case <-ticker.C: + jSess := s.jSess.Load() + if jSess == nil { + continue + } + br := jSess.Bridge() + if br == nil { + continue + } + _ = br.SendJSON(map[string]any{ + "colibriClass": "PinnedEndpointsChangedEvent", + "pinnedEndpoints": []string{}, + }) + } + } +} + // trickleDrainLoop reads the XMPP stanza channel and feeds any // transport-info ICE candidates into the PeerConnection. It also drains // non-jingle stanzas so the channel never fills and blocks the read loop.