Skip to content
Open
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
4 changes: 0 additions & 4 deletions dogfooding/linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <gtk/gtk_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <stream_webrtc_flutter/flutter_web_r_t_c_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
Expand All @@ -24,9 +23,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
Expand Down
1 change: 0 additions & 1 deletion dogfooding/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
file_selector_linux
gtk
media_kit_video
record_linux
stream_webrtc_flutter
url_launcher_linux
Expand Down
2 changes: 1 addition & 1 deletion dogfooding/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies:
rxdart: ^0.28.0
share_plus: ^13.0.0
shared_preferences: ^2.5.3
stream_chat_flutter: ^10.0.0
stream_chat_flutter: ^10.2.0
stream_video_filters: ^1.4.2
stream_video_flutter: ^1.4.2
stream_video_noise_cancellation: ^1.4.2
Expand Down
1 change: 0 additions & 1 deletion dogfooding/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
firebase_auth
firebase_core
gal
media_kit_video
permission_handler_windows
record_windows
share_plus
Expand Down
6 changes: 6 additions & 0 deletions packages/stream_video/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

### 🐞 Fixed

- Serialized remote-description and ICE-candidate handling so a candidate arriving mid-negotiation can no longer be dropped.

## 1.4.2

### ✅ Added
Expand Down
1 change: 1 addition & 0 deletions packages/stream_video/lib/src/call/stats/trace_tag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ abstract class TraceTag {
static const String setLocalDescriptionError = 'setLocalDescription.error';
static const String addIceCandidate = 'addIceCandidate';
static const String addIceCandidateSuccess = 'addIceCandidate.success';
static const String addIceCandidatePending = 'addIceCandidate.pending';
static const String addIceCandidateError = 'addIceCandidate.error';

// Peer connection factory
Expand Down
77 changes: 54 additions & 23 deletions packages/stream_video/lib/src/webrtc/peer_connection.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:stream_webrtc_flutter/stream_webrtc_flutter.dart' as rtc;
import 'package:synchronized/synchronized.dart';

import '../../protobuf/video/sfu/models/models.pbenum.dart';
import '../../protobuf/video/sfu/signal_rpc/signal.pb.dart';
Expand All @@ -10,7 +11,6 @@ import '../logger/impl/tagged_logger.dart';
import '../models/call_cid.dart';
import '../sfu/data/models/sfu_error.dart';
import '../sfu/sfu_client.dart';
import '../utils/none.dart';
import '../utils/result.dart';
import '../utils/standard.dart';
import 'model/stats/rtc_printable_stats.dart';
Expand Down Expand Up @@ -58,6 +58,16 @@ typedef OnTrack =
rtc.RTCTrackEvent,
);

enum AddIceCandidateResult {
/// The candidate was applied to the peer connection immediately.
added,

/// The remote description was not set yet, so the candidate was buffered
/// and will be applied when [StreamPeerConnection.setRemoteDescription]
/// completes. This is a normal, deferred-success outcome — not a drop.
buffered,
}

