@@ -54,6 +54,7 @@ import '../types/rpc.dart';
5454import '../types/transcription_segment.dart' ;
5555import '../utils.dart' show unpackStreamId;
5656import 'engine.dart' ;
57+ import 'pending_track_queue.dart' ;
5758
5859/// Room is the primary construct for LiveKit conferences. It contains a
5960/// group of [Participant] s, each publishing and subscribing to [Track] s.
@@ -135,6 +136,9 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
135136 @internal
136137 late final PreConnectAudioBuffer preConnectAudioBuffer;
137138
139+ // Pending subscriber tracks keyed by participantSid, for tracks arriving before metadata or before the room connected.
140+ late final PendingTrackQueue _pendingTrackQueue;
141+
138142 // for testing
139143 @internal
140144 Map <String , RpcRequestHandler > get rpcHandlers => _rpcHandlers;
@@ -152,6 +156,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
152156 Engine ? engine,
153157 }) : engine = engine ??
154158 Engine (
159+ connectOptions: connectOptions,
155160 roomOptions: roomOptions,
156161 ) {
157162 //
@@ -161,11 +166,18 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
161166 _signalListener = this .engine.signalClient.createListener ();
162167 _setUpSignalListeners ();
163168
169+ _pendingTrackQueue = PendingTrackQueue (
170+ ttl: this .engine.connectOptions.timeouts.subscribe,
171+ emitException: (event) => events.emit (event),
172+ );
173+
164174 // Any event emitted will trigger ChangeNotifier
165175 events.listen ((event) {
166176 logger.finer ('[RoomEvent] $event , will notifyListeners()' );
167177 notifyListeners ();
168178 });
179+ // Keep a connected flush as a fallback in case tracks arrive pre-connected but before metadata.
180+ events.on < RoomConnectedEvent > ((event) => _flushPendingTracks ());
169181
170182 _setupRpcListeners ();
171183
@@ -232,6 +244,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
232244 }) async {
233245 var roomOptions = this .roomOptions;
234246 connectOptions ?? = ConnectOptions ();
247+ _pendingTrackQueue.updateTtl (connectOptions.timeouts.subscribe);
235248 // ignore: deprecated_member_use_from_same_package
236249 if ((roomOptions.encryption != null || roomOptions.e2eeOptions != null ) && engine.e2eeManager == null ) {
237250 if (! lkPlatformSupportsE2EE ()) {
@@ -596,12 +609,18 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
596609 reason: TrackSubscribeFailReason .invalidServerResponse,
597610 );
598611 }
599- if (participant == null ) {
600- throw TrackSubscriptionExceptionEvent (
601- participant: participant,
602- sid: trackSid,
603- reason: TrackSubscribeFailReason .noParticipantFound,
612+
613+ final shouldDefer = connectionState != ConnectionState .connected || participant == null ;
614+ if (shouldDefer) {
615+ _pendingTrackQueue.enqueue (
616+ track: event.track,
617+ stream: event.stream,
618+ receiver: event.receiver,
619+ participantSid: participantSid,
620+ trackSid: trackSid,
621+ connectionState: connectionState,
604622 );
623+ return ;
605624 }
606625 await participant.addSubscribedMediaTrack (
607626 event.track,
@@ -678,6 +697,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
678697
679698 _remoteParticipants[result.participant.identity] = result.participant;
680699 _sidToIdentity[result.participant.sid] = result.participant.identity;
700+ await _flushPendingTracks (participant: result.participant);
681701 return result;
682702 }
683703
@@ -722,10 +742,12 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
722742 }
723743 }
724744 _sidToIdentity[info.sid] = info.identity;
745+ await _flushPendingTracks (participant: result.participant);
725746 } else {
726747 final wasUpdated = await result.participant.updateFromInfo (info);
727748 if (wasUpdated) {
728749 _sidToIdentity[info.sid] = info.identity;
750+ await _flushPendingTracks (participant: result.participant);
729751 }
730752 }
731753 }
@@ -760,6 +782,32 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
760782 emitWhenConnected (ActiveSpeakersChangedEvent (speakers: activeSpeakers));
761783 }
762784
785+ Future <void > _flushPendingTracks ({RemoteParticipant ? participant}) => _pendingTrackQueue.flush (
786+ isConnected: connectionState == ConnectionState .connected,
787+ participantSid: participant? .sid,
788+ subscriber: (pending) async {
789+ final target = participant ?? _getRemoteParticipantBySid (pending.participantSid);
790+ if (target == null ) return false ;
791+ try {
792+ await target.addSubscribedMediaTrack (
793+ pending.track,
794+ pending.stream,
795+ pending.trackSid,
796+ receiver: pending.receiver,
797+ audioOutputOptions: roomOptions.defaultAudioOutputOptions,
798+ );
799+ return true ;
800+ } on TrackSubscriptionExceptionEvent catch (event) {
801+ logger.severe ('Track subscription failed during flush: ${event }' );
802+ events.emit (event);
803+ return true ;
804+ } catch (exception) {
805+ logger.warning ('Unknown exception during pending track flush: ${exception }' );
806+ return false ;
807+ }
808+ },
809+ );
810+
763811 // from data channel
764812 // updates are sent only when there's a change to speaker ordering
765813 void _onEngineActiveSpeakersUpdateEvent (List <lk_models.SpeakerInfo > speakers) {
@@ -941,6 +989,7 @@ extension RoomPrivateMethods on Room {
941989 }
942990 _remoteParticipants.clear ();
943991 _sidToIdentity.clear ();
992+ _pendingTrackQueue.clear ();
944993
945994 // clean up LocalParticipant
946995 await localParticipant? .unpublishAllTracks ();
0 commit comments