diff --git a/callback.go b/callback.go index ea0978fc..90f25654 100644 --- a/callback.go +++ b/callback.go @@ -178,6 +178,11 @@ type RoomCallback struct { OnSignalReconnecting func() OnLocalTrackSubscribed func(publication *LocalTrackPublication, lp *LocalParticipant) + // OnRoomSIDChanged is called when the room's session ID changes after it was first + // known, either because the server started a new session for the room or because + // the participant was moved to another room. + OnRoomSIDChanged func(roomSID string) + // participant events are sent to the room as well ParticipantCallback } @@ -197,6 +202,7 @@ func NewRoomCallback() *RoomCallback { OnRecordingStatusChanged: func(isRecording bool) {}, OnRoomMoved: func(roomName string, token string) {}, OnRoomMovedWithSID: func(roomName string, roomSID string, token string) {}, + OnRoomSIDChanged: func(roomSID string) {}, OnReconnecting: func() {}, OnReconnected: func() {}, OnSignalReconnecting: func() {}, @@ -237,6 +243,9 @@ func (cb *RoomCallback) Merge(other *RoomCallback) { if other.OnRoomMovedWithSID != nil { cb.OnRoomMovedWithSID = other.OnRoomMovedWithSID } + if other.OnRoomSIDChanged != nil { + cb.OnRoomSIDChanged = other.OnRoomSIDChanged + } if other.OnReconnecting != nil { cb.OnReconnecting = other.OnReconnecting } diff --git a/room.go b/room.go index bfa3039f..4523a142 100644 --- a/room.go +++ b/room.go @@ -409,6 +409,7 @@ func (r *Room) Name() string { // SID returns the unique session ID of the room. // This will block until session ID is available, which could take up to 2s after joining the room. +// The ID can change while connected, see RoomCallback.OnRoomSIDChanged. func (r *Room) SID() string { <-r.sidReady r.lock.RLock() @@ -811,18 +812,26 @@ func (r *Room) cleanup() { r.LocalParticipant.cleanup() } -func (r *Room) setSid(sid string, allowEmpty bool) { +// setSid records the room's session ID and unblocks SID(). Once the ID is known, a +// different non-empty ID replaces it and reports changed, so callers can notify +// OnRoomSIDChanged when the server re-keys the session the room belongs to. +func (r *Room) setSid(sid string, allowEmpty bool) (changed bool) { r.lock.Lock() - if sid != "" || allowEmpty { - select { - case <-r.sidReady: - // already closed - default: + defer r.lock.Unlock() + + select { + case <-r.sidReady: + if sid != "" && sid != r.sid { + changed = r.sid != "" + r.sid = sid + } + default: + if sid != "" || allowEmpty { r.sid = sid close(r.sidReady) } } - r.lock.Unlock() + return changed } // Simulate triggers various test scenarios for debugging and testing purposes. @@ -1269,7 +1278,9 @@ func (r *Room) OnRoomUpdate(room *livekit.Room) { r.activeRecording = room.ActiveRecording } r.lock.Unlock() - r.setSid(room.Sid, false) + if r.setSid(room.Sid, false) { + go r.callback.OnRoomSIDChanged(room.Sid) + } if metadataChanged { go r.callback.OnRoomMetadataChanged(room.Metadata) } @@ -1280,13 +1291,12 @@ func (r *Room) OnRoomUpdate(room *livekit.Room) { func (r *Room) OnRoomMoved(moved *livekit.RoomMovedResponse) { r.log.Infow("room moved", "newRoom", moved.Room.Name) - r.OnRoomUpdate(moved.Room) - // setSid is one-shot; update name/sid directly so the getters report the destination room. + // set the name first so OnRoomSIDChanged observes the destination room's name r.lock.Lock() r.name = moved.Room.Name - r.sid = moved.Room.Sid r.lock.Unlock() + r.OnRoomUpdate(moved.Room) for _, rp := range r.GetRemoteParticipants() { r.OnParticipantDisconnect(rp, livekit.DisconnectReason_MIGRATION) diff --git a/room_test.go b/room_test.go index bef141f9..8db841ea 100644 --- a/room_test.go +++ b/room_test.go @@ -116,3 +116,57 @@ func TestOnRoomUpdateDeliversLateSID(t *testing.T) { return room.SID() == "RM_late" }, time.Second, 10*time.Millisecond, "SID() never returned the SID delivered via OnRoomUpdate") } + +func TestOnRoomUpdateReportsSIDChange(t *testing.T) { + changed := make(chan string, 4) + room := NewRoom(&RoomCallback{ + OnRoomSIDChanged: func(sid string) { changed <- sid }, + }) + + room.OnRoomUpdate(&livekit.Room{Sid: "RM_first"}) + require.Equal(t, "RM_first", room.SID()) + + // the same ID or an empty one is not a change + room.OnRoomUpdate(&livekit.Room{Sid: "RM_first"}) + room.OnRoomUpdate(&livekit.Room{}) + require.Equal(t, "RM_first", room.SID()) + + room.OnRoomUpdate(&livekit.Room{Sid: "RM_second"}) + require.Equal(t, "RM_second", room.SID()) + + select { + case sid := <-changed: + require.Equal(t, "RM_second", sid) + case <-time.After(time.Second): + t.Fatal("OnRoomSIDChanged not called") + } + select { + case sid := <-changed: + t.Fatalf("unexpected OnRoomSIDChanged(%q)", sid) + case <-time.After(50 * time.Millisecond): + } +} + +func TestOnRoomMovedReportsSIDChangeWithNewName(t *testing.T) { + names := make(chan string, 1) + var room *Room + room = NewRoom(&RoomCallback{ + OnRoomSIDChanged: func(string) { names <- room.Name() }, + }) + room.LocalParticipant.updateInfo(&livekit.ParticipantInfo{Sid: "local", Identity: "local-identity"}) + room.name = "old-room" + room.setSid("RM_old", false) + + room.OnRoomMoved(&livekit.RoomMovedResponse{ + Room: &livekit.Room{Name: "new-room", Sid: "RM_new"}, + Participant: &livekit.ParticipantInfo{Sid: "local", Identity: "local-identity"}, + Token: "token", + }) + + select { + case name := <-names: + require.Equal(t, "new-room", name) + case <-time.After(time.Second): + t.Fatal("OnRoomSIDChanged not called") + } +}