livekit-client 2.18.3 → 2.18.5

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 (53) hide show
  1. package/dist/livekit-client.esm.mjs +703 -334
  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 +12 -4
  8. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  9. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  10. package/dist/src/room/Room.d.ts +3 -0
  11. package/dist/src/room/Room.d.ts.map +1 -1
  12. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
  13. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  14. package/dist/src/room/events.d.ts +3 -1
  15. package/dist/src/room/events.d.ts.map +1 -1
  16. package/dist/src/room/participant/LocalParticipant.d.ts +8 -0
  17. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  18. package/dist/src/room/participant/RemoteParticipant.d.ts +4 -3
  19. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  20. package/dist/src/room/track/LocalTrack.d.ts +7 -0
  21. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  22. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  23. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  24. package/dist/src/room/types.d.ts +1 -1
  25. package/dist/src/room/types.d.ts.map +1 -1
  26. package/dist/ts4.2/room/RTCEngine.d.ts +12 -4
  27. package/dist/ts4.2/room/Room.d.ts +3 -0
  28. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -0
  29. package/dist/ts4.2/room/events.d.ts +3 -1
  30. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -0
  31. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +4 -3
  32. package/dist/ts4.2/room/track/LocalTrack.d.ts +7 -0
  33. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  34. package/dist/ts4.2/room/types.d.ts +1 -1
  35. package/package.json +3 -3
  36. package/src/api/SignalClient.ts +4 -0
  37. package/src/room/PCTransport.ts +10 -8
  38. package/src/room/RTCEngine.ts +59 -28
  39. package/src/room/RegionUrlProvider.ts +7 -0
  40. package/src/room/Room.ts +93 -23
  41. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +331 -16
  42. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +92 -41
  43. package/src/room/events.ts +2 -0
  44. package/src/room/participant/LocalParticipant.ts +70 -5
  45. package/src/room/participant/RemoteParticipant.ts +14 -2
  46. package/src/room/token-source/TokenSource.test.ts +337 -0
  47. package/src/room/token-source/test-tokens.ts +28 -0
  48. package/src/room/token-source/utils.test.ts +12 -20
  49. package/src/room/track/LocalTrack.ts +15 -1
  50. package/src/room/track/LocalVideoTrack.ts +126 -2
  51. package/src/room/track/RemoteVideoTrack.ts +8 -2
  52. package/src/room/types.ts +2 -1
  53. package/src/utils/deferrable-map.ts +2 -2
@@ -1,7 +1,8 @@
1
1
  import type { ParticipantInfo } from '@livekit/protocol';
2
2
  import type { SignalClient } from '../../api/SignalClient';
3
3
  import { DeferrableMap } from '../../utils/deferrable-map';
4
- import type RemoteDataTrack from '../data-track/RemoteDataTrack';
4
+ import RemoteDataTrack from '../data-track/RemoteDataTrack';
5
+ import type IncomingDataTrackManager from '../data-track/incoming/IncomingDataTrackManager';
5
6
  import RemoteTrackPublication from '../track/RemoteTrackPublication';
6
7
  import { Track } from '../track/Track';
7
8
  import type { AudioOutputOptions } from '../track/options';
@@ -24,13 +25,13 @@ export default class RemoteParticipant extends Participant {
24
25
  private volumeMap;
25
26
  private audioOutput?;
26
27
  /** @internal */
27
- static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo, loggerOptions: LoggerOptions): RemoteParticipant;
28
+ static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo, loggerOptions: LoggerOptions, manager: IncomingDataTrackManager): RemoteParticipant;
28
29
  protected get logContext(): {
29
30
  remoteParticipantID: string;
30
31
  remoteParticipant: string;
31
32
  };
32
33
  /** @internal */
33
- constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, attributes?: Record<string, string>, loggerOptions?: LoggerOptions, kind?: ParticipantKind);
34
+ constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, attributes?: Record<string, string>, loggerOptions?: LoggerOptions, kind?: ParticipantKind, remoteDataTracks?: Array<RemoteDataTrack>);
34
35
  protected addTrackPublication(publication: RemoteTrackPublication): void;
