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