livekit-client 1.1.1 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/livekit-client.esm.mjs +299 -122
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/proto/livekit_rtc.d.ts +7 -0
  6. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  7. package/dist/src/room/Room.d.ts +2 -0
  8. package/dist/src/room/Room.d.ts.map +1 -1
  9. package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
  10. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  11. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  12. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  13. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  14. package/dist/src/room/track/LocalTrack.d.ts +4 -2
  15. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  16. package/dist/src/room/track/LocalVideoTrack.d.ts +3 -2
  17. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  18. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  19. package/dist/src/room/utils.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/proto/livekit_rtc.ts +12 -0
  22. package/src/room/Room.ts +79 -30
  23. package/src/room/participant/LocalParticipant.ts +135 -61
  24. package/src/room/participant/RemoteParticipant.ts +5 -11
  25. package/src/room/participant/publishUtils.ts +2 -2
  26. package/src/room/track/LocalAudioTrack.ts +7 -3
  27. package/src/room/track/LocalTrack.ts +11 -2
  28. package/src/room/track/LocalVideoTrack.ts +41 -10
  29. package/src/room/track/RemoteVideoTrack.ts +30 -2
  30. package/src/room/track/create.ts +2 -2
  31. package/src/room/utils.ts +3 -2
package/src/room/Room.ts CHANGED
@@ -108,14 +108,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
108
108
  this.identityToSid = new Map();
109
109
  this.options = options || {};
110
110
 
111
- switch (this.options?.publishDefaults?.videoCodec) {
112
- case 'av1':
113
- case 'vp9':
114
- this.options.publishDefaults.simulcast = undefined;
115
- break;
116
- default:
117
- }
118
-
119
111
  this.options.audioCaptureDefaults = {
120
112
  ...audioDefaults,
121
113
  ...options?.audioCaptureDefaults,
@@ -178,6 +170,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
178
170
  })
179
171
  .on(EngineEvent.Restarting, this.handleRestarting)
180
172
  .on(EngineEvent.Restarted, this.handleRestarted);
173
+
174
+ if (this.localParticipant) {
175
+ this.localParticipant.engine = this.engine;
176
+ }
181
177
  }
182
178
 
183
179
  /**
@@ -295,7 +291,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
295
291
  this.metadata = joinResponse.room!.metadata;
296
292
  this.emit(RoomEvent.SignalConnected);
297
293
  } catch (err) {
298
- this.engine.close();
294
+ this.recreateEngine();
299
295
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
300
296
  throw err;
301
297
  }
@@ -304,14 +300,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
304
300
  return new Promise<Room>((resolve, reject) => {
305
301
  const connectTimeout = setTimeout(() => {
306
302
  // timeout
307
- this.engine.close();
303
+ this.recreateEngine();
308
304
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
309
305
  reject(new ConnectionError('could not connect after timeout'));
310
306
  }, maxICEConnectTimeout);
311
307
  const abortHandler = () => {
312
308
  log.warn('closing engine');
313
309
  clearTimeout(connectTimeout);
314
- this.engine.close();
310
+ this.recreateEngine();
315
311
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
316
312
  reject(new ConnectionError('room connection has been cancelled'));
317
313
  };
@@ -503,11 +499,37 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
503
499
  }
504
500
  }
505
501
 
502
+ private recreateEngine() {
503
+ this.engine.close();
504
+ /* @ts-ignore */
505
+ this.engine = undefined;
506
+
507
+ // clear out existing remote participants, since they may have attached
508
+ // the old engine
509
+ this.participants.clear();
510
+
511
+ this.createEngine();
512
+ }
513
+
506
514
  private onTrackAdded(
507
515
  mediaTrack: MediaStreamTrack,
508
516
  stream: MediaStream,
509
517
  receiver?: RTCRtpReceiver,
510
518
  ) {
519
+ // don't fire onSubscribed when connecting
520
+ // WebRTC fires onTrack as soon as setRemoteDescription is called on the offer
521
+ // at that time, ICE connectivity has not been established so the track is not
522
+ // technically subscribed.
523
+ // We'll defer these events until when the room is connected or eventually disconnected.
524
+ if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
525
+ setTimeout(() => {
526
+ this.onTrackAdded(mediaTrack, stream, receiver);
527
+ }, 50);
528
+ return;
529
+ }
530
+ if (this.state === ConnectionState.Disconnected) {
531
+ log.warn('skipping incoming track after Room disconnected');
532
+ }
511
533
  const parts = unpackStreamId(stream.id);
