Skip to content

Commit 5a4ca84

Browse files
authored
[Bug]Handle SFU events for missing participant (#996)
1 parent 617f029 commit 5a4ca84

File tree

3 files changed

+65
-18
lines changed

3 files changed

+65
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

55
# Upcoming
66

7-
### 🔄 Changed
7+
### 🐞 Fixed
8+
- Ensure SFU track and participant updates create missing participants. [#996](https://github.com/GetStream/stream-video-swift/pull/996)
89

910
# [1.35.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.35.0)
1011
_November 05, 2025_

Sources/StreamVideo/WebRTC/v2/SFU/SFUEventAdapter.swift

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,7 @@ final class SFUEventAdapter: @unchecked Sendable {
330330
await stateAdapter.enqueue { participants in
331331
var updatedParticipants = participants
332332

333-
guard let participant = updatedParticipants[sessionID] else {
334-
return participants
335-
}
333+
let participant = updatedParticipants[sessionID] ?? event.participant.toCallParticipant()
336334

337335
switch event.type {
338336
case .audio:
@@ -390,9 +388,7 @@ final class SFUEventAdapter: @unchecked Sendable {
390388
await stateAdapter.enqueue { participants in
391389
var updatedParticipants = participants
392390

393-
guard let participant = updatedParticipants[sessionID] else {
394-
return participants
395-
}
391+
let participant = updatedParticipants[sessionID] ?? event.participant.toCallParticipant()
396392

397393
switch event.type {
398394
case .audio:
@@ -484,19 +480,20 @@ final class SFUEventAdapter: @unchecked Sendable {
484480
await stateAdapter.enqueue { participants in
485481
var updatedParticipants = participants
486482

487-
guard
488-
let participant = updatedParticipants[event.participant.sessionID]
489-
else {
490-
return participants
483+
if let participant = updatedParticipants[event.participant.sessionID] {
484+
updatedParticipants[event.participant.sessionID] = event
485+
.participant
486+
.toCallParticipant()
487+
.withUpdated(showTrack: participant.showTrack)
488+
.withUpdated(pin: participant.pin)
489+
.withUpdated(track: participant.track)
490+
.withUpdated(screensharingTrack: participant.screenshareTrack)
491+
} else {
492+
updatedParticipants[event.participant.sessionID] = event
493+
.participant
494+
.toCallParticipant()
491495
}
492496

493-
updatedParticipants[event.participant.sessionID] = event
494-
.participant
495-
.toCallParticipant()
496-
.withUpdated(showTrack: participant.showTrack)
497-
.withUpdated(pin: participant.pin)
498-
.withUpdated(track: participant.track)
499-
.withUpdated(screensharingTrack: participant.screenshareTrack)
500497
return updatedParticipants
501498
}
502499
}

StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,22 @@ final class SFUEventAdapter_Tests: XCTestCase, @unchecked Sendable {
406406
}
407407
}
408408

409+
func test_handleTrackPublished_givenAudioEvent_participantDoesNotExist_whenPublished_thenAddsAndUpdatesParticipantAudioStatus(
410+
) async throws {
411+
let participant = CallParticipant.dummy()
412+
var event = Stream_Video_Sfu_Event_TrackPublished()
413+
event.sessionID = participant.sessionId
414+
event.type = .audio
415+
416+
try await assert(
417+
event,
418+
wrappedEvent: .sfuEvent(.trackPublished(event)),
419+
initialState: [:]
420+
) {
421+
$0.count == 1 && $0[participant.sessionId]?.hasAudio == true
422+
}
423+
}
424+
409425
// MARK: trackUnpublished
410426

411427
func test_handleTrackUnpublished_givenAudioEvent_whenPublished_thenUpdatesParticipantAudioStatus() async throws {
@@ -507,6 +523,22 @@ final class SFUEventAdapter_Tests: XCTestCase, @unchecked Sendable {
507523
}
508524
}
509525

526+
func test_handleTrackUnpublished_givenAudioEvent_participantDoesNotExist_whenPublished_thenAddsAndUpdatesParticipantAudioStatus(
527+
) async throws {
528+
let participant = CallParticipant.dummy(hasAudio: true)
529+
var event = Stream_Video_Sfu_Event_TrackUnpublished()
530+
event.sessionID = participant.sessionId
531+
event.type = .audio
532+
533+
try await assert(
534+
event,
535+
wrappedEvent: .sfuEvent(.trackUnpublished(event)),
536+
initialState: [:]
537+
) {
538+
$0.count == 1 && $0[participant.sessionId]?.hasAudio == false
539+
}
540+
}
541+
510542
// MARK: pinsChanged
511543

512544
func test_handlePinsChanged_givenEvent_whenPublished_thenUpdatesPinnedParticipants() async throws {
@@ -587,6 +619,23 @@ final class SFUEventAdapter_Tests: XCTestCase, @unchecked Sendable {
587619
}
588620
}
589621

622+
func test_handleParticipantUpdated_givenEvent_participantDoesNotExist_whenPublished_thenUAddsAndpdatesParticipant(
623+
) async throws {
624+
var event = Stream_Video_Sfu_Event_ParticipantUpdated()
625+
// We add the showTrack and audioLevels to match what the `event.participant.toCallParticipant()`
626+
// does (defaults to showTrack:True and uses the audioLevel to create an array for the audioLevels.)
627+
let expectedParticipant = CallParticipant.dummy(showTrack: true, audioLevels: [0])
628+
event.participant = .init(expectedParticipant)
629+
630+
try await assert(
631+
event,
632+
wrappedEvent: .sfuEvent(.participantUpdated(event)),
633+
initialState: [:]
634+
) {
635+
$0.count == 1 && $0[expectedParticipant.sessionId] == expectedParticipant
636+
}
637+
}
638+
590639
// MARK: publishOptionsChanged
591640

592641
func test_handleChangePublishOptions_givenEvent_whenPublished_thenUpdatesPublishOptions() async throws {

0 commit comments

Comments
 (0)