Hello community!
We are using MoveParticipant on LiveKit Cloud to route participants between rooms, and we have hit something we cannot explain.
After a move, the moved participant’s tracks only get auto-subscribed by people who were already in the destination room at the moment the move ran. Anyone arriving later, by another move or by a normal join, does get participantConnected and trackPublished for them, and the publication shows up with permissionStatus: 'allowed', but the subscription never completes. It sits at isDesired: true / isSubscribed: false. We left one like that for 13 minutes and nothing changed. An explicit setSubscribed(false) then setSubscribed(true) fixes it instantly.
We moved four participants into one room 5 seconds apart and got a very clean pattern: the first one moved sees all the others, the last one moved sees none of the other three. All four subscribe normally to someone who had joined that room the normal way, and a participant who joins afterwards sees none of the four. So it is not about how the subscriber arrived, only about whether they were in the room when the publisher’s move ran. For a group move of N that comes out to N(N-1)/2 broken pairs, which hurts us because our main use case is admitting a group from a waiting room into a session.
Has anyone run into this? Is this expected for MoveParticipant, or should a moved participant’s tracks be subscribable by people who arrive afterwards?
We have a workaround (the moved participant calls republishAllTracks() after the move, which routes the tracks through the normal publish path and fixes it completely). But we would rather understand the cause than ship around it.
Versions: livekit-client@2.20.1, livekit-server-sdk@2.17.0, server 1.13.3 (Cloud), Chrome 150, autoSubscribe and adaptiveStream both on.
Counting room events on the last-moved participant, in the destination room after its move:
5 participantConnected
10 trackPublished
4 trackSubscribed <- only the two normally-joined participants
All ten publications are advertised, only the four from normal joiners are subscribed. In chrome://webrtc-internals the server’s answer after the move carries the normal joiner’s two tracks and nothing else, with six m-lines sitting inactive with no msid and no ssrc, so it was not short of slots. Two more answers follow before we intervene and neither carries the moved participants’ tracks. When we finally toggle setSubscribed, the server fills one of those idle m-lines on the first request, in the same negotiation cycle, no error.
One detail that surprised us: the track SID does not change across the move. The last-moved participant had been subscribed to that exact camera SID back in the source room, and after both were moved it is the same SID, now unsubscribable.
Ruled out: adaptiveStream (audio fails identically and is not visibility-gated, and isDesired is true throughout), permissions (allowed on every stuck publication), a reconnect (ICE ufrag unchanged all session, ICE connected once and never again), transceiver exhaustion (six idle m-lines), a stalled renegotiation (16 offer/answer cycles, all reaching stable), and a race (the pattern is exact and repeatable across sessions and two different Cloud nodes).
This is not something specific to our app. We reproduced the same behaviour using LiveKit’s own examples: Meet as the client, and realtime-room-monitor to trigger the moves, which we forked to add a MoveParticipant action since it does not ship with one. Same result: the moved participants show up in the grid, but their tiles stay black for anyone who was not already in the room when the move ran.
Thanks for any pointers.