35
36
  getTrackPublication(source: Track.Source): RemoteTrackPublication | undefined;
36
37
  getTrackPublicationByName(name: string): RemoteTrackPublication | undefined;
@@ -60,6 +60,13 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
60
60
  unmute(): Promise<this>;
61
61
  replaceTrack(track: MediaStreamTrack, options?: ReplaceTrackOptions): Promise<typeof this>;
62
62
  replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<typeof this>;
63
+ /**
64
+ * Hook invoked after the MediaStreamTrack on the sender has been swapped
65
+ * (via replaceTrack, setProcessor, or stopProcessor). Fires outside the
66
+ * trackChangeLock so subclasses can do asynchronous work such as polling
67
+ * for new dimensions without blocking other track operations.
68
+ */
69
+ protected onSenderTrackSwapped(): Promise<void>;
63
70
  protected restart(constraints?: MediaTrackConstraints, isUnmuting?: boolean): Promise<this>;
64
71
  protected setTrackMuted(muted: boolean): void;
65
72
  protected get needsReAcquisition(): boolean;
@@ -4,7 +4,7 @@ import type { VideoSenderStats } from '../stats';
4
4
  import type { LoggerOptions } from '../types';
5
5
  import LocalTrack from './LocalTrack';
6
6
  import { Track, VideoQuality } from './Track';
7
- import type { VideoCaptureOptions, VideoCodec } from './options';
7
+ import type { TrackPublishOptions, VideoCaptureOptions, VideoCodec } from './options';
8
8
  import type { TrackProcessor } from './processor/types';
9
9
  export declare class SimulcastTrackInfo {
10
10
  codec: VideoCodec;
@@ -23,6 +23,8 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
23
23
  private degradationPreference;
24
24
  private isCpuConstrained;
25
25
  private optimizeForPerformance;
26
+ publishOptions?: TrackPublishOptions;
27
+ lastEncodedDimensions?: Track.Dimensions;
26
28
  get sender(): RTCRtpSender | undefined;
27
29
  set sender(sender: RTCRtpSender | undefined);
28
30
  /**
@@ -43,6 +45,15 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
43
45
  getSenderStats(): Promise<VideoSenderStats[]>;
44
46
  setPublishingQuality(maxQuality: VideoQuality): void;
45
47
  restartTrack(options?: VideoCaptureOptions): Promise<void>;
48
+ protected onSenderTrackSwapped(): Promise<void>;
49
+ /**
50
+ * Recomputes encoding parameters for this track's senders based on the current
51
+ * MediaStreamTrack dimensions and reapplies them via setParameters. This is a no-op
52
+ * if the track hasn't been published yet or if the track is in performance-optimized
53
+ * mode (which manages its own encodings).
54
+ */
55
+ private refreshSenderEncodings;
56
+ private applyEncodingsToSender;
46
57
  setProcessor(processor: TrackProcessor<Track.Kind.Video>, showProcessedStreamLocally?: boolean): Promise<void>;
47
58
  setDegradationPreference(preference: RTCDegradationPreference): Promise<void>;
48
59
  addSimulcastTrack(codec: VideoCodec, encodings?: RTCRtpEncodingParameters[]): SimulcastTrackInfo | undefined;
@@ -62,7 +62,7 @@ export type LiveKitReactNativeInfo = {
62
62
  platform: 'ios' | 'android' | 'windows' | 'macos' | 'web' | 'native';
63
63
  devicePixelRatio: number;
64
64
  };
65
- export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect' | 'subscriber-bandwidth' | 'disconnect-signal-on-resume' | 'disconnect-signal-on-resume-no-messages' | 'leave-full-reconnect';
65
+ export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect' | 'subscriber-bandwidth' | 'disconnect-signal-on-resume' | 'disconnect-signal-on-resume-no-messages' | 'leave-full-reconnect' | 'fail-on-v1-path';
66
66
  export type LoggerOptions = {
67
67
  loggerName?: string;
68
68
  loggerContextCb?: () => Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "2.18.3",
3
+ "version": "2.18.5",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -44,7 +44,7 @@
44
44
  "sdp-transform": "^2.15.0",
45
45
  "tslib": "2.8.1",
46
46
  "typed-emitter": "^2.1.0",
47
- "webrtc-adapter": "^9.0.1"
47
+ "webrtc-adapter": "9.0.5"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@types/dom-mediacapture-record": "^1"
@@ -57,7 +57,7 @@
57
57
  "@eslint/js": "9.39.2",
58
58
  "@livekit/changesets-changelog-github": "^0.0.4",
59
59
  "@livekit/throws-transformer": "^0.1.3",
60
- "@rollup/plugin-babel": "6.1.0",
60
+ "@rollup/plugin-babel": "7.0.0",
61
61
  "@rollup/plugin-commonjs": "28.0.9",
62
62
  "@rollup/plugin-json": "6.1.0",
63
63
  "@rollup/plugin-node-resolve": "16.0.3",
@@ -1081,6 +1081,10 @@ export class SignalClient {
1081
1081
 
1082
1082
  switch (resp.status) {
1083
1083
  case 404:
1084
+ const errorMsg = await resp.text();
1085
+ if (errorMsg.includes('requested room does not exist')) {
1086
+ return ConnectionError.notAllowed(errorMsg, resp.status);
1087
+ }
1084
1088
  return ConnectionError.serviceNotFound(
1085
1089
  'v1 RTC path not found. Consider upgrading your LiveKit server version',
1086
1090
  'v0-rtc',
@@ -165,7 +165,7 @@ export default class PCTransport extends EventEmitter {
165
165
  this.remoteStereoMids = stereoMids;
166
166
  this.remoteNackMids = nackMids;
167
167
  } else if (sd.type === 'answer') {
168
- if (this.pendingInitialOffer) {
168
+ if (this.pendingInitialOffer && this._pc) {
169
169
  const initialOffer = this.pendingInitialOffer;
170
170
  this.pendingInitialOffer = undefined;
171
171
  const sdpParsed = parse(initialOffer.sdp ?? '');
@@ -523,6 +523,7 @@ export default class PCTransport extends EventEmitter {
523
523
  if (!this._pc) {
524
524
  return;
525
525
  }
526
+ this.pendingInitialOffer = undefined;
526
527
  this._pc.close();
527
528
  this._pc.onconnectionstatechange = null;
528
529
  this._pc.oniceconnectionstatechange = null;
@@ -539,8 +540,8 @@ export default class PCTransport extends EventEmitter {
539
540
  };
540
541
 
541
542
  private async setMungedSDP(sd: RTCSessionDescriptionInit, munged?: string, remote?: boolean) {
543
+ const originalSdp = sd.sdp;
542
544
  if (munged) {
543
- const originalSdp = sd.sdp;
544
545
  sd.sdp = munged;
545
546
  try {
546
547
  this.log.debug(
@@ -557,7 +558,8 @@ export default class PCTransport extends EventEmitter {
557
558
  this.log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
558
559
  ...this.logContext,
559
560
  error: e,
560
- sdp: munged,
561
+ mungedSdp: munged,
562
+ originalSdp,
561
563
  });
562
564
  sd.sdp = originalSdp;
563
565
  }
@@ -565,9 +567,9 @@ export default class PCTransport extends EventEmitter {
565
567
 
566
568
  try {
567
569
  if (remote) {
568
- await this.pc.setRemoteDescription(sd);
570
+ await this._pc?.setRemoteDescription(sd);
569
571
  } else {
570
- await this.pc.setLocalDescription(sd);
572
+ await this._pc?.setLocalDescription(sd);
571
573
  }
572
574
  } catch (e) {
573
575
  let msg = 'unknown error';
@@ -581,6 +583,9 @@ export default class PCTransport extends EventEmitter {
581
583
  error: msg,
582
584
  sdp: sd.sdp,
583
585
  };
586
+ if (munged && munged !== originalSdp) {
587
+ fields.mungedSdp = munged;
588
+ }
584
589
  if (!remote && this.pc.remoteDescription) {
585
590
  fields.remoteSdp = this.pc.remoteDescription;
586
591
  }
@@ -609,9 +614,6 @@ export default class PCTransport extends EventEmitter {
609
614
  if (this.ddExtID === 0) {
610
615
  let maxID = 0;
611
616
  sdp.media.forEach((m) => {
612
- if (m.type !== 'video') {
613
- return;
614
- }
615
617
  m.ext?.forEach((ext) => {
616
618
  if (ext.value > maxID) {
617
619
  maxID = ext.value;
@@ -21,6 +21,7 @@ import {
21
21
  PublishDataTrackResponse,
22
22
  ReconnectReason,
23
23
  type ReconnectResponse,
24
+ type RegionSettings,
24
25
  RequestResponse,
25
26
  Room as RoomModel,
26
27
  RoomMovedResponse,
@@ -62,7 +63,6 @@ import { TTLMap } from '../utils/ttlmap';
62
63
  import PCTransport, { PCEvents } from './PCTransport';
63
64
  import { PCTransportManager, PCTransportState } from './PCTransportManager';
64
65
  import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
65
- import { DEFAULT_MAX_AGE_MS, type RegionUrlProvider } from './RegionUrlProvider';
66
66
  import { DataTrackInfo } from './data-track/types';
67
67
  import { roomConnectOptionDefaults } from './defaults';
68
68
  import {
@@ -86,6 +86,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
86
86
  import { getTrackPublicationInfo } from './track/utils';
87
87
  import type { LoggerOptions } from './types';
88
88
  import {
89
+ Future,
89
90
  isCompressionStreamSupported,
90
91
  isVideoCodec,
91
92
  isVideoTrack,
@@ -220,7 +221,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
220
221
 
221
222
  private shouldFailNext: boolean = false;
222
223
 
223
- private regionUrlProvider?: RegionUrlProvider;
224
+ private shouldFailOnV1Path: boolean = false;
225
+
226
+ private regionStrategy?: RegionStrategy;
224
227
 
225
228
  private log = log;
226
229
 
@@ -247,6 +250,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
247
250
  /** used to indicate whether the browser is currently waiting to reconnect */
248
251
  private isWaitingForNetworkReconnect: boolean = false;
249
252
 
253
+ private bufferStatusLowClosingFuture = new Future<never, UnexpectedConnectionState>();
254
+
250
255
  constructor(private options: InternalRoomOptions) {
251
256
  super();
252
257
  this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
@@ -280,6 +285,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
280
285
  this.client.onParticipantUpdate = (updates) =>
281
286
  this.emit(EngineEvent.ParticipantUpdate, updates);
282
287
  this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
288
+
289
+ this.on(EngineEvent.Closing, () => {
290
+ this.bufferStatusLowClosingFuture.reject?.(new UnexpectedConnectionState('engine closed'));
291
+ });
292
+ // Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
293
+ // when no waitForBufferStatusLow callers are attached.
294
+ this.bufferStatusLowClosingFuture.promise.catch(() => {});
283
295
  }
284
296
 
285
297
  /** @internal */
@@ -321,9 +333,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
321
333
  offerProto = toProtoSessionDescription(offer.offer, offer.offerId);
322
334
  }
323
335
  }
336
+
324
337
  if (abortSignal?.aborted) {
325
338
  throw ConnectionError.cancelled('Connection aborted');
326
339
  }
340
+
341
+ if (!useV0Path && this.shouldFailOnV1Path) {
342
+ this.shouldFailOnV1Path = false;
343
+ throw ConnectionError.serviceNotFound('Simulated v1 path failure', 'v0-rtc');
344
+ }
327
345
  const joinResponse = await this.client.join(
328
346
  url,
329
347
  token,
@@ -383,6 +401,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
383
401
  }
384
402
  } else if (e.reason === ConnectionErrorReason.ServiceNotFound) {
385
403
  this.log.warn(`Initial connection failed: ${e.message} – Retrying`);
404
+ if (this.pcManager) {
405
+ this.pcManager.onStateChange = undefined;
406
+ await this.cleanupPeerConnections();
407
+ }
386
408
  return this.join(url, token, opts, abortSignal, true);
387
409
  }
388
410
  }
@@ -456,6 +478,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
456
478
  async cleanupClient() {
457
479
  await this.client.close();
458
480
  this.client.resetCallbacks();
481
+ // Any in-flight addTrack requests are orphaned by the signal reconnect — the new session
482
+ // won't deliver `trackPublishedResponse` for them, so reject the pending resolvers and
483
+ // clear the map. Otherwise a subsequent `addTrack` call with the same client id (e.g. a
484
+ // publish retry after a `NegotiationError`) throws `TrackInvalidError`.
485
+ for (const cid of Object.keys(this.pendingTrackResolvers)) {
486
+ this.pendingTrackResolvers[cid].reject();
487
+ }
488
+ this.pendingTrackResolvers = {};
459
489
  }
460
490
 
461
491
  addTrack(req: AddTrackRequest): Promise<TrackInfo> {
@@ -519,8 +549,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
519
549
  }
520
550
 
521
551
  /* @internal */
522
- setRegionUrlProvider(provider: RegionUrlProvider) {
523
- this.regionUrlProvider = provider;
552
+ setRegionStrategy(strategy: RegionStrategy | undefined) {
553
+ this.regionStrategy = strategy;
524
554
  }
525
555
 
526
556
  private async configure(joinResponse?: JoinResponse, useSinglePeerConnection?: boolean) {
@@ -671,7 +701,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
671
701
 
672
702
  this.client.onTokenRefresh = (token: string) => {
673
703
  this.token = token;
674
- this.regionUrlProvider?.updateToken(token);
704
+ this.emit(EngineEvent.TokenRefreshed, token);
675
705
  };
676
706
 
677
707
  this.client.onRemoteMuteChanged = (trackSid: string, muted: boolean) => {
@@ -713,13 +743,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
713
743
 
714
744
  this.client.onLeave = (leave: LeaveRequest) => {
715
745
  this.log.debug('client leave request', { ...this.logContext, reason: leave?.reason });
716
- if (leave.regions && this.regionUrlProvider) {
746
+ if (leave.regions) {
717
747
  this.log.debug('updating regions', this.logContext);
718
- this.regionUrlProvider.setServerReportedRegions({
719
- updatedAtInMs: Date.now(),
720
- maxAgeInMs: DEFAULT_MAX_AGE_MS,
721
- regionSettings: leave.regions,
722
- });
748
+ this.emit(EngineEvent.ServerRegionsReported, leave.regions);
723
749
  }
724
750
  switch (leave.action) {
725
751
  case LeaveRequest_Action.DISCONNECT:
@@ -1124,10 +1150,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1124
1150
  this.log.debug(`reconnecting in ${delay}ms`, this.logContext);
1125
1151
 
1126
1152
  this.clearReconnectTimeout();
1127
- if (this.token && this.regionUrlProvider) {
1153
+ if (this.token) {
1128
1154
  // token may have been refreshed, we do not want to recreate the regionUrlProvider
1129
1155
  // since the current engine may have inherited a regional url
1130
- this.regionUrlProvider.updateToken(this.token);
1156
+ this.emit(EngineEvent.TokenRefreshed, this.token);
1131
1157
  }
1132
1158
  this.reconnectTimeout = CriticalTimers.setTimeout(
1133
1159
  () =>
@@ -1260,17 +1286,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1260
1286
  throw new SignalReconnectError('Signal connection got severed during reconnect');
1261
1287
  }
1262
1288
 
1263
- this.regionUrlProvider?.resetAttempts();
1289
+ this.regionStrategy?.resetAttempts();
1264
1290
  // reconnect success
1265
1291
  this.emit(EngineEvent.Restarted);
1266
1292
  } catch (error) {
1267
- const nextRegionUrl = await this.regionUrlProvider?.getNextBestRegionUrl();
1293
+ const nextRegionUrl = await this.regionStrategy?.getNextUrl();
1268
1294
  if (nextRegionUrl) {
1269
1295
  await this.restartConnection(nextRegionUrl);
1270
1296
  return;
1271
1297
  } else {
1272
1298
  // no more regions to try (or we're not on cloud)
1273
- this.regionUrlProvider?.resetAttempts();
1299
+ this.regionStrategy?.resetAttempts();
1274
1300
  throw error;
1275
1301
  }
1276
1302
  }
@@ -1565,23 +1591,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1565
1591
  if (this.isBufferStatusLow(kind)) {
1566
1592
  resolve();
1567
1593
  } else {
1568
- const onClosing = () => reject(new UnexpectedConnectionState('engine closed'));
1569
- this.once(EngineEvent.Closing, onClosing);
1570
1594
  const dc = this.dataChannelForKind(kind);
1571
1595
  if (!dc) {
1572
1596
  reject(new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`));
1573
1597
  return;
1574
1598
  }
1575
- dc.addEventListener(
1576
- 'bufferedamountlow',
1577
- () => {
1578
- this.off(EngineEvent.Closing, onClosing);
1579
- resolve();
1580
- },
1581
- {
1582
- once: true,
1583
- },
1584
- );
1599
+ this.bufferStatusLowClosingFuture.promise.catch((e) => reject(e));
1600
+ dc.addEventListener('bufferedamountlow', () => resolve(), {
1601
+ once: true,
1602
+ });
1585
1603
  }
1586
1604
  });
1587
1605
  }
@@ -1860,6 +1878,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1860
1878
  this.shouldFailNext = true;
1861
1879
  }
1862
1880
 
1881
+ /* @internal */
1882
+ failNextV1Path() {
1883
+ // debugging method to fail the next connection attempt for /rtc/v1 to trigger the fallback version
1884
+ this.shouldFailOnV1Path = true;
1885
+ }
1886
+
1863
1887
  private dataChannelsInfo(): DataChannelInfo[] {
1864
1888
  const infos: DataChannelInfo[] = [];
1865
1889
  const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
@@ -2004,8 +2028,15 @@ export type EngineEventCallbacks = {
2004
2028
  dataTrackSubscriberHandles: (event: DataTrackSubscriberHandles) => void;
2005
2029
  dataTrackPacketReceived: (packet: Uint8Array) => void;
2006
2030
  joined: (joinResponse: JoinResponse) => void;
2031
+ tokenRefreshed: (token: string) => void;
2032
+ serverRegionsReported: (regions: RegionSettings) => void;
2007
2033
  };
2008
2034
 
2035
+ export interface RegionStrategy {
2036
+ getNextUrl(abortSignal?: AbortSignal): Promise<string | null>;
2037
+ resetAttempts(): void;
2038
+ }
2039
+
2009
2040
  function applyUserDataCompat(newObj: DataPacket, oldObj: UserPacket) {
2010
2041
  const participantIdentity = newObj.participantIdentity
2011
2042
  ? newObj.participantIdentity
@@ -189,6 +189,13 @@ export class RegionUrlProvider {
189
189
 
190
190
  updateToken(token: string) {
191
191
  this.token = token;
192
+ const url = this.getServerUrl();
193
+ const settings = RegionUrlProvider.cache.get(url.hostname);
194
+ RegionUrlProvider.scheduleRefetch(
195
+ this.serverUrl,
196
+ this.token,
197
+ settings?.maxAgeInMs ?? DEFAULT_MAX_AGE_MS,
198
+ );
192
199
  }
193
200
 
194
201
  isCloud() {
package/src/room/Room.ts CHANGED
@@ -47,8 +47,8 @@ import TypedPromise from '../utils/TypedPromise';
47
47
  import { getBrowser } from '../utils/browserParser';
48
48
  import { BackOffStrategy } from './BackOffStrategy';
49
49
  import DeviceManager from './DeviceManager';
50
- import RTCEngine, { DataChannelKind } from './RTCEngine';
51
- import { RegionUrlProvider } from './RegionUrlProvider';
50
+ import RTCEngine, { DataChannelKind, type RegionStrategy } from './RTCEngine';
51
+ import { DEFAULT_MAX_AGE_MS, RegionUrlProvider } from './RegionUrlProvider';
52
52
  import IncomingDataStreamManager from './data-stream/incoming/IncomingDataStreamManager';
53
53
  import {
54
54
  type ByteStreamHandler,
@@ -588,22 +588,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
588
588
  this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
589
589
  })
590
590
  .on(EngineEvent.LocalTrackSubscribed, (subscribedSid) => {
591
- const trackPublication = this.localParticipant
592
- .getTrackPublications()
593
- .find(({ trackSid }) => trackSid === subscribedSid) as LocalTrackPublication | undefined;
594
- if (!trackPublication) {
595
- this.log.warn(
596
- 'could not find local track subscription for subscribed event',
597
- this.logContext,
598
- );
599
- return;
600
- }
601
- this.localParticipant.emit(ParticipantEvent.LocalTrackSubscribed, trackPublication);
602
- this.emitWhenConnected(
603
- RoomEvent.LocalTrackSubscribed,
604
- trackPublication,
605
- this.localParticipant,
606
- );
591
+ this.handleLocalTrackSubscribed(subscribedSid);
607
592
  })
608
593
  .on(EngineEvent.RoomMoved, (roomMoved) => {
609
594
  this.log.debug('room moved', roomMoved);
@@ -683,6 +668,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
683
668
  }),
684
669
  );
685
670
  this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
671
+ })
672
+ .on(EngineEvent.TokenRefreshed, (token) => {
673
+ this.regionUrlProvider?.updateToken(token);
674
+ })
675
+ .on(EngineEvent.ServerRegionsReported, (regions) => {
676
+ this.regionUrlProvider?.setServerReportedRegions({
677
+ regionSettings: regions,
678
+ updatedAtInMs: Date.now(),
679
+ maxAgeInMs: DEFAULT_MAX_AGE_MS,
680
+ });
686
681
  });
687
682
 
688
683
  if (this.localParticipant) {
@@ -696,6 +691,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
696
691
  }
697
692
  }
698
693
 
694
+ private createRegionStrategy(): RegionStrategy {
695
+ return {
696
+ getNextUrl: async (signal?: AbortSignal) =>
697
+ this.regionUrlProvider ? this.regionUrlProvider.getNextBestRegionUrl(signal) : null,
698
+ resetAttempts: () => this.regionUrlProvider?.resetAttempts(),
699
+ };
700
+ }
701
+
699
702
  /**
700
703
  * getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
701
704
  * In particular, it requests device permissions by default if needed
@@ -980,7 +983,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
980
983
  this.maybeCreateEngine();
981
984
  }
982
985
  if (this.regionUrlProvider?.isCloud()) {
983
- this.engine.setRegionUrlProvider(this.regionUrlProvider);
986
+ this.engine.setRegionStrategy(this.createRegionStrategy());
984
987
  }
985
988
 
986
989
  this.acquireAudioContext();
@@ -1137,6 +1140,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1137
1140
  // @ts-expect-error function is private
1138
1141
  await this.engine.client.handleOnClose('simulate disconnect');
1139
1142
  break;
1143
+ case 'fail-on-v1-path':
1144
+ this.engine.failNextV1Path();
1145
+ break;
1140
1146
  case 'speaker':
1141
1147
  req = new SimulateScenario({
1142
1148
  scenario: {
@@ -1623,6 +1629,65 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1623
1629
  }
1624
1630
  }
1625
1631
 
1632
+ private handleLocalTrackSubscribed(subscribedSid: string) {
1633
+ const findPublication = () =>
1634
+ this.localParticipant
1635
+ .getTrackPublications()
1636
+ .find(({ trackSid }) => trackSid === subscribedSid) as LocalTrackPublication | undefined;
1637
+
1638
+ const trackPublication = findPublication();
1639
+ if (trackPublication) {
1640
+ this.emitLocalTrackSubscribed(trackPublication);
1641
+ return;
1642
+ }
1643
+
1644
+ // the track publication may not be registered yet if the server signals
1645
+ // the subscription before publishTrack has finished adding the publication.
1646
+ // defer with a timeout until LocalTrackPublished fires for the matching trackSid
1647
+ this.log.debug('deferring LocalTrackSubscribed, publication not yet available', {
1648
+ ...this.logContext,
1649
+ subscribedSid,
1650
+ });
1651
+
1652
+ const TIMEOUT_MS = 10_000;
1653
+ let timer: ReturnType<typeof setTimeout>;
1654
+
1655
+ const onPublished = (pub: LocalTrackPublication) => {
1656
+ if (pub.trackSid === subscribedSid) {
1657
+ cleanup();
1658
+ this.emitLocalTrackSubscribed(pub);
1659
+ }
1660
+ };
1661
+
1662
+ const cleanup = () => {
1663
+ clearTimeout(timer);
1664
+ this.localParticipant.off(ParticipantEvent.LocalTrackPublished, onPublished);
1665
+ this.off(RoomEvent.Disconnected, cleanup);
1666
+ };
1667
+
1668
+ this.localParticipant.on(ParticipantEvent.LocalTrackPublished, onPublished);
1669
+ this.once(RoomEvent.Disconnected, cleanup);
1670
+
1671
+ timer = setTimeout(() => {
1672
+ cleanup();
1673
+ // final attempt in case the publication was added without emitting the event
1674
+ const pub = findPublication();
1675
+ if (pub) {
1676
+ this.emitLocalTrackSubscribed(pub);
1677
+ } else {
1678
+ this.log.warn(
1679
+ 'could not find local track publication for LocalTrackSubscribed event after timeout',
1680
+ { ...this.logContext, subscribedSid },
1681
+ );
1682
+ }
1683
+ }, TIMEOUT_MS);
1684
+ }
1685
+
1686
+ private emitLocalTrackSubscribed(trackPublication: LocalTrackPublication) {
1687
+ this.localParticipant.emit(ParticipantEvent.LocalTrackSubscribed, trackPublication);
1688
+ this.emitWhenConnected(RoomEvent.LocalTrackSubscribed, trackPublication, this.localParticipant);
1689
+ }
1690
+
1626
1691
  private handleRestarting = () => {
1627
1692
  this.clearConnectionReconcile();
1628
1693
  // in case we went from resuming to full-reconnect, make sure to reflect it on the isResuming flag
@@ -2253,10 +2318,15 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
2253
2318
  private createParticipant(identity: string, info?: ParticipantInfo): RemoteParticipant {
2254
2319
  let participant: RemoteParticipant;
2255
2320
  if (info) {
2256
- participant = RemoteParticipant.fromParticipantInfo(this.engine.client, info, {
2257
- loggerContextCb: () => this.logContext,
2258
- loggerName: this.options.loggerName,
2259
- });
2321
+ participant = RemoteParticipant.fromParticipantInfo(
2322
+ this.engine.client,
2323
+ info,
2324
+ {
2325
+ loggerContextCb: () => this.logContext,
2326
+ loggerName: this.options.loggerName,
2327
+ },
2328
+ this.incomingDataTrackManager,
2329
+ );
2260
2330
  } else {
2261
2331
  participant = new RemoteParticipant(
2262
2332
  this.engine.client,