Files
client-sdk-flutter/lib/src/participant/participant.dart
T

228 lines
6.4 KiB
Dart

import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import '../events.dart';
import '../extensions.dart';
import '../logger.dart';
import '../managers/event.dart';
import '../proto/livekit_models.pb.dart' as lk_models;
import '../support/disposable.dart';
import '../track/track.dart';
import '../publication/track_publication.dart';
import '../types.dart';
import 'remote_participant.dart';
/// Represents a Participant in the room, notifies changes via delegates as
/// well as ChangeNotifier/providers.
/// A change notification is triggered when
/// - speaking status changed
/// - mute status changed
/// - added/removed subscribed tracks
/// - metadata changed
/// Base for [RemoteParticipant] and [LocalParticipant],
/// can not be instantiated directly.
abstract class Participant extends DisposableChangeNotifier
with EventsEmittable<ParticipantEvent> {
/// map of track sid => published track
final trackPublications = <String, TrackPublication>{};
/// audio level between 0-1, 1 being the loudest
double audioLevel = 0;
/// server assigned unique id
final String sid;
/// user-assigned identity
String identity;
/// client-assigned metadata, opaque to livekit
String? metadata;
/// when the participant had last spoken
DateTime? lastSpokeAt;
lk_models.ParticipantInfo? _participantInfo;
bool _isSpeaking = false;
ConnectionQuality _connectionQuality = ConnectionQuality.unknown;
// suppport for multiple event listeners
final EventsEmitter<RoomEvent> roomEvents;
/// when the participant joined the room
DateTime get joinedAt {
final pi = _participantInfo;
if (pi != null) {
return DateTime.fromMillisecondsSinceEpoch(pi.joinedAt.toInt() * 1000,
isUtc: true);
}
return DateTime.now();
}
/// if participant is currently speaking
bool get isSpeaking => _isSpeaking;
/// true if participant is publishing an audio track and is muted
bool get isMuted => audioTracks.firstOrNull?.muted ?? true;
bool get hasAudio => audioTracks.isNotEmpty;
bool get hasVideo => videoTracks.isNotEmpty;
/// Connection quality of the participant
ConnectionQuality get connectionQuality => _connectionQuality;
/// tracks that are subscribed to
List<TrackPublication> get subscribedTracks =>
trackPublications.values.where((e) => e.subscribed).toList();
List<TrackPublication> get videoTracks => trackPublications.values
.where((e) => e.kind == lk_models.TrackType.VIDEO)
.toList();
List<TrackPublication> get audioTracks => trackPublications.values
.where((e) => e.kind == lk_models.TrackType.AUDIO)
.toList();
/// for internal use
/// {@nodoc}
@internal
bool get hasInfo => _participantInfo != null;
Participant(
this.sid,
this.identity, {
required this.roomEvents,
}) {
// Any event emitted will trigger ChangeNotifier
events.listen((event) {
logger.fine('[ParticipantEvent] $event, will notifyListeners()');
notifyListeners();
});
onDispose(() async {
await events.dispose();
await unpublishAllTracks();
});
}
/// for internal use
/// {@nodoc}
@internal
set isSpeaking(bool speaking) {
if (_isSpeaking == speaking) {
return;
}
_isSpeaking = speaking;
if (speaking) {
lastSpokeAt = DateTime.now();
}
events.emit(SpeakingChangedEvent(
participant: this,
speaking: speaking,
));
}
void _setMetadata(String md) {
final changed = _participantInfo?.metadata != md;
metadata = md;
if (changed) {
[events, roomEvents].emit(ParticipantMetadataUpdatedEvent(
participant: this,
));
}
}
@internal
void updateConnectionQuality(ConnectionQuality quality) {
if (_connectionQuality == quality) return;
_connectionQuality = quality;
[events, roomEvents].emit(ParticipantConnectionQualityUpdatedEvent(
participant: this,
connectionQuality: _connectionQuality,
));
}
/// for internal use
/// {@nodoc}
@internal
void updateFromInfo(lk_models.ParticipantInfo info) {
identity = info.identity;
// participantSid = info.sid;
if (info.metadata.isNotEmpty) {
_setMetadata(info.metadata);
}
_participantInfo = info;
}
/// for internal use
/// {@nodoc}
@internal
void addTrackPublication(TrackPublication pub) {
pub.track?.sid = pub.sid;
trackPublications[pub.sid] = pub;
}
// Must implement
Future<void> unpublishTrack(String trackSid, {bool notify = true});
Future<void> unpublishAllTracks({bool notify = true}) async {
final trackSids = trackPublications.keys.toSet();
for (final trackid in trackSids) {
await unpublishTrack(trackid, notify: notify);
}
}
//
// Equality operators
// Object is considered equal when sid is equal
//
@override
int get hashCode => sid.hashCode;
@override
bool operator ==(Object other) => other is Participant && sid == other.sid;
}
extension ParticipantTrackSourceExt on Participant {
bool isCameraEnabled() {
return !(getTrackPublicationBySource(TrackSource.camera)?.muted ?? true);
}
bool isMicrophoneEnabled() {
return !(getTrackPublicationBySource(TrackSource.microphone)?.muted ??
true);
}
bool isScreenShareEnabled() {
return !(getTrackPublicationBySource(TrackSource.screenShareVideo)?.muted ??
true);
}
/// Find a track publication by its [TrackSource]
TrackPublication? getTrackPublicationBySource(TrackSource source) {
if (source == TrackSource.unknown) return null;
// try to find by source
final result =
trackPublications.values.firstWhereOrNull((e) => e.source == source);
if (result != null) return result;
// try to find by compatibility
return trackPublications.values
.where((e) => e.source == TrackSource.unknown)
.firstWhereOrNull((e) =>
(source == TrackSource.microphone &&
e.kind == lk_models.TrackType.AUDIO) ||
(source == TrackSource.camera &&
e.kind == lk_models.TrackType.VIDEO &&
e.name != Track.screenShareName) ||
(source == TrackSource.screenShareVideo &&
e.kind == lk_models.TrackType.VIDEO &&
e.name == Track.screenShareName) ||
(source == TrackSource.screenShareAudio &&
e.kind == lk_models.TrackType.AUDIO &&
e.name == Track.screenShareName));
}
}