Switch to new event system (#9)

* `SignalEvents` and `synchronized` mode for `EventsListenable`

* cascade syntax

* clean up

* `RoomEvents`

* `ParticipantEvents`

* all events implemented

* make it compile with M1 macs

* dispose logic

* cleaner connect logic

* ask to publish

* fix initial EngineTrackAddedEvent glitch

* fix unpublishTrack bug

* clean up

* better events docs

* organize event types

* clean up

* cleaner wait logic

* wait for event instead of using Completer

* notifyListeners
This commit is contained in:
Hiroshi Horie
2021-09-24 23:37:26 +09:00
committed by GitHub
parent f4bd82a919
commit 6445b8d2a8
36 changed files with 1270 additions and 948 deletions
+192 -251
View File
@@ -2,8 +2,9 @@ import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'classes/change_notifier.dart';
import 'constants.dart';
import 'errors.dart';
import 'events.dart';
import 'extensions.dart';
@@ -14,84 +15,13 @@ import 'participant/local_participant.dart';
import 'participant/participant.dart';
import 'participant/remote_participant.dart';
import 'proto/livekit_models.pb.dart' as lk_models;
import 'proto/livekit_rtc.pb.dart' as lk_rtc;
import 'rtc_engine.dart';
import 'signal_client.dart';
import 'track/remote_track_publication.dart';
import 'track/track.dart';
import 'track/track_publication.dart';
import 'types.dart';
enum RoomState {
disconnected,
connected,
reconnecting,
}
/// Delegate for [Room] callbacks
mixin RoomDelegate {
// room level callbacks
/// When the connection to the server has been interrupted and it's attempting
/// to reconnect.
void onReconnecting() {}
/// Connection to room is re-established. All existing state is preserved.
void onReconnected() {}
/// Disconnected from the room
void onDisconnected() {}
/// When a new [RemoteParticipant] joins *after* the current participant has connected
/// It will not fire for participants that are already in the room
void onParticipantConnected(Participant participant) {}
/// When a [RemoteParticipant] leaves the room
void onParticipantDisconnected(Participant participant) {}
/// Active speakers changed. List of speakers are ordered by their audio level.
/// loudest speakers first. This will include the [LocalParticipant] too.
void onActiveSpeakersChanged(List<Participant> participants) {}
// callbacks about participant events
/// Participant metadata is a simple way for app-specific state to be pushed to
/// all users.
/// When RoomService.UpdateParticipantMetadata is called to change a
/// participant's state, *all* participants in the room will fire this event.
void onMetadataChanged(Participant participant) {}
/// A track that was muted, fires on both [RemoteParticipant]s and
/// [LocalParticipant]
void onTrackMuted(Participant participant, TrackPublication publication) {}
/// A track that was unmuted, fires on both [RemoteParticipant]s and
/// [LocalParticipant]
void onTrackUnmuted(Participant participant, TrackPublication publication) {}
/// When a new track is published to room *after* the current participant has
/// joined. It will not fire for tracks that are already published
void onTrackPublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
/// A [RemoteParticipant] has unpublished a track
void onTrackUnpublished(RemoteParticipant participant, RemoteTrackPublication publication) {}
/// The [LocalParticipant] has subscribed to a new track. This event will **always**
/// fire as long as new tracks are ready for use.
void onTrackSubscribed(
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
/// A subscribed track is no longer available.
void onTrackUnsubscribed(
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {}
/// Data received from another [RemoteParticipant].
/// Data packets provides the ability to use LiveKit to send/receive arbitrary
/// payloads.
void onDataReceived(RemoteParticipant participant, List<int> data) {}
/// Encountered failure attempting to subscribe to track.
void onTrackSubscriptionFailed(RemoteParticipant participant, String sid, String? message) {}
}
/// 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
@@ -101,11 +31,12 @@ mixin RoomDelegate {
/// * participant membership changes
/// * active speakers are different
/// {@category Room}
class Room extends ChangeNotifier with ParticipantDelegate {
RoomState _connectionState = RoomState.disconnected;
class Room extends LKChangeNotifier {
// Room is only instantiated if connected, so defaults to connected.
ConnectionState _connectionState = ConnectionState.connected;
/// connection state of the room
RoomState get state => _connectionState;
ConnectionState get connectionState => _connectionState;
final Map<String, RemoteParticipant> _participants = {};
@@ -114,13 +45,13 @@ class Room extends ChangeNotifier with ParticipantDelegate {
UnmodifiableMapView(_participants);
/// the current participant
late LocalParticipant localParticipant;
late final LocalParticipant localParticipant;
/// name of the room
late String name;
late final String name;
/// sid of the room
late String sid;
late final String sid;
List<Participant> _activeSpeakers = [];
@@ -128,63 +59,28 @@ class Room extends ChangeNotifier with ParticipantDelegate {
UnmodifiableListView<Participant> get activeSpeakers =>
UnmodifiableListView<Participant>(_activeSpeakers);
/// delegate for room events
RoomDelegate? delegate;
final RTCEngine _engine;
final RTCEngine engine;
// suppport for multiple event listeners
final events = EventsEmitter<RoomEvent>();
late final _engineListener = EventsListener<EngineEvent>(emitter: _engine.events);
late final _engineListener = EventsListener<LiveKitEvent>(engine.events);
/// internal use
/// {@nodoc}
Room([RTCConfiguration? rtcConfig]) : _engine = RTCEngine(SignalClient(), rtcConfig) {
_engine.onTrack = _onTrackAdded;
_engine.onICEConnected = _handleICEConnected;
_engine.onDisconnected = _handleDisconnect;
_engine.onParticipantUpdated = _handleParticipantUpdate;
_engine.onActiveSpeakerUpdated = _handleSpeakerUpdate;
_engine.onDataMessage = _handleDataPacket;
_engine.onRemoteMute = _onRemoteMuteChanged;
_engine.onReconnected = () {
_connectionState = RoomState.connected;
delegate?.onReconnected();
notifyListeners();
};
_engine.onReconnecting = () {
_connectionState = RoomState.reconnecting;
delegate?.onReconnecting();
notifyListeners();
};
}
@override
Future<void> dispose() async {
await events.dispose();
await _engineListener.dispose();
super.dispose();
}
Future<Room> connect(
String url,
String token, {
ConnectOptions? options,
}) async {
final joinResponse = await _engine.join(
url,
token,
options: options,
);
logger.fine('connected to LiveKit server, version: ${joinResponse.serverVersion}');
Room._({
required this.engine,
required lk_rtc.JoinResponse joinResponse,
ConnectOptions? connectOptions,
}) {
//
_setUpListeners();
localParticipant = LocalParticipant(
engine: _engine,
engine: engine,
info: joinResponse.participant,
defaultPublishOptions: options?.defaultPublishOptions,
defaultPublishOptions: connectOptions?.defaultPublishOptions,
roomEvents: events,
);
localParticipant.roomDelegate = this;
sid = joinResponse.room.sid;
name = joinResponse.room.name;
@@ -193,89 +89,199 @@ class Room extends ChangeNotifier with ParticipantDelegate {
_getOrCreateRemoteParticipant(info.sid, info);
}
// room is not ready until ICE is connected.
// Any event emitted will trigger ChangeNotifier
events.listen((event) {
logger.fine('[RoomEvent] $event, will notifyListeners()');
notifyListeners();
});
}
@override
Future<void> dispose() async {
// dispose local participant
await localParticipant.dispose();
// dispose Room's events emitter
await events.dispose();
// dispose all listeners for RTCEngine
await _engineListener.dispose();
// dispose the engine
await engine.dispose();
super.dispose();
}
static Future<Room> connect(
String url,
String token, {
ConnectOptions? options,
RTCConfiguration? rtcConfig,
}) async {
//
final engine = RTCEngine(
SignalClient(),
rtcConfig,
);
Room? room;
try {
await _engineListener.waitFor<EngineIceStateUpdatedEvent>(
filter: (event) => event.iceState.isConnected(),
duration: const Duration(seconds: 5),
final joinResponse = await engine.join(
url,
token,
options: options,
);
logger.fine('Connected to LiveKit server, version: ${joinResponse.serverVersion}');
// create Room first to listen to events
room = Room._(
engine: engine,
joinResponse: joinResponse,
);
logger.fine('Waiting to engine connect...');
// wait until engine is connected
await room._engineListener.waitFor<EngineConnectedEvent>(
duration: Timeouts.connection,
onTimeout: () => throw ConnectException(),
);
return room;
// catch any exception
} catch (_) {
_connectionState = RoomState.disconnected;
notifyListeners();
// pass on the exception
// dispose engine if there was any exception while connecting
if (room != null) {
// room.dispose will also dispose engine
await room.dispose();
} else {
await engine.dispose();
}
rethrow;
}
return this;
}
void _setUpListeners() => _engineListener
..on<EngineConnectedEvent>((event) async {
_connectionState = ConnectionState.connected;
notifyListeners();
})
..on<EngineReconnectedEvent>((event) async {
_connectionState = ConnectionState.connected;
events.emit(const RoomReconnectedEvent());
notifyListeners();
})
..on<EngineReconnectingEvent>((event) async {
_connectionState = ConnectionState.reconnecting;
events.emit(const RoomReconnectingEvent());
notifyListeners();
})
..on<EngineDisconnectedEvent>((event) => _onDisconnectedEvent())
..on<EngineParticipantUpdateEvent>((event) => _onParticipantUpdateEvent(event.participants))
..on<EngineSpeakersUpdateEvent>((event) => _onSpeakerUpdateEvent(event.speakers))
..on<EngineDataPacketReceivedEvent>(_onDataMessageEvent)
..on<EngineRemoteMuteChangedEvent>((event) async {
final track = localParticipant.trackPublications[event.sid];
track?.muted = event.muted;
})
..on<EngineTrackAddedEvent>((event) async {
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.warning('addSubscribedMediaTrack() throwed ${event}');
[participant.roomEvents, 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 {
_engine.client.sendLeave();
await _handleDisconnect();
engine.signalClient.sendLeave();
await _onDisconnectedEvent();
}
Future<void> reconnect() async {
await _engine.reconnect();
await engine.reconnect();
}
RemoteParticipant _getOrCreateRemoteParticipant(String sid, lk_models.ParticipantInfo? info) {
var participant = _participants[sid];
RemoteParticipant? participant = _participants[sid];
if (participant != null) {
return participant;
}
if (info == null) {
participant = RemoteParticipant(_engine.client, sid, '');
participant = RemoteParticipant(
engine.signalClient,
sid,
'',
roomEvents: events,
);
} else {
participant = RemoteParticipant.fromInfo(_engine.client, info);
participant = RemoteParticipant.fromInfo(
engine.signalClient,
info,
roomEvents: events,
);
}
participant.roomDelegate = this;
_participants[sid] = participant;
return participant;
}
void _handleICEConnected() {
// _connectCompleter?.complete(this);
// _connectCompleter = null;
_connectionState = RoomState.connected;
notifyListeners();
}
Future<void> _handleDisconnect() async {
if (_connectionState == RoomState.disconnected) {
Future<void> _onDisconnectedEvent() async {
if (_connectionState == ConnectionState.disconnected) {
logger.fine('$objectId: _handleDisconnect() already disconnected');
return;
}
// we need to flag room as disconnected immediately to avoid
// this method firing multiple times since the following code
// is being awaited
_connectionState = RoomState.disconnected;
_connectionState = ConnectionState.disconnected;
for (final p in _participants.values) {
final tracks = List<TrackPublication>.from(p.tracks.values);
for (final pub in tracks) {
await p.unpublishTrack(pub.sid);
}
// clean up RemoteParticipants
for (final _ in _participants.values) {
// RemoteParticipant is responsible for disposing resources
await _.unpublishAllTracks();
await _.dispose();
}
for (final pub in localParticipant.tracks.values) {
await pub.track?.stop();
}
await _engine.close();
_participants.clear();
// clean up LocalParticipant
// for (final pub in localParticipant.tracks.values) {
// await pub.track?.stop();
// }
await localParticipant.unpublishAllTracks();
// await localParticipant.dispose();
// localParticipant = null;
await engine.close();
_activeSpeakers.clear();
notifyListeners();
delegate?.onDisconnected();
events.emit(const RoomDisconnectedEvent());
}
void _handleParticipantUpdate(List<lk_models.ParticipantInfo> updates) {
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) {
@@ -295,9 +301,9 @@ class Room extends ChangeNotifier with ParticipantDelegate {
if (isNew) {
hasChanged = true;
delegate?.onParticipantConnected(participant);
events.emit(ParticipantConnectedEvent(participant: participant));
} else {
participant.updateFromInfo(info);
await participant.updateFromInfo(info);
}
}
@@ -306,7 +312,7 @@ class Room extends ChangeNotifier with ParticipantDelegate {
}
}
void _handleSpeakerUpdate(List<lk_models.SpeakerInfo> speakers) {
void _onSpeakerUpdateEvent(List<lk_models.SpeakerInfo> speakers) {
final seenSids = <String>{};
List<Participant> newSpeakers = [];
for (final info in speakers) {
@@ -339,47 +345,29 @@ class Room extends ChangeNotifier with ParticipantDelegate {
}
}
events.emit(ActiveSpeakersChangedEvent(speakers: newSpeakers));
_activeSpeakers = newSpeakers;
delegate?.onActiveSpeakersChanged(newSpeakers);
notifyListeners();
}
void _handleDataPacket(lk_models.UserPacket packet, lk_models.DataPacket_Kind kind) {
final participant = participants[packet.participantSid];
if (participant == null) {
return;
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, packet.payload);
delegate?.onDataReceived(participant, packet.payload);
}
// participant.delegate?.onDataReceived(participant, event.packet.payload);
void _onRemoteMuteChanged(String sid, bool mute) {
final track = localParticipant.tracks[sid];
//
// This will trigger signalClient.sendMuteTrack(sid, mute);
//
track?.muted = mute;
}
final event = DataReceivedEvent(
participant: senderParticipant,
data: dataPacketEvent.packet.payload,
);
void _onTrackAdded(
rtc.MediaStreamTrack track,
rtc.MediaStream? stream,
rtc.RTCRtpReceiver? receiver,
) {
if (stream == null) {
// we need the stream to get the track's id
logger.severe('received track without mediastream');
return;
}
final idParts = stream.id.split('|');
final participantSid = idParts[0];
final trackSid = idParts.elementAtOrNull(1) ?? track.id;
final participant = _getOrCreateRemoteParticipant(participantSid, null);
participant.addSubscribedMediaTrack(track, stream, trackSid);
senderParticipant?.events.emit(event);
events.emit(event);
}
void _handleParticipantDisconnect(String sid) {
@@ -388,58 +376,11 @@ class Room extends ChangeNotifier with ParticipantDelegate {
return;
}
final toRemove = List<TrackPublication>.from(participant.tracks.values);
final toRemove = List<TrackPublication>.from(participant.trackPublications.values);
for (final track in toRemove) {
participant.unpublishTrack(track.sid, true);
participant.unpublishTrack(track.sid, notify: true);
}
delegate?.onParticipantDisconnected(participant);
}
//----------------- forward participant delegate calls ---------------------//
@override
void onMetadataChanged(Participant participant) {
delegate?.onMetadataChanged(participant);
}
@override
void onTrackMuted(Participant participant, TrackPublication publication) {
delegate?.onTrackMuted(participant, publication);
}
@override
void onTrackUnmuted(Participant participant, TrackPublication publication) {
delegate?.onTrackUnmuted(participant, publication);
}
@override
void onTrackPublished(RemoteParticipant participant, RemoteTrackPublication publication) {
delegate?.onTrackPublished(participant, publication);
}
@override
void onTrackUnpublished(RemoteParticipant participant, RemoteTrackPublication publication) {
delegate?.onTrackUnpublished(participant, publication);
}
@override
void onTrackSubscribed(
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {
delegate?.onTrackSubscribed(participant, track, publication);
}
@override
void onTrackUnsubscribed(
RemoteParticipant participant, Track track, RemoteTrackPublication publication) {
delegate?.onTrackUnsubscribed(participant, track, publication);
}
// omitted because data dispatching is handled in _handleDataPacket
@override
void onDataReceived(RemoteParticipant participant, List<int> data) {}
@override
void onTrackSubscriptionFailed(RemoteParticipant participant, String sid, String? message) {
delegate?.onTrackSubscriptionFailed(participant, sid, message);
events.emit(ParticipantDisconnectedEvent(participant: participant));
}
}