Skip to content

[Enhancement]Expose subscribed audio track to CallParticipant#1136

Merged
ipavlidakis merged 2 commits intodevelopfrom
iliaspavlidakis/ios-1639-enhancementaudiotrack-on-callparticipant
Apr 24, 2026
Merged

[Enhancement]Expose subscribed audio track to CallParticipant#1136
ipavlidakis merged 2 commits intodevelopfrom
iliaspavlidakis/ios-1639-enhancementaudiotrack-on-callparticipant

Conversation

@ipavlidakis
Copy link
Copy Markdown
Contributor

@ipavlidakis ipavlidakis commented Apr 23, 2026

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-1639/enhancementaudiotrack-on-callparticipant

🎯 Goal

Expose each participant's subscribed remote RTCAudioTrack so consumers can access remote audio tracks directly from CallParticipant.

📝 Summary

  • Added CallParticipant.audioTrack.
  • Added subscriber-side remote audio receiver handling.
  • Store and clear remote audio tracks through shared WebRTC track storage.

🛠 Implementation

Remote audio is surfaced by WebRTC through RTCRtpReceiver callbacks rather than media stream add/remove callbacks. This PR adds a remote audio adapter that observes audio receiver additions and removals, maps each receiver to the participant track lookup id, and emits the existing track add/remove flow.

WebRTCStateAdapter now assigns stored audio tracks to matching participants alongside video and screenshare tracks.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

Summary by CodeRabbit

  • New Features

    • CallParticipant now exposes the subscribed remote audio track, and the system publishes remote-audio add/remove events so apps can react to remote audio availability.
  • Bug Fixes

    • Reconnection cleanup now clears stale audio/screen-share state to prevent leftover non-video media references.
  • Tests

    • Added tests covering remote-audio events and audio track assignment/removal in participant state.

@ipavlidakis ipavlidakis self-assigned this Apr 23, 2026
@ipavlidakis ipavlidakis requested a review from a team as a code owner April 23, 2026 14:01
@ipavlidakis ipavlidakis added the enhancement New feature or request label Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds remote audio track exposure: CallParticipant now holds an optional RTCAudioTrack. A new RemoteAudioMediaAdapter observes peer connection receiver events and emits track add/remove events. Media adapter wiring and WebRTC state handling are updated to persist audio lifecycle; tests and mocks extended accordingly.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Documents the API exposure change for remote audio track on CallParticipant.
Model Updates
Sources/StreamVideo/Models/CallParticipant.swift, StreamVideoTests/Mock/CallParticipant_Mock.swift
Adds public var audioTrack: RTCAudioTrack?, updates initializer and Equatable reference comparison, adds withUpdated(audioTrack:), and extends test dummy helper to accept audioTrack.
Remote Audio Adapter
Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift, StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/.../RemoteAudioMediaAdapter_Tests.swift
New RemoteAudioMediaAdapter that subscribes to StreamRTCPeerConnectionProtocol receiver events, maps audio receivers to .added/.removed TrackEvents, stores tracks by receiverId, and unit tests validating add/remove flows.
Media Adapter Integration
Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift
Replaces the previous local-audio no-op adapter with RemoteAudioMediaAdapter in the subscriber initialization path.
WebRTC State Handling
Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift, StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift
Clears stale participants' audioTrack/screensharingTrack during reconnection cleanup, assigns audioTrack alongside video/screenshare on track updates, updates debug logging, and expands tests to cover audio track assignment/removal and reconnection cleanup.
Minor Formatting
Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift
Formatting-only blank-line insertion; no behavior change.

Sequence Diagram