/// Wrapper around the WebRTC connection that contains tracks.
class StreamPeerConnection extends Disposable {
/// Creates [StreamPeerConnection] instance.
Expand Down Expand Up @@ -121,6 +131,9 @@ class StreamPeerConnection extends Disposable {

final _pendingCandidates = <rtc.RTCIceCandidate>[];

/// Serializes [setRemoteDescription] and [addIceCandidate]
final _candidateLock = Lock();

/// Attempts to restart ICE on the `RTCPeerConnection`.
/// If the restart fails, this method will trigger onReconnectionNeeded with
/// the appropriate reconnection strategy based on the error.
Expand Down Expand Up @@ -264,17 +277,30 @@ class StreamPeerConnection extends Disposable {
/// Sets the remote description and adds any pending ice candidates.
Future<Result<void>> setRemoteDescription(
rtc.RTCSessionDescription sd,
) async {
try {
final result = await pc.setRemoteDescription(sd);
for (final candidate in _pendingCandidates) {
await pc.addCandidate(candidate);
) {
return _candidateLock.synchronized(() async {
try {
await pc.setRemoteDescription(sd);
} catch (e, stk) {
return Result.failure(VideoErrors.compose(e, stk));
}
Comment thread
Brazol marked this conversation as resolved.

final pending = List<rtc.RTCIceCandidate>.of(_pendingCandidates);
_pendingCandidates.clear();
return Result.success(result);
} catch (e, stk) {
return Result.failure(VideoErrors.compose(e, stk));
}
for (final candidate in pending) {
try {
await pc.addCandidate(candidate);
} catch (e) {
_logger.w(
() =>
'[setRemoteDescription] #$type; dropping candidate '
'${candidate.candidate}; failed: $e',
);
}
}

return const Result.success(null);
});
}

//Sets the local description
Expand Down Expand Up @@ -314,20 +340,25 @@ class StreamPeerConnection extends Disposable {

/// Adds an ice candidate to the peer connection.
///
/// If the peer connection is not yet ready, the candidate is added to a list
/// of pending candidates.
Future<Result<None>> addIceCandidate(rtc.RTCIceCandidate candidate) async {
try {
final remoteDescription = await pc.getRemoteDescription();
if (remoteDescription == null) {
_pendingCandidates.add(candidate);
return Result.error('no remoteDescription set');
/// If the remote description has not been set yet, the candidate cannot be
/// applied immediately, so it is buffered in [_pendingCandidates] and flushed
/// by [setRemoteDescription].
Future<Result<AddIceCandidateResult>> addIceCandidate(
rtc.RTCIceCandidate candidate,
) {
return _candidateLock.synchronized(() async {
try {
final remoteDescription = await pc.getRemoteDescription();
if (remoteDescription == null) {
_pendingCandidates.add(candidate);
return const Result.success(AddIceCandidateResult.buffered);
}
await pc.addCandidate(candidate);
return const Result.success(AddIceCandidateResult.added);
} catch (e, stk) {
return Result.failure(VideoErrors.compose(e, stk));
}
await pc.addCandidate(candidate);
return const Result.success(none);
} catch (e, stk) {
return Result.failure(VideoErrors.compose(e, stk));
}
});
}

/// Adds a local [rtc.MediaStreamTrack] with audio to the current connection.
Expand Down
11 changes: 7 additions & 4 deletions packages/stream_video/lib/src/webrtc/rtc_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,16 @@ class RtcManager extends Disposable {
required String iceCandidate,
}) async {
final candidate = RtcIceCandidateParser.fromJsonString(iceCandidate);
final Result<AddIceCandidateResult>? result;
if (peerType == StreamPeerType.publisher) {
return publisher?.addIceCandidate(candidate) ??
Result.error('no publisher created');
result = await publisher?.addIceCandidate(candidate);
if (result == null) return Result.error('no publisher created');
} else if (peerType == StreamPeerType.subscriber) {
return subscriber.addIceCandidate(candidate);
result = await subscriber.addIceCandidate(candidate);
} else {
return Result.error('unexpected peerType: $peerType');
}
return Result.error('unexpected peerType: $peerType');
return result.map((_) => none);
}

void _onRemoteTrack(StreamPeerConnection pc, rtc.RTCTrackEvent event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import '../call/stats/tracer.dart';
import '../sfu/data/models/sfu_codec.dart';
import '../sfu/data/models/sfu_model_mapper_extensions.dart';
import '../sfu/data/models/sfu_track_type.dart';
import '../utils/none.dart';
import '../utils/result.dart';
import 'model/stats/rtc_codec.dart';
import 'model/stats/rtc_inbound_rtp_video_stream.dart';
Expand Down Expand Up @@ -471,19 +470,25 @@ class TracedStreamPeerConnection extends StreamPeerConnection {
}

@override
Future<Result<None>> addIceCandidate(rtc.RTCIceCandidate candidate) async {
Future<Result<AddIceCandidateResult>> addIceCandidate(
rtc.RTCIceCandidate candidate,
) async {
tracer.trace(TraceTag.addIceCandidate, candidate.toMap());

final result = await super.addIceCandidate(candidate);

if (result.isSuccess) {
tracer.trace(TraceTag.addIceCandidateSuccess, null);
} else {
tracer.trace(
result.when(
success: (outcome) => tracer.trace(
outcome == AddIceCandidateResult.buffered
? TraceTag.addIceCandidatePending
: TraceTag.addIceCandidateSuccess,
null,
),
failure: (error) => tracer.trace(
TraceTag.addIceCandidateError,
result.getErrorOrNull()?.toString(),
);
}
error.toString(),
),
);

return result;
}
Expand Down
Loading
Loading