Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Each call now owns an isolated native PeerConnectionFactory — fixes cross-call

### 🐞 Fixed

- Added safety nets and recovery for cases when the publisher connection doesn't establish after a reconnection (e.g. the SFU answer is lost or ICE stays in `new` state).
- Fixed sibling-call audio capture being silently broken when another concurrently-active call ended (e.g. a 1:1 ringing call ending alongside a running livestream, or a previous ringing call ending before a new one was accepted).
- Fixed a sibling call's audio breaking when a ringing 1:1 call ended via `dropIfAloneInRingingFlow` (the remote party hung up first). `Call.end()` and `Call.leave()` now share a single `_disconnect` cleanup path.
- Made the audio processor teardown in `Call._clear` multi-call aware. The audio processor is owned by `StreamVideo`, not by an individual `Call`, so disabling it on one call's teardown silently dropped noise cancellation on any other still-active call. `_clear` now only stops the global processor when no other active call is configured to use `NoiceCancellationSettingsMode.autoOn`.
Expand Down
2 changes: 2 additions & 0 deletions packages/stream_video/lib/src/call/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,8 @@ class Call {
_fastReconnectDeadline;
}

_session?.startPublisherConnectionCheck();

// make sure we only track connection timing if we are not calling this method as part of a migration flow
connectionTimeStopwatch.stop();
if (!performingMigration) {
Expand Down
61 changes: 58 additions & 3 deletions packages/stream_video/lib/src/call/session/call_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const _tag = 'SV:CallSession';

const _debounceDuration = Duration(milliseconds: 200);
const _migrationCompleteEventTimeout = Duration(seconds: 7);
const _publisherConnectionCheckDelay = Duration(seconds: 15);

class CallSession extends Disposable {
CallSession({
Expand Down Expand Up @@ -128,7 +129,8 @@ class CallSession extends Disposable {
/// to record the track in its tracking map.
final void Function(String trackId) onSuspendedAudioTrackRecorded;

Timer? _peerConnectionCheckTimer;
Timer? _publisherConnectionCheckTimer;

bool _isLeavingOrClosed = false;

SharedEmitter<SfuEvent> get events => sfuWS.events;
Expand Down Expand Up @@ -442,7 +444,13 @@ class CallSession extends Disposable {
result;

_logger.d(() => '[fastReconnect] sfu not connected, recreating');
await sfuWS.recreate();
final wsResult = await sfuWS.recreate();
if (wsResult.isFailure) {
_logger.w(() => '[fastReconnect] sfu recreate failed: $wsResult');
return const Result.failure(
VideoError(message: 'SFU WS reconnect failed'),
);
}

_logger.d(() => '[fastReconnect] sfu connected, sending join request');
sfuWS.send(
Expand Down Expand Up @@ -526,6 +534,47 @@ class CallSession extends Disposable {
}
}

/// Starts a one-shot timer that verifies the publisher's ICE transport
/// transitions out of the `new` state within [_publisherConnectionCheckDelay].
///
/// If the transport is still `new` after the deadline, the SDP answer was
/// likely never received (e.g. network dropped before the SFU could reply)
/// and we trigger a reconnection.
void startPublisherConnectionCheck() {
_publisherConnectionCheckTimer?.cancel();

_publisherConnectionCheckTimer = Timer(
_publisherConnectionCheckDelay,
() {
if (_isLeavingOrClosed) return;

final publisher = rtcManager?.publisher;
if (publisher == null) return;

final iceState = publisher.pc.iceConnectionState;
if (iceState == rtc.RTCIceConnectionState.RTCIceConnectionStateNew) {
_logger.w(
() =>
'[publisherConnectionCheck] publisher ICE still in "new" '
'state after ${_publisherConnectionCheckDelay.inSeconds}s '
'— triggering reconnection',
);
_tracer.trace('publisherConnectionCheck.stalled', {
'iceState': iceState.toString(),
'timeoutSeconds': _publisherConnectionCheckDelay.inSeconds,
});
onReconnectionNeeded(publisher, SfuReconnectionStrategy.rejoin);
} else {
_logger.v(
() =>
'[publisherConnectionCheck] publisher ICE state: '
'$iceState — OK',
);
}
},
);
}

void leave({String? reason}) {
_logger.d(() => '[leave] reason: $reason');
_isLeavingOrClosed = true;
Expand All @@ -542,14 +591,15 @@ class CallSession extends Disposable {
);
_isLeavingOrClosed = true;

_publisherConnectionCheckTimer?.cancel();

await _eventsSubscription?.cancel();
await _networkStatusSubscription?.cancel();

statsReporter?.dispose();
statsReporter = null;

_tracer.dispose();
_peerConnectionCheckTimer?.cancel();

unawaited(
sfuWS
Expand Down Expand Up @@ -941,6 +991,11 @@ class CallSession extends Disposable {
return Result.error('Call is disconnected');
}

if (!sfuWS.isConnected) {
_logger.w(() => '[negotiate] skipped — SFU WS not connected');
return Result.error('SFU WS is not connected');
}

await _negotiationLock.synchronized(() async {
_logger.d(() => '[negotiate] type: ${pc.type}');

Expand Down
10 changes: 9 additions & 1 deletion packages/stream_video/lib/src/webrtc/peer_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,16 @@ class StreamPeerConnection extends Disposable {
}

void _onRenegotiationNeeded() {
_logger.v(() => '[onRenegotiationNeeded] no args');
if (_isReconnecting) {
_logger.i(
() =>
'[onRenegotiationNeeded] suppressed — reconnect in progress for '
'$type, will renegotiate explicitly after reconnect',
);
return;
}

_logger.v(() => '[onRenegotiationNeeded] no args');
onRenegotiationNeeded?.call(this);
}

Expand Down
1 change: 1 addition & 0 deletions packages/stream_video/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies:
webrtc_interface: ^1.1.1

dev_dependencies:
fake_async: ^1.3.1
flutter_test:
sdk: flutter
mockito: ^5.4.2
Expand Down
Loading
Loading