From 5ca3cf0a4883d7e4645fa9134101585aaaa2a774 Mon Sep 17 00:00:00 2001 From: Brazol Date: Tue, 10 Mar 2026 13:52:13 +0100 Subject: [PATCH 1/4] fixed ringing race when terminated iOS --- packages/stream_video/lib/src/call/call.dart | 2 +- .../StreamVideoPushNotificationPlugin.swift | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index 2774b3f2b..d9753a4f5 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -844,7 +844,7 @@ class Call { timeLimit: _stateManager.callState.preferences.connectTimeout, ); - if (status is! CallStatusConnected) { + if (status.status is! CallStatusConnected) { return const Result.success(none); } else { _logger.e(() => '[join] original "connect" failed'); diff --git a/packages/stream_video_push_notification/ios/stream_video_push_notification/Sources/stream_video_push_notification/StreamVideoPushNotificationPlugin.swift b/packages/stream_video_push_notification/ios/stream_video_push_notification/Sources/stream_video_push_notification/StreamVideoPushNotificationPlugin.swift index 1fc967549..7a3636efb 100644 --- a/packages/stream_video_push_notification/ios/stream_video_push_notification/Sources/stream_video_push_notification/StreamVideoPushNotificationPlugin.swift +++ b/packages/stream_video_push_notification/ios/stream_video_push_notification/Sources/stream_video_push_notification/StreamVideoPushNotificationPlugin.swift @@ -92,6 +92,7 @@ public class StreamVideoPushNotificationPlugin: NSObject, FlutterPlugin { public class EventCallbackHandler: NSObject, FlutterStreamHandler { private var eventSink: FlutterEventSink? + private var pendingEvents: [[String: Any]] = [] public func send(_ event: String, _ body: Any) { let data: [String: Any] = [ @@ -100,7 +101,12 @@ public class EventCallbackHandler: NSObject, FlutterStreamHandler { ] DispatchQueue.main.async { [weak self] in - self?.eventSink?(data) + guard let self = self else { return } + if let eventSink = self.eventSink { + eventSink(data) + } else { + self.pendingEvents.append(data) + } } } @@ -110,6 +116,10 @@ public class EventCallbackHandler: NSObject, FlutterStreamHandler { -> FlutterError? { self.eventSink = events + for event in pendingEvents { + events(event) + } + pendingEvents.removeAll() return nil } From e86a58d0556ad56339616d5dd33e427687a01c1c Mon Sep 17 00:00:00 2001 From: Brazol Date: Wed, 11 Mar 2026 13:08:16 +0100 Subject: [PATCH 2/4] tweaks around call connect race from cold start --- packages/stream_video/lib/src/call/call.dart | 30 +++++++---- .../stream_video/lib/src/stream_video.dart | 50 +++++++++++++------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index d9753a4f5..f16050ee7 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -837,18 +837,28 @@ class Call { } if (state.value.status is CallStatusConnecting) { - _logger.v(() => '[join] await "connecting" change'); + _logger.v(() => '[join] await ongoing connect to resolve'); - final status = await state.firstWhere( - (it) => it.status is CallStatusConnected, - timeLimit: _stateManager.callState.preferences.connectTimeout, - ); + try { + final currentState = await state.firstWhere( + (it) => it.status is! CallStatusConnecting, + timeLimit: _stateManager.callState.preferences.connectTimeout, + ); - if (status.status is! CallStatusConnected) { - return const Result.success(none); - } else { - _logger.e(() => '[join] original "connect" failed'); - return Result.error('original "connect" failed'); + if (currentState.status is CallStatusConnected) { + _logger.v(() => '[join] ongoing connect succeeded'); + return const Result.success(none); + } else { + _logger.e( + () => '[join] ongoing connect failed: ${currentState.status}', + ); + return Result.error( + 'ongoing connect failed: ${currentState.status}', + ); + } + } on TimeoutException { + _logger.e(() => '[join] timed out waiting for ongoing connect'); + return Result.error('timed out waiting for ongoing connect'); } } diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index 6d56185a5..710fd868a 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -755,31 +755,49 @@ class StreamVideo extends Disposable { final calls = await pushNotificationManager?.activeCalls(); if (calls == null || calls.isEmpty) return false; + // Ensure the coordinator WS is connected before proceeding. + // During cold start, autoConnect may still be in progress so we need to wait for it to complete. + final connectResult = await connect(); + if (connectResult.isFailure) { + _logger.e( + () => + '[consumeAndAcceptActiveCall] failed to connect: ' + '${connectResult.getErrorOrNull()}', + ); + return false; + } + final callResult = await consumeIncomingCall( uuid: calls.first.uuid!, cid: calls.first.callCid!, preferences: callPreferences, ); - callResult.fold( - success: (result) async { - final call = result.data; - await call.accept(); + if (callResult.isFailure) { + _logger.d( + () => + '[consumeAndAcceptActiveCall] error consuming incoming call: ' + '${callResult.getErrorOrNull()}', + ); + return false; + } - onCallAccepted?.call(call); + final call = callResult.getDataOrNull(); + if (call == null) return false; - return true; - }, - failure: (error) { - _logger.d( - () => - '[consumeAndAcceptActiveCall] error consuming incoming call: $error', - ); - return false; - }, - ); + final acceptResult = await call.accept(); + if (acceptResult.isFailure) { + _logger.d( + () => + '[consumeAndAcceptActiveCall] error accepting call: ' + '${acceptResult.getErrorOrNull()}', + ); + return false; + } + + onCallAccepted?.call(call); - return false; + return true; } @Deprecated('Use observeCoreRingingEvents instead.') From f90e2213e20b2836690995457f8a31036578d2b2 Mon Sep 17 00:00:00 2001 From: Brazol Date: Wed, 11 Mar 2026 13:16:02 +0100 Subject: [PATCH 3/4] changelog --- packages/stream_video/CHANGELOG.md | 6 ++++++ packages/stream_video_push_notification/CHANGELOG.md | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/packages/stream_video/CHANGELOG.md b/packages/stream_video/CHANGELOG.md index 48cf52d1c..9cad26f1e 100644 --- a/packages/stream_video/CHANGELOG.md +++ b/packages/stream_video/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +### 🐞 Fixed +* Fixed race condition in `Call.join` when another connect is already in progress, with proper timeout handling. +* Fixed `consumeAndAcceptActiveCall` to ensure the coordinator WS is connected before consuming incoming calls during cold start. + ## 1.3.0 ### 🐞 Fixed diff --git a/packages/stream_video_push_notification/CHANGELOG.md b/packages/stream_video_push_notification/CHANGELOG.md index 06d126d80..d2488bcea 100644 --- a/packages/stream_video_push_notification/CHANGELOG.md +++ b/packages/stream_video_push_notification/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +### 🐞 Fixed +* [iOS] Fixed race condition where push notification events could be lost if the Flutter EventChannel listener wasn't registered yet. + ## 1.3.0 * Sync version with `stream_video_flutter` 1.3.0 From eaf18429276b4e3a983870737a93ecdabb3659a5 Mon Sep 17 00:00:00 2001 From: Brazol Date: Wed, 11 Mar 2026 13:57:40 +0100 Subject: [PATCH 4/4] tweak --- packages/stream_video/lib/src/call/call.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index f16050ee7..c93d3708e 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -836,16 +836,20 @@ class Call { return Result.error('a call with the same cid is in progress'); } - if (state.value.status is CallStatusConnecting) { + if (state.value.status is CallStatusConnecting || + state.value.status is CallStatusJoining) { _logger.v(() => '[join] await ongoing connect to resolve'); try { final currentState = await state.firstWhere( - (it) => it.status is! CallStatusConnecting, + (it) => + it.status is! CallStatusConnecting && + it.status is! CallStatusJoining, timeLimit: _stateManager.callState.preferences.connectTimeout, ); - if (currentState.status is CallStatusConnected) { + if (currentState.status is CallStatusConnected || + currentState.status is CallStatusJoined) { _logger.v(() => '[join] ongoing connect succeeded'); return const Result.success(none); } else {