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:
+192
-251
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user