Skip to content

Commit 5208d5f

Browse files
fix(rtc): guard remote track event handlers against racing disconnect
track_published, track_subscribed, track_unsubscribed and track_subscription_failed looked up the remote participant (and publication) with an unguarded dict access, raising KeyError when the participant had already been removed by a racing participant_disconnected or unpublish. This mirrors the defensive lookup already applied to track_unpublished in #743, using fresh locals and skipping the emit with a debug log when the entry is gone instead of letting _listen_task log a KeyError.
1 parent 0acc039 commit 5208d5f

1 file changed

Lines changed: 78 additions & 33 deletions

File tree

livekit-rtc/livekit/rtc/room.py

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -817,10 +817,19 @@ def _on_room_event(self, event: proto_room.RoomEvent) -> None:
817817
lpublication._first_subscription.set_result(None)
818818
self.emit("local_track_subscribed", lpublication.track)
819819
elif which == "track_published":
820-
rparticipant = self._remote_participants[event.track_published.participant_identity]
821-
rpublication = RemoteTrackPublication(event.track_published.publication)
822-
rparticipant._track_publications[rpublication.sid] = rpublication
823-
self.emit("track_published", rpublication, rparticipant)
820+
# The participant may already have been removed by a racing
821+
# disconnect or a duplicate event, so the lookup is done defensively
822+
# and the emit is skipped when the participant is gone, mirroring the
823+
# track_unpublished handler, instead of raising a KeyError that
824+
# _listen_task logs as an error.
825+
identity = event.track_published.participant_identity
826+
rp = self._remote_participants.get(identity)
827+
if rp is not None:
828+
rpub = RemoteTrackPublication(event.track_published.publication)
829+
rp._track_publications[rpub.sid] = rpub
830+
self.emit("track_published", rpub, rp)
831+
else:
832+
logger.debug("track_published for untracked participant %s", identity)
824833
elif which == "track_unpublished":
825834
# The participant or publication may already have been removed by a
826835
# racing disconnect or a duplicate event, so both lookups are done
@@ -839,41 +848,77 @@ def _on_room_event(self, event: proto_room.RoomEvent) -> None:
839848
else:
840849
logger.debug("track_unpublished for untracked participant %s", identity)
841850
elif which == "track_subscribed":
851+
# The participant or publication may already have been removed by a
852+
# racing disconnect or unpublish, so both lookups are done
853+
# defensively and the emit is skipped when either entry is gone,
854+
# mirroring the track_unpublished handler, instead of raising a
855+
# KeyError that _listen_task logs as an error.
842856
owned_track_info = event.track_subscribed.track
843857
track_info = owned_track_info.info
844-
rparticipant = self._remote_participants[event.track_subscribed.participant_identity]
845-
rpublication = rparticipant.track_publications[track_info.sid]
846-
rpublication._subscribed = True
847-
if track_info.kind == TrackKind.KIND_VIDEO:
848-
remote_video_track = RemoteVideoTrack(owned_track_info)
849-
remote_video_track._set_room(self)
850-
rpublication._track = remote_video_track
851-
self.emit("track_subscribed", remote_video_track, rpublication, rparticipant)
852-
elif track_info.kind == TrackKind.KIND_AUDIO:
853-
remote_audio_track = RemoteAudioTrack(owned_track_info)
854-
remote_audio_track._set_room(self)
855-
rpublication._track = remote_audio_track
856-
self.emit("track_subscribed", remote_audio_track, rpublication, rparticipant)
858+
identity = event.track_subscribed.participant_identity
859+
rp = self._remote_participants.get(identity)
860+
rpub = rp.track_publications.get(track_info.sid) if rp is not None else None
861+
if rp is not None and rpub is not None:
862+
rpub._subscribed = True
863+
if track_info.kind == TrackKind.KIND_VIDEO:
864+
remote_video_track = RemoteVideoTrack(owned_track_info)
865+
remote_video_track._set_room(self)
866+
rpub._track = remote_video_track
867+
self.emit("track_subscribed", remote_video_track, rpub, rp)
868+
elif track_info.kind == TrackKind.KIND_AUDIO:
869+
remote_audio_track = RemoteAudioTrack(owned_track_info)
870+
remote_audio_track._set_room(self)
871+
rpub._track = remote_audio_track
872+
self.emit("track_subscribed", remote_audio_track, rpub, rp)
873+
else:
874+
logger.debug(
875+
"track_subscribed for untracked participant %s or publication %s",
876+
identity,
877+
track_info.sid,
878+
)
857879
elif which == "track_unsubscribed":
880+
# The participant or publication may already have been removed by a
881+
# racing disconnect or unpublish, so both lookups are done
882+
# defensively and the emit is skipped when either entry is gone,
883+
# mirroring the track_unpublished handler, instead of raising a
884+
# KeyError that _listen_task logs as an error.
858885
identity = event.track_unsubscribed.participant_identity
859-
rparticipant = self._remote_participants[identity]
860-
rpublication = rparticipant.track_publications[event.track_unsubscribed.track_sid]
861-
rtrack = rpublication.track
862-
if rtrack is not None:
863-
rtrack._set_room(None)
864-
rpublication._track = None
865-
rpublication._subscribed = False
866-
self.emit("track_unsubscribed", rtrack, rpublication, rparticipant)
886+
rp = self._remote_participants.get(identity)
887+
rpub = (
888+
rp.track_publications.get(event.track_unsubscribed.track_sid)
889+
if rp is not None
890+
else None
891+
)
892+
if rp is not None and rpub is not None:
893+
rtrack = rpub.track
894+
if rtrack is not None:
895+
rtrack._set_room(None)
896+
rpub._track = None
897+
rpub._subscribed = False
898+
self.emit("track_unsubscribed", rtrack, rpub, rp)
899+
else:
900+
logger.debug(
901+
"track_unsubscribed for untracked participant %s or publication %s",
902+
identity,
903+
event.track_unsubscribed.track_sid,
904+
)
867905
elif which == "track_subscription_failed":
906+
# The participant may already have been removed by a racing
907+
# disconnect, so the lookup is done defensively and the emit is
908+
# skipped when the participant is gone, instead of raising a KeyError
909+
# that _listen_task logs as an error.
868910
identity = event.track_subscription_failed.participant_identity
869-
rparticipant = self._remote_participants[identity]
870-
error = event.track_subscription_failed.error
871-
self.emit(
872-
"track_subscription_failed",
873-
rparticipant,
874-
event.track_subscription_failed.track_sid,
875-
error,
876-
)
911+
rp = self._remote_participants.get(identity)
912+
if rp is not None:
913+
error = event.track_subscription_failed.error
914+
self.emit(
915+
"track_subscription_failed",
916+
rp,
917+
event.track_subscription_failed.track_sid,
918+
error,
919+
)
920+
else:
921+
logger.debug("track_subscription_failed for untracked participant %s", identity)
877922
elif which == "track_muted":
878923
identity = event.track_muted.participant_identity
879924
# TODO: pass participant identity

0 commit comments

Comments
 (0)