Skip to content

Commit a2c8c94

Browse files
tmkarthiclaude
andauthored
fix(rtc): avoid KeyError in local_track_unpublished handler during teardown (#692)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fba0eee commit a2c8c94

2 files changed

Lines changed: 25 additions & 4 deletions

File tree

livekit-rtc/livekit/rtc/participant.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,13 @@ async def unpublish_track(self, track_sid: str) -> None:
826826
Raises:
827827
UnpublishTrackError: If there is an error in unpublishing the track.
828828
"""
829+
# Capture the publication before the FFI round-trip. The
830+
# local_track_unpublished room event races this async response and may
831+
# remove it from _track_publications first; holding our own reference
832+
# guarantees the track is cleared once unpublish completes, regardless
833+
# of which path removes the publication from the dict.
834+
publication = self._track_publications.get(track_sid)
835+
829836
req = proto_ffi.FfiRequest()
830837
req.unpublish_track.local_participant_handle = self._ffi_handle.handle
831838
req.unpublish_track.track_sid = track_sid
@@ -841,8 +848,11 @@ async def unpublish_track(self, track_sid: str) -> None:
841848
if cb.unpublish_track.error:
842849
raise UnpublishTrackError(cb.unpublish_track.error)
843850

844-
publication = self._track_publications.pop(track_sid)
845-
publication._track = None
851+
# Remove defensively: the room-event handler may already have done
852+
# so when it processed local_track_unpublished first.
853+
self._track_publications.pop(track_sid, None)
854+
if publication is not None:
855+
publication._track = None
846856
queue.task_done()
847857
finally:
848858
self._room_queue.unsubscribe(queue)

livekit-rtc/livekit/rtc/room.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,20 @@ def _on_room_event(self, event: proto_room.RoomEvent) -> None:
746746
ltrack = lpublication.track
747747
self.emit("local_track_published", lpublication, ltrack)
748748
elif which == "local_track_unpublished":
749+
# During teardown the publication may already have been removed
750+
# from the participant's dict by LocalParticipant.unpublish_track
751+
# (the FFI event races that async response), so the SID can be gone
752+
# by the time this event is dispatched. Look it up defensively and
753+
# skip the emit when it is no longer tracked, mirroring the
754+
# local_track_republished and remote track_unpublished handlers,
755+
# instead of raising a KeyError that _listen_task logs as an error.
749756
sid = event.local_track_unpublished.publication_sid
750-
lpublication = self.local_participant.track_publications[sid]
751-
self.emit("local_track_unpublished", lpublication)
757+
unpublished = self.local_participant._track_publications.get(sid)
758+
if unpublished is not None:
759+
del self.local_participant._track_publications[sid]
760+
self.emit("local_track_unpublished", unpublished)
761+
else:
762+
logging.debug("local_track_unpublished for untracked publication sid %s", sid)
752763
elif which == "local_track_republished":
753764
# The SDK auto-republished a local track during a full
754765
# reconnect: the underlying Track (and its bound source) is

0 commit comments

Comments
 (0)