sequenceDiagram
    participant Peer as StreamRTC PeerConnection
    participant Adapter as RemoteAudioMediaAdapter
    participant Subject as Track Subject
    participant State as WebRTC StateAdapter
    participant Model as CallParticipant

    Peer->>Adapter: AddedReceiverEvent(audio)
    activate Adapter
    Adapter->>Adapter: extract RTCAudioTrack, receiverId, trackId
    Adapter->>Subject: send .added(id, trackId, .audio, track)
    deactivate Adapter

    Subject->>State: receive .added TrackEvent
    activate State
    State->>Model: assign audioTrack to participant
    deactivate State

    Peer->>Adapter: RemovedReceiverEvent(receiverId)
    activate Adapter
    Adapter->>Subject: send .removed(id, .audio)
    deactivate Adapter

    Subject->>State: receive .removed TrackEvent
    activate State
    State->>Model: clear audioTrack for participant
    deactivate State
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop to the signal, I sniff out the sound,
New adapters listen, where voices are found.
Tracks add and they leave, I note every part —
A twitch of my whiskers, a thump of my heart.
Tests danced and passed; now the callers are heard. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: exposing subscribed audio track access on CallParticipant, which is the core objective of this enhancement PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iliaspavlidakis/ios-1639-enhancementaudiotrack-on-callparticipant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift (1)

17-35: Nit: AudioTrack.trackId naming is slightly misleading.

The doc comment describes trackId as "the participant lookup id it belongs to", but the value assigned is stream.trackId (the stream/track lookup prefix). Consider renaming to lookupId (or trackLookupId) to avoid confusion with RTCAudioTrack.trackId / event.receiver.receiverId, both of which are also in scope here.

