From e98a8e17e288df57426d44a8f254fabf98132478 Mon Sep 17 00:00:00 2001 From: Sam Pepose Date: Sat, 7 Jun 2025 16:00:23 -0400 Subject: [PATCH 1/2] fix: remove main thread blocking from WebRTC peer connection setup - Remove @MainActor from setLocalDescription and setRemoteDescription - Fixes ~1s UI freeze when joining calls after app launch --- .../WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift index c95a298b6..71726f088 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift @@ -72,7 +72,6 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked /// /// - Parameter sessionDescription: The RTCSessionDescription to set as the local description. /// - Throws: An error if setting the local description fails. - @MainActor func setLocalDescription( _ sessionDescription: RTCSessionDescription ) async throws { @@ -98,7 +97,6 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked /// /// - Parameter sessionDescription: The RTCSessionDescription to set as the remote description. /// - Throws: An error if setting the remote description fails. - @MainActor func setRemoteDescription( _ sessionDescription: RTCSessionDescription ) async throws { From 322e4e4a29b00d896a8ad924bdf9f6dd266fb26c Mon Sep 17 00:00:00 2001 From: Sam Pepose Date: Sat, 7 Jun 2025 16:00:48 -0400 Subject: [PATCH 2/2] Fix thread safety for RTCPeerConnection access - Add connectionQueue to serialize all RTCPeerConnection access - Prevent 968ms UI hangs by moving WebRTC operations off main thread - Maintain HasRemoteDescription event for proper video flow - Fix thread safety for addTransceiver, restartIce, and close operations --- .../StreamRTCPeerConnection.swift | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift index 71726f088..e42cd6821 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/StreamRTCPeerConnection.swift @@ -29,6 +29,9 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked /// A dispatch queue for handling peer connection operations. let dispatchQueue = DispatchQueue(label: "io.getstream.peerconnection") + + /// A dispatch queue for safely accessing `source`. RTCPeerConnection is not thread-safe. + private let connectionQueue = DispatchQueue(label: "io.getstream.peerconnection.connection") /// A publisher for RTCPeerConnectionEvents. lazy var publisher: AnyPublisher = delegatePublisher @@ -83,11 +86,14 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked return } - source.setLocalDescription(sessionDescription) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: ()) + connectionQueue.async { [weak self] in + guard let self else { return } + source.setLocalDescription(sessionDescription) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } } } } as () @@ -108,12 +114,15 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked return } - source.setRemoteDescription(sessionDescription) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - self.subject.send(HasRemoteDescription(sessionDescription: sessionDescription)) - continuation.resume(returning: ()) + connectionQueue.async { [weak self] in + guard let self else { return } + source.setRemoteDescription(sessionDescription) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + self.subject.send(HasRemoteDescription(sessionDescription: sessionDescription)) + continuation.resume(returning: ()) + } } } } as () @@ -162,8 +171,13 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked with track: RTCMediaStreamTrack, init transceiverInit: RTCRtpTransceiverInit ) -> RTCRtpTransceiver? { - let result = source.addTransceiver(with: track, init: transceiverInit) - storeTransceiver(result, trackType: trackType) + var result: RTCRtpTransceiver? + connectionQueue.sync { + result = source.addTransceiver(with: track, init: transceiverInit) + } + if let result { + storeTransceiver(result, trackType: trackType) + } return result } @@ -195,18 +209,25 @@ final class StreamRTCPeerConnection: StreamRTCPeerConnectionProtocol, @unchecked /// Restarts the ICE gathering process. func restartIce() { - source.restartIce() + connectionQueue.async { [weak self] in + guard let self else { return } + self.source.restartIce() + } } /// Closes the peer connection. func close() async { - Task { @MainActor in - /// It's very important to close any transceivers **before** we close the connection, to make - /// sure that access to `RTCVideoTrack` properties, will be handled correctly. Otherwise - /// if we try to access any property/method on a `RTCVideoTrack` instance whose - /// peerConnection has closed, we will get blocked on the Main Thread. - source.transceivers.forEach { $0.stopInternal() } - source.close() + await withCheckedContinuation { continuation in + connectionQueue.async { [weak self] in + guard let self else { return } + /// It's very important to close any transceivers **before** we close the connection, to make + /// sure that access to `RTCVideoTrack` properties, will be handled correctly. Otherwise + /// if we try to access any property/method on a `RTCVideoTrack` instance whose + /// peerConnection has closed, we will get blocked on the Main Thread. + self.source.transceivers.forEach { $0.stopInternal() } + self.source.close() + continuation.resume() + } } }