Files
client-sdk-flutter/lib/src/core/signal_client.dart
T
CloudWebRTC 0d8441ebfb 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.
2022-11-08 16:06:09 +08:00

522 lines
15 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
import '../managers/event.dart';
import '../options.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import '../support/disposable.dart';
import '../support/websocket.dart';
import '../types/other.dart';
import '../types/video_dimensions.dart';
import '../utils.dart';
class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
// Connection state of the socket conection.
ConnectionState _connectionState = ConnectionState.disconnected;
final WebSocketConnector _wsConnector;
LiveKitWebSocket? _ws;
final _queue = Queue<lk_rtc.SignalRequest>();
Duration? _pingTimeoutDuration;
Timer? _pingTimeoutTimer;
Duration? _pingIntervalDuration;
Timer? _pingIntervalTimer;
int get pingCount => _pingCount;
int _pingCount = 0;
@internal
SignalClient(WebSocketConnector wsConnector) : _wsConnector = wsConnector {
events.listen((event) {
logger.fine('[SignalEvent] $event');
});
onDispose(() async {
await cleanUp();
await events.dispose();
});
}
@internal
Future<void> connect(
String uriString,
String token, {
required ConnectOptions connectOptions,
required RoomOptions roomOptions,
bool reconnect = false,
String? sid,
}) async {
final rtcUri = await Utils.buildUri(
uriString,
token: token,
connectOptions: connectOptions,
roomOptions: roomOptions,
reconnect: reconnect,
sid: sid,
);
logger.fine('SignalClient connecting with url: $rtcUri');
try {
_updateConnectionState(reconnect
? ConnectionState.reconnecting
: ConnectionState.connecting);
// Clean up existing socket
await cleanUp();
// Attempt to connect
_ws = await _wsConnector(
rtcUri,
WebSocketEventHandlers(
onData: _onSocketData,
onDispose: _onSocketDispose,
onError: _onSocketError,
),
);
// Successful connection
_updateConnectionState(ConnectionState.connected);
} catch (socketError) {
// Attempt Validation
try {
// Skip validation if reconnect mode
if (reconnect) rethrow;
// Re-build same uri for validate mode
final validateUri = await Utils.buildUri(
uriString,
token: token,
connectOptions: connectOptions,
roomOptions: roomOptions,
validate: true,
forceSecure: rtcUri.isSecureScheme,
);
final validateResponse = await http.get(validateUri);
if (validateResponse.statusCode != 200) {
throw ConnectException(validateResponse.body);
}
throw ConnectException();
} catch (error) {
// Pass it up if it's already a `ConnectError`
if (error is ConnectException) rethrow;
// HTTP doesn't work either
throw ConnectException();
} finally {
_updateConnectionState(ConnectionState.disconnected);
rethrow;
}
}
}
// resets internal state to a re-usable state
@internal
Future<void> cleanUp() async {
logger.fine('[${objectId}] cleanUp()');
await _ws?.dispose();
_ws = null;
_queue.clear();
_clearPingInterval();
}
void _sendRequest(
lk_rtc.SignalRequest req, {
bool enqueueIfReconnecting = true,
}) {
if (isDisposed) {
logger.warning('[$objectId] Could not send message, already disposed');
return;
}
if (_connectionState == ConnectionState.reconnecting &&
req._canQueue() &&
enqueueIfReconnecting) {
_queue.add(req);
return;
}
if (_ws == null) {
logger.warning('[$objectId] Could not send message, socket is null');
return;
}
_ws?.send(req.writeToBuffer());
}
Future<void> _onSocketData(dynamic message) async {
if (message is! List<int>) return;
final msg = lk_rtc.SignalResponse.fromBuffer(message);
switch (msg.whichMessage()) {
case lk_rtc.SignalResponse_Message.join:
if (msg.join.pingTimeout > 0) {
_pingTimeoutDuration = Duration(seconds: msg.join.pingTimeout);
_pingIntervalDuration = Duration(seconds: msg.join.pingInterval);
logger.info(
'ping config timeout: ${msg.join.pingTimeout}, interval: ${msg.join.pingInterval} ');
_startPingInterval();
}
events.emit(SignalJoinResponseEvent(response: msg.join));
break;
case lk_rtc.SignalResponse_Message.answer:
events.emit(SignalAnswerEvent(sd: msg.answer.toSDKType()));
break;
case lk_rtc.SignalResponse_Message.offer:
events.emit(SignalOfferEvent(sd: msg.offer.toSDKType()));
break;
case lk_rtc.SignalResponse_Message.trickle:
events.emit(SignalTrickleEvent(
candidate: RTCIceCandidateExt.fromJson(msg.trickle.candidateInit),
target: msg.trickle.target,
));
break;
case lk_rtc.SignalResponse_Message.update:
events.emit(SignalParticipantUpdateEvent(
participants: msg.update.participants));
break;
case lk_rtc.SignalResponse_Message.trackPublished:
events.emit(SignalLocalTrackPublishedEvent(
cid: msg.trackPublished.cid,
track: msg.trackPublished.track,
));
break;
case lk_rtc.SignalResponse_Message.trackUnpublished:
events.emit(SignalTrackUnpublishedEvent(
trackSid: msg.trackUnpublished.trackSid,
));
break;
case lk_rtc.SignalResponse_Message.speakersChanged:
events.emit(
SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
break;
case lk_rtc.SignalResponse_Message.roomUpdate:
events.emit(SignalRoomUpdateEvent(room: msg.roomUpdate.room));
break;
case lk_rtc.SignalResponse_Message.connectionQuality:
events.emit(SignalConnectionQualityUpdateEvent(
updates: msg.connectionQuality.updates,
));
break;
case lk_rtc.SignalResponse_Message.leave:
events.emit(SignalLeaveEvent(canReconnect: msg.leave.canReconnect));
break;
case lk_rtc.SignalResponse_Message.mute:
events.emit(SignalRemoteMuteTrackEvent(
sid: msg.mute.sid,
muted: msg.mute.muted,
));
break;
case lk_rtc.SignalResponse_Message.streamStateUpdate:
events.emit(SignalStreamStateUpdatedEvent(
updates: msg.streamStateUpdate.streamStates,
));
break;
case lk_rtc.SignalResponse_Message.subscribedQualityUpdate:
events.emit(SignalSubscribedQualityUpdatedEvent(
trackSid: msg.subscribedQualityUpdate.trackSid,
updates: msg.subscribedQualityUpdate.subscribedQualities,
));
break;
case lk_rtc.SignalResponse_Message.subscriptionPermissionUpdate:
events.emit(SignalSubscriptionPermissionUpdateEvent(
participantSid: msg.subscriptionPermissionUpdate.participantSid,
trackSid: msg.subscriptionPermissionUpdate.trackSid,
allowed: msg.subscriptionPermissionUpdate.allowed,
));
break;
case lk_rtc.SignalResponse_Message.refreshToken:
events.emit(SignalTokenUpdatedEvent(token: msg.refreshToken));
break;
case lk_rtc.SignalResponse_Message.notSet:
logger.info('signal message not set');
break;
case lk_rtc.SignalResponse_Message.pong:
_pingCount++;
_resetPingTimeout();
break;
default:
logger.warning('received unknown signal message');
}
}
void _onSocketError(dynamic error) {
logger.warning('received websocket error $error');
}
void _onSocketDispose() {
logger.fine('SignalClient onSocketDispose $_connectionState');
// don't emit event's when reconnecting state
if (_connectionState != ConnectionState.reconnecting) {
logger.fine('SignalClient did disconnect ${_connectionState}');
_updateConnectionState(ConnectionState.disconnected);
}
}
void _sendPing() {
_sendRequest(lk_rtc.SignalRequest()
..ping = Int64(DateTime.now().millisecondsSinceEpoch));
}
void _startPingInterval() {
_clearPingInterval();
_resetPingTimeout();
if (_pingIntervalDuration == null) {
logger.warning('ping timeout duration not set');
return;
}
_pingIntervalTimer ??=
Timer.periodic(_pingIntervalDuration!, (_) => _sendPing());
}
void _clearPingInterval() {
_clearPingTimeout();
_pingIntervalTimer?.cancel();
_pingIntervalTimer = null;
}
void _resetPingTimeout() {
_clearPingTimeout();
if (_pingTimeoutDuration == null) {
logger.warning('ping timeout duration not set');
return;
}
_pingTimeoutTimer ??= Timer(_pingTimeoutDuration!, () {
logger.warning('ping timeout');
_onSocketDispose();
});
}
void _clearPingTimeout() {
_pingTimeoutTimer?.cancel();
_pingTimeoutTimer = null;
}
}
extension SignalClientRequests on SignalClient {
@internal
void sendOffer(rtc.RTCSessionDescription offer) =>
_sendRequest(lk_rtc.SignalRequest(
offer: offer.toPBType(),
));
@internal
void sendAnswer(rtc.RTCSessionDescription answer) =>
_sendRequest(lk_rtc.SignalRequest(
answer: answer.toPBType(),
));
@internal
void sendIceCandidate(
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
_sendRequest(
lk_rtc.SignalRequest(
trickle: lk_rtc.TrickleRequest(
candidateInit: candidate.toJson(),
target: target,
),
),
);
@internal
void sendMuteTrack(String trackSid, bool muted) =>
_sendRequest(lk_rtc.SignalRequest(
mute: lk_rtc.MuteTrackRequest(
sid: trackSid,
muted: muted,
),
));
@internal
void sendAddTrack({
required String cid,
required String name,
required lk_models.TrackType type,
required lk_models.TrackSource source,
VideoDimensions? dimensions,
bool? dtx,
Iterable<lk_models.VideoLayer>? videoLayers,
}) {
final req = lk_rtc.AddTrackRequest(
cid: cid,
name: name,
type: type,
source: source,
);
if (type == lk_models.TrackType.VIDEO) {
// video specific
if (dimensions != null) {
req.width = dimensions.width;
req.height = dimensions.height;
}
if (videoLayers != null && videoLayers.isNotEmpty) {
req.layers
..clear()
..addAll(videoLayers);
}
}
if (type == lk_models.TrackType.AUDIO && dtx != null) {
// audio specific
req.disableDtx = !dtx;
}
_sendRequest(lk_rtc.SignalRequest(
addTrack: req,
));
}
@internal
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
_sendRequest(lk_rtc.SignalRequest(
trackSetting: settings,
));
@internal
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
_sendRequest(lk_rtc.SignalRequest(
subscription: subscription,
));
@internal
void sendUpdateVideoLayers(
String trackSid,
Iterable<lk_models.VideoLayer> layers,
) =>
_sendRequest(lk_rtc.SignalRequest(
updateLayers: lk_rtc.UpdateVideoLayers(
trackSid: trackSid,
layers: layers,
),
));
@internal
void sendUpdateSubscriptionPermissions({
required bool allParticipants,
required Iterable<lk_rtc.TrackPermission> trackPermissions,
}) =>
_sendRequest(lk_rtc.SignalRequest(
subscriptionPermission: lk_rtc.SubscriptionPermission(
allParticipants: allParticipants,
trackPermissions: trackPermissions,
),
));
@internal
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
leave: lk_rtc.LeaveRequest(),
));
@internal
void sendSyncState({
required lk_rtc.SessionDescription? answer,
required lk_rtc.UpdateSubscription subscription,
required Iterable<lk_rtc.TrackPublishedResponse>? publishTracks,
required Iterable<lk_rtc.DataChannelInfo>? dataChannelInfo,
}) =>
_sendRequest(lk_rtc.SignalRequest(
syncState: lk_rtc.SyncState(
answer: answer,
subscription: subscription,
publishTracks: publishTracks,
dataChannels: dataChannelInfo,
),
));
@internal
void sendSimulateScenario({
int? speakerUpdate,
bool? nodeFailure,
bool? migration,
bool? serverLeave,
bool? switchCandidate,
}) =>
_sendRequest(lk_rtc.SignalRequest(
simulate: lk_rtc.SimulateScenario(
speakerUpdate: speakerUpdate,
nodeFailure: nodeFailure,
migration: migration,
serverLeave: serverLeave,
switchCandidateProtocol: (switchCandidate != null && switchCandidate)
? lk_rtc.CandidateProtocol.TCP
: null,
),
));
}
// private methods
extension on lk_rtc.SignalRequest {
// returns if this request can be queued
bool _canQueue() => ![
// list of types that cannot be queued
lk_rtc.SignalRequest_Message.syncState,
lk_rtc.SignalRequest_Message.trickle,
lk_rtc.SignalRequest_Message.offer,
lk_rtc.SignalRequest_Message.answer,
lk_rtc.SignalRequest_Message.simulate
].contains(whichMessage());
}
extension SignalClientPrivateMethods on SignalClient {
void _updateConnectionState(ConnectionState newValue) {
if (_connectionState == newValue) return;
logger.fine('SignalClient ConnectionState '
'${_connectionState.name} -> ${newValue.name}');
bool didReconnect = _connectionState == ConnectionState.reconnecting &&
newValue == ConnectionState.connected;
final oldState = _connectionState;
if (newValue == ConnectionState.connected &&
oldState == ConnectionState.reconnecting) {
// restart ping interval as it's cleared for reconnection
_startPingInterval();
} else if (newValue == ConnectionState.reconnecting) {
// clear ping interval and restart it once reconnected
_clearPingInterval();
}
_connectionState = newValue;
events.emit(SignalConnectionStateUpdatedEvent(
newState: _connectionState,
oldState: oldState,
didReconnect: didReconnect,
));
}
}
// internal methods
extension SignalClientInternalMethods on SignalClient {
@internal
void sendQueuedRequests() {
// queue is empty
if (_queue.isEmpty) return;
// send requests
for (final request in _queue) {
_sendRequest(request, enqueueIfReconnecting: false);
}
_queue.clear();
}
@internal
void clearQueue() => _queue.clear();
}