livekit-client 1.1.9 → 1.2.2

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 (34) hide show
  1. package/dist/livekit-client.esm.mjs +218 -70
  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/api/SignalClient.d.ts +3 -1
  6. package/dist/src/api/SignalClient.d.ts.map +1 -1
  7. package/dist/src/options.d.ts +1 -0
  8. package/dist/src/options.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +9 -0
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/RTCEngine.d.ts +1 -0
  12. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  13. package/dist/src/room/Room.d.ts +1 -1
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/events.d.ts +8 -1
  16. package/dist/src/room/events.d.ts.map +1 -1
  17. package/dist/src/room/participant/LocalParticipant.d.ts +3 -3
  18. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  19. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  20. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  21. package/dist/src/room/track/RemoteTrackPublication.d.ts +4 -1
  22. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/api/SignalClient.ts +3 -1
  25. package/src/options.ts +1 -0
  26. package/src/room/PCTransport.ts +39 -0
  27. package/src/room/RTCEngine.ts +41 -16
  28. package/src/room/Room.ts +20 -16
  29. package/src/room/events.ts +7 -0
  30. package/src/room/participant/LocalParticipant.ts +70 -10
  31. package/src/room/participant/RemoteParticipant.ts +12 -10
  32. package/src/room/participant/publishUtils.ts +1 -1
  33. package/src/room/track/LocalTrack.ts +4 -0
  34. package/src/room/track/RemoteTrackPublication.ts +37 -11
@@ -32,7 +32,7 @@ import {
32
32
  } from '../track/options';
33
33
  import { Track } from '../track/Track';
34
34
  import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
35
- import { isFireFox } from '../utils';
35
+ import { isFireFox, isWeb } from '../utils';
36
36
  import Participant from './Participant';
37
37
  import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