512
534
  const participantId = parts[0];
513
535
  let trackId = parts[1];
@@ -532,14 +554,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
532
554
  }
533
555
 
534
556
  private handleRestarting = () => {
535
- if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
536
- this.emit(RoomEvent.Reconnecting);
537
- }
538
-
539
557
  // also unwind existing participants & existing subscriptions
540
558
  for (const p of this.participants.values()) {
541
559
  this.handleParticipantDisconnected(p.sid, p);
542
560
  }
561
+
562
+ if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
563
+ this.emit(RoomEvent.Reconnecting);
564
+ }
543
565
  };
544
566
 
545
567
  private handleRestarted = async (joinResponse: JoinResponse) => {
@@ -570,7 +592,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
570
592
  const track = pub.track!;
571
593
  this.localParticipant.unpublishTrack(track, false);
572
594
  if (!track.isMuted) {
573
- if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
595
+ if (
596
+ (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) &&
597
+ !track.isUserProvided
598
+ ) {
574
599
  // we need to restart the track before publishing, often a full reconnect
575
600
  // is necessary because computer had gone to sleep.
576
601
  log.debug('restarting existing track', {
@@ -644,7 +669,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
644
669
  this.handleParticipantDisconnected(info.sid, remoteParticipant);
645
670
  } else if (isNewParticipant) {
646
671
  // fire connected event
647
- this.emit(RoomEvent.ParticipantConnected, remoteParticipant);
672
+ this.emitWhenConnected(RoomEvent.ParticipantConnected, remoteParticipant);
648
673
  } else {
649
674
  // just update, no events
650
675
  remoteParticipant.updateInfo(info);
@@ -663,7 +688,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
663
688
  participant.tracks.forEach((publication) => {
664
689
  participant.unpublishTrack(publication.trackSid, true);
665
690
  });
666
- this.emit(RoomEvent.ParticipantDisconnected, participant);
691
+ this.emitWhenConnected(RoomEvent.ParticipantDisconnected, participant);
667
692
  }
668
693
 
669
694
  // updates are sent only when there's a change to speaker ordering
@@ -698,7 +723,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
698
723
  });
699
724
 
700
725
  this.activeSpeakers = activeSpeakers;
701
- this.emit(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
726
+ this.emitWhenConnected(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
702
727
  };
703
728
 
704
729
  // process list of changed speakers
@@ -727,7 +752,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
727
752
  const activeSpeakers = Array.from(lastSpeakers.values());
728
753
  activeSpeakers.sort((a, b) => b.audioLevel - a.audioLevel);
729
754
  this.activeSpeakers = activeSpeakers;
730
- this.emit(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
755
+ this.emitWhenConnected(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
731
756
  };
732
757
 
733
758
  private handleStreamStateUpdate = (streamStateUpdate: StreamStateUpdate) => {
@@ -742,7 +767,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
742
767
  }
743
768
  pub.track.streamState = Track.streamStateFromProto(streamState.state);
744
769
  participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
745
- this.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
770
+ this.emitWhenConnected(
771
+ ParticipantEvent.TrackStreamStateChanged,
772
+ pub,
773
+ pub.track.streamState,
774
+ participant,
775
+ );
746
776
  });
747
777
  };
748
778
 
@@ -762,7 +792,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
762
792
  pub,
763
793
  pub.subscriptionStatus,
764
794
  );
