1515@Timeout (Duration (seconds: 5 ))
1616library ;
1717
18+ import 'dart:typed_data' ;
19+
1820import 'package:flutter_test/flutter_test.dart' ;
21+ import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
22+ import 'package:logging/logging.dart' ;
1923
2024import 'package:livekit_client/livekit_client.dart' ;
25+ import 'package:livekit_client/src/internal/events.dart' ;
2126import '../mock/e2e_container.dart' ;
2227import '../mock/test_data.dart' ;
2328import '../mock/websocket_mock.dart' ;
@@ -28,6 +33,12 @@ void main() {
2833 late Room room;
2934 late MockWebSocketConnector ws;
3035 setUp (() async {
36+ // configure logs for debugging
37+ Logger .root.level = Level .FINEST ;
38+ Logger .root.onRecord.listen ((record) {
39+ print ('[${record .level .name }]: ${record .message }' );
40+ });
41+
3142 container = E2EContainer ();
3243 room = container.room;
3344 ws = container.wsConnector;
@@ -167,5 +178,144 @@ void main() {
167178 emits (predicate <RoomDisconnectedEvent >((event) => event.reason == DisconnectReason .unknown)));
168179 ws.onData (leaveResponse.writeToBuffer ());
169180 });
181+
182+ test ('tracks arriving before participant metadata are handled once metadata arrives' , () async {
183+ final fakeStream = _FakeMediaStream ('${remoteParticipantData .sid }|remote_stream' );
184+ final fakeTrack = _FakeMediaStreamTrack (
185+ id: remoteAudioTrack.sid,
186+ kind: 'audio' ,
187+ );
188+
189+ var subscriptionException = false ;
190+ room.events.on < TrackSubscriptionExceptionEvent > ((event) {
191+ subscriptionException = true ;
192+ });
193+
194+ // Emit onTrack before participant update arrives.
195+ container.engine.events.emit (EngineTrackAddedEvent (
196+ track: fakeTrack,
197+ stream: fakeStream,
198+ receiver: null ,
199+ ));
200+
201+ // Now deliver participant metadata.
202+ ws.onData (participantJoinResponse.writeToBuffer ());
203+
204+ // Track should eventually subscribe once metadata is available.
205+ final trackSubscribed = await room.events.waitFor <TrackSubscribedEvent >(
206+ duration: const Duration (seconds: 1 ),
207+ );
208+
209+ expect (subscriptionException, isFalse, reason: 'Track subscription should not fail when metadata arrives later' );
210+ expect (trackSubscribed.participant.sid, remoteParticipantData.sid);
211+ expect (trackSubscribed.publication.track, isNotNull);
212+ });
170213 });
171214}
215+
216+ class _FakeMediaStream extends rtc.MediaStream {
217+ final List <rtc.MediaStreamTrack > _tracks = [];
218+
219+ _FakeMediaStream (String id) : super (id, 'fake-owner' );
220+
221+ @override
222+ bool ? get active => true ;
223+
224+ @override
225+ Future <void > addTrack (rtc.MediaStreamTrack track, {bool addToNative = true }) async {
226+ _tracks.add (track);
227+ }
228+
229+ @override
230+ Future <rtc.MediaStream > clone () async => _FakeMediaStream ('${id }_clone' );
231+
232+ @override
233+ List <rtc.MediaStreamTrack > getAudioTracks () => _tracks.where ((t) => t.kind == 'audio' ).toList ();
234+
235+ @override
236+ Future <void > getMediaTracks () async {}
237+
238+ @override
239+ List <rtc.MediaStreamTrack > getTracks () => List <rtc.MediaStreamTrack >.from (_tracks);
240+
241+ @override
242+ List <rtc.MediaStreamTrack > getVideoTracks () => _tracks.where ((t) => t.kind == 'video' ).toList ();
243+
244+ @override
245+ Future <void > removeTrack (rtc.MediaStreamTrack track, {bool removeFromNative = true }) async {
246+ _tracks.remove (track);
247+ }
248+ }
249+
250+ class _FakeMediaStreamTrack implements rtc.MediaStreamTrack {
251+ @override
252+ rtc.StreamTrackCallback ? onEnded;
253+
254+ @override
255+ rtc.StreamTrackCallback ? onMute;
256+
257+ @override
258+ rtc.StreamTrackCallback ? onUnMute;
259+
260+ @override
261+ bool enabled;
262+
263+ @override
264+ final String id;
265+
266+ @override
267+ final String kind;
268+
269+ @override
270+ String ? get label => '$kind -track' ;
271+
272+ @override
273+ bool ? get muted => false ;
274+
275+ _FakeMediaStreamTrack ({
276+ required this .id,
277+ required this .kind,
278+ this .enabled = true ,
279+ });
280+
281+ @override
282+ Future <void > applyConstraints ([Map <String , dynamic >? constraints]) async {}
283+
284+ @override
285+ Future <rtc.MediaStreamTrack > clone () async => _FakeMediaStreamTrack (id: id, kind: kind, enabled: enabled);
286+
287+ @override
288+ Future <void > dispose () async {}
289+
290+ @override
291+ Future <void > adaptRes (int width, int height) async {}
292+
293+ @override
294+ Map <String , dynamic > getConstraints () => const {};
295+
296+ @override
297+ Map <String , dynamic > getSettings () => const {};
298+
299+ @override
300+ Future <void > stop () async {}
301+
302+ @override
303+ void enableSpeakerphone (bool enable) {}
304+
305+ @override
306+ Future <ByteBuffer > captureFrame () {
307+ throw UnimplementedError ();
308+ }
309+
310+ @override
311+ Future <bool > hasTorch () async => false ;
312+
313+ @override
314+ Future <void > setTorch (bool torch) async {}
315+
316+ @override
317+ Future <bool > switchCamera () async => false ;
318+
319+ @override
320+ String toString () => 'FakeMediaStreamTrack(id: $id , kind: $kind , enabled: $enabled )' ;
321+ }
0 commit comments