38
38
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
@@ -125,8 +125,9 @@ export default class LocalParticipant extends Participant {
125
125
  setCameraEnabled(
126
126
  enabled: boolean,
127
127
  options?: VideoCaptureOptions,
128
+ publishOptions?: TrackPublishOptions,
128
129
  ): Promise<LocalTrackPublication | undefined> {
129
- return this.setTrackEnabled(Track.Source.Camera, enabled, options);
130
+ return this.setTrackEnabled(Track.Source.Camera, enabled, options, publishOptions);
130
131
  }
131
132
 
132
133
  /**
@@ -138,8 +139,9 @@ export default class LocalParticipant extends Participant {
138
139
  setMicrophoneEnabled(
139
140
  enabled: boolean,
140
141
  options?: AudioCaptureOptions,
142
+ publishOptions?: TrackPublishOptions,
141
143
  ): Promise<LocalTrackPublication | undefined> {
142
- return this.setTrackEnabled(Track.Source.Microphone, enabled, options);
144
+ return this.setTrackEnabled(Track.Source.Microphone, enabled, options, publishOptions);
143
145
  }
144
146
 
145
147
  /**
@@ -149,8 +151,9 @@ export default class LocalParticipant extends Participant {
149
151
  setScreenShareEnabled(
150
152
  enabled: boolean,
151
153
  options?: ScreenShareCaptureOptions,
154
+ publishOptions?: TrackPublishOptions,
152
155
  ): Promise<LocalTrackPublication | undefined> {
153
- return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options);
156
+ return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options, publishOptions);
154
157
  }
155
158
 
156
159
  /** @internal */
@@ -172,21 +175,25 @@ export default class LocalParticipant extends Participant {
172
175
  source: Extract<Track.Source, Track.Source.Camera>,
173
176
  enabled: boolean,
174
177
  options?: VideoCaptureOptions,
178
+ publishOptions?: TrackPublishOptions,
175
179
  ): Promise<LocalTrackPublication | undefined>;
176
180
  private async setTrackEnabled(
177
181
  source: Extract<Track.Source, Track.Source.Microphone>,
178
182
  enabled: boolean,
179
183
  options?: AudioCaptureOptions,
184
+ publishOptions?: TrackPublishOptions,
180
185
  ): Promise<LocalTrackPublication | undefined>;
181
186
  private async setTrackEnabled(
182
187
  source: Extract<Track.Source, Track.Source.ScreenShare>,
183
188
  enabled: boolean,
184
189
  options?: ScreenShareCaptureOptions,
190
+ publishOptions?: TrackPublishOptions,
185
191
  ): Promise<LocalTrackPublication | undefined>;
186
192
  private async setTrackEnabled(
187
193
  source: Track.Source,
188
194
  enabled: true,
189
195
  options?: VideoCaptureOptions | AudioCaptureOptions | ScreenShareCaptureOptions,
196
+ publishOptions?: TrackPublishOptions,
190
197
  ) {
191
198
  log.debug('setTrackEnabled', { source, enabled });
192
199
  let track = this.getTrack(source);
@@ -224,7 +231,7 @@ export default class LocalParticipant extends Participant {
224
231
  }
225
232
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
226
233
  for (const localTrack of localTracks) {
227
- publishPromises.push(this.publishTrack(localTrack));
234
+ publishPromises.push(this.publishTrack(localTrack, publishOptions));
228
235
  }
229
236
  const publishedTracks = await Promise.all(publishPromises);
230
237
  // for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
@@ -544,6 +551,14 @@ export default class LocalParticipant extends Participant {
544
551
  track.codec = opts.videoCodec;
545
552
  }
546
553
 
554
+ if (track.codec === 'av1' && encodings && encodings[0]?.maxBitrate) {
555
+ this.engine.publisher.setTrackCodecBitrate(
556
+ req.cid,
557
+ track.codec,
558
+ encodings[0].maxBitrate / 1000,
559
+ );
560
+ }
561
+
547
562
  this.engine.negotiate();
548
563
 
549
564
  // store RTPSender
@@ -642,6 +657,13 @@ export default class LocalParticipant extends Participant {
642
657
  this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
643
658
  track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
644
659
 
660
+ if (videoCodec === 'av1' && encodings[0]?.maxBitrate) {
661
+ this.engine.publisher.setTrackCodecBitrate(
662
+ req.cid,
663
+ videoCodec,
664
+ encodings[0].maxBitrate / 1000,
665
+ );
666
+ }
645
667
  this.engine.negotiate();
646
668
  log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
647
669
  }
@@ -894,11 +916,49 @@ export default class LocalParticipant extends Participant {
894
916
  this.unpublishTrack(track.track!);
895
917
  };
896
918
 
897
- private handleTrackEnded = (track: LocalTrack) => {
898
- log.debug('unpublishing local track due to TrackEnded', {
899
- track: track.sid,
900
- });
901
- this.unpublishTrack(track);
919
+ private handleTrackEnded = async (track: LocalTrack) => {
920
+ if (
921
+ track.source === Track.Source.ScreenShare ||
922
+ track.source === Track.Source.ScreenShareAudio
923
+ ) {
924
+ log.debug('unpublishing local track due to TrackEnded', {
925
+ track: track.sid,
926
+ });
927
+ this.unpublishTrack(track);
928
+ } else if (track.isUserProvided) {
929
+ await track.pauseUpstream();
930
+ } else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
931
+ try {
932
+ if (isWeb()) {
933
+ try {
934
+ const currentPermissions = await navigator?.permissions.query({
935
+ // the permission query for camera and microphone currently not supported in Safari and Firefox
936
+ // @ts-ignore
937
+ name: track.source === Track.Source.Camera ? 'camera' : 'microphone',
938
+ });
939
+ if (currentPermissions && currentPermissions.state === 'denied') {
940
+ log.warn(`user has revoked access to ${track.source}`);
941
+
942
+ // detect granted change after permissions were denied to try and resume then
943
+ currentPermissions.onchange = () => {
944
+ if (currentPermissions.state !== 'denied') {
945
+ track.restartTrack();
946
+ currentPermissions.onchange = null;
947
+ }
948
+ };
949
+ throw new Error('GetUserMedia Permission denied');
950
+ }
951
+ } catch (e: any) {
952
+ // permissions query fails for firefox, we continue and try to restart the track
953
+ }
954
+ }
955
+ log.debug('track ended, attempting to use a different device');
956
+ await track.restartTrack();
957
+ } catch (e) {
958
+ log.warn(`could not restart track, pausing upstream instead`);
959
+ await track.pauseUpstream();
960
+ }
961
+ }
902
962
  };
903
963
 
904
964
  private getPublicationForTrack(
@@ -7,6 +7,7 @@ import RemoteAudioTrack from '../track/RemoteAudioTrack';
7
7
  import RemoteTrackPublication from '../track/RemoteTrackPublication';
8
8
  import RemoteVideoTrack from '../track/RemoteVideoTrack';
9
9
  import { Track } from '../track/Track';
10
+ import { TrackPublication } from '../track/TrackPublication';
10
11
  import { AdaptiveStreamSettings, RemoteTrack } from '../track/types';
11
12
  import Participant, { ParticipantEventCallbacks } from './Participant';
12
13
 
@@ -49,8 +50,17 @@ export default class RemoteParticipant extends Participant {
49
50
  });
50
51
  this.signalClient.sendUpdateSubscription(sub);
51
52
  });
52
- publication.on(TrackEvent.Ended, (track: RemoteTrack) => {
53
- this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
53
+ publication.on(
54
+ TrackEvent.SubscriptionPermissionChanged,
55
+ (status: TrackPublication.SubscriptionStatus) => {
56
+ this.emit(ParticipantEvent.TrackSubscriptionPermissionChanged, publication, status);
57
+ },
58
+ );
59
+ publication.on(TrackEvent.Subscribed, (track: RemoteTrack) => {
60
+ this.emit(ParticipantEvent.TrackSubscribed, track, publication);
61
+ });
62
+ publication.on(TrackEvent.Unsubscribed, (previousTrack: RemoteTrack) => {
63
+ this.emit(ParticipantEvent.TrackUnsubscribed, previousTrack, publication);
54
64
  });
55
65
  }
56
66
 
@@ -156,8 +166,6 @@ export default class RemoteParticipant extends Participant {
156
166
  track.start();
157
167
 
158
168
  publication.setTrack(track);
159
- // subscription means participant has permissions to subscribe
160
- publication._allowed = true;
161
169
  // set participant volume on new microphone tracks
162
170
  if (
163
171
  this.volume !== undefined &&
@@ -166,7 +174,6 @@ export default class RemoteParticipant extends Participant {
166
174
  ) {
167
175
  track.setVolume(this.volume);
168
176
  }
169
- this.emit(ParticipantEvent.TrackSubscribed, track, publication);
170
177
 
171
178
  return publication;
172
179
  }
@@ -251,13 +258,8 @@ export default class RemoteParticipant extends Participant {
251
258
  // also send unsubscribe, if track is actively subscribed
252
259
  const { track } = publication;
253
260
  if (track) {
254
- const { isSubscribed } = publication;
255
261
  track.stop();
256
262
  publication.setTrack(undefined);
257
- // always send unsubscribed, since apps may rely on this
258
- if (isSubscribed) {
259
- this.emit(ParticipantEvent.TrackUnsubscribed, track, publication);
260
- }
261
263
  }
262
264
  if (sendUnpublish) {
263
265
  this.emit(ParticipantEvent.TrackUnpublished, publication);
@@ -106,7 +106,7 @@ export function computeVideoEncodings(
106
106
  encodings.push({
107
107
  rid: videoRids[2 - i],
108
108
  scaleResolutionDownBy: 2 ** i,
109
- maxBitrate: videoEncoding ? videoEncoding.maxBitrate / 2 ** i : 0,
109
+ maxBitrate: videoEncoding ? videoEncoding.maxBitrate / 3 ** i : 0,
110
110
  /* @ts-ignore */
111
111
  maxFramerate: original.encoding.maxFramerate,
112
112
  /* @ts-ignore */
@@ -117,6 +117,8 @@ export default class LocalTrack extends Track {
117
117
  }
118
118
  this._mediaStreamTrack = track;
119
119
 
120
+ await this.resumeUpstream();
121
+
120
122
  this.attachedElements.forEach((el) => {
121
123
  attachToElement(track, el);
122
124
  });
@@ -166,6 +168,8 @@ export default class LocalTrack extends Track {
166
168
 
167
169
  this._mediaStreamTrack = newTrack;
168
170
 
171
+ await this.resumeUpstream();
172
+
169
173
  this.attachedElements.forEach((el) => {
170
174
  attachToElement(newTrack, el);
171
175
  });
@@ -11,7 +11,7 @@ export default class RemoteTrackPublication extends TrackPublication {
11
11
  track?: RemoteTrack;
12
12
 
13
13
  /** @internal */
14
- _allowed = true;
14
+ protected allowed = true;
15
15
 
16
16
  // keeps track of client's desire to subscribe to a track
17
17
  protected subscribed?: boolean;
@@ -28,6 +28,9 @@ export default class RemoteTrackPublication extends TrackPublication {
28
28
  */
29
29
  setSubscribed(subscribed: boolean) {
30
30
  this.subscribed = subscribed;
31
+ // reset allowed status when desired subscription state changes
32
+ // server will notify client via signal message if it's not allowed
33
+ this.allowed = true;
31
34
 
32
35
  const sub: UpdateSubscription = {
33
36
  trackSids: [this.trackSid],
@@ -46,11 +49,11 @@ export default class RemoteTrackPublication extends TrackPublication {
46
49
 
47
50
  get subscriptionStatus(): TrackPublication.SubscriptionStatus {
48
51
  if (this.subscribed === false || !super.isSubscribed) {
52
+ if (!this.allowed) {
53
+ return TrackPublication.SubscriptionStatus.NotAllowed;
54
+ }
49
55
  return TrackPublication.SubscriptionStatus.Unsubscribed;
50
56
  }
51
- if (!this._allowed) {
52
- return TrackPublication.SubscriptionStatus.NotAllowed;
53
- }
54
57
  return TrackPublication.SubscriptionStatus.Subscribed;
55
58
  }
56
59
 
@@ -61,9 +64,6 @@ export default class RemoteTrackPublication extends TrackPublication {
61
64
  if (this.subscribed === false) {
62
65
  return false;
63
66
  }
64
- if (!this._allowed) {
65
- return false;
66
- }
67
67
  return super.isSubscribed;
68
68
  }
69
69
 
@@ -127,11 +127,13 @@ export default class RemoteTrackPublication extends TrackPublication {
127
127
 
128
128
  /** @internal */
129
129
  setTrack(track?: Track) {
130
- if (this.track) {
130
+ const prevStatus = this.subscriptionStatus;
131
+ const prevTrack = this.track;
132
+ if (prevTrack) {
131
133
  // unregister listener
132
- this.track.off(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
133
- this.track.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
134
- this.track.off(TrackEvent.Ended, this.handleEnded);
134
+ prevTrack.off(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
135
+ prevTrack.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
136
+ prevTrack.off(TrackEvent.Ended, this.handleEnded);
135
137
  }
136
138
  super.setTrack(track);
137
139
  if (track) {
@@ -140,6 +142,22 @@ export default class RemoteTrackPublication extends TrackPublication {
140
142
  track.on(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
141
143
  track.on(TrackEvent.Ended, this.handleEnded);
142
144
  }
145
+ this.emitSubscriptionUpdateIfChanged(prevStatus);
146
+ if (!!track !== !!prevTrack) {
147
+ // when undefined status changes, there's a subscription changed event
148
+ if (track) {
149
+ this.emit(TrackEvent.Subscribed, track);
150
+ } else {
151
+ this.emit(TrackEvent.Unsubscribed, prevTrack);
152
+ }
153
+ }
154
+ }
155
+
156
+ /** @internal */
157
+ setAllowed(allowed: boolean) {
158
+ const prevStatus = this.subscriptionStatus;
159
+ this.allowed = allowed;
160
+ this.emitSubscriptionUpdateIfChanged(prevStatus);
143
161
  }
144
162
 
145
163
  /** @internal */
@@ -149,6 +167,14 @@ export default class RemoteTrackPublication extends TrackPublication {
149
167
  this.track?.setMuted(info.muted);
150
168
  }
151
169
 
170
+ private emitSubscriptionUpdateIfChanged(previousStatus: TrackPublication.SubscriptionStatus) {
171
+ const currentStatus = this.subscriptionStatus;
172
+ if (previousStatus === currentStatus) {
173
+ return;
174
+ }
175
+ this.emit(TrackEvent.SubscriptionPermissionChanged, currentStatus, previousStatus);
176
+ }
177
+
152
178
  private isManualOperationAllowed(): boolean {
153
179
  if (this.isAdaptiveStream) {
154
180
  log.warn('adaptive stream is enabled, cannot change track settings', {