organize dirs

This commit is contained in:
Hiroshi Horie
2021-12-21 17:57:18 +07:00
parent 249525ee90
commit 1ed757aca0
32 changed files with 128 additions and 122 deletions
+607
View File
@@ -0,0 +1,607 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';
import '../constants.dart';
import '../events.dart';
import '../exceptions.dart';
import '../extensions.dart';
import '../internal/events.dart';
import '../logger.dart';
import '../managers/delay.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 '../types.dart';
import 'room.dart';
import 'signal_client.dart';
import 'transport.dart';
class Engine extends Disposable with EventsEmittable<EngineEvent> {
static const _lossyDCLabel = '_lossy';
static const _reliableDCLabel = '_reliable';
static const _maxReconnectAttempts = 5;
// Reference to the Room
final Room room;
final SignalClient signalClient;
@internal
PCTransport? publisher;
@internal
PCTransport? subscriber;
@internal
PCTransport? get primary => _subscriberPrimary ? subscriber : publisher;
// data channels for packets
rtc.RTCDataChannel? _reliableDC;
rtc.RTCDataChannel? _lossyDC;
rtc.RTCDataChannel? _reliableDCSub;
rtc.RTCDataChannel? _lossyDCSub;
rtc.RTCDataChannelState get reliableDataChannelState =>
_reliableDC?.state ?? rtc.RTCDataChannelState.RTCDataChannelClosed;
rtc.RTCDataChannelState get lossyDataChannelState =>
_lossyDC?.state ?? rtc.RTCDataChannelState.RTCDataChannelClosed;
bool _iceConnected = false;
ConnectionState _connectionState = ConnectionState.disconnected;
/// Connection state of the [Room].
ConnectionState get connectionState => _connectionState;
// true if publisher connection has already been established.
// this is helpful to know if we need to restart ICE on the publisher connection
bool _hasPublished = false;
// remember url and token for reconnect
String? url;
String? token;
bool _subscriberPrimary = false;
// server-provided ice servers
List<lk_rtc.ICEServer> _serverProvidedIceServers = [];
// internal
int _reconnectAttempts = 0;
late final _signalListener = signalClient.createListener(synchronized: true);
final delays = CancelableDelayManager();
Engine({
required this.room,
SignalClient? signalClient,
}) : signalClient = signalClient ?? SignalClient() {
if (kDebugMode) {
// log all EngineEvents
events.listen((event) =>
logger.fine('[EngineEvent] $objectId ${event.runtimeType}'));
}
_setUpListeners();
onDispose(() async {
await events.dispose();
await delays.dispose();
await close();
await _signalListener.dispose();
});
}
Future<lk_rtc.JoinResponse> connect(
String url,
String token,
) async {
this.url = url;
this.token = token;
// connect to rtc server
await signalClient.connect(
url,
token,
connectOptions: room.connectOptions,
);
// wait for join response
final event = await _signalListener.waitFor<SignalConnectedEvent>(
duration: Timeouts.connection,
onTimeout: () => throw ConnectException(),
);
return event.response;
}
/// Close connection between the server.
Future<void> close() async {
logger.fine('[$objectId] close()');
if (_connectionState == ConnectionState.disconnected) {
logger.warning('[$objectId]: close() already disconnected');
}
// _statsTimer.cancel();
// cancel all ongoing delays
await delays.cancelAll();
// PCTransport is responsible for disposing RTCPeerConnection
await publisher?.dispose();
publisher = null;
await subscriber?.dispose();
subscriber = null;
await signalClient.close();
_connectionState = ConnectionState.disconnected;
// notifyListeners();
}
@internal
Future<lk_models.TrackInfo> addTrack({
required String cid,
required String name,
required lk_models.TrackType kind,
required lk_models.TrackSource source,
VideoDimensions? dimensions,
bool? dtx,
List<lk_models.VideoLayer>? videoLayers,
}) async {
// TODO: Check if cid already published
// send request to add track
signalClient.sendAddTrack(
cid: cid,
name: name,
type: kind,
source: source,
dimensions: dimensions,
dtx: dtx,
videoLayers: videoLayers,
);
// wait for response, or timeout
final event = await _signalListener.waitFor<SignalLocalTrackPublishedEvent>(
filter: (event) => event.cid == cid,
duration: Timeouts.publish,
onTimeout: () => throw TrackPublishException(),
);
return event.track;
}
@internal
Future<void> negotiate({bool? iceRestart}) async {
if (publisher == null) {
return;
}
_hasPublished = true;
publisher!.negotiate(null);
}
@internal
Future<void> sendDataPacket(
lk_models.DataPacket packet,
) async {
// make sure we do have a data connection
await _ensurePublisherConnected();
// construct the data channel message
final message =
rtc.RTCDataChannelMessage.fromBinary(packet.writeToBuffer());
// chose data channel
final rtc.RTCDataChannel? channel =
packet.kind == lk_models.DataPacket_Kind.LOSSY ? _lossyDC : _reliableDC;
// send if channel exists
if (channel == null) {
throw UnexpectedStateException('Data channel is not ready');
}
logger.fine('sendDataPacket(label:${channel.label})');
await channel.send(message);
}
Future<void> _ensurePublisherConnected() async {
logger.fine('ensurePublisherConnected()');
if (!_subscriberPrimary) {
return;
}
if (publisher?.pc.iceConnectionState?.isConnected() == true) {
logger.warning('[$objectId] publisher is already connected');
return;
}
// start negotiation
await negotiate();
logger.fine('[PUBLISHER] waiting for to ice-connect '
'(current: ${publisher?.pc.iceConnectionState})');
await events.waitFor<EnginePublisherIceStateUpdatedEvent>(
filter: (event) => event.iceState.isConnected(),
duration: Timeouts.iceConnection,
);
logger.fine('[PUBLISHER] connected');
}
@internal
Future<void> reconnect() async {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('$objectId reconnect() already closed');
return;
}
final url = this.url;
final token = this.token;
if (url == null || token == null) {
throw ConnectException('could not reconnect without url and token');
}
if (_reconnectAttempts == 0) {
events.emit(const EngineReconnectingEvent());
}
_reconnectAttempts++;
try {
// isReconnecting = true;
_connectionState = ConnectionState.reconnecting;
await signalClient.reconnect(
url,
token,
connectOptions: room.connectOptions,
);
if (publisher == null || subscriber == null) {
throw UnexpectedStateException('publisher or subscribers is null');
}
subscriber!.restartingIce = true;
// await negotiate(iceRestart: true);
if (_hasPublished) {
logger.fine('reconnect: publisher.createAndSendOffer');
await publisher!
.createAndSendOffer(const RTCOfferOptions(iceRestart: true));
}
if (!(primary?.pc.iceConnectionState?.isConnected() ?? false)) {
logger.fine('reconnect: waiting for primary to ice-connect...');
await events.waitFor<EngineIceStateUpdatedEvent>(
filter: (event) => event.isPrimary && event.iceState.isConnected(),
duration: Timeouts.iceRestart,
);
}
logger.fine('reconnect: success');
events.emit(const EngineReconnectedEvent());
_reconnectAttempts = 0;
// don't catch and pass up any exception
} finally {
// always set reconnecting to false
// isReconnecting = false;
_connectionState = ConnectionState.disconnected;
}
}
Future<void> _configurePeerConnections() async {
if (publisher != null || subscriber != null) {
logger.warning('Already configured');
return;
}
// RTCConfiguration? config;
// use server-provided iceServers if not provided by user
final connectOptions = room.connectOptions ?? const ConnectOptions();
final serverIceServers =
_serverProvidedIceServers.map((e) => e.toSDKType()).toList();
RTCConfiguration rtcConfiguration = connectOptions.rtcConfiguration;
if (serverIceServers.isNotEmpty) {
// use server provided iceServers if exists
rtcConfiguration = connectOptions.rtcConfiguration
.copyWith(iceServers: serverIceServers);
}
publisher = await PCTransport.create(rtcConfiguration);
subscriber = await PCTransport.create(rtcConfiguration);
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
logger.fine('publisher onIceCandidate');
signalClient.sendIceCandidate(candidate, lk_rtc.SignalTarget.PUBLISHER);
};
subscriber?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
logger.fine('subscriber onIceCandidate');
signalClient.sendIceCandidate(candidate, lk_rtc.SignalTarget.SUBSCRIBER);
};
publisher?.onOffer = (offer) {
logger.fine('publisher onOffer');
signalClient.sendOffer(offer);
};
// in subscriber primary mode, server side opens sub data channels.
if (_subscriberPrimary) {
subscriber?.pc.onDataChannel = _onDataChannel;
}
subscriber?.pc.onIceConnectionState =
(state) => events.emit(EngineSubscriberIceStateUpdatedEvent(
state: state,
isPrimary: _subscriberPrimary,
));
publisher?.pc.onIceConnectionState =
(state) => events.emit(EnginePublisherIceStateUpdatedEvent(
state: state,
isPrimary: !_subscriberPrimary,
));
events.on<EngineIceStateUpdatedEvent>((event) {
// only listen to primary ice events
if (!event.isPrimary) return;
if (event.iceState ==
rtc.RTCIceConnectionState.RTCIceConnectionStateConnected) {
if (!_iceConnected) {
_iceConnected = true;
if (_connectionState == ConnectionState.reconnecting) {
events.emit(const EngineReconnectedEvent());
} else {
events.emit(const EngineConnectedEvent());
}
}
} else if (event.iceState ==
rtc.RTCIceConnectionState.RTCIceConnectionStateFailed) {
// trigger reconnect sequence
if (_iceConnected) {
_iceConnected = false;
_onDisconnected('peerconnection');
}
}
});
subscriber?.pc.onTrack = (rtc.RTCTrackEvent event) {
logger.fine('[WebRTC] pc.onTrack');
final stream = event.streams.firstOrNull;
if (stream == null) {
// we need the stream to get the track's id
logger.severe('received track without mediastream');
return;
}
// doesn't get called reliably
event.track.onEnded = () {
logger.fine('[WebRTC] track.onEnded');
};
// doesn't get called reliably
stream.onRemoveTrack = (_) {
logger.fine('[WebRTC] stream.onRemoveTrack');
};
events.emit(EngineTrackAddedEvent(
track: event.track,
stream: stream,
receiver: event.receiver,
));
};
// doesn't get called reliably, doesn't work on mac
subscriber?.pc.onRemoveTrack =
(rtc.MediaStream stream, rtc.MediaStreamTrack track) {
logger.fine('[WebRTC] ${track.id} pc.onRemoveTrack');
};
// also handle messages over the pub channel, for backwards compatibility
try {
final lossyInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true
..maxRetransmits = 0;
_lossyDC =
await publisher?.pc.createDataChannel(_lossyDCLabel, lossyInit);
_lossyDC?.onMessage = _onDCMessage;
_lossyDC?.stateChangeStream
.listen((state) => _onDCStateUpdated(Reliability.lossy, state));
} catch (_) {
logger.severe('[$objectId] createDataChannel() did throw $_');
}
try {
final reliableInit = rtc.RTCDataChannelInit()
..binaryType = 'binary'
..ordered = true;
_reliableDC =
await publisher?.pc.createDataChannel(_reliableDCLabel, reliableInit);
_reliableDC?.onMessage = _onDCMessage;
_reliableDC?.stateChangeStream
.listen((state) => _onDCStateUpdated(Reliability.reliable, state));
} catch (_) {
logger.severe('[$objectId] createDataChannel() did throw $_');
}
}
void _onDataChannel(rtc.RTCDataChannel dc) {
switch (dc.label) {
case _reliableDCLabel:
logger.fine('Server opened DC label: ${dc.label}');
_reliableDCSub = dc;
_reliableDCSub?.onMessage = _onDCMessage;
_reliableDCSub?.stateChangeStream
.listen((state) => _onDCStateUpdated(Reliability.reliable, state));
break;
case _lossyDCLabel:
logger.fine('Server opened DC label: ${dc.label}');
_lossyDCSub = dc;
_lossyDCSub?.onMessage = _onDCMessage;
_lossyDCSub?.stateChangeStream
.listen((event) => _onDCStateUpdated(Reliability.lossy, event));
break;
default:
logger.warning('Unknown DC label: ${dc.label}');
break;
}
}
void _onDCStateUpdated(
Reliability channel,
rtc.RTCDataChannelState state,
) {
logger.fine('Data channel state updated ${channel} ${state}');
}
void _onDCMessage(rtc.RTCDataChannelMessage message) {
// always expect binary
if (!message.isBinary) {
logger.warning('Data message is not binary');
return;
}
final dp = lk_models.DataPacket.fromBuffer(message.binary);
if (dp.whichValue() == lk_models.DataPacket_Value.speaker) {
// Speaker packet
events
.emit(EngineActiveSpeakersUpdateEvent(speakers: dp.speaker.speakers));
} else if (dp.whichValue() == lk_models.DataPacket_Value.user) {
// User packet
events.emit(EngineDataPacketReceivedEvent(
packet: dp.user,
kind: dp.kind,
));
}
}
Future<void> _onDisconnected(String reason) async {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('[$objectId] Already disconnected $reason');
return;
}
logger.fine('[$objectId] Disconnected $reason');
if (_reconnectAttempts >= _maxReconnectAttempts) {
logger.info('[$objectId] Could not connect '
'after ${_reconnectAttempts} attempts, giving up');
await close();
events.emit(const EngineDisconnectedEvent());
return;
}
final delay =
Duration(milliseconds: (_reconnectAttempts * _reconnectAttempts) * 300);
// if this instance is disposed, we probably don't want to continue any more
// so the whole block will be canceled from being executed
await delays.waitFor(delay, ifNotCancelled: () async {
try {
await reconnect();
_reconnectAttempts = 0;
} catch (_) {
// doesn't need to be awaited
// ignore: unawaited_futures
_onDisconnected(reason);
}
});
}
void _setUpListeners() => _signalListener
..on<SignalConnectedEvent>((event) async {
// create peer connections
_connectionState = ConnectionState.connected;
_subscriberPrimary = event.response.subscriberPrimary;
_serverProvidedIceServers = event.response.iceServers;
logger.fine('onConnected subscriberPrimary: ${_subscriberPrimary}, '
'serverVersion: ${event.response.serverVersion}, '
'iceServers: ${event.response.iceServers}');
await _configurePeerConnections();
if (!_subscriberPrimary) {
// for subscriberPrimary, we negotiate when necessary (lazy)
await negotiate();
}
})
..on<SignalCloseEvent>((_) async {
await _onDisconnected('signal');
})
..on<SignalOfferEvent>((event) async {
if (subscriber == null) {
logger.warning('[$objectId] subscriber is null');
return;
}
logger.fine('[$objectId] Received server offer(type: ${event.sd.type}, '
'${subscriber!.pc.signalingState})');
logger.finer('sdp: ${event.sd.sdp}');
await subscriber!.setRemoteDescription(event.sd);
try {
final answer = await subscriber!.pc.createAnswer();
logger.fine('Created answer');
logger.finer('sdp: ${answer.sdp}');
await subscriber!.pc.setLocalDescription(answer);
signalClient.sendAnswer(answer);
} catch (_) {
logger.severe('[$objectId] Failed to createAnswer()');
}
})
..on<SignalAnswerEvent>((event) async {
if (publisher == null) {
return;
}
logger.fine('received answer (type: ${event.sd.type})');
logger.finer('sdp: ${event.sd.sdp}');
await publisher!.setRemoteDescription(event.sd);
})
..on<SignalTrickleEvent>((event) async {
if (publisher == null || subscriber == null) {
logger.warning(
'Received ${SignalTrickleEvent} but publisher or subscriber was null.');
return;
}
logger.fine('got ICE candidate from peer');
if (event.target == lk_rtc.SignalTarget.SUBSCRIBER) {
await subscriber!.addIceCandidate(event.candidate);
} else if (event.target == lk_rtc.SignalTarget.PUBLISHER) {
await publisher!.addIceCandidate(event.candidate);
}
})
// relay
..on<SignalParticipantUpdateEvent>((event) => events.emit(event))
// relay
..on<SignalSpeakersChangedEvent>((event) => events.emit(event))
// relay
..on<SignalConnectionQualityUpdateEvent>((event) => events.emit(event))
// relay
..on<SignalStreamStateUpdatedEvent>((event) => events.emit(event))
..on<SignalLeaveEvent>((event) async {
await close();
events.emit(const EngineDisconnectedEvent());
})
..on<SignalMuteTrackEvent>(
(event) => events.emit(EngineRemoteMuteChangedEvent(
sid: event.sid,
muted: event.muted,
)));
}
+418
View File
@@ -0,0 +1,418 @@
import 'dart:collection';
import 'package:collection/collection.dart';
import '../constants.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 '../participant/local.dart';
import '../participant/participant.dart';
import '../participant/remote.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../proto/livekit_rtc.pb.dart' as lk_rtc;
import '../support/disposable.dart';
import '../track/track.dart';
import '../types.dart';
import 'engine.dart';
/// Room is the primary construct for LiveKit conferences. It contains a
/// group of [Participant]s, each publishing and subscribing to [Track]s.
/// Notifies changes to its state via two ways, by assigning a delegate, or using
/// it as a provider.
/// Room will trigger a change notification update when
/// * state changes
/// * participant membership changes
/// * active speakers are different
/// {@category Room}
class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
// Room is only instantiated if connected, so defaults to connected.
ConnectionState _connectionState = ConnectionState.connected;
/// connection state of the room
ConnectionState get connectionState => _connectionState;
final _participants = <String, RemoteParticipant>{};
/// map of SID to RemoteParticipant
UnmodifiableMapView<String, RemoteParticipant> get participants =>
UnmodifiableMapView(_participants);
ConnectOptions? connectOptions;
RoomOptions? roomOptions;
/// the current participant
LocalParticipant? localParticipant;
/// name of the room
String? name;
/// sid of the room
String? sid;
List<Participant> _activeSpeakers = [];
/// a list of participants that are actively speaking, including local participant.
UnmodifiableListView<Participant> get activeSpeakers =>
UnmodifiableListView<Participant>(_activeSpeakers);
late final engine = Engine(room: this);
// suppport for multiple event listeners
late final _engineListener = engine.createListener();
Room({
this.connectOptions,
this.roomOptions,
}) {
//
_setUpListeners();
// Any event emitted will trigger ChangeNotifier
events.listen((event) {
logger.fine('[RoomEvent] $event, will notifyListeners()');
notifyListeners();
});
onDispose(() async {
// dispose events
await events.dispose();
// dispose local participant
await localParticipant?.dispose();
// dispose all listeners for RTCEngine
await _engineListener.dispose();
// dispose the engine
await engine.dispose();
});
}
Future<void> connect(
String url,
String token, {
ConnectOptions? connectOptions,
RoomOptions? roomOptions,
}) async {
// update options if provided
this.connectOptions = connectOptions ?? this.connectOptions;
this.roomOptions = roomOptions ?? this.roomOptions;
final joinResponse = await engine.connect(
url,
token,
);
sid = joinResponse.room.sid;
name = joinResponse.room.name;
logger.fine(
'Connected to LiveKit server, version: ${joinResponse.serverVersion}');
localParticipant = LocalParticipant(
room: this,
info: joinResponse.participant,
);
for (final info in joinResponse.otherParticipants) {
logger.fine('Creating RemoteParticipant: ${info.sid}(${info.identity}) '
'tracks:${info.tracks.map((e) => e.sid)}');
_getOrCreateRemoteParticipant(info.sid, info);
}
logger.fine('Waiting to engine connect...');
// wait until engine is connected
await _engineListener.waitFor<EngineConnectedEvent>(
duration: Timeouts.connection,
onTimeout: () => throw ConnectException(),
);
logger.fine('Room Connect completed');
}
void _setUpListeners() => _engineListener
..on<EngineConnectedEvent>((event) async {
_connectionState = ConnectionState.connected;
notifyListeners();
})
..on<EngineReconnectedEvent>((event) async {
_connectionState = ConnectionState.connected;
events.emit(const RoomReconnectedEvent());
})
..on<EngineReconnectingEvent>((event) async {
_connectionState = ConnectionState.reconnecting;
events.emit(const RoomReconnectingEvent());
})
..on<EngineDisconnectedEvent>((event) => _handleClose())
..on<SignalParticipantUpdateEvent>(
(event) => _onParticipantUpdateEvent(event.participants))
..on<EngineActiveSpeakersUpdateEvent>(
(event) => _onEngineActiveSpeakersUpdateEvent(event.speakers))
..on<SignalSpeakersChangedEvent>(
(event) => _onSignalSpeakersChangedEvent(event.speakers))
..on<SignalConnectionQualityUpdateEvent>(
(event) => _onSignalConnectionQualityUpdateEvent(event.updates))
..on<SignalStreamStateUpdatedEvent>(
(event) => _onSignalStreamStateUpdateEvent(event.updates))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
..on<EngineRemoteMuteChangedEvent>((event) async {
final publication = localParticipant?.trackPublications[event.sid];
if (event.muted) {
await publication?.mute();
} else {
await publication?.unmute();
}
})
..on<EngineTrackAddedEvent>((event) async {
logger.fine('EngineTrackAddedEvent trackSid:${event.track.id}');
final idParts = event.stream.id.split('|');
final participantSid = idParts[0];
final trackSid = idParts.elementAtOrNull(1) ?? event.track.id;
final participant = _getOrCreateRemoteParticipant(participantSid, null);
try {
if (trackSid == null || trackSid.isEmpty) {
throw TrackSubscriptionExceptionEvent(
participant: participant,
reason: TrackSubscribeFailReason.invalidServerResponse,
);
}
await participant.addSubscribedMediaTrack(
event.track,
event.stream,
trackSid,
);
} on TrackSubscriptionExceptionEvent catch (event) {
logger.severe('addSubscribedMediaTrack() throwed ${event}');
[participant.room.events, participant.events].emit(event);
} catch (exception) {
// We don't want to pass up any exception so catch everything here.
logger.warning(
'Unknown exception on addSubscribedMediaTrack() ${exception}');
}
});
/// Disconnects from the room, notifying server of disconnection.
Future<void> disconnect() async {
if (_connectionState != ConnectionState.disconnected) {
engine.signalClient.sendLeave();
}
await _handleClose();
}
Future<void> reconnect() async {
await engine.reconnect();
}
RemoteParticipant _getOrCreateRemoteParticipant(
String sid, lk_models.ParticipantInfo? info) {
RemoteParticipant? participant = _participants[sid];
if (participant != null) {
return participant;
}
if (info == null) {
logger.warning('RemoteParticipant.info is null trackSid: $sid');
participant = RemoteParticipant(
room: this,
sid: sid,
identity: '',
);
} else {
participant = RemoteParticipant.fromInfo(
room: this,
info: info,
);
}
_participants[sid] = participant;
return participant;
}
// there should be no problem calling this method multiple times
Future<void> _handleClose() async {
logger.fine('[$objectId] _handleClose()');
if (_connectionState == ConnectionState.disconnected) {
logger.warning('[$objectId]: close() already disconnected');
}
// clean up RemoteParticipants
for (final _ in _participants.values.toList()) {
// RemoteParticipant is responsible for disposing resources
await _.dispose();
}
_participants.clear();
// clean up LocalParticipant
await localParticipant?.unpublishAllTracks();
// clean up engine
await engine.close();
_activeSpeakers.clear();
// only notify if was not disconnected
if (_connectionState != ConnectionState.disconnected) {
_connectionState = ConnectionState.disconnected;
events.emit(const RoomDisconnectedEvent());
}
}
Future<void> _onParticipantUpdateEvent(
List<lk_models.ParticipantInfo> updates) async {
// trigger change notifier only if list of participants membership is changed
var hasChanged = false;
for (final info in updates) {
if (localParticipant?.sid == info.sid) {
localParticipant?.updateFromInfo(info);
continue;
}
if (info.state == lk_models.ParticipantInfo_State.DISCONNECTED) {
hasChanged = true;
await _handleParticipantDisconnect(info.sid);
continue;
}
final isNew = !_participants.containsKey(info.sid);
final participant = _getOrCreateRemoteParticipant(info.sid, info);
if (isNew) {
hasChanged = true;
events.emit(ParticipantConnectedEvent(participant: participant));
} else {
await participant.updateFromInfo(info);
}
}
if (hasChanged) {
notifyListeners();
}
}
void _onSignalSpeakersChangedEvent(List<lk_models.SpeakerInfo> speakers) {
//
final lastSpeakers = {
for (final p in _activeSpeakers) p.sid: p,
};
for (final speaker in speakers) {
Participant? p = _participants[speaker.sid];
if (speaker.sid == localParticipant?.sid) p = localParticipant;
if (p == null) continue;
p.audioLevel = speaker.level;
p.isSpeaking = speaker.active;
if (speaker.active) {
lastSpeakers[speaker.sid] = p;
} else {
lastSpeakers.remove(speaker.sid);
}
}
final activeSpeakers = lastSpeakers.values.toList();
activeSpeakers.sort((a, b) => b.audioLevel.compareTo(a.audioLevel));
_activeSpeakers = activeSpeakers;
events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
}
// from data channel
// updates are sent only when there's a change to speaker ordering
void _onEngineActiveSpeakersUpdateEvent(
List<lk_models.SpeakerInfo> speakers) {
List<Participant> activeSpeakers = [];
// localParticipant & remote participants
final allParticipants = <String, Participant>{
if (localParticipant != null) localParticipant!.sid: localParticipant!,
..._participants,
};
for (final speaker in speakers) {
final p = allParticipants[speaker.sid];
if (p != null) {
p.audioLevel = speaker.level;
p.isSpeaking = true;
activeSpeakers.add(p);
}
}
// clear if not in the speakers list
final speakerSids = speakers.map((e) => e.sid).toSet();
for (final p in allParticipants.values) {
if (!speakerSids.contains(p.sid)) {
p.audioLevel = 0;
p.isSpeaking = false;
}
}
_activeSpeakers = activeSpeakers;
events.emit(ActiveSpeakersChangedEvent(speakers: activeSpeakers));
}
void _onSignalConnectionQualityUpdateEvent(
List<lk_rtc.ConnectionQualityInfo> updates) {
for (final entry in updates) {
Participant? participant;
if (entry.participantSid == localParticipant?.sid) {
participant = localParticipant;
} else {
participant = _participants[entry.participantSid];
}
if (participant != null) {
// update the connection quality if the participant is found
participant.updateConnectionQuality(entry.quality.toLKType());
}
}
}
void _onSignalStreamStateUpdateEvent(
List<lk_rtc.StreamStateInfo> updates) async {
for (final update in updates) {
// try to find RemoteParticipant
final participant = participants[update.participantSid];
if (participant == null) continue;
// try to find RemoteTrackPublication
final trackPublication = participant.trackPublications[update.trackSid];
if (trackPublication == null) continue;
// update the stream state
await trackPublication.updateStreamState(update.state.toLKType());
}
}
void _onDataMessageEvent(EngineDataPacketReceivedEvent dataPacketEvent) {
// participant may be null if data is sent from Server-API
final senderSid = dataPacketEvent.packet.participantSid;
RemoteParticipant? senderParticipant;
if (senderSid.isNotEmpty) {
senderParticipant = participants[dataPacketEvent.packet.participantSid];
}
// participant.delegate?.onDataReceived(participant, event.packet.payload);
final event = DataReceivedEvent(
participant: senderParticipant,
data: dataPacketEvent.packet.payload,
);
senderParticipant?.events.emit(event);
events.emit(event);
}
Future<void> _handleParticipantDisconnect(String sid) async {
final participant = _participants.remove(sid);
if (participant == null) {
return;
}
await participant.unpublishAllTracks(notify: true);
events.emit(ParticipantDisconnectedEvent(participant: participant));
}
}
+292
View File
@@ -0,0 +1,292 @@
import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:http/http.dart' as http;
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.dart';
import '../utils.dart';
class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
//
bool _connected = false;
LiveKitWebSocket? _ws;
SignalClient() {
events.listen((event) {
logger.fine('[SignalEvent] $event');
});
onDispose(() async {
await events.dispose();
await close();
});
}
bool get connected => _connected;
Future<void> connect(
String uriString,
String token, {
ConnectOptions? connectOptions,
}) async {
final rtcUri = Utils.buildUri(
uriString,
token: token,
connectOptions: connectOptions,
);
try {
_ws = await LiveKitWebSocket.connect(
rtcUri,
WebSocketEventHandlers(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
),
);
} catch (socketError) {
// Re-build same uri for validate mode
final validateUri = Utils.buildUri(
uriString,
token: token,
connectOptions: connectOptions,
validate: true,
forceSecure: rtcUri.isSecureScheme,
);
// Attempt Validation
try {
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();
}
}
}
Future<void> reconnect(
String uriString,
String token, {
ConnectOptions? connectOptions,
}) async {
_connected = false;
await _ws?.dispose();
_ws = null;
final rtcUri = Utils.buildUri(
uriString,
token: token,
reconnect: true,
connectOptions: connectOptions,
);
_ws = await LiveKitWebSocket.connect(
rtcUri,
WebSocketEventHandlers(
onData: _onSocketData,
onDispose: _onSocketDone,
onError: _handleError,
),
);
_connected = true;
}
Future<void> close() async {
_connected = false;
await _ws?.dispose();
}
void sendOffer(rtc.RTCSessionDescription offer) =>
_sendRequest(lk_rtc.SignalRequest(
offer: offer.toSDKType(),
));
void sendAnswer(rtc.RTCSessionDescription answer) =>
_sendRequest(lk_rtc.SignalRequest(
answer: answer.toSDKType(),
));
void sendIceCandidate(
rtc.RTCIceCandidate candidate, lk_rtc.SignalTarget target) =>
_sendRequest(
lk_rtc.SignalRequest(
trickle: lk_rtc.TrickleRequest(
candidateInit: candidate.toJson(),
target: target,
),
),
);
void sendMuteTrack(String trackSid, bool muted) =>
_sendRequest(lk_rtc.SignalRequest(
mute: lk_rtc.MuteTrackRequest(
sid: trackSid,
muted: muted,
),
));
void sendAddTrack({
required String cid,
required String name,
required lk_models.TrackType type,
required lk_models.TrackSource source,
VideoDimensions? dimensions,
bool? dtx,
List<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,
));
}
void sendUpdateTrackSettings(lk_rtc.UpdateTrackSettings settings) =>
_sendRequest(lk_rtc.SignalRequest(
trackSetting: settings,
));
void sendUpdateSubscription(lk_rtc.UpdateSubscription subscription) =>
_sendRequest(lk_rtc.SignalRequest(
subscription: subscription,
));
void sendUpdateVideoLayers(
String trackSid,
List<lk_models.VideoLayer> layers,
) =>
_sendRequest(lk_rtc.SignalRequest(
updateLayers: lk_rtc.UpdateVideoLayers(
trackSid: trackSid,
layers: layers,
),
));
void sendLeave() => _sendRequest(lk_rtc.SignalRequest(
leave: lk_rtc.LeaveRequest(),
));
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);
}
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 (!_connected) {
_connected = true;
events.emit(SignalConnectedEvent(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.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;
default:
logger.warning('skipping unsupported signal message');
}
}
void _handleError(dynamic error) {
logger.warning('received websocket error $error');
}
void _onSocketDone() {
if (!_connected) return;
_ws = null;
_connected = false;
events.emit(const SignalCloseEvent());
}
}
+162
View File
@@ -0,0 +1,162 @@
import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../constants.dart';
import '../extensions.dart';
import '../logger.dart';
import '../support/disposable.dart';
import '../types.dart';
import '../utils.dart';
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
/// a wrapper around PeerConnection
class PCTransport extends Disposable {
final rtc.RTCPeerConnection pc;
final List<rtc.RTCIceCandidate> _pendingCandidates = [];
bool restartingIce = false;
bool renegotiate = false;
PCTransportOnOffer? onOffer;
Function? _cancelDebounce;
// private constructor
PCTransport._(this.pc) {
//
onDispose(() async {
_cancelDebounce?.call();
_cancelDebounce = null;
// Ensure callbacks won't fire any more
pc.onRenegotiationNeeded = null;
pc.onIceCandidate = null;
pc.onIceConnectionState = null;
pc.onTrack = null;
// Remove all senders
List<rtc.RTCRtpSender> senders = [];
try {
senders = await pc.getSenders();
} catch (_) {
logger.warning('getSenders() failed with error: $_');
}
for (final e in senders) {
try {
await pc.removeTrack(e);
} catch (_) {
logger.warning('removeTrack() failed with error: $_');
}
}
await pc.close();
await pc.dispose();
});
}
static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async {
rtcConfig ??= const RTCConfiguration();
logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
final _ = await rtc.createPeerConnection(rtcConfig.toMap());
return PCTransport._(_);
}
late final negotiate = Utils.createDebounceFunc(
(void _) => createAndSendOffer(),
cancelFunc: (f) => _cancelDebounce = f,
wait: Timeouts.debounce,
);
Future<void> setRemoteDescription(rtc.RTCSessionDescription sd) async {
if (isDisposed) {
logger.warning('[$objectId] setRemoteDescription() already disposed');
return;
}
await pc.setRemoteDescription(sd);
for (final candidate in _pendingCandidates) {
await pc.addCandidate(candidate);
}
_pendingCandidates.clear();
restartingIce = false;
if (renegotiate) {
renegotiate = false;
await createAndSendOffer(); // await or un-awaited ?
}
}
Future<void> createAndSendOffer([RTCOfferOptions? options]) async {
if (isDisposed) {
logger.warning('[$objectId] createAndSendOffer() already disposed');
return;
}
if (onOffer == null) {
logger.warning('onOffer is null');
return;
}
if (options?.iceRestart ?? false) {
logger.fine('restarting ICE');
restartingIce = true;
}
if (pc.signalingState ==
rtc.RTCSignalingState.RTCSignalingStateHaveLocalOffer) {
// we're waiting for the peer to accept our offer, so we'll just wait
// the only exception to this is when ICE restart is needed
final currentSD = await getRemoteDescription();
if ((options?.iceRestart ?? false) && currentSD != null) {
// TODO: handle when ICE restart is needed but we don't have a remote description
// the best thing to do is to recreate the peerconnection
await pc.setRemoteDescription(currentSD);
} else {
renegotiate = true;
return;
}
}
// actually negotiate
logger.fine('starting to negotiate');
final offer = await pc.createOffer(options?.toMap() ?? <String, dynamic>{});
await pc.setLocalDescription(offer);
onOffer?.call(offer);
}
Future<void> addIceCandidate(rtc.RTCIceCandidate candidate) async {
if (isDisposed) {
logger.warning('[$objectId] addIceCandidate() already disposed');
return;
}
final desc = await getRemoteDescription();
if (desc != null && !restartingIce) {
await pc.addCandidate(candidate);
return;
}
_pendingCandidates.add(candidate);
}
Future<rtc.RTCSessionDescription?> getRemoteDescription() async {
if (isDisposed) {
logger.warning('[$objectId] getRemoteDescription() already disposed');
return null;
}
// Checking agains null doesn't work as intended
// if (pc.iceConnectionState == null) return null;
try {
final result = await pc.getRemoteDescription();
logger.fine('pc.getRemoteDescription $result');
return result;
} catch (_) {
logger.warning('pc.getRemoteDescription failed with error: $_');
}
}
}