e2e: fix resume/full-reconnect. (#194)

* fix: full re-connect.

* chore: Reset fullReConnect state after ConnectionState is emitted.

* Support full reconnect, fix Migration, NodeFailure, FullReconnect test cases.

* revert Timeouts.connection.

* update.

* Refactor the resume/reconnect implementation.

* more changes.

* update.

* Changed to simpler reconnect/recovery logic.

* update.

* update.

* code format.

* tidy.

* bump version for flutter-webrtc.

* chore: Remove unused exception class (SignalReconnectError).

* chore: rename `fullReconnectOnNext` to `fullReconnect`.

* update.

* chore: Revert changes for `unpublishTrack`.

* chore: Reconnect signalClient when resumeConnection.

* revert `rethrow` in engine.connect.

* Use removePublishedTrack instead of unpublishTrack (RemoteParticipant).

* chore: Correctly handle PeerConnectionState for resume/full-reconnect.

* tidy.
This commit is contained in:
CloudWebRTC
2022-11-08 16:06:09 +08:00
committed by GitHub
parent 9b0f0604e1
commit 0d8441ebfb
17 changed files with 300 additions and 144 deletions
+1
View File
@@ -171,6 +171,7 @@ extension LKExampleExt on BuildContext {
}
enum SimulateScenarioResult {
signalReconnect,
nodeFailure,
migration,
serverLeave,
+2
View File
@@ -219,6 +219,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (result != null) {
print('${result}');
await widget.room.sendSimulateScenario(
signalReconnect:
result == SimulateScenarioResult.signalReconnect ? true : null,
nodeFailure: result == SimulateScenarioResult.nodeFailure ? true : null,
migration: result == SimulateScenarioResult.migration ? true : null,
serverLeave: result == SimulateScenarioResult.serverLeave ? true : null,
+4 -4
View File
@@ -98,7 +98,7 @@ packages:
name: dart_webrtc
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
version: "1.0.10"
dbus:
dependency: transitive
description:
@@ -225,7 +225,7 @@ packages:
name: flutter_webrtc
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.11"
version: "0.9.12"
google_fonts:
dependency: "direct main"
description:
@@ -274,7 +274,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.1.6"
version: "1.1.7"
logging:
dependency: "direct main"
description:
@@ -566,7 +566,7 @@ packages:
name: webrtc_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.8"
version: "1.0.9"
win32:
dependency: transitive
description:
+155 -98
View File
@@ -28,7 +28,6 @@ import 'transport.dart';
class Engine extends Disposable with EventsEmittable<EngineEvent> {
static const _lossyDCLabel = '_lossy';
static const _reliableDCLabel = '_reliable';
final SignalClient signalClient;
final PeerConnectionCreate _peerConnectionCreate;
@@ -61,6 +60,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
// this is helpful to know if we need to restart ICE on the publisher connection
bool _hasPublished = false;
lk_models.ClientConfiguration? _clientConfiguration;
// remember url and token for reconnect
String? url;
String? token;
@@ -75,10 +76,13 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
String? _connectedServerAddress;
String? get connectedServerAddress => _connectedServerAddress;
bool fullReconnect = false;
// server-provided ice servers
List<RTCIceServer> _serverProvidedIceServers = [];
late final _signalListener = signalClient.createListener(synchronized: true);
late EventsListener<SignalEvent> _signalListener =
signalClient.createListener(synchronized: true);
Engine({
required this.connectOptions,
@@ -117,11 +121,6 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
this.roomOptions = roomOptions ?? this.roomOptions;
this.fastConnectOptions = fastConnectOptions;
if (connectionState == ConnectionState.connected) {
logger.fine('already connected');
return;
}
_updateConnectionState(ConnectionState.connecting);
try {
@@ -136,7 +135,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
// wait for join response
await _signalListener.waitFor<SignalJoinResponseEvent>(
duration: this.connectOptions.timeouts.connection,
onTimeout: () => throw ConnectException(),
onTimeout: () => throw ConnectException(
'Timed out waiting for SignalJoinResponseEvent'),
);
logger.fine('Waiting for engine to connect...');
@@ -145,7 +145,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
await events.waitFor<EnginePeerStateUpdatedEvent>(
filter: (event) => event.isPrimary && event.state.isConnected(),
duration: this.connectOptions.timeouts.connection,
onTimeout: () => throw ConnectException(),
onTimeout: () => throw ConnectException(
'Timed out waiting for EnginePeerStateUpdatedEvent'),
);
_updateConnectionState(ConnectionState.connected);
@@ -210,9 +211,15 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
if (publisher == null) {
return;
}
_hasPublished = true;
try {
publisher!.negotiate(null);
} catch (error) {
if (error is NegotiationError) {
fullReconnect = true;
}
await handleDisconnect(DisconnectReason.negotiationFailed);
}
}
@internal
@@ -268,75 +275,6 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
await channel.send(message);
}
@internal
Future<void> reconnect() async {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('Reconnect: Already closed.');
return;
}
if (url == null || token == null) {
throw ConnectException('could not reconnect without url and token');
}
Future<void> sequence() async {
//
await signalClient.connect(
url!,
token!,
connectOptions: connectOptions,
roomOptions: roomOptions,
reconnect: true,
sid: _participantSid,
);
if (publisher == null || subscriber == null) {
throw UnexpectedStateException('publisher or subscribers is null');
}
subscriber!.restartingIce = true;
if (_hasPublished) {
logger.fine('Reconnect: negotiating publisher...');
await publisher!.createAndSendOffer(const RTCOfferOptions(
iceRestart: true,
));
}
final iceConnected = primary?.pc.connectionState?.isConnected() ?? false;
logger.fine('Reconnect: iceConnected: $iceConnected');
if (!iceConnected) {
logger.fine('Reconnect: Waiting for primary to connect...');
await events.waitFor<EnginePeerStateUpdatedEvent>(
filter: (event) => event.isPrimary && event.state.isConnected(),
duration: connectOptions.timeouts.iceRestart,
onTimeout: () => throw ConnectException(),
);
}
}
try {
_updateConnectionState(ConnectionState.reconnecting);
await Utils.retry<void>(
(tries, errors) {
logger.fine('Retrying connect sequence remaining ${tries} tries...');
return sequence();
},
retryCondition: (_, __) =>
_connectionState == ConnectionState.reconnecting,
tries: 3,
delay: const Duration(seconds: 3),
);
_updateConnectionState(ConnectionState.connected);
} catch (error) {
//
_updateConnectionState(ConnectionState.disconnected);
}
}
Future<void> _configurePeerConnections(
{required lk_models.ClientConfigSetting forceRelay,
required List<RTCIceServer> serverProvidedIceServers}) async {
@@ -429,13 +367,10 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
));
events.on<EnginePeerStateUpdatedEvent>((event) {
//
final isPrimaryOrPublisher = event.isPrimary ||
(_hasPublished && event is EnginePublisherPeerStateUpdatedEvent);
if (isPrimaryOrPublisher && event.state.isDisconnectedOrFailed()) {
// trigger reconnect sequence
_onDisconnected(DisconnectReason.peerConnection);
if (event.state.isDisconnectedOrFailed()) {
handleDisconnect(DisconnectReason.reconnect);
} else if (event.state.isClosed()) {
handleDisconnect(DisconnectReason.peerConnectionClosed);
}
});
@@ -599,21 +534,130 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
}
}
Future<void> _onDisconnected(DisconnectReason reason) async {
Future<void> handleDisconnect(DisconnectReason reason) async {
logger
.info('onDisconnected state:${_connectionState} reason:${reason.name}');
if (_connectionState == ConnectionState.disconnected) {
logger.fine('[$objectId] Already disconnected... $reason');
return;
if (!fullReconnect) {
fullReconnect = _clientConfiguration?.resumeConnection ==
lk_models.ClientConfigSetting.DISABLED ||
[
DisconnectReason.leaveReconnect,
DisconnectReason.negotiationFailed,
DisconnectReason.peerConnectionClosed
].contains(reason);
}
if (_connectionState == ConnectionState.reconnecting) {
if (_connectionState == ConnectionState.reconnecting && !fullReconnect) {
logger.fine('[$objectId] Already reconnecting...');
return;
}
logger.fine('[$runtimeType] Should attempt reconnect sequence...');
if (_connectionState == ConnectionState.disconnected) {
logger.fine('[$objectId] Already disconnected... $reason');
return;
}
await reconnect();
logger.fine('[$runtimeType] Should attempt reconnect sequence...');
if (fullReconnect) {
await restartConnection();
} else {
await resumeConnection();
}
}
Future<void> resumeConnection() async {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('resumeConnection: Already closed.');
return;
}
if (url == null || token == null) {
throw ConnectException(
'could not resume connection without url and token');
}
Future<void> sequence() async {
await signalClient.connect(
url!,
token!,
connectOptions: connectOptions,
roomOptions: roomOptions,
reconnect: true,
sid: _participantSid,
);
if (publisher == null || subscriber == null) {
throw UnexpectedStateException('publisher or subscribers is null');
}
subscriber!.restartingIce = true;
if (_hasPublished) {
logger.fine('resumeConnection: negotiating publisher...');
await publisher!.createAndSendOffer(const RTCOfferOptions(
iceRestart: true,
));
}
final iceConnected = primary?.pc.connectionState?.isConnected() ?? false;
logger.fine('resumeConnection: iceConnected: $iceConnected');
if (!iceConnected) {
logger.fine('resumeConnection: Waiting for primary to connect...');
await events.waitFor<EnginePeerStateUpdatedEvent>(
filter: (event) => event.isPrimary && event.state.isConnected(),
duration: connectOptions.timeouts.iceRestart,
onTimeout: () => throw ConnectException(),
);
}
}
try {
_updateConnectionState(ConnectionState.reconnecting);
await Utils.retry<void>(
(tries, errors) {
logger.fine('Retrying connect sequence remaining ${tries} tries...');
return sequence();
},
retryCondition: (_, __) =>
_connectionState == ConnectionState.reconnecting,
tries: 3,
delay: const Duration(seconds: 3),
);
_updateConnectionState(ConnectionState.connected);
} catch (error) {
_updateConnectionState(ConnectionState.disconnected);
}
}
Future<void> restartConnection([bool signalEvents = false]) async {
await publisher?.dispose();
publisher = null;
_hasPublished = false;
await subscriber?.dispose();
subscriber = null;
_reliableDCSub = null;
_reliableDCPub = null;
_lossyDCSub = null;
_lossyDCPub = null;
await _signalListener.cancelAll();
_signalListener = signalClient.createListener(synchronized: true);
_setUpSignalListeners();
await connect(
url!,
token!,
roomOptions: roomOptions,
connectOptions: connectOptions,
fastConnectOptions: fastConnectOptions,
);
fullReconnect = false;
}
@internal
@@ -650,6 +694,8 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
_serverProvidedIceServers = iceServersFromServer;
}
_clientConfiguration = event.response.clientConfiguration;
logger.fine('onConnected subscriberPrimary: ${_subscriberPrimary}, '
'serverVersion: ${event.response.serverVersion}, '
'iceServers: ${event.response.iceServers}, '
@@ -666,7 +712,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
})
..on<SignalConnectionStateUpdatedEvent>((event) async {
if (event.newState == ConnectionState.disconnected) {
await _onDisconnected(DisconnectReason.signal);
await handleDisconnect(DisconnectReason.signal);
}
})
..on<SignalOfferEvent>((event) async {
@@ -717,12 +763,20 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
token = event.token;
})
..on<SignalLeaveEvent>((event) async {
if (event.canReconnect) {
fullReconnect = true;
// reconnect immediately instead of waiting for next attempt
_connectionState = ConnectionState.reconnecting;
await handleDisconnect(DisconnectReason.leaveReconnect);
} else {
if (_connectionState == ConnectionState.reconnecting) {
logger.warning(
'[Signal] Received Leave while engine is reconnecting, ignoring...');
return;
}
_updateConnectionState(ConnectionState.disconnected);
await cleanUp();
}
});
}
@@ -752,16 +806,19 @@ extension EnginePrivateMethods on Engine {
newState: _connectionState,
oldState: oldState,
didReconnect: didReconnect,
fullReconnect: fullReconnect,
));
}
}
extension EngineInternalMethods on Engine {
@internal
List<lk_rtc.DataChannelInfo> dataChannelInfo() => [
_reliableDCPub,
_lossyDCPub
].whereNotNull().map((e) => e.toLKInfoType()).toList();
List<lk_rtc.DataChannelInfo> dataChannelInfo() =>
[_reliableDCPub, _lossyDCPub]
.whereNotNull()
.where((e) => e.id != -1)
.map((e) => e.toLKInfoType())
.toList();
}
Future<String?> getConnectedAddress(rtc.RTCPeerConnection pc) async {
+46 -10
View File
@@ -74,7 +74,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
// suppport for multiple event listeners
late final EventsListener<EngineEvent> _engineListener;
//
late final EventsListener<SignalEvent> _signalListener;
late EventsListener<SignalEvent> _signalListener;
Room({
ConnectOptions connectOptions = const ConnectOptions(),
@@ -140,11 +140,15 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
logger.fine('[Engine] Received JoinResponse, '
'serverVersion: ${event.response.serverVersion}');
_localParticipant = LocalParticipant(
_localParticipant ??= LocalParticipant(
room: this,
info: event.response.participant,
);
if (engine.fullReconnect) {
_localParticipant!.updateFromInfo(event.response.participant);
}
if (connectOptions.protocolVersion.index >= ProtocolVersion.v8.index &&
engine.fastConnectOptions != null) {
var options = engine.fastConnectOptions!;
@@ -264,19 +268,47 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
void _setUpEngineListeners() => _engineListener
..on<EngineConnectionStateUpdatedEvent>((event) async {
if (event.didReconnect) {
events.emit(const RoomReconnectedEvent());
// re-send tracks permissions
localParticipant?.sendTrackSubscriptionPermissions();
events.emit(const RoomReconnectedEvent());
await _handlePostReconnect(false);
} else if (event.fullReconnect &&
event.newState == ConnectionState.connecting) {
events.emit(const RoomRestartingEvent());
// clean up RemoteParticipants
for (final participant in _participants.values) {
events.emit(ParticipantDisconnectedEvent(participant: participant));
await participant.dispose();
}
_participants.clear();
_activeSpeakers.clear();
// reset params
_name = null;
_sid = null;
_metadata = null;
_serverVersion = null;
_serverRegion = null;
} else if (event.fullReconnect &&
event.newState == ConnectionState.connected) {
events.emit(const RoomRestartedEvent());
// recreate signal listener.
await _signalListener.cancelAll();
await _signalListener.dispose();
_signalListener = engine.signalClient.createListener();
_setUpSignalListeners();
await _handlePostReconnect(event.fullReconnect);
} else if (event.newState == ConnectionState.reconnecting) {
events.emit(const RoomReconnectingEvent());
} else if (event.newState == ConnectionState.disconnected) {
if (!event.fullReconnect) {
await _cleanUp();
events.emit(const RoomDisconnectedEvent());
}
}
// always notify ChangeNotifier
notifyListeners();
})
..on<RoomRestartingEvent>((event) {})
..on<RoomRestartedEvent>((event) {})
..on<EngineActiveSpeakersUpdateEvent>(
(event) => _onEngineActiveSpeakersUpdateEvent(event.speakers))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
@@ -319,7 +351,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
}
Future<void> reconnect() async {
await engine.reconnect();
await engine.restartConnection();
}
RemoteParticipant _getOrCreateRemoteParticipant(
@@ -528,8 +560,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
Future<void> _handlePostReconnect(bool isFullReconnect) async {
if (isFullReconnect) {
// TODO republish tracks on full reconnect
} else {
// re-publish all tracks
await localParticipant?.rePublishAllTracks();
}
for (var participant in participants.values) {
for (var pub in participant.trackPublications.values) {
if (pub.subscribed) {
@@ -539,7 +572,6 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
}
}
}
}
extension RoomPrivateMethods on Room {
// resets internal state to a re-usable state
@@ -585,13 +617,17 @@ extension RoomDebugMethods on Room {
bool? migration,
bool? serverLeave,
bool? switchCandidate,
bool? signalReconnect,
}) async {
if (signalReconnect != null && signalReconnect) {
await engine.signalClient.cleanUp();
return;
}
engine.signalClient.sendSimulateScenario(
speakerUpdate: speakerUpdate,
nodeFailure: nodeFailure,
migration: migration,
serverLeave: serverLeave,
switchCandidate: switchCandidate,
);
switchCandidate: switchCandidate);
}
}
+5
View File
@@ -35,6 +35,10 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
Duration? _pingIntervalDuration;
Timer? _pingIntervalTimer;
int get pingCount => _pingCount;
int _pingCount = 0;
@internal
SignalClient(WebSocketConnector wsConnector) : _wsConnector = wsConnector {
events.listen((event) {
@@ -240,6 +244,7 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
logger.info('signal message not set');
break;
case lk_rtc.SignalResponse_Message.pong:
_pingCount++;
_resetPingTimeout();
break;
default:
+9
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/src/options.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../internal/types.dart';
import '../logger.dart';
@@ -126,10 +127,18 @@ class Transport extends Disposable {
}
}
if (restartingIce && !rtc.WebRTC.platformIsWeb) {
await pc.restartIce();
}
// actually negotiate
logger.fine('starting to negotiate');
final offer = await pc.createOffer(options?.toMap() ?? <String, dynamic>{});
try {
await pc.setLocalDescription(offer);
} catch (e) {
throw NegotiationError(e.toString());
}
onOffer?.call(offer);
}
+14
View File
@@ -48,6 +48,20 @@ class RoomReconnectedEvent with RoomEvent {
String toString() => '${runtimeType}()';
}
class RoomRestartingEvent with RoomEvent {
const RoomRestartingEvent();
@override
String toString() => '${runtimeType}()';
}
class RoomRestartedEvent with RoomEvent {
const RoomRestartedEvent();
@override
String toString() => '${runtimeType}()';
}
/// Disconnected from the room
/// Emitted by [Room].
class RoomDisconnectedEvent with RoomEvent {
+5
View File
@@ -23,6 +23,11 @@ class UnexpectedStateException extends LiveKitException {
: super._(msg);
}
/// Exception thrown when pc negotiation fails.
class NegotiationError extends LiveKitException {
NegotiationError([String msg = 'Negotiation Error']) : super._(msg);
}
/// Failed to create a local track.
/// Common reasons:
/// - Required permissions not yet granted to the platform.
+3
View File
@@ -80,6 +80,9 @@ extension RTCPeerConnectionStateExt on rtc.RTCPeerConnectionState {
bool isConnected() =>
this == rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected;
bool isClosed() =>
this == rtc.RTCPeerConnectionState.RTCPeerConnectionStateClosed;
bool isDisconnectedOrFailed() => [
rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected,
rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed,
+2
View File
@@ -139,10 +139,12 @@ class SignalConnectionStateUpdatedEvent extends ConnectionStateUpdatedEvent
@internal
class EngineConnectionStateUpdatedEvent extends ConnectionStateUpdatedEvent
with EngineEvent {
final bool fullReconnect;
const EngineConnectionStateUpdatedEvent({
required ConnectionState newState,
required ConnectionState oldState,
required bool didReconnect,
required this.fullReconnect,
DisconnectReason? disconnectReason,
}) : super(
newState: newState,
+12
View File
@@ -240,6 +240,18 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
await pub.dispose();
}
Future<void> rePublishAllTracks() async {
final tracks = trackPublications.values.toList();
trackPublications.clear();
for (LocalTrackPublication track in tracks) {
if (track.track is LocalAudioTrack) {
await publishAudioTrack(track.track as LocalAudioTrack);
} else if (track.track is LocalVideoTrack) {
await publishVideoTrack(track.track as LocalVideoTrack);
}
}
}
/// Publish a new data payload to the room.
/// @param destinationSids When empty, data will be forwarded to each participant in the room.
Future<void> publishData(
+4 -3
View File
@@ -39,7 +39,7 @@ abstract class Participant<T extends TrackPublication>
double audioLevel = 0;
/// Server assigned unique id.
final String sid;
String sid;
/// User-assigned identity.
String identity;
@@ -160,7 +160,7 @@ abstract class Participant<T extends TrackPublication>
void updateFromInfo(lk_models.ParticipantInfo info) {
identity = info.identity;
_name = info.name;
// participantSid = info.sid;
sid = info.sid;
if (info.metadata.isNotEmpty) {
_setMetadata(info.metadata);
}
@@ -189,7 +189,8 @@ abstract class Participant<T extends TrackPublication>
Future<void> unpublishTrack(String trackSid, {bool notify = true});
/// Convenience method to unpublish all tracks.
Future<void> unpublishAllTracks({bool notify = true}) async {
Future<void> unpublishAllTracks(
{bool notify = true, bool? stopOnUnpublish}) async {
final trackSids = trackPublications.keys.toSet();
for (final trackid in trackSids) {
await unpublishTrack(trackid, notify: notify);
+11 -5
View File
@@ -180,18 +180,18 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
}
}
// unpublish any track that is not in the info
// remove any published track that is not in the info
final validSids = info.tracks.map((e) => e.sid);
final removeSids =
trackPublications.keys.where((e) => !validSids.contains(e)).toSet();
for (final sid in removeSids) {
await unpublishTrack(sid);
await removePublishedTrack(sid);
}
}
@override
Future<void> unpublishTrack(String trackSid, {bool notify = true}) async {
logger.finer('Unpublish track sid: $trackSid, notify: $notify');
Future<void> removePublishedTrack(String trackSid,
{bool notify = true}) async {
logger.finer('removePublishedTrack track sid: $trackSid, notify: $notify');
final pub = trackPublications.remove(trackSid);
if (pub == null) {
logger.warning('Publication not found $trackSid');
@@ -220,6 +220,12 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
await pub.dispose();
}
@Deprecated(
'`unpublishTrack` is deprecated, use `removePublishedTrack` instead')
@override
Future<void> unpublishTrack(String trackSid, {bool notify = true}) =>
removePublishedTrack(trackSid, notify: notify);
@internal
lk_models.ParticipantTracks participantTracks() =>
lk_models.ParticipantTracks(
+4 -1
View File
@@ -62,8 +62,11 @@ enum StreamState {
enum DisconnectReason {
user,
peerConnection,
peerConnectionClosed,
negotiationFailed,
signal,
reconnect,
leaveReconnect,
}
/// The reason why a track failed to publish.
+3 -3
View File
@@ -154,7 +154,7 @@ packages:
name: dart_webrtc
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
version: "1.0.10"
dbus:
dependency: transitive
description:
@@ -260,7 +260,7 @@ packages:
name: flutter_webrtc
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.11"
version: "0.9.12"
glob:
dependency: transitive
description:
@@ -552,7 +552,7 @@ packages:
name: webrtc_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.8"
version: "1.0.9"
win32:
dependency: transitive
description:
+3 -3
View File
@@ -23,10 +23,10 @@ dependencies:
uuid: ^3.0.6
synchronized: ^3.0.0+3
protobuf: ^2.1.0
flutter_webrtc: 0.9.11
dart_webrtc: 1.0.9
flutter_webrtc: 0.9.12
dart_webrtc: 1.0.10
device_info_plus: ^6.0.0
webrtc_interface: 1.0.8
webrtc_interface: 1.0.9
dev_dependencies:
flutter_test: