Files
client-sdk-flutter/lib/src/core/signal_client.dart
T
2022-02-25 15:45:46 +09:00

382 lines
11 KiB
Dart

import 'dart:async';
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;
@internal
SignalClient(WebSocketConnector wsConnector) : _wsConnector = wsConnector {
events.listen((event) {
logger.fine('[SignalEvent] $event');
});
onDispose(() async {
await events.dispose();
await _cleanUp();
});
}
@internal
Future<void> connect(
String uriString,
String token, {
ConnectOptions? connectOptions,
bool reconnect = false,
}) async {
final rtcUri = await Utils.buildUri(
uriString,
token: token,
connectOptions: connectOptions,
reconnect: reconnect,
);
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: _handleError,
),
);
// 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,
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;
}
}
}
Future<void> _cleanUp() async {
await _ws?.dispose();
_ws = null;
}
@internal
Future<void> disconnect() async {
logger.fine('SignalClient disconnect');
await _cleanUp();
}
void _sendRequest(lk_rtc.SignalRequest req) {
if (_ws == null || isDisposed) {
logger.warning(
'[$objectId] Could not send message, not connected or already disposed');
return;
}
final buf = req.writeToBuffer();
_ws?.send(buf);
}
void _updateConnectionState(ConnectionState newValue) {
if (_connectionState == newValue) return;
logger.fine('SignalClient ConnectionState '
'${_connectionState.name} -> ${newValue.name}');
bool didReconnect = _connectionState == ConnectionState.reconnecting &&
newValue == ConnectionState.connected;
_connectionState = newValue;
events.emit(SignalConnectionStateUpdatedEvent(
connectionState: _connectionState,
didReconnect: didReconnect,
));
}
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:
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.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(SignalMuteTrackEvent(
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;
}
}
void _handleError(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);
}
}
}
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,
}) =>
_sendRequest(lk_rtc.SignalRequest(
syncState: lk_rtc.SyncState(
answer: answer,
subscription: subscription,
publishTracks: publishTracks,
),
));
@internal
void sendSimulateScenario({
int? speakerUpdate,
bool? nodeFailure,
bool? migration,
bool? serverLeave,
}) =>
_sendRequest(lk_rtc.SignalRequest(
simulate: lk_rtc.SimulateScenario(
speakerUpdate: speakerUpdate,
nodeFailure: nodeFailure,
migration: migration,
serverLeave: serverLeave,
),
));
}