livekit-client 2.1.2 → 2.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 (56) hide show
  1. package/dist/livekit-client.esm.mjs +153 -49
  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.map +1 -1
  6. package/dist/src/room/PCTransport.d.ts.map +1 -1
  7. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  8. package/dist/src/room/RegionUrlProvider.d.ts +1 -0
  9. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  10. package/dist/src/room/Room.d.ts.map +1 -1
  11. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  12. package/dist/src/room/participant/publishUtils.d.ts +1 -0
  13. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  14. package/dist/src/room/track/LocalTrack.d.ts +5 -1
  15. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  16. package/dist/src/room/track/LocalVideoTrack.d.ts +3 -0
  17. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  18. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  19. package/dist/src/room/track/Track.d.ts +4 -1
  20. package/dist/src/room/track/Track.d.ts.map +1 -1
  21. package/dist/src/room/track/options.d.ts +4 -0
  22. package/dist/src/room/track/options.d.ts.map +1 -1
  23. package/dist/src/room/track/utils.d.ts +1 -0
  24. package/dist/src/room/track/utils.d.ts.map +1 -1
  25. package/dist/src/room/types.d.ts +1 -1
  26. package/dist/src/room/types.d.ts.map +1 -1
  27. package/dist/src/room/utils.d.ts +1 -0
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/src/version.d.ts +1 -1
  30. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +1 -0
  31. package/dist/ts4.2/src/room/participant/publishUtils.d.ts +1 -0
  32. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +5 -1
  33. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +3 -0
  34. package/dist/ts4.2/src/room/track/Track.d.ts +4 -1
  35. package/dist/ts4.2/src/room/track/options.d.ts +4 -0
  36. package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
  37. package/dist/ts4.2/src/room/types.d.ts +1 -1
  38. package/dist/ts4.2/src/room/utils.d.ts +1 -0
  39. package/dist/ts4.2/src/version.d.ts +1 -1
  40. package/package.json +2 -2
  41. package/src/api/SignalClient.ts +3 -1
  42. package/src/room/PCTransport.ts +10 -7
  43. package/src/room/RTCEngine.ts +47 -10
  44. package/src/room/RegionUrlProvider.ts +5 -0
  45. package/src/room/Room.ts +9 -2
  46. package/src/room/participant/LocalParticipant.ts +7 -16
  47. package/src/room/participant/publishUtils.ts +20 -0
  48. package/src/room/track/LocalTrack.ts +21 -2
  49. package/src/room/track/LocalVideoTrack.ts +23 -0
  50. package/src/room/track/RemoteTrack.ts +11 -5
  51. package/src/room/track/Track.ts +1 -1
  52. package/src/room/track/options.ts +5 -0
  53. package/src/room/track/utils.ts +4 -0
  54. package/src/room/types.ts +3 -1
  55. package/src/room/utils.ts +4 -2
  56. package/src/version.ts +1 -1
@@ -9,6 +9,7 @@ import {
9
9
  DisconnectReason,
10
10
  type JoinResponse,
11
11
  type LeaveRequest,
12
+ LeaveRequest_Action,
12
13
  ParticipantInfo,
13
14
  ReconnectReason,
14
15
  type ReconnectResponse,
@@ -435,7 +436,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
435
436
  this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
436
437
  };
437
438
 
438
- this.createDataChannels();
439
+ if (!supportOptionalDatachannel(joinResponse.serverInfo?.protocol)) {
440
+ this.createDataChannels();
441
+ }
439
442
  }
440
443
 
441
444
  private setupSignalClientCallbacks() {
@@ -504,16 +507,28 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
504
507
  this.handleDisconnect('signal', ReconnectReason.RR_SIGNAL_DISCONNECTED);
505
508
  };
506
509
 
507
- this.client.onLeave = (leave?: LeaveRequest) => {
508
- if (leave?.canReconnect) {
509
- this.fullReconnectOnNext = true;
510
- // reconnect immediately instead of waiting for next attempt
511
- this.handleDisconnect(leaveReconnect);
512
- } else {
513
- this.emit(EngineEvent.Disconnected, leave?.reason);
514
- this.close();
515
- }
510
+ this.client.onLeave = (leave: LeaveRequest) => {
516
511
  this.log.debug('client leave request', { ...this.logContext, reason: leave?.reason });
512
+ if (leave.regions && this.regionUrlProvider) {
513
+ this.log.debug('updating regions', this.logContext);
514
+ this.regionUrlProvider.setServerReportedRegions(leave.regions);
515
+ }
516
+ switch (leave.action) {
517
+ case LeaveRequest_Action.DISCONNECT:
518
+ this.emit(EngineEvent.Disconnected, leave?.reason);
519
+ this.close();
520
+ break;
521
+ case LeaveRequest_Action.RECONNECT:
522
+ this.fullReconnectOnNext = true;
523
+ // reconnect immediately instead of waiting for next attempt
524
+ this.handleDisconnect(leaveReconnect);
525
+ break;
526
+ case LeaveRequest_Action.RESUME:
527
+ // reconnect immediately instead of waiting for next attempt
528
+ this.handleDisconnect(leaveReconnect);
529
+ default:
530
+ break;
531
+ }
517
532
  };
518
533
  }
519
534
 
@@ -1103,11 +1118,21 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1103
1118
  throw new ConnectionError(`${transportName} connection not set`);
1104
1119
  }
1105
1120
 
1121
+ let needNegotiation = false;
1122
+ if (!subscriber && !this.dataChannelForKind(kind, subscriber)) {
1123
+ this.createDataChannels();
1124
+ needNegotiation = true;
1125
+ }
1126
+
1106
1127
  if (
1128
+ !needNegotiation &&
1107
1129
  !subscriber &&
1108
1130
  !this.pcManager.publisher.isICEConnected &&
1109
1131
  this.pcManager.publisher.getICEConnectionState() !== 'checking'
1110
1132
  ) {
1133
+ needNegotiation = true;
1134
+ }
1135
+ if (needNegotiation) {
1111
1136
  // start negotiation
1112
1137
  this.negotiate();
1113
1138
  }
@@ -1168,6 +1193,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1168
1193
  }
1169
1194
 
1170
1195
  this.pcManager.requirePublisher();
1196
+ // don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
1197
+ if (
1198
+ this.pcManager.publisher.getTransceivers().length == 0 &&
1199
+ !this.lossyDC &&
1200
+ !this.reliableDC
1201
+ ) {
1202
+ this.createDataChannels();
1203
+ }
1171
1204
 
1172
1205
  const abortController = new AbortController();
1173
1206
 
@@ -1378,3 +1411,7 @@ export type EngineEventCallbacks = {
1378
1411
  remoteMute: (trackSid: string, muted: boolean) => void;
1379
1412
  offline: () => void;
1380
1413
  };
1414
+
1415
+ function supportOptionalDatachannel(protocol: number | undefined): boolean {
1416
+ return protocol !== undefined && protocol > 13;
1417
+ }
@@ -75,6 +75,11 @@ export class RegionUrlProvider {
75
75
  );
76
76
  }
77
77
  }
78
+
79
+ setServerReportedRegions(regions: RegionSettings) {
80
+ this.regionSettings = regions;
81
+ this.lastUpdateAt = Date.now();
82
+ }
78
83
  }
79
84
 
80
85
  function getCloudConfigUrl(serverUrl: URL) {
package/src/room/Room.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  DisconnectReason,
5
5
  JoinResponse,
6
6
  LeaveRequest,
7
+ LeaveRequest_Action,
7
8
  ParticipantInfo,
8
9
  ParticipantInfo_State,
9
10
  ParticipantPermission,
@@ -859,7 +860,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
859
860
  onLeave(
860
861
  new LeaveRequest({
861
862
  reason: DisconnectReason.CLIENT_INITIATED,
862
- canReconnect: true,
863
+ action: LeaveRequest_Action.RECONNECT,
863
864
  }),
864
865
  );
865
866
  }
@@ -876,7 +877,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
876
877
  },
877
878
  });
878
879
  break;
879
-
880
+ case 'leave-full-reconnect':
881
+ req = new SimulateScenario({
882
+ scenario: {
883
+ case: 'leaveRequestFullReconnect',
884
+ value: true,
885
+ },
886
+ });
880
887
  default:
881
888
  }
882
889
  if (req) {
@@ -54,6 +54,7 @@ import { trackPermissionToProto } from './ParticipantTrackPermission';
54
54
  import {
55
55
  computeTrackBackupEncodings,
56
56
  computeVideoEncodings,
57
+ getDefaultDegradationPreference,
57
58
  mediaTrackToLocalTrack,
58
59
  } from './publishUtils';
59
60
 
@@ -857,6 +858,11 @@ export default class LocalParticipant extends Participant {
857
858
 
858
859
  track.sender = await this.engine.createSender(track, opts, encodings);
859
860
 
861
+ if (track instanceof LocalVideoTrack) {
862
+ opts.degradationPreference ??= getDefaultDegradationPreference(track);
863
+ track.setDegradationPreference(opts.degradationPreference);
864
+ }
865
+
860
866
  if (encodings) {
861
867
  if (isFireFox() && track.kind === Track.Kind.Audio) {
862
868
  /* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
@@ -880,8 +886,7 @@ export default class LocalParticipant extends Participant {
880
886
  maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
881
887
  });
882
888
  }
883
- } else if (track.codec && track.codec == 'av1' && encodings[0]?.maxBitrate) {
884
- // AV1 requires setting x-start-bitrate in SDP
889
+ } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
885
890
  this.engine.pcManager.publisher.setTrackCodecBitrate({
886
891
  cid: req.cid,
887
892
  codec: track.codec,
@@ -890,20 +895,6 @@ export default class LocalParticipant extends Participant {
890
895
  }
891
896
  }
892
897
 
893
- if (track.kind === Track.Kind.Video && track.source === Track.Source.ScreenShare) {
894
- // a few of reasons we are forcing this setting without allowing overrides:
895
- // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue
896
- // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced`
897
- try {
898
- this.log.debug(`setting degradationPreference to maintain-resolution`);
899
- const params = track.sender.getParameters();
900
- params.degradationPreference = 'maintain-resolution';
901
- await track.sender.setParameters(params);
902
- } catch (e) {
903
- this.log.warn(`failed to set degradationPreference: ${e}`);
904
- }
905
- }
906
-
907
898
  await this.engine.negotiate();
908
899
 
909
900
  if (track instanceof LocalVideoTrack) {
@@ -19,6 +19,7 @@ import {
19
19
  isReactNative,
20
20
  isSVCCodec,
21
21
  isSafari,
22
+ unwrapConstraint,
22
23
  } from '../utils';
23
24
 
24
25
  /** @internal */
@@ -171,6 +172,11 @@ export function computeVideoEncodings(
171
172
  });
172
173
  }
173
174
 
175
+ if (original.encoding.priority) {
176
+ encodings[0].priority = original.encoding.priority;
177
+ encodings[0].networkPriority = original.encoding.priority;
178
+ }
179
+
174
180
  log.debug(`using svc encoding`, { encodings });
175
181
  return encodings;
176
182
  }
@@ -435,3 +441,17 @@ export class ScalabilityMode {
435
441
  return `L${this.spatial}T${this.temporal}${this.suffix ?? ''}`;
436
442
  }
437
443
  }
444
+
445
+ export function getDefaultDegradationPreference(track: LocalVideoTrack): RTCDegradationPreference {
446
+ // a few of reasons we have different default paths:
447
+ // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue
448
+ // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced`
449
+ if (
450
+ track.source === Track.Source.ScreenShare ||
451
+ (track.constraints.height && unwrapConstraint(track.constraints.height) >= 1080)
452
+ ) {
453
+ return 'maintain-resolution';
454
+ } else {
455
+ return 'balanced';
456
+ }
457
+ }
@@ -15,8 +15,17 @@ const defaultDimensionsTimeout = 1000;
15
15
  export default abstract class LocalTrack<
16
16
  TrackKind extends Track.Kind = Track.Kind,
17
17
  > extends Track<TrackKind> {
18
+ protected _sender?: RTCRtpSender;
19
+
18
20
  /** @internal */
19
- sender?: RTCRtpSender;
21
+ get sender(): RTCRtpSender | undefined {
22
+ return this._sender;
23
+ }
24
+
25
+ /** @internal */
26
+ set sender(sender: RTCRtpSender | undefined) {
27
+ this._sender = sender;
28
+ }
20
29
 
21
30
  /** @internal */
22
31
  codec?: VideoCodec;
@@ -43,6 +52,8 @@ export default abstract class LocalTrack<
43
52
 
44
53
  protected audioContext?: AudioContext;
45
54
 
55
+ protected manuallyStopped: boolean = false;
56
+
46
57
  private restartLock: Mutex;
47
58
 
48
59
  /**
@@ -259,6 +270,7 @@ export default abstract class LocalTrack<
259
270
  }
260
271
 
261
272
  protected async restart(constraints?: MediaTrackConstraints) {
273
+ this.manuallyStopped = false;
262
274
  const unlock = await this.restartLock.lock();
263
275
  try {
264
276
  if (!constraints) {
@@ -296,8 +308,14 @@ export default abstract class LocalTrack<
296
308
 
297
309
  await this.setMediaStreamTrack(newTrack);
298
310
  this._constraints = constraints;
299
-
300
311
  this.emit(TrackEvent.Restarted, this);
312
+ if (this.manuallyStopped) {
313
+ this.log.warn(
314
+ 'track was stopped during a restart, stopping restarted track',
315
+ this.logContext,
316
+ );
317
+ this.stop();
318
+ }
301
319
  return this;
302
320
  } finally {
303
321
  unlock();
@@ -361,6 +379,7 @@ export default abstract class LocalTrack<
361
379
  };
362
380
 
363
381
  stop() {
382
+ this.manuallyStopped = true;
364
383
  super.stop();
365
384
 
366
385
  this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
@@ -53,6 +53,15 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
53
53
  // a missing `getParameter` call.
54
54
  private senderLock: Mutex;
55
55
 
56
+ private degradationPreference: RTCDegradationPreference = 'balanced';
57
+
58
+ override set sender(sender: RTCRtpSender | undefined) {
59
+ this._sender = sender;
60
+ if (this.degradationPreference) {
61
+ this.setDegradationPreference(this.degradationPreference);
62
+ }
63
+ }
64
+
56
65
  /**
57
66
  *
58
67
  * @param mediaTrack
@@ -273,6 +282,20 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
273
282
  }
274
283
  }
275
284
 
285
+ async setDegradationPreference(preference: RTCDegradationPreference) {
286
+ this.degradationPreference = preference;
287
+ if (this.sender) {
288
+ try {
289
+ this.log.debug(`setting degradationPreference to ${preference}`, this.logContext);
290
+ const params = this.sender.getParameters();
291
+ params.degradationPreference = preference;
292
+ this.sender.setParameters(params);
293
+ } catch (e: any) {
294
+ this.log.warn(`failed to set degradationPreference`, { error: e, ...this.logContext });
295
+ }
296
+ }
297
+ }
298
+
276
299
  addSimulcastTrack(
277
300
  codec: VideoCodec,
278
301
  encodings?: RTCRtpEncodingParameters[],
@@ -2,6 +2,7 @@ import { TrackEvent } from '../events';
2
2
  import { monitorFrequency } from '../stats';
3
3
  import type { LoggerOptions } from '../types';
4
4
  import { Track } from './Track';
5
+ import { supportsSynchronizationSources } from './utils';
5
6
 
6
7
  export default abstract class RemoteTrack<
7
8
  TrackKind extends Track.Kind = Track.Kind,
@@ -77,7 +78,9 @@ export default abstract class RemoteTrack<
77
78
  if (!this.monitorInterval) {
78
79
  this.monitorInterval = setInterval(() => this.monitorReceiver(), monitorFrequency);
79
80
  }
80
- this.registerTimeSyncUpdate();
81
+ if (supportsSynchronizationSources()) {
82
+ this.registerTimeSyncUpdate();
83
+ }
81
84
  }
82
85
 
83
86
  protected abstract monitorReceiver(): void;
@@ -85,10 +88,13 @@ export default abstract class RemoteTrack<
85
88
  registerTimeSyncUpdate() {
86
89
  const loop = () => {
87
90
  this.timeSyncHandle = requestAnimationFrame(() => loop());
88
- const newTime = this.receiver?.getSynchronizationSources()[0]?.rtpTimestamp;
89
- if (newTime && this.rtpTimestamp !== newTime) {
90
- this.emit(TrackEvent.TimeSyncUpdate, newTime);
91
- this.rtpTimestamp = newTime;
91
+ const sources = this.receiver?.getSynchronizationSources()[0];
92
+ if (sources) {
93
+ const { timestamp, rtpTimestamp } = sources;
94
+ if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
95
+ this.emit(TrackEvent.TimeSyncUpdate, { timestamp, rtpTimestamp });
96
+ this.rtpTimestamp = rtpTimestamp;
97
+ }
92
98
  }
93
99
  };
94
100
  loop();
@@ -525,5 +525,5 @@ export type TrackEventCallbacks = {
525
525
  upstreamResumed: (track: any) => void;
526
526
  trackProcessorUpdate: (processor?: TrackProcessor<Track.Kind, any>) => void;
527
527
  audioTrackFeatureUpdate: (track: any, feature: AudioTrackFeature, enabled: boolean) => void;
528
- timeSyncUpdate: (timestamp: number) => void;
528
+ timeSyncUpdate: (update: { timestamp: number; rtpTimestamp: number }) => void;
529
529
  };
@@ -64,6 +64,11 @@ export interface TrackPublishDefaults {
64
64
  */
65
65
  scalabilityMode?: ScalabilityMode;
66
66
 
67
+ /**
68
+ * degradation preference
69
+ */
70
+ degradationPreference?: RTCDegradationPreference;
71
+
67
72
  /**
68
73
  * Up to two additional simulcast layers to publish in addition to the original
69
74
  * Track.
@@ -239,3 +239,7 @@ export function getLogContextFromTrack(track: Track | TrackPublication): Record<
239
239
  };
240
240
  }
241
241
  }
242
+
243
+ export function supportsSynchronizationSources(): boolean {
244
+ return typeof RTCRtpReceiver !== 'undefined' && 'getSynchronizationSources' in RTCRtpReceiver;
245
+ }
package/src/room/types.ts CHANGED
@@ -49,7 +49,9 @@ export type SimulationScenario =
49
49
  // to disable congestion control entirely (by setting bandwidth to 100Mbps)
50
50
  | 'subscriber-bandwidth'
51
51
  | 'disconnect-signal-on-resume'
52
- | 'disconnect-signal-on-resume-no-messages';
52
+ | 'disconnect-signal-on-resume-no-messages'
53
+ // instructs the server to send a full reconnect reconnect action to the client
54
+ | 'leave-full-reconnect';
53
55
 
54
56
  export type LoggerOptions = {
55
57
  loggerName?: string;
package/src/room/utils.ts CHANGED
@@ -491,8 +491,10 @@ export function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec {
491
491
  return videoCodecs.includes(maybeCodec as VideoCodec);
492
492
  }
493
493
 
494
- export function unwrapConstraint(constraint: ConstrainDOMString): string {
495
- if (typeof constraint === 'string') {
494
+ export function unwrapConstraint(constraint: ConstrainDOMString): string;
495
+ export function unwrapConstraint(constraint: ConstrainULong): number;
496
+ export function unwrapConstraint(constraint: ConstrainDOMString | ConstrainULong): string | number {
497
+ if (typeof constraint === 'string' || typeof constraint === 'number') {
496
498
  return constraint;
497
499
  }
498
500
 
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { version as v } from '../package.json';
2
2
 
3
3
  export const version = v;
4
- export const protocolVersion = 12;
4
+ export const protocolVersion = 13;