765
- this.emit(
795
+ this.emitWhenConnected(
766
796
  ParticipantEvent.TrackSubscriptionPermissionChanged,
767
797
  pub,
768
798
  pub.subscriptionStatus,
@@ -803,7 +833,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
803
833
 
804
834
  private handleRoomUpdate = (r: RoomModel) => {
805
835
  this.metadata = r.metadata;
806
- this.emit(RoomEvent.RoomMetadataChanged, r.metadata);
836
+ this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
807
837
  };
808
838
 
809
839
  private handleConnectionQualityUpdate = (update: ConnectionQualityUpdate) => {
@@ -858,7 +888,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
858
888
  // and remote participant joined the room
859
889
  participant
860
890
  .on(ParticipantEvent.TrackPublished, (trackPublication: RemoteTrackPublication) => {
861
- this.emit(RoomEvent.TrackPublished, trackPublication, participant);
891
+ this.emitWhenConnected(RoomEvent.TrackPublished, trackPublication, participant);
862
892
  })
863
893
  .on(
864
894
  ParticipantEvent.TrackSubscribed,
@@ -872,7 +902,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
872
902
  },
873
903
  )
874
904
  .on(ParticipantEvent.TrackUnpublished, (publication: RemoteTrackPublication) => {
875
- this.emit(RoomEvent.TrackUnpublished, publication, participant);
905
+ this.emitWhenConnected(RoomEvent.TrackUnpublished, publication, participant);
876
906
  })
877
907
  .on(
878
908
  ParticipantEvent.TrackUnsubscribed,
@@ -884,23 +914,32 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
884
914
  this.emit(RoomEvent.TrackSubscriptionFailed, sid, participant);
885
915
  })
886
916
  .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
887
- this.emit(RoomEvent.TrackMuted, pub, participant);
917
+ this.emitWhenConnected(RoomEvent.TrackMuted, pub, participant);
888
918
  })
889
919
  .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
890
- this.emit(RoomEvent.TrackUnmuted, pub, participant);
920
+ this.emitWhenConnected(RoomEvent.TrackUnmuted, pub, participant);
891
921
  })
892
922
  .on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
893
- this.emit(RoomEvent.ParticipantMetadataChanged, metadata, participant);
923
+ this.emitWhenConnected(RoomEvent.ParticipantMetadataChanged, metadata, participant);
894
924
  })
895
925
  .on(ParticipantEvent.ConnectionQualityChanged, (quality: ConnectionQuality) => {
896
- this.emit(RoomEvent.ConnectionQualityChanged, quality, participant);
926
+ this.emitWhenConnected(RoomEvent.ConnectionQualityChanged, quality, participant);
897
927
  })
898
928
  .on(
899
929
  ParticipantEvent.ParticipantPermissionsChanged,
900
930
  (prevPermissions: ParticipantPermission) => {
901
- this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, participant);
931
+ this.emitWhenConnected(
932
+ RoomEvent.ParticipantPermissionsChanged,
933
+ prevPermissions,
934
+ participant,
935
+ );
902
936
  },
903
937
  );
938
+
939
+ // update info at the end after callbacks have been set up
940
+ if (info) {
941
+ participant.updateInfo(info);
942
+ }
904
943
  return participant;
905
944
  }
906
945
 
@@ -967,6 +1006,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
967
1006
  return true;
968
1007
  }
969
1008
 
1009
+ private emitWhenConnected<E extends keyof RoomEventCallbacks>(
1010
+ event: E,
1011
+ ...args: Parameters<RoomEventCallbacks[E]>
1012
+ ): boolean {
1013
+ if (this.state === ConnectionState.Connected) {
1014
+ return this.emit(event, ...args);
1015
+ }
1016
+ return false;
1017
+ }
1018
+
970
1019
  // /** @internal */
971
1020
  emit<E extends keyof RoomEventCallbacks>(
972
1021
  event: E,
@@ -15,10 +15,7 @@ import RTCEngine from '../RTCEngine';
15
15
  import LocalAudioTrack from '../track/LocalAudioTrack';
16
16
  import LocalTrack from '../track/LocalTrack';
17
17
  import LocalTrackPublication from '../track/LocalTrackPublication';
18
- import LocalVideoTrack, {
19
- SimulcastTrackInfo,
20
- videoLayersFromEncodings,
21
- } from '../track/LocalVideoTrack';
18
+ import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
22
19
  import {
23
20
  AudioCaptureOptions,
24
21
  CreateLocalTracksOptions,
@@ -36,7 +33,7 @@ import { ParticipantTrackPermission, trackPermissionToProto } from './Participan
36
33
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
37
34
  import RemoteParticipant from './RemoteParticipant';
38
35
 
39
- const compatibleCodecForSVC = 'vp8';
36
+ const compatibleCodec = 'vp8';
40
37
  export default class LocalParticipant extends Participant {
41
38
  audioTracks: Map<string, LocalTrackPublication>;
42
39
 
@@ -45,14 +42,15 @@ export default class LocalParticipant extends Participant {
45
42
  /** map of track sid => all published tracks */
46
43
  tracks: Map<string, LocalTrackPublication>;
47
44
 
45
+ /** @internal */
46
+ engine: RTCEngine;
47
+
48
48
  private pendingPublishing = new Set<Track.Source>();
49
49
 
50
50
  private cameraError: Error | undefined;
51
51
 
52
52
  private microphoneError: Error | undefined;
53
53
 
54
- private engine: RTCEngine;
55
-
56
54
  private participantTrackPermissions: Array<ParticipantTrackPermission> = [];
57
55
 
58
56
  private allParticipantsAllowedToSubscribe: boolean = true;
@@ -369,11 +367,11 @@ export default class LocalParticipant extends Participant {
369
367
  if (tracks.length === 0) {
370
368
  throw new TrackInvalidError('no video track found');
371
369
  }
372
- const screenVideo = new LocalVideoTrack(tracks[0]);
370
+ const screenVideo = new LocalVideoTrack(tracks[0], undefined, false);
373
371
  screenVideo.source = Track.Source.ScreenShare;
374
372
  const localTracks: Array<LocalTrack> = [screenVideo];
375
373
  if (stream.getAudioTracks().length > 0) {
376
- const screenAudio = new LocalAudioTrack(stream.getAudioTracks()[0]);
374
+ const screenAudio = new LocalAudioTrack(stream.getAudioTracks()[0], undefined, false);
377
375
  screenAudio.source = Track.Source.ScreenShareAudio;
378
376
  localTracks.push(screenAudio);
379
377
  }
@@ -398,10 +396,10 @@ export default class LocalParticipant extends Participant {
398
396
  if (track instanceof MediaStreamTrack) {
399
397
  switch (track.kind) {
400
398
  case 'audio':
401
- track = new LocalAudioTrack(track);
399
+ track = new LocalAudioTrack(track, undefined, true);
402
400
  break;
403
401
  case 'video':
404
- track = new LocalVideoTrack(track);
402
+ track = new LocalVideoTrack(track, undefined, true);
405
403
  break;
406
404
  default:
407
405
  throw new TrackInvalidError(`unsupported MediaStreamTrack kind ${track.kind}`);
@@ -455,7 +453,6 @@ export default class LocalParticipant extends Participant {
455
453
  // compute encodings and layers for video
456
454
  let encodings: RTCRtpEncodingParameters[] | undefined;
457
455
  let simEncodings: RTCRtpEncodingParameters[] | undefined;
458
- let simulcastTracks: SimulcastTrackInfo[] | undefined;
459
456
  if (track.kind === Track.Kind.Video) {
460
457
  // TODO: support react native, which doesn't expose getSettings
461
458
  const settings = track.mediaStreamTrack.getSettings();
@@ -465,37 +462,38 @@ export default class LocalParticipant extends Participant {
465
462
  req.width = width ?? 0;
466
463
  req.height = height ?? 0;
467
464
  // for svc codecs, disable simulcast and use vp8 for backup codec
468
- if (
469
- track instanceof LocalVideoTrack &&
470
- (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1')
471
- ) {
472
- // set scalabilityMode to 'L3T3' by default
473
- opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
474
-
475
- // add backup codec track
476
- const simOpts = { ...opts };
477
- simOpts.simulcast = true;
478
- simOpts.scalabilityMode = undefined;
479
- simEncodings = computeVideoEncodings(
480
- track.source === Track.Source.ScreenShare,
481
- width,
482
- height,
483
- simOpts,
484
- );
485
- const simulcastTrack = track.addSimulcastTrack(compatibleCodecForSVC, simEncodings);
486
- simulcastTracks = [simulcastTrack];
487
- req.simulcastCodecs = [
488
- {
489
- codec: opts.videoCodec,
490
- cid: track.mediaStreamTrack.id,
491
- enableSimulcastLayers: true,
492
- },
493
- {
494
- codec: simulcastTrack.codec,
495
- cid: simulcastTrack.mediaStreamTrack.id,
496
- enableSimulcastLayers: true,
497
- },
498
- ];
465
+ if (track instanceof LocalVideoTrack) {
466
+ if (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1') {
467
+ // set scalabilityMode to 'L3T3' by default
468
+ opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
469
+
470
+ // add backup codec track
471
+ const simOpts = { ...opts };
472
+ simOpts.simulcast = true;
473
+ simOpts.scalabilityMode = undefined;
474
+ simEncodings = computeVideoEncodings(
475
+ track.source === Track.Source.ScreenShare,
476
+ width,
477
+ height,
478
+ simOpts,
479
+ );
480
+ }
481
+
482
+ // set vp8 codec as backup for any other codecs
483
+ if (opts.videoCodec && opts.videoCodec !== 'vp8') {
484
+ req.simulcastCodecs = [
485
+ {
486
+ codec: opts.videoCodec,
487
+ cid: track.mediaStreamTrack.id,
488
+ enableSimulcastLayers: true,
489
+ },
490
+ {
491
+ codec: compatibleCodec,
492
+ cid: '',
493
+ enableSimulcastLayers: true,
494
+ },
495
+ ];
496
+ }
499
497
  }
500
498
 
501
499
  encodings = computeVideoEncodings(
@@ -541,22 +539,6 @@ export default class LocalParticipant extends Participant {
541
539
  track.codec = opts.videoCodec;
542
540
  }
543
541
 
544
- const localTrack = track as LocalVideoTrack;
545
- if (simulcastTracks) {
546
- for await (const simulcastTrack of simulcastTracks) {
547
- const simTransceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
548
- if (simulcastTrack.encodings) {
549
- simTransceiverInit.sendEncodings = simulcastTrack.encodings;
550
- }
551
- const simTransceiver = await this.engine.publisher!.pc.addTransceiver(
552
- simulcastTrack.mediaStreamTrack,
553
- simTransceiverInit,
554
- );
555
- this.setPreferredCodec(simTransceiver, localTrack.kind, simulcastTrack.codec);
556
- localTrack.setSimulcastTrackSender(simulcastTrack.codec, simTransceiver.sender);
557
- }
558
- }
559
-
560
542
  this.engine.negotiate();
561
543
 
562
544
  // store RTPSender
@@ -574,6 +556,91 @@ export default class LocalParticipant extends Participant {
574
556
  return publication;
575
557
  }
576
558
 
559
+ /** @internal
560
+ * publish additional codec to existing track
561
+ */
562
+ async publishAdditionalCodecForTrack(
563
+ track: LocalTrack | MediaStreamTrack,
564
+ videoCodec: VideoCodec,
565
+ options?: TrackPublishOptions,
566
+ ) {
567
+ const opts: TrackPublishOptions = {
568
+ ...this.roomOptions?.publishDefaults,
569
+ ...options,
570
+ };
571
+ // clear scalabilityMode setting for backup codec
572
+ opts.scalabilityMode = undefined;
573
+ opts.videoCodec = videoCodec;
574
+ // is it not published? if so skip
575
+ let existingPublication: LocalTrackPublication | undefined;
576
+ this.tracks.forEach((publication) => {
577
+ if (!publication.track) {
578
+ return;
579
+ }
580
+ if (publication.track === track) {
581
+ existingPublication = <LocalTrackPublication>publication;
582
+ }
583
+ });
584
+ if (!existingPublication) {
585
+ throw new TrackInvalidError('track is not published');
586
+ }
587
+
588
+ if (!(track instanceof LocalVideoTrack)) {
589
+ throw new TrackInvalidError('track is not a video track');
590
+ }
591
+
592
+ const settings = track.mediaStreamTrack.getSettings();
593
+ const width = settings.width ?? track.dimensions?.width;
594
+ const height = settings.height ?? track.dimensions?.height;
595
+
596
+ const encodings = computeVideoEncodings(
597
+ track.source === Track.Source.ScreenShare,
598
+ width,
599
+ height,
600
+ opts,
601
+ );
602
+ const simulcastTrack = track.addSimulcastTrack(opts.videoCodec, encodings);
603
+ const req = AddTrackRequest.fromPartial({
604
+ cid: simulcastTrack.mediaStreamTrack.id,
605
+ type: Track.kindToProto(track.kind),
606
+ muted: track.isMuted,
607
+ source: Track.sourceToProto(track.source),
608
+ sid: track.sid,
609
+ simulcastCodecs: [
610
+ {
611
+ codec: opts.videoCodec,
612
+ cid: simulcastTrack.mediaStreamTrack.id,
613
+ enableSimulcastLayers: opts.simulcast,
614
+ },
615
+ ],
616
+ });
617
+ req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
618
+
619
+ if (!this.engine || this.engine.isClosed) {
620
+ throw new UnexpectedConnectionState('cannot publish track when not connected');
621
+ }
622
+
623
+ const ti = await this.engine.addTrack(req);
624
+
625
+ if (!this.engine.publisher) {
626
+ throw new UnexpectedConnectionState('publisher is closed');
627
+ }
628
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
629
+ if (encodings) {
630
+ transceiverInit.sendEncodings = encodings;
631
+ }
632
+ // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
633
+ const transceiver = await this.engine.publisher.pc.addTransceiver(
634
+ simulcastTrack.mediaStreamTrack,
635
+ transceiverInit,
636
+ );
637
+ this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
638
+ track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
639
+
640
+ this.engine.negotiate();
641
+ log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
642
+ }
643
+
577
644
  unpublishTrack(
578
645
  track: LocalTrack | MediaStreamTrack,
579
646
  stopOnUnpublish?: boolean,
@@ -761,7 +828,7 @@ export default class LocalParticipant extends Participant {
761
828
  this.onTrackMuted(track, track.isMuted);
762
829
  };
763
830
 
764
- private handleSubscribedQualityUpdate = (update: SubscribedQualityUpdate) => {
831
+ private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
765
832
  if (!this.roomOptions?.dynacast) {
766
833
  return;
767
834
  }
@@ -774,7 +841,14 @@ export default class LocalParticipant extends Participant {
774
841
  return;
775
842
  }
776
843
  if (update.subscribedCodecs.length > 0) {
777
- pub.videoTrack?.setPublishingCodecs(update.subscribedCodecs);
844
+ if (!pub.videoTrack) {
845
+ return;
846
+ }
847
+ const newCodecs = await pub.videoTrack.setPublishingCodecs(update.subscribedCodecs);
848
+ for await (const codec of newCodecs) {
849
+ log.debug(`publish ${codec} for ${pub.videoTrack.sid}`);
850
+ await this.publishAdditionalCodecForTrack(pub.videoTrack, codec, pub.options);
851
+ }
778
852
  } else if (update.subscribedQualities.length > 0) {
779
853
  pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
780
854
  }
@@ -23,9 +23,7 @@ export default class RemoteParticipant extends Participant {
23
23
 
24
24
  /** @internal */
25
25
  static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant {
26
- const rp = new RemoteParticipant(signalClient, pi.sid, pi.identity);
27
- rp.updateInfo(pi);
28
- return rp;
26
+ return new RemoteParticipant(signalClient, pi.sid, pi.identity);
29
27
  }
30
28
 
31
29
  /** @internal */
@@ -182,8 +180,6 @@ export default class RemoteParticipant extends Participant {
182
180
 
183
181
  /** @internal */
184
182
  updateInfo(info: ParticipantInfo) {
185
- const alreadyHasMetadata = this.hasMetadata;
186
-
187
183
  super.updateInfo(info);
188
184
 
189
185
  // we are getting a list of all available tracks, reconcile in here
@@ -212,12 +208,10 @@ export default class RemoteParticipant extends Participant {
212
208
  validTracks.set(ti.sid, publication);
213
209
  });
214
210
 
215
- // send new tracks
216
- if (alreadyHasMetadata) {
217
- newTracks.forEach((publication) => {
218
- this.emit(ParticipantEvent.TrackPublished, publication);
219
- });
220
- }
211
+ // always emit events for new publications, Room will not forward them unless it's ready
212
+ newTracks.forEach((publication) => {
213
+ this.emit(ParticipantEvent.TrackPublished, publication);
214
+ });
221
215
 
222
216
  // detect removed tracks
223
217
  this.tracks.forEach((publication) => {
@@ -18,9 +18,9 @@ export function mediaTrackToLocalTrack(
18
18
  ): LocalVideoTrack | LocalAudioTrack {
19
19
  switch (mediaStreamTrack.kind) {
20
20
  case 'audio':
21
- return new LocalAudioTrack(mediaStreamTrack, constraints);
21
+ return new LocalAudioTrack(mediaStreamTrack, constraints, false);
22
22
  case 'video':
23
- return new LocalVideoTrack(mediaStreamTrack, constraints);
23
+ return new LocalVideoTrack(mediaStreamTrack, constraints, false);
24
24
  default:
25
25
  throw new TrackInvalidError(`unsupported track type: ${mediaStreamTrack.kind}`);
26
26
  }
@@ -15,8 +15,12 @@ export default class LocalAudioTrack extends LocalTrack {
15
15
 
16
16
  private prevStats?: AudioSenderStats;
17
17
 
18
- constructor(mediaTrack: MediaStreamTrack, constraints?: MediaTrackConstraints) {
19
- super(mediaTrack, Track.Kind.Audio, constraints);
18
+ constructor(
19
+ mediaTrack: MediaStreamTrack,
20
+ constraints?: MediaTrackConstraints,
21
+ userProvidedTrack = true,
22
+ ) {
23
+ super(mediaTrack, Track.Kind.Audio, constraints, userProvidedTrack);
20
24
  this.checkForSilence();
21
25
  }
22
26
 
@@ -42,7 +46,7 @@ export default class LocalAudioTrack extends LocalTrack {
42
46
  }
43
47
 
44
48
  async unmute(): Promise<LocalAudioTrack> {
45
- if (this.source === Track.Source.Microphone && this.stopOnMute) {
49
+ if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
46
50
  log.debug('reacquiring mic track');
47
51
  await this.restartTrack();
48
52
  }
@@ -19,16 +19,20 @@ export default class LocalTrack extends Track {
19
19
 
20
20
  protected reacquireTrack: boolean;
21
21
 
22
+ protected providedByUser: boolean;
23
+
22
24
  protected constructor(
23
25
  mediaTrack: MediaStreamTrack,
24
26
  kind: Track.Kind,
25
27
  constraints?: MediaTrackConstraints,
28
+ userProvidedTrack = false,
26
29
  ) {
27
30
  super(mediaTrack, kind);
28
31
  this._mediaStreamTrack.addEventListener('ended', this.handleEnded);
29
32
  this.constraints = constraints ?? mediaTrack.getConstraints();
30
33
  this.reacquireTrack = false;
31
34
  this.wasMuted = false;
35
+ this.providedByUser = userProvidedTrack;
32
36
  }
33
37
 
34
38
  get id(): string {
@@ -56,6 +60,10 @@ export default class LocalTrack extends Track {
56
60
  return this._isUpstreamPaused;
57
61
  }
58
62
 
63
+ get isUserProvided() {
64
+ return this.providedByUser;
65
+ }
66
+
59
67
  /**
60
68
  * @returns DeviceID of the device that is currently being used for this track
61
69
  */
@@ -80,7 +88,7 @@ export default class LocalTrack extends Track {
80
88
  return this;
81
89
  }
82
90
 
83
- async replaceTrack(track: MediaStreamTrack): Promise<LocalTrack> {
91
+ async replaceTrack(track: MediaStreamTrack, userProvidedTrack = true): Promise<LocalTrack> {
84
92
  if (!this.sender) {
85
93
  throw new TrackInvalidError('unable to replace an unpublished track');
86
94
  }
@@ -108,6 +116,7 @@ export default class LocalTrack extends Track {
108
116
  });
109
117
 
110
118
  this.mediaStream = new MediaStream([track]);
119
+ this.providedByUser = userProvidedTrack;
111
120
  return this;
112
121
  }
113
122
 
@@ -184,7 +193,7 @@ export default class LocalTrack extends Track {
184
193
  if (!isMobile()) return;
185
194
  log.debug(`visibility changed, is in Background: ${this.isInBackground}`);
186
195
 
187
- if (!this.isInBackground && this.needsReAcquisition) {
196
+ if (!this.isInBackground && this.needsReAcquisition && !this.isUserProvided) {
188
197
  log.debug(`track needs to be reaquired, restarting ${this.source}`);
189
198
  await this.restart();
190
199
  this.reacquireTrack = false;