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 {
|
||||
signalReconnect,
|
||||
nodeFailure,
|
||||
migration,
|
||||
serverLeave,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
@@ -538,7 +571,6 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomPrivateMethods on Room {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user