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 { enum SimulateScenarioResult {
signalReconnect,
nodeFailure, nodeFailure,
migration, migration,
serverLeave, serverLeave,
+2
View File
@@ -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,
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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);
);
} }
} }
+5
View File
@@ -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:
+10 -1
View File
@@ -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);
} }
+14
View File
@@ -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 {
+5
View File
@@ -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.
+3
View File
@@ -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,
+2
View File
@@ -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,
+12
View File
@@ -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(
+4 -3
View File
@@ -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);
+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 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(
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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: