Skip to content

Commit f26cefa

Browse files
authored
chore(llc): improved sfu error handling in call flow (#1004)
* improved sfu error handling in call flow * linter fix * changelog
1 parent f6e0f47 commit f26cefa

File tree

8 files changed

+90
-51
lines changed

8 files changed

+90
-51
lines changed

packages/stream_video/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* Added `setMirrorVideo` method to `Call` class to control video mirroring for participants.
55
* Added `call.partialState` for more specific and efficient state updates.
66

7+
🐞 Fixed
8+
* Improved SFU error handling in Call flow and disconnect reason handling. The disconnected call state now accurately reflects the original cause of disconnection.
9+
710
## 0.9.6
811

912
✅ Added

packages/stream_video/lib/src/call/call.dart

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ class Call {
283283
Future<InternetStatus>? _awaitNetworkAvailableFuture;
284284
Future<Result<None>>? _awaitMigrationCompleteFuture;
285285
bool _initialized = false;
286+
bool _leaveCallTriggered = false;
286287

287288
final List<Timer> _reactionTimers = [];
288289
final Map<String, Timer> _captionsTimers = {};
@@ -673,7 +674,11 @@ class Call {
673674
_logger.v(() => '[join] finished: $result');
674675
} else {
675676
_logger.e(() => '[join] failed: $result');
676-
await leave();
677+
final videoError = result.getErrorOrNull();
678+
await leave(
679+
reason:
680+
videoError != null ? DisconnectReason.failure(videoError) : null,
681+
);
677682
}
678683

679684
return result;
@@ -1135,7 +1140,7 @@ class Call {
11351140
callParticipants.first.userId == _streamVideo.currentUser.id &&
11361141
state.value.isRingingFlow &&
11371142
_stateManager.callState.preferences.dropIfAloneInRingingFlow) {
1138-
await leave();
1143+
await leave(reason: DisconnectReason.lastParticipantLeft());
11391144
}
11401145
} else if (sfuEvent is SfuHealthCheckResponseEvent) {
11411146
_stateManager.setParticipantsCount(
@@ -1149,11 +1154,18 @@ class Call {
11491154

11501155
if (sfuEvent is SfuSocketDisconnected) {
11511156
await _sfuStatsReporter?.sendSfuStats();
1152-
if (!StreamWebSocketCloseCode.isIntentionalClosure(
1153-
sfuEvent.reason.closeCode,
1154-
)) {
1157+
// Don't attempt reconnection if leaving the call was triggered
1158+
if (!_leaveCallTriggered &&
1159+
!StreamWebSocketCloseCode.isIntentionalClosure(
1160+
sfuEvent.reason.closeCode,
1161+
)) {
11551162
_logger.w(() => '[onSfuEvent] socket disconnected');
11561163
await _reconnect(SfuReconnectionStrategy.fast);
1164+
} else if (_leaveCallTriggered) {
1165+
_logger.d(
1166+
() =>
1167+
'[onSfuEvent] socket disconnected, leaving call was triggered - no reconnection',
1168+
);
11571169
}
11581170
} else if (sfuEvent is SfuSocketFailed) {
11591171
_logger.w(() => '[onSfuEvent] socket failed');
@@ -1178,7 +1190,7 @@ class Call {
11781190
_logger.w(
11791191
() => '[onSfuEvent] SFU error: ${sfuEvent.error}, leaving call',
11801192
);
1181-
await leave();
1193+
await leave(reason: DisconnectReason.sfuError(sfuEvent.error));
11821194
break;
11831195
case SfuReconnectionStrategy.unspecified:
11841196
_logger.w(() => '[onSfuEvent] SFU error: ${sfuEvent.error}');
@@ -1407,25 +1419,31 @@ class Call {
14071419
///
14081420
/// - [reason]: optional reason for leaving the call
14091421
Future<Result<None>> leave({DisconnectReason? reason}) async {
1410-
final state = this.state.value;
1411-
_logger.i(() => '[leave] state: $state');
1422+
try {
1423+
_leaveCallTriggered = true;
14121424

1413-
if (state.status.isDisconnected) {
1414-
_logger.w(() => '[leave] rejected (state.status is disconnected)');
1415-
return const Result.success(none);
1416-
}
1425+
final state = this.state.value;
1426+
_logger.i(() => '[leave] state: $state');
14171427

1418-
try {
1419-
_session?.leave(reason: 'user is leaving the call');
1420-
} finally {
1421-
await _clear('leave');
1422-
}
1428+
if (state.status.isDisconnected) {
1429+
_logger.w(() => '[leave] rejected (state.status is disconnected)');
1430+
return const Result.success(none);
1431+
}
1432+
1433+
try {
1434+
_session?.leave(reason: 'user is leaving the call');
1435+
} finally {
1436+
await _clear('leave');
1437+
}
14231438

1424-
_stateManager.lifecycleCallDisconnected(reason: reason);
1439+
_stateManager.lifecycleCallDisconnected(reason: reason);
14251440

1426-
_logger.v(() => '[leave] finished');
1441+
_logger.v(() => '[leave] finished');
14271442

1428-
return const Result.success(none);
1443+
return const Result.success(none);
1444+
} finally {
1445+
_leaveCallTriggered = false;
1446+
}
14291447
}
14301448

14311449
Future<void> _clear(String src) async {

packages/stream_video/lib/src/call/session/call_session.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,11 @@ class CallSession extends Disposable {
856856
return;
857857
}
858858

859-
final ansResult = await pc.setRemoteAnswer(pubResult.data.sdp);
860-
if (ansResult is! Success<void>) {
861-
_logger.w(() => '[negotiate] #setRemoteAnswer; failed: $ansResult');
859+
if (pubResult.data.hasSdp()) {
860+
final ansResult = await pc.setRemoteAnswer(pubResult.data.sdp);
861+
if (ansResult is! Success<void>) {
862+
_logger.w(() => '[negotiate] #setRemoteAnswer; failed: $ansResult');
863+
}
862864
}
863865
});
864866
}

packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
166166
}
167167

168168
void lifecycleCallDisconnected({DisconnectReason? reason}) {
169+
if (state.status.isDisconnected) {
170+
_logger.w(
171+
() => '[lifecycleCallDisconnected] already disconnected: $state',
172+
);
173+
return;
174+
}
175+
169176
_logger.i(
170177
() => '[lifecycleCallDisconnected] state: $state, reason: $reason',
171178
);
@@ -187,16 +194,14 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
187194

188195
void lifecycleCallTimeout() {
189196
_logger.e(() => '[lifecycleCallTimeout] state: $state');
197+
lifecycleCallDisconnected(reason: const DisconnectReason.timeout());
198+
}
190199

191-
state = state.copyWith(
192-
status: CallStatus.disconnected(
193-
const DisconnectReason.timeout(),
194-
),
195-
sessionId: '',
196-
localStats: LocalStats.empty(),
197-
publisherStats: PeerConnectionStats.empty(),
198-
subscriberStats: PeerConnectionStats.empty(),
199-
);
200+
void lifecycleCallConnectFailed({
201+
required VideoError error,
202+
}) {
203+
_logger.e(() => '[lifecycleCallConnectFailed] state: $state');
204+
lifecycleCallDisconnected(reason: DisconnectReason.failure(error));
200205
}
201206

202207
void lifecycleCallConnecting({
@@ -221,20 +226,6 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
221226
);
222227
}
223228

224-
void lifecycleCallConnectFailed({
225-
required VideoError error,
226-
}) {
227-
_logger.e(() => '[lifecycleCallConnectFailed] state: $state');
228-
state = state.copyWith(
229-
status: CallStatus.disconnected(
230-
DisconnectReason.failure(error),
231-
),
232-
localStats: LocalStats.empty(),
233-
publisherStats: PeerConnectionStats.empty(),
234-
subscriberStats: PeerConnectionStats.empty(),
235-
);
236-
}
237-
238229
void lifecycleCallSessionStart({
239230
required String sessionId,
240231
LocalStats? localStats,

packages/stream_video/lib/src/coordinator/open_api/event/open_api_event.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class OpenApiEvent with EquatableMixin {
209209
final event = open.CallFrameRecordingFrameReadyEvent.fromJson(jsonObj);
210210
return result.copyWith(callFrameRecordingFrameReady: event);
211211
case EventType.unknown:
212-
streamLog.e(_tag, () => '[fromJson] unexpected event: $jsonObj');
212+
streamLog.d(_tag, () => '[fromJson] unexpected event: $jsonObj');
213213
return result.copyWith(unknown: jsonObj);
214214
}
215215
}

packages/stream_video/lib/src/models/disconnect_reason.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
33

44
import '../call/call_reject_reason.dart';
55
import '../errors/video_error.dart';
6+
import '../sfu/data/models/sfu_error.dart';
67

78
@immutable
89
abstract class DisconnectReason extends Equatable {
@@ -14,6 +15,10 @@ abstract class DisconnectReason extends Equatable {
1415
VideoError error,
1516
) = DisconnectReasonFailure;
1617

18+
const factory DisconnectReason.sfuError(
19+
SfuError error,
20+
) = DisconnectReasonSfuError;
21+
1722
const factory DisconnectReason.rejected({
1823
required String byUserId,
1924
CallRejectReason? reason,
@@ -68,6 +73,20 @@ class DisconnectReasonFailure extends DisconnectReason {
6873
}
6974
}
7075

76+
class DisconnectReasonSfuError extends DisconnectReason {
77+
const DisconnectReasonSfuError(this.error);
78+
79+
final SfuError error;
80+
81+
@override
82+
List<Object?> get props => [error];
83+
84+
@override
85+
String toString() {
86+
return 'SfuError{error: $error}';
87+
}
88+
}
89+
7190
class DisconnectReasonRejected extends DisconnectReason {
7291
const DisconnectReasonRejected({
7392
required this.byUserId,

packages/stream_video/lib/src/sfu/data/events/sfu_event_mapper_extensions.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ extension SfuConnectionQualityExtension on sfu_models.ConnectionQuality {
250250
case sfu_models.ConnectionQuality.CONNECTION_QUALITY_UNSPECIFIED:
251251
return SfuConnectionQuality.unspecified;
252252
default:
253-
throw StateError('unexpected quality: $this');
253+
return SfuConnectionQuality.unspecified;
254254
}
255255
}
256256
}
@@ -265,7 +265,7 @@ extension SfuGoAwayReasonExtension on sfu_models.GoAwayReason {
265265
case sfu_models.GoAwayReason.GO_AWAY_REASON_UNSPECIFIED:
266266
return SfuGoAwayReason.unspecified;
267267
default:
268-
throw StateError('unexpected go away reason: $this');
268+
return SfuGoAwayReason.unspecified;
269269
}
270270
}
271271
}
@@ -284,7 +284,7 @@ extension SfuCallEndedReasonExtension on sfu_models.CallEndedReason {
284284
case sfu_models.CallEndedReason.CALL_ENDED_REASON_SESSION_ENDED:
285285
return SfuCallEndedReason.sessionEnded;
286286
default:
287-
throw StateError('unexpected call ended reason: $this');
287+
return SfuCallEndedReason.unspecified;
288288
}
289289
}
290290
}
@@ -303,7 +303,7 @@ extension SfuTrackTypeExtension on sfu_models.TrackType {
303303
case sfu_models.TrackType.TRACK_TYPE_UNSPECIFIED:
304304
return SfuTrackType.unspecified;
305305
default:
306-
throw StateError('unexpected track type: $this');
306+
return SfuTrackType.unspecified;
307307
}
308308
}
309309
}

packages/stream_video_flutter/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ By (only) using these callbacks the root widgets will use more efficient partial
1414
* Added `PartialCallStateBuilder` to help with making widgets that depend on `partialState`.
1515
* Deprecated old callbacks
1616

17+
✅ Added
18+
* Added `setMirrorVideo` method to `Call` class to control video mirroring for participants.
19+
20+
🐞 Fixed
21+
* Improved SFU error handling in Call flow and disconnect reason handling. The disconnected call state now accurately reflects the original cause of disconnection.
22+
1723
## 0.9.6
1824

1925
✅ Added

0 commit comments

Comments
 (0)