♻️ Optional rename for clarity
-    private struct AudioTrack {
-        var receiverId: String
-        var trackId: String
-        var track: RTCAudioTrack
+    private struct AudioTrack {
+        var receiverId: String
+        /// Lookup id derived from the audio stream, used as the participant
+        /// track lookup key (not to be confused with `RTCAudioTrack.trackId`).
+        var lookupId: String
+        var track: RTCAudioTrack
@@
-            self.receiverId = event.receiver.receiverId
-            self.trackId = stream.trackId
-            self.track = audioTrack
+            self.receiverId = event.receiver.receiverId
+            self.lookupId = stream.trackId
+            self.track = audioTrack

Then update processAddedTrack / processRemovedTrack to emit id: trackEntry.lookupId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift`
around lines 17 - 35, Rename the AudioTrack property trackId to a clearer name
(e.g., lookupId or trackLookupId) throughout the RemoteAudioMediaAdapter: update
the private struct AudioTrack to declare lookupId and assign stream.trackId in
its initializer, and then update all usages (notably processAddedTrack and
processRemovedTrack) to emit id: trackEntry.lookupId; ensure any references to
AudioTrack.trackId are replaced so the receiverId and RTCAudioTrack identifiers
remain unambiguous.
StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift (1)

126-128: Optional: TrackEventRecorder write isn't fully atomic.

@Atomic var events guards individual get/set of the array reference, but recorder.events.append($0) is read-modify-write and not atomic. In practice this is fine here because only a single Combine subscription writes to it, but if this recorder is ever reused from multiple sinks/threads it could race. If you want to keep it defensive:

♻️ Optional tightening
 private final class TrackEventRecorder: `@unchecked` Sendable {
-    `@Atomic` var events: [TrackEvent] = []
+    `@Atomic` var events: [TrackEvent] = []
+
+    func record(_ event: TrackEvent) {
+        _events.mutate { $0.append(event) }
+    }
 }

(Only relevant if @Atomic in this codebase exposes a mutate closure; otherwise ignore.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift`
around lines 126 - 128, The TrackEventRecorder's `@Atomic` var events only makes
the getter/setter atomic but recorder.events.append(...) is a non-atomic
read-modify-write; update TrackEventRecorder to perform mutations via a
thread-safe API (e.g., use the `@Atomic-provided` mutate { } closure if available
on events, or guard accesses with a dedicated serial DispatchQueue or NSLock) so
all appends and reads happen inside the atomic/mutating block; update any call
sites that do recorder.events.append(...) to use the chosen mutate/locked access
pattern to avoid races.
Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift (1)

798-834: Combined audio/video presence logging looks fine.

The four-way switch covers all combinations and produces a readable debug line. Minor nit (optional): in the (false, false) case, the two names lists are appended independently, so a participant with both tracks appears in both clauses — acceptable for debug telemetry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift` around lines 798 -
834, The logging in set(participants:) can list the same participant twice when
they have both audio and video; update the generation of
participantsWithAudioTracks and participantsWithVideoTracks (and/or the combined
message in the (false, false) branch) to deduplicate names (e.g., use a Set or
union of names) so each participant appears only once in the debug output;
reference the participantsWithAudioTracks and participantsWithVideoTracks
symbols and ensure the final logMessage still reports counts correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift`:
- Around line 17-35: Rename the AudioTrack property trackId to a clearer name
(e.g., lookupId or trackLookupId) throughout the RemoteAudioMediaAdapter: update
the private struct AudioTrack to declare lookupId and assign stream.trackId in
its initializer, and then update all usages (notably processAddedTrack and
processRemovedTrack) to emit id: trackEntry.lookupId; ensure any references to
AudioTrack.trackId are replaced so the receiverId and RTCAudioTrack identifiers
remain unambiguous.

In `@Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift`:
- Around line 798-834: The logging in set(participants:) can list the same
participant twice when they have both audio and video; update the generation of
participantsWithAudioTracks and participantsWithVideoTracks (and/or the combined
message in the (false, false) branch) to deduplicate names (e.g., use a Set or
union of names) so each participant appears only once in the debug output;
reference the participantsWithAudioTracks and participantsWithVideoTracks
symbols and ensure the final logMessage still reports counts correctly.

In
`@StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift`:
- Around line 126-128: The TrackEventRecorder's `@Atomic` var events only makes
the getter/setter atomic but recorder.events.append(...) is a non-atomic
read-modify-write; update TrackEventRecorder to perform mutations via a
thread-safe API (e.g., use the `@Atomic-provided` mutate { } closure if available
on events, or guard accesses with a dedicated serial DispatchQueue or NSLock) so
all appends and reads happen inside the atomic/mutating block; update any call
sites that do recorder.events.append(...) to use the chosen mutate/locked access
pattern to avoid races.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef2f14a7-868a-485a-91ea-475dbb57f811

📥 Commits

Reviewing files that changed from the base of the PR and between 636fe9e and 1427247.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • Sources/StreamVideo/Models/CallParticipant.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
  • StreamVideoTests/Mock/CallParticipant_Mock.swift
  • StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift
  • StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift

@ipavlidakis ipavlidakis force-pushed the iliaspavlidakis/ios-1639-enhancementaudiotrack-on-callparticipant branch from 1427247 to e76e67b Compare April 24, 2026 09:00
@github-actions
Copy link
Copy Markdown

1 Error
🚫 Please start subject with capital letter.
e76e67b

Generated by 🚫 Danger

@github-actions
Copy link
Copy Markdown

Public Interface

 public struct CallParticipant: Identifiable, Sendable, Hashable  
-   public var track: RTCVideoTrack?
+   public var audioTrack: RTCAudioTrack?
-   public var trackSize: CGSize
+   public var track: RTCVideoTrack?
-   public var screenshareTrack: RTCVideoTrack?
+   public var trackSize: CGSize
-   public var showTrack: Bool
+   public var screenshareTrack: RTCVideoTrack?
-   public var isSpeaking: Bool
+   public var showTrack: Bool
-   public var isDominantSpeaker: Bool
+   public var isSpeaking: Bool
-   public var sessionId: String
+   public var isDominantSpeaker: Bool
-   public var connectionQuality: ConnectionQuality
+   public var sessionId: String
-   public var joinedAt: Date
+   public var connectionQuality: ConnectionQuality
-   public var audioLevel: Float
+   public var joinedAt: Date
-   public var audioLevels: [Float]
+   public var audioLevel: Float
-   public var pin: PinInfo?
+   public var audioLevels: [Float]
-   public var pausedTracks: Set<TrackType>
+   public var pin: PinInfo?
-   public var source: ParticipantSource
+   public var pausedTracks: Set<TrackType>
-   public var userId: String
+   public var source: ParticipantSource
-   public var name: String
+   public var userId: String
-   public var profileImageURL: URL?
+   public var name: String
-   public var isPinned: Bool
+   public var profileImageURL: URL?
-   public var isPinnedRemotely: Bool
+   public var isPinned: Bool
-   public var shouldDisplayTrack: Bool
+   public var isPinnedRemotely: Bool
-   
+   public var shouldDisplayTrack: Bool
- 
+   
-   public init(id: String,userId: String,roles: [String],name: String,profileImageURL: URL?,trackLookupPrefix: String?,hasVideo: Bool,hasAudio: Bool,isScreenSharing: Bool,showTrack: Bool,track: RTCVideoTrack? = nil,trackSize: CGSize = CGSize(width: 1024, height: 720),screenshareTrack: RTCVideoTrack? = nil,isSpeaking: Bool = false,isDominantSpeaker: Bool,sessionId: String,connectionQuality: ConnectionQuality,joinedAt: Date,audioLevel: Float,audioLevels: [Float],pin: PinInfo?,pausedTracks: Set<TrackType>,source: ParticipantSource = .webRTCUnspecified)
+ 
-   
+   public init(id: String,userId: String,roles: [String],name: String,profileImageURL: URL?,trackLookupPrefix: String?,hasVideo: Bool,hasAudio: Bool,isScreenSharing: Bool,showTrack: Bool,audioTrack: RTCAudioTrack? = nil,track: RTCVideoTrack? = nil,trackSize: CGSize = CGSize(width: 1024, height: 720),screenshareTrack: RTCVideoTrack? = nil,isSpeaking: Bool = false,isDominantSpeaker: Bool,sessionId: String,connectionQuality: ConnectionQuality,joinedAt: Date,audioLevel: Float,audioLevels: [Float],pin: PinInfo?,pausedTracks: Set<TrackType>,source: ParticipantSource = .webRTCUnspecified)
- 
+   
-   public static func ==(lhs: CallParticipant,rhs: CallParticipant)-> Bool
+ 
-   public func withUpdated(trackSize: CGSize)-> CallParticipant
+   public static func ==(lhs: CallParticipant,rhs: CallParticipant)-> Bool
-   public func withUpdated(track: RTCVideoTrack?)-> CallParticipant
+   public func withUpdated(trackSize: CGSize)-> CallParticipant
-   public func withUpdated(screensharingTrack: RTCVideoTrack?)-> CallParticipant
+   public func withUpdated(audioTrack: RTCAudioTrack?)-> CallParticipant
-   public func withUpdated(audio: Bool)-> CallParticipant
+   public func withUpdated(track: RTCVideoTrack?)-> CallParticipant
-   public func withUpdated(video: Bool)-> CallParticipant
+   public func withUpdated(screensharingTrack: RTCVideoTrack?)-> CallParticipant
-   public func withUpdated(screensharing: Bool)-> CallParticipant
+   public func withUpdated(audio: Bool)-> CallParticipant
-   public func withUpdated(showTrack: Bool)-> CallParticipant
+   public func withUpdated(video: Bool)-> CallParticipant
-   public func withUpdated(trackLookupPrefix: String)-> CallParticipant
+   public func withUpdated(screensharing: Bool)-> CallParticipant
-   public func withUpdated(isSpeaking: Bool,audioLevel: Float)-> CallParticipant
+   public func withUpdated(showTrack: Bool)-> CallParticipant
-   public func withUpdated(dominantSpeaker: Bool)-> CallParticipant
+   public func withUpdated(trackLookupPrefix: String)-> CallParticipant
-   public func withUpdated(connectionQuality: ConnectionQuality)-> CallParticipant
+   public func withUpdated(isSpeaking: Bool,audioLevel: Float)-> CallParticipant
-   public func withUpdated(pin: PinInfo?)-> CallParticipant
+   public func withUpdated(dominantSpeaker: Bool)-> CallParticipant
-   public func withPausedTrack(_ trackType: TrackType)-> CallParticipant
+   public func withUpdated(connectionQuality: ConnectionQuality)-> CallParticipant
-   public func withUnpausedTrack(_ trackType: TrackType)-> CallParticipant
+   public func withUpdated(pin: PinInfo?)-> CallParticipant
+   public func withPausedTrack(_ trackType: TrackType)-> CallParticipant
+   public func withUnpausedTrack(_ trackType: TrackType)-> CallParticipant

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift (1)

59-71: Consider symmetric scheduling of the two subscriptions.

The added-receiver chain runs compactMap(AudioTrack.init) before receive(on: processingQueue), while the removed-receiver chain runs compactMap after. The added branch's mapping therefore executes on whichever thread the peer connection publisher emits from (dereferencing event.receiver.track). Moving receive(on:) earlier would make both flows serialize on the same queue and keep WebRTC track inspection on a predictable executor:

♻️ Proposed reorder
 peerConnection
     .publisher(eventType: StreamRTCPeerConnection.AddedReceiverEvent.self)
-    .compactMap(AudioTrack.init)
     .receive(on: processingQueue)
+    .compactMap(AudioTrack.init)
     .sink { [weak self] in self?.processAddedTrack($0) }
     .store(in: disposableBag)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift`
around lines 59 - 71, The two peerConnection publisher subscriptions should
schedule onto processingQueue before mapping to keep execution symmetric: in the
added-receiver chain that uses peerConnection.publisher(eventType:
StreamRTCPeerConnection.AddedReceiverEvent.self) and compactMap(AudioTrack.init)
then .receive(on: processingQueue) (which currently runs the compactMap on the
publisher thread), move .receive(on: processingQueue) earlier so the chain
becomes publisher(...).receive(on:
processingQueue).compactMap(AudioTrack.init).sink { [weak self] in
self?.processAddedTrack($0) } .store(in: disposableBag) so both added and
removed flows serialize on processingQueue and track inspection happens on the
predictable executor; keep processAddedTrack, processRemovedTrack,
audioReceivers and disposableBag references intact.
StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift (1)

33-85: Consider adding negative-path coverage.

Both existing tests exercise the happy path. The adapter's AudioTrack.init? silently drops receivers whose first matching stream isn't audio (or whose track isn't an RTCAudioTrack), and the removed pipeline's compactMap drops unknown receiverIds. A small test each would lock down those filters against future regressions (e.g., if someone changes the stream-selection predicate in AudioTrack.init?).

Suggested additions:

  • test_addedReceiver_nonAudioStream_doesNotPublishTrackEvent() — send an AddedReceiverEvent whose stream trackType is .video, assert recorder stays empty.
  • test_removedReceiver_unknownReceiver_doesNotPublishTrackEvent() — send RemovedReceiverEvent with a receiver that was never added, assert recorder stays empty.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift`
around lines 33 - 85, Add two negative-path tests in
RemoteAudioMediaAdapter_Tests.swift: implement
test_addedReceiver_nonAudioStream_doesNotPublishTrackEvent() that creates a
TrackEventRecorder, a receiver via makeAudioReceiver(), and a stream whose
trackType is .video (use makeAudioStream but adjust to video or create a stream
with video track type), call observeEvents(with: recorder), send
StreamRTCPeerConnection.AddedReceiverEvent(receiver: receiver, streams:
[stream]) via mockPeerConnection.subject.send and then assert the recorder has
no events; and implement
test_removedReceiver_unknownReceiver_doesNotPublishTrackEvent() that creates a
TrackEventRecorder, a receiver that was never added, calls observeEvents(with:
recorder), sends StreamRTCPeerConnection.RemovedReceiverEvent(receiver:
receiver) and then asserts the recorder remains empty. Use the same helpers used
in other tests (TrackEventRecorder, observeEvents(with:),
mockPeerConnection.subject.send, waitForEvents/wait helpers) and name the tests
exactly as suggested.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift`:
- Around line 801-833: The current logic computes participantsWithAudioTracks
and participantsWithVideoTracks as joined String values and then uses isEmpty on
those strings, which falsely treats a single participant with an empty name as
"no track"; change to first compute filtered arrays (e.g., audioParticipants =
participants.filter { $0.value.audioTrack != nil }.map(\.value.name) and
videoParticipants = participants.filter { $0.value.track != nil
}.map(\.value.name)), branch on audioParticipants.isEmpty and
videoParticipants.isEmpty in the switch, and only call joined(separator: ",")
when building logMessage (e.g., join audioParticipants/videoParticipants for the
message), updating references to
participantsWithAudioTracks/participantsWithVideoTracks accordingly and
preserving the logMessage construction and log.debug call.

---

Nitpick comments:
In
`@Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift`:
- Around line 59-71: The two peerConnection publisher subscriptions should
schedule onto processingQueue before mapping to keep execution symmetric: in the
added-receiver chain that uses peerConnection.publisher(eventType:
StreamRTCPeerConnection.AddedReceiverEvent.self) and compactMap(AudioTrack.init)
then .receive(on: processingQueue) (which currently runs the compactMap on the
publisher thread), move .receive(on: processingQueue) earlier so the chain
becomes publisher(...).receive(on:
processingQueue).compactMap(AudioTrack.init).sink { [weak self] in
self?.processAddedTrack($0) } .store(in: disposableBag) so both added and
removed flows serialize on processingQueue and track inspection happens on the
predictable executor; keep processAddedTrack, processRemovedTrack,
audioReceivers and disposableBag references intact.

In
`@StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift`:
- Around line 33-85: Add two negative-path tests in
RemoteAudioMediaAdapter_Tests.swift: implement
test_addedReceiver_nonAudioStream_doesNotPublishTrackEvent() that creates a
TrackEventRecorder, a receiver via makeAudioReceiver(), and a stream whose
trackType is .video (use makeAudioStream but adjust to video or create a stream
with video track type), call observeEvents(with: recorder), send
StreamRTCPeerConnection.AddedReceiverEvent(receiver: receiver, streams:
[stream]) via mockPeerConnection.subject.send and then assert the recorder has
no events; and implement
test_removedReceiver_unknownReceiver_doesNotPublishTrackEvent() that creates a
TrackEventRecorder, a receiver that was never added, calls observeEvents(with:
recorder), sends StreamRTCPeerConnection.RemovedReceiverEvent(receiver:
receiver) and then asserts the recorder remains empty. Use the same helpers used
in other tests (TrackEventRecorder, observeEvents(with:),
mockPeerConnection.subject.send, waitForEvents/wait helpers) and name the tests
exactly as suggested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba534fc3-4726-46e0-859b-71103c0d22d5

📥 Commits

Reviewing files that changed from the base of the PR and between 1427247 and e76e67b.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • Sources/StreamVideo/Models/CallParticipant.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
  • StreamVideoTests/Mock/CallParticipant_Mock.swift
  • StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/RemoteAudioMediaAdapter_Tests.swift
  • StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift
✅ Files skipped from review due to trivial changes (4)
  • CHANGELOG.md
  • StreamVideoTests/Mock/CallParticipant_Mock.swift
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift
  • Sources/StreamVideo/Models/CallParticipant.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift
  • StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift

Comment thread Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamVideo 10.26 MB 10.28 MB +19 KB 🟢
StreamVideoSwiftUI 2.47 MB 2.47 MB 0 KB 🟢
StreamVideoUIKit 2.6 MB 2.6 MB 0 KB 🟢
StreamWebRTC 11.09 MB 11.09 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamVideo XCSize

Object Diff (bytes)
WebRTCStateAdapter.o +7630
RemoteAudioMediaAdapter.o +3495
CallParticipant.o +1946
APIHelper.o +1576
CallKitAlwaysAvailabilityPolicy.o +364
DisposableBag.o +252
ScreenSharingSession.o +120

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamVideoSwiftUI XCSize

Object Diff (bytes)
VideoParticipantsView.o +792
PictureInPictureStore.o +428
LocalParticipantViewModifier.o +264
ParticipantsFullScreenLayout.o +264
PictureInPictureContentView.o +228
Show 11 more objects
Object Diff (bytes)
PictureInPictureContent.o +212
SpotlightSpeakerView.o +168
CallParticipantsInfoView.o +144
ParticipantPopoverView.o +144
ParticipantsSpotlightLayout.o +144
PictureInPictureParticipantModifier.o +132
ScreenSharingView.o +132
PictureInPictureVideoParticipantView.o +132
LocalVideoView.o +120
PictureInPictureVideoRendererView.o +120
PictureInPictureScreenSharingView.o +120

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice stuff, let's test this next week

@ipavlidakis ipavlidakis merged commit 704dbdd into develop Apr 24, 2026
23 of 25 checks passed
@ipavlidakis ipavlidakis deleted the iliaspavlidakis/ios-1639-enhancementaudiotrack-on-callparticipant branch April 24, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants