livekit-client 2.1.2 → 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
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;