import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:meta/meta.dart'; import '../core/room.dart'; import '../events.dart'; import '../exceptions.dart'; import '../extensions.dart'; import '../logger.dart'; import '../proto/livekit_models.pb.dart' as lk_models; import '../publication/remote.dart'; import '../support/platform.dart'; import '../track/options.dart'; import '../track/remote/audio.dart'; import '../track/remote/remote.dart'; import '../track/remote/video.dart'; import '../types/other.dart'; import 'participant.dart'; /// Represents other participant in the [Room]. class RemoteParticipant extends Participant { @internal RemoteParticipant({ required Room room, required String sid, required String identity, required String name, }) : super( room: room, sid: sid, identity: identity, name: name, ); @internal RemoteParticipant.fromInfo({ required Room room, required lk_models.ParticipantInfo info, }) : super( room: room, sid: info.sid, identity: info.identity, name: info.name, ) { updateFromInfo(info); } /// A convenience property to get all video tracks. @override List> get videoTracks => trackPublications.values .whereType>() .toList(); /// A convenience property to get all audio tracks. @override List> get audioTracks => trackPublications.values .whereType>() .toList(); List get subscribedTracks => trackPublications.values .where((e) => e.subscribed) .cast() .toList(); RemoteTrackPublication? getTrackPublication(String sid) { final pub = trackPublications[sid]; if (pub is RemoteTrackPublication) return pub; return null; } /// for internal use /// {@nodoc} @internal Future addSubscribedMediaTrack( rtc.MediaStreamTrack mediaTrack, rtc.MediaStream stream, String trackSid, { rtc.RTCRtpReceiver? receiver, AudioOutputOptions audioOutputOptions = const AudioOutputOptions(), }) async { logger.fine('addSubscribedMediaTrack()'); // If publication doesn't exist yet... RemoteTrackPublication? pub = getTrackPublication(trackSid); if (pub == null) { logger.fine('addSubscribedMediaTrack() pub is null, will wait...'); logger.fine('addSubscribedMediaTrack() tracks: $trackPublications'); // Wait for the metadata to arrive final event = await events.waitFor( filter: (event) => event.participant == this && event.publication.sid == trackSid, duration: room.connectOptions.timeouts.publish, onTimeout: () => throw TrackSubscriptionExceptionEvent( participant: this, sid: trackSid, reason: TrackSubscribeFailReason.notTrackMetadataFound, ), ); pub = event.publication; logger.fine('addSubscribedMediaTrack() did receive pub'); } // Check if track type is supported, throw if not. if (![lk_models.TrackType.AUDIO, lk_models.TrackType.VIDEO] .contains(pub.kind)) { throw TrackSubscriptionExceptionEvent( participant: this, sid: trackSid, reason: TrackSubscribeFailReason.unsupportedTrackType, ); } // create Track final RemoteTrack track; if (pub.kind == lk_models.TrackType.VIDEO) { // video track track = RemoteVideoTrack(pub.name, pub.source, stream, mediaTrack, receiver: receiver); } else if (pub.kind == lk_models.TrackType.AUDIO) { // audio track track = RemoteAudioTrack(pub.name, pub.source, stream, mediaTrack, receiver: receiver); } else { throw UnexpectedStateException('Unknown track type'); } await track.start(); /// Apply audio output selection for the web. if (pub.kind == lk_models.TrackType.AUDIO && lkPlatformIs(PlatformType.web)) { if (audioOutputOptions.deviceId != null) { await (track as RemoteAudioTrack) .setSinkId(audioOutputOptions.deviceId!); } } await pub.updateTrack(track); await pub.updateSubscriptionAllowed(true); addTrackPublication(pub); [events, room.events].emit(TrackSubscribedEvent( participant: this, track: track, publication: pub, )); } /// for internal use /// {@nodoc} @override @internal Future updateFromInfo(lk_models.ParticipantInfo info) async { logger.fine('RemoteParticipant.updateFromInfo(info: $info)'); super.updateFromInfo(info); // figuring out deltas between tracks final newPubs = {}; for (final trackInfo in info.tracks) { RemoteTrackPublication? pub = getTrackPublication(trackInfo.sid); if (pub == null) { final RemoteTrackPublication pub; if (trackInfo.type == lk_models.TrackType.VIDEO) { pub = RemoteTrackPublication( participant: this, info: trackInfo, ); } else if (trackInfo.type == lk_models.TrackType.AUDIO) { pub = RemoteTrackPublication( participant: this, info: trackInfo, ); } else { throw UnexpectedStateException('Unknown track type'); } newPubs.add(pub); addTrackPublication(pub); } else { pub.updateFromInfo(trackInfo); } } // always emit events for new publications, Room will not forward them unless it's ready for (final pub in newPubs) { final event = TrackPublishedEvent( participant: this, publication: pub, ); if (room.connectionState == ConnectionState.connected) { [events, room.events].emit(event); } } // remove any published track that is not in the info final validSids = info.tracks.map((e) => e.sid); final removeSids = trackPublications.keys.where((e) => !validSids.contains(e)).toSet(); for (final sid in removeSids) { await removePublishedTrack(sid); } } Future removePublishedTrack(String trackSid, {bool notify = true}) async { logger.finer('removePublishedTrack track sid: $trackSid, notify: $notify'); final pub = trackPublications.remove(trackSid); if (pub == null) { logger.warning('Publication not found $trackSid'); return; } await pub.dispose(); final track = pub.track; // if has track if (track != null) { await track.stop(); [events, room.events].emit(TrackUnsubscribedEvent( participant: this, track: track, publication: pub, )); } if (notify) { [events, room.events].emit(TrackUnpublishedEvent( participant: this, publication: pub, )); } await pub.dispose(); } @Deprecated( '`unpublishTrack` is deprecated, use `removePublishedTrack` instead') @override Future unpublishTrack(String trackSid, {bool notify = true}) => removePublishedTrack(trackSid, notify: notify); @internal lk_models.ParticipantTracks participantTracks() => lk_models.ParticipantTracks( participantSid: sid, trackSids: trackPublications.values.map((e) => e.sid), ); }