livekit-client 1.14.4 → 1.15.1

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/README.md +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  4. package/dist/livekit-client.esm.mjs +5488 -5230
  5. package/dist/livekit-client.esm.mjs.map +1 -1
  6. package/dist/livekit-client.umd.js +1 -1
  7. package/dist/livekit-client.umd.js.map +1 -1
  8. package/dist/src/api/SignalClient.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +10 -4
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/PCTransportManager.d.ts +51 -0
  12. package/dist/src/room/PCTransportManager.d.ts.map +1 -0
  13. package/dist/src/room/RTCEngine.d.ts +8 -5
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/Room.d.ts +9 -0
  16. package/dist/src/room/Room.d.ts.map +1 -1
  17. package/dist/src/room/events.d.ts +10 -0
  18. package/dist/src/room/events.d.ts.map +1 -1
  19. package/dist/src/room/participant/LocalParticipant.d.ts +0 -5
  20. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  22. package/dist/src/room/track/Track.d.ts +6 -2
  23. package/dist/src/room/track/Track.d.ts.map +1 -1
  24. package/dist/src/room/track/options.d.ts +2 -0
  25. package/dist/src/room/track/options.d.ts.map +1 -1
  26. package/dist/src/room/track/utils.d.ts +3 -0
  27. package/dist/src/room/track/utils.d.ts.map +1 -1
  28. package/dist/src/room/utils.d.ts.map +1 -1
  29. package/dist/src/test/mocks.d.ts +1 -1
  30. package/dist/src/test/mocks.d.ts.map +1 -1
  31. package/dist/ts4.2/src/room/PCTransport.d.ts +10 -4
  32. package/dist/ts4.2/src/room/PCTransportManager.d.ts +51 -0
  33. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -5
  34. package/dist/ts4.2/src/room/Room.d.ts +9 -0
  35. package/dist/ts4.2/src/room/events.d.ts +10 -0
  36. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +0 -5
  37. package/dist/ts4.2/src/room/track/Track.d.ts +6 -2
  38. package/dist/ts4.2/src/room/track/options.d.ts +2 -0
  39. package/dist/ts4.2/src/room/track/utils.d.ts +3 -0
  40. package/dist/ts4.2/src/test/mocks.d.ts +1 -1
  41. package/package.json +20 -19
  42. package/src/api/SignalClient.ts +7 -1
  43. package/src/connectionHelper/checks/webrtc.ts +2 -2
  44. package/src/room/PCTransport.ts +66 -29
  45. package/src/room/PCTransportManager.ts +336 -0
  46. package/src/room/RTCEngine.ts +178 -246
  47. package/src/room/Room.ts +49 -46
  48. package/src/room/defaults.ts +1 -1
  49. package/src/room/events.ts +11 -0
  50. package/src/room/participant/LocalParticipant.ts +9 -51
  51. package/src/room/track/LocalTrack.ts +2 -0
  52. package/src/room/track/Track.ts +30 -9
  53. package/src/room/track/options.ts +2 -0
  54. package/src/room/track/utils.ts +19 -0
  55. package/src/room/utils.ts +2 -1
  56. package/src/test/mocks.ts +5 -1
package/src/room/Room.ts CHANGED
@@ -2,7 +2,6 @@ import { protoInt64 } from '@bufbuild/protobuf';
2
2
  import { EventEmitter } from 'events';
3
3
  import type TypedEmitter from 'typed-emitter';
4
4
  import 'webrtc-adapter';
5
- import { toProtoSessionDescription } from '../api/SignalClient';
6
5
  import { EncryptionEvent } from '../e2ee';
7
6
  import { E2EEManager } from '../e2ee/E2eeManager';
8
7
  import log from '../logger';
@@ -35,8 +34,6 @@ import {
35
34
  StreamStateUpdate,
36
35
  SubscriptionPermissionUpdate,
37
36
  SubscriptionResponse,
38
- SyncState,
39
- UpdateSubscription,
40
37
  } from '../proto/livekit_rtc_pb';
41
38
  import { getBrowser } from '../utils/browserParser';
42
39
  import DeviceManager from './DeviceManager';
@@ -152,6 +149,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
152
149
 
153
150
  private regionUrl?: string;
154
151
 
152
+ private isVideoPlaybackBlocked: boolean = false;
153
+
155
154
  /**
156
155
  * Creates a new Room, the primary construct for a LiveKit session.
157
156
  * @param options
@@ -865,6 +864,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
865
864
  }
866
865
  };
867
866
 
867
+ startVideo = async () => {
868
+ for (const p of this.participants.values()) {
869
+ p.videoTracks.forEach((tr) => {
870
+ tr.track?.attachedElements.forEach((el) => {
871
+ el.play().catch((e) => {
872
+ if (e.name === 'NotAllowedError') {
873
+ log.warn(
874
+ 'Resuming video playback failed, make sure you call `startVideo` directly in a user gesture handler',
875
+ );
876
+ }
877
+ });
878
+ });
879
+ });
880
+ }
881
+ };
882
+
868
883
  /**
869
884
  * Returns true if audio playback is enabled
870
885
  */
@@ -872,6 +887,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
872
887
  return this.audioEnabled;
873
888
  }
874
889
 
890
+ /**
891
+ * Returns true if video playback is enabled
892
+ */
893
+ get canPlaybackVideo(): boolean {
894
+ return !this.isVideoPlaybackBlocked;
895
+ }
896
+
875
897
  /**
876
898
  * Returns the active audio output device used in this room.
877
899
  * @return the previously successfully set audio output device ID or an empty string if the default device is used.
@@ -1384,6 +1406,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1384
1406
  this.emit(RoomEvent.AudioPlaybackStatusChanged, false);
1385
1407
  };
1386
1408
 
1409
+ private handleVideoPlaybackStarted = () => {
1410
+ if (this.isVideoPlaybackBlocked) {
1411
+ this.isVideoPlaybackBlocked = false;
1412
+ this.emit(RoomEvent.VideoPlaybackStatusChanged, true);
1413
+ }
1414
+ };
1415
+
1416
+ private handleVideoPlaybackFailed = () => {
1417
+ if (!this.isVideoPlaybackBlocked) {
1418
+ this.isVideoPlaybackBlocked = true;
1419
+ this.emit(RoomEvent.VideoPlaybackStatusChanged, false);
1420
+ }
1421
+ };
1422
+
1387
1423
  private handleDeviceChange = async () => {
1388
1424
  this.emit(RoomEvent.MediaDevicesChanged);
1389
1425
  };
@@ -1487,6 +1523,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1487
1523
  if (track.kind === Track.Kind.Audio) {
1488
1524
  track.on(TrackEvent.AudioPlaybackStarted, this.handleAudioPlaybackStarted);
1489
1525
  track.on(TrackEvent.AudioPlaybackFailed, this.handleAudioPlaybackFailed);
1526
+ } else if (track.kind === Track.Kind.Video) {
1527
+ track.on(TrackEvent.VideoPlaybackFailed, this.handleVideoPlaybackFailed);
1528
+ track.on(TrackEvent.VideoPlaybackStarted, this.handleVideoPlaybackStarted);
1490
1529
  }
1491
1530
  this.emit(RoomEvent.TrackSubscribed, track, publication, participant);
1492
1531
  },
@@ -1551,49 +1590,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1551
1590
  }
1552
1591
 
1553
1592
  private sendSyncState() {
1554
- const previousAnswer = this.engine.subscriber?.getLocalDescription();
1555
- const previousOffer = this.engine.subscriber?.getRemoteDescription();
1556
-
1557
- if (!previousAnswer) {
1558
- return;
1559
- }
1560
-
1561
- /* 1. autosubscribe on, so subscribed tracks = all tracks - unsub tracks,
1562
- in this case, we send unsub tracks, so server add all tracks to this
1563
- subscribe pc and unsub special tracks from it.
1564
- 2. autosubscribe off, we send subscribed tracks.
1565
- */
1566
- const autoSubscribe = this.connOptions?.autoSubscribe ?? true;
1567
- const trackSids = new Array<string>();
1568
- this.participants.forEach((participant) => {
1569
- participant.tracks.forEach((track) => {
1570
- if (track.isDesired !== autoSubscribe) {
1571
- trackSids.push(track.trackSid);
1572
- }
1573
- });
1574
- });
1575
-
1576
- this.engine.client.sendSyncState(
1577
- new SyncState({
1578
- answer: toProtoSessionDescription({
1579
- sdp: previousAnswer.sdp,
1580
- type: previousAnswer.type,
1581
- }),
1582
- offer: previousOffer
1583
- ? toProtoSessionDescription({
1584
- sdp: previousOffer.sdp,
1585
- type: previousOffer.type,
1586
- })
1587
- : undefined,
1588
- subscription: new UpdateSubscription({
1589
- trackSids,
1590
- subscribe: !autoSubscribe,
1591
- participantTracks: [],
1592
- }),
1593
- publishTracks: this.localParticipant.publishedTracksInfo(),
1594
- dataChannels: this.localParticipant.dataChannelsInfo(),
1595
- }),
1596
- );
1593
+ const remoteTracks = Array.from(this.participants.values()).reduce((acc, participant) => {
1594
+ acc.push(...(participant.getTracks() as RemoteTrackPublication[])); // FIXME would be nice to have this return RemoteTrackPublications directly instead of the type cast
1595
+ return acc;
1596
+ }, [] as RemoteTrackPublication[]);
1597
+ const localTracks = this.localParticipant.getTracks() as LocalTrackPublication[]; // FIXME would be nice to have this return LocalTrackPublications directly instead of the type cast
1598
+ this.engine.sendSyncState(remoteTracks, localTracks);
1597
1599
  }
1598
1600
 
1599
1601
  /**
@@ -1929,6 +1931,7 @@ export type RoomEventCallbacks = {
1929
1931
  participant: RemoteParticipant,
1930
1932
  ) => void;
1931
1933
  audioPlaybackChanged: (playing: boolean) => void;
1934
+ videoPlaybackChanged: (playing: boolean) => void;
1932
1935
  signalConnected: () => void;
1933
1936
  recordingStatusChanged: (recording: boolean) => void;
1934
1937
  participantEncryptionStatusChanged: (encrypted: boolean, participant?: Participant) => void;
@@ -22,7 +22,7 @@ export const publishDefaults: TrackPublishDefaults = {
22
22
  screenShareEncoding: ScreenSharePresets.h1080fps15.encoding,
23
23
  stopMicTrackOnMute: false,
24
24
  videoCodec: defaultVideoCodec,
25
- backupCodec: false,
25
+ backupCodec: true,
26
26
  } as const;
27
27
 
28
28
  export const audioDefaults: AudioCaptureOptions = {
@@ -245,6 +245,13 @@ export enum RoomEvent {
245
245
  */
246
246
  AudioPlaybackStatusChanged = 'audioPlaybackChanged',
247
247
 
248
+ /**
249
+ * LiveKit will attempt to autoplay all video tracks when you attach them to
250
+ * a video element. However, if that fails, we'll notify you via VideoPlaybackStatusChanged.
251
+ * Calling `room.startVideo()` in a user gesture event handler will resume the video playback.
252
+ */
253
+ VideoPlaybackStatusChanged = 'videoPlaybackChanged',
254
+
248
255
  /**
249
256
  * When we have encountered an error while attempting to create a track.
250
257
  * The errors take place in getUserMedia().
@@ -510,6 +517,10 @@ export enum TrackEvent {
510
517
  /** @internal */
511
518
  VideoDimensionsChanged = 'videoDimensionsChanged',
512
519
  /** @internal */
520
+ VideoPlaybackStarted = 'videoPlaybackStarted',
521
+ /** @internal */
522
+ VideoPlaybackFailed = 'videoPlaybackFailed',
523
+ /** @internal */
513
524
  ElementAttached = 'elementAttached',
514
525
  /** @internal */
515
526
  ElementDetached = 'elementDetached',
@@ -10,13 +10,11 @@ import {
10
10
  } from '../../proto/livekit_models_pb';
11
11
  import {
12
12
  AddTrackRequest,
13
- DataChannelInfo,
14
- SignalTarget,
15
13
  SimulcastCodec,
16
14
  SubscribedQualityUpdate,
17
- TrackPublishedResponse,
18
15
  TrackUnpublishedResponse,
19
16
  } from '../../proto/livekit_rtc_pb';
17
+ import { PCTransportState } from '../PCTransportManager';
20
18
  import type RTCEngine from '../RTCEngine';
21
19
  import { defaultVideoCodec } from '../defaults';
22
20
  import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
@@ -766,8 +764,8 @@ export default class LocalParticipant extends Participant {
766
764
  publication.options = opts;
767
765
  track.sid = ti.sid;
768
766
 
769
- if (!this.engine.publisher) {
770
- throw new UnexpectedConnectionState('publisher is closed');
767
+ if (!this.engine.pcManager) {
768
+ throw new UnexpectedConnectionState('pcManager is not ready');
771
769
  }
772
770
  log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
773
771
 
@@ -783,21 +781,21 @@ export default class LocalParticipant extends Participant {
783
781
  fix the issue.
784
782
  */
785
783
  let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
786
- for (const transceiver of this.engine.publisher.getTransceivers()) {
784
+ for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
787
785
  if (transceiver.sender === track.sender) {
788
786
  trackTransceiver = transceiver;
789
787
  break;
790
788
  }
791
789
  }
792
790
  if (trackTransceiver) {
793
- this.engine.publisher.setTrackCodecBitrate({
791
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
794
792
  transceiver: trackTransceiver,
795
793
  codec: 'opus',
796
794
  maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
797
795
  });
798
796
  }
799
797
  } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
800
- this.engine.publisher.setTrackCodecBitrate({
798
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
801
799
  cid: req.cid,
802
800
  codec: track.codec,
803
801
  maxbr: encodings[0].maxBitrate / 1000,
@@ -929,12 +927,12 @@ export default class LocalParticipant extends Participant {
929
927
  const trackSender = track.sender;
930
928
  track.sender = undefined;
931
929
  if (
932
- this.engine.publisher &&
933
- this.engine.publisher.getConnectionState() !== 'closed' &&
930
+ this.engine.pcManager &&
931
+ this.engine.pcManager.currentState < PCTransportState.FAILED &&
934
932
  trackSender
935
933
  ) {
936
934
  try {
937
- for (const transceiver of this.engine.publisher.getTransceivers()) {
935
+ for (const transceiver of this.engine.pcManager.publisher.getTransceivers()) {
938
936
  // if sender is not currently sending (after replaceTrack(null))
939
937
  // removeTrack would have no effect.
940
938
  // to ensure we end up successfully removing the track, manually set
@@ -1310,44 +1308,4 @@ export default class LocalParticipant extends Participant {
1310
1308
  });
1311
1309
  return publication;
1312
1310
  }
1313
-
1314
- /** @internal */
1315
- publishedTracksInfo(): TrackPublishedResponse[] {
1316
- const infos: TrackPublishedResponse[] = [];
1317
- this.tracks.forEach((track: LocalTrackPublication) => {
1318
- if (track.track !== undefined) {
1319
- infos.push(
1320
- new TrackPublishedResponse({
1321
- cid: track.track.mediaStreamID,
1322
- track: track.trackInfo,
1323
- }),
1324
- );
1325
- }
1326
- });
1327
- return infos;
1328
- }
1329
-
1330
- /** @internal */
1331
- dataChannelsInfo(): DataChannelInfo[] {
1332
- const infos: DataChannelInfo[] = [];
1333
- const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {
1334
- if (dc?.id !== undefined && dc.id !== null) {
1335
- infos.push(
1336
- new DataChannelInfo({
1337
- label: dc.label,
1338
- id: dc.id,
1339
- target,
1340
- }),
1341
- );
1342
- }
1343
- };
1344
- getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY), SignalTarget.PUBLISHER);
1345
- getInfo(this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE), SignalTarget.PUBLISHER);
1346
- getInfo(this.engine.dataChannelForKind(DataPacket_Kind.LOSSY, true), SignalTarget.SUBSCRIBER);
1347
- getInfo(
1348
- this.engine.dataChannelForKind(DataPacket_Kind.RELIABLE, true),
1349
- SignalTarget.SUBSCRIBER,
1350
- );
1351
- return infos;
1352
- }
1353
1311
  }
@@ -108,9 +108,11 @@ export default abstract class LocalTrack extends Track {
108
108
  this.attachedElements.forEach((el) => {
109
109
  detachTrack(this._mediaStreamTrack, el);
110
110
  });
111
+ this.debouncedTrackMuteHandler.cancel('new-track');
111
112
  this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
112
113
  this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
113
114
  this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
115
+
114
116
  if (!this.providedByUser && this._mediaStreamTrack !== newTrack) {
115
117
  this._mediaStreamTrack.stop();
116
118
  }
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { debounce } from 'ts-debounce';
2
3
  import type TypedEventEmitter from 'typed-emitter';
3
4
  import type { SignalClient } from '../../api/SignalClient';
4
- import log from '../../logger';
5
5
  import { TrackSource, TrackType } from '../../proto/livekit_models_pb';
6
6
  import { StreamState as ProtoStreamState } from '../../proto/livekit_rtc_pb';
7
7
  import { TrackEvent } from '../events';
@@ -113,6 +113,9 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
113
113
 
114
114
  if (!this.attachedElements.includes(element)) {
115
115
  this.attachedElements.push(element);
116
+ // listen to suspend events in order to detect auto playback issues
117
+ element.addEventListener('suspend', this.handleElementSuspended);
118
+ element.addEventListener('playing', this.handleElementPlay);
116
119
  }
117
120
 
118
121
  // even if we believe it's already attached to the element, it's possible
@@ -130,11 +133,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
130
133
  this.emit(TrackEvent.AudioPlaybackStarted);
131
134
  })
132
135
  .catch((e) => {
133
- if (e.name === 'NotAllowedError') {
134
- this.emit(TrackEvent.AudioPlaybackFailed, e);
135
- } else {
136
- log.warn('could not playback audio', e);
137
- }
138
136
  // If audio playback isn't allowed make sure we still play back the video
139
137
  if (
140
138
  element &&
@@ -172,6 +170,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
172
170
  if (idx >= 0) {
173
171
  this.attachedElements.splice(idx, 1);
174
172
  this.recycleElement(element);
173
+ element.removeEventListener('suspend', this.handleElementSuspended);
174
+ element.removeEventListener('playing', this.handleElementPlay);
175
175
  this.emit(TrackEvent.ElementDetached, element);
176
176
  }
177
177
  return element;
@@ -182,6 +182,8 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
182
182
  detachTrack(this.mediaStreamTrack, elm);
183
183
  detached.push(elm);
184
184
  this.recycleElement(elm);
185
+ elm.removeEventListener('suspend', this.handleElementSuspended);
186
+ elm.removeEventListener('playing', this.handleElementPlay);
185
187
  this.emit(TrackEvent.ElementDetached, elm);
186
188
  });
187
189
 
@@ -268,9 +270,26 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
268
270
  document.removeEventListener('visibilitychange', this.appVisibilityChangedListener);
269
271
  }
270
272
  }
273
+
274
+ private handleElementSuspended = () => {
275
+ this.debouncedPlaybackStateChange(false);
276
+ };
277
+
278
+ private handleElementPlay = () => {
279
+ this.debouncedPlaybackStateChange(true);
280
+ };
281
+
282
+ private debouncedPlaybackStateChange = debounce((allowed: boolean) => {
283
+ // we debounce this as Safari triggers both `playing` and `suspend` shortly after one another
284
+ // in order not to raise the wrong event, we debounce the call to make sure we only emit the correct status
285
+ if (this.kind === Track.Kind.Audio) {
286
+ this.emit(allowed ? TrackEvent.AudioPlaybackStarted : TrackEvent.AudioPlaybackFailed);
287
+ } else if (this.kind === Track.Kind.Video) {
288
+ this.emit(allowed ? TrackEvent.VideoPlaybackStarted : TrackEvent.VideoPlaybackFailed);
289
+ }
290
+ }, 300);
271
291
  }
272
292
 
273
- /** @internal */
274
293
  export function attachToElement(track: MediaStreamTrack, element: HTMLMediaElement) {
275
294
  let mediaStream: MediaStream;
276
295
  if (element.srcObject instanceof MediaStream) {
@@ -321,7 +340,7 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
321
340
  // when the window is backgrounded before the first frame is drawn
322
341
  // manually calling play here seems to fix that
323
342
  element.play().catch(() => {
324
- /* do nothing */
343
+ /** do nothing, we watch the `suspended` event do deal with these failures */
325
344
  });
326
345
  }, 0);
327
346
  }
@@ -446,10 +465,12 @@ export type TrackEventCallbacks = {
446
465
  updateSettings: () => void;
447
466
  updateSubscription: () => void;
448
467
  audioPlaybackStarted: () => void;
449
- audioPlaybackFailed: (error: Error) => void;
468
+ audioPlaybackFailed: (error?: Error) => void;
450
469
  audioSilenceDetected: () => void;
451
470
  visibilityChanged: (visible: boolean, track?: any) => void;
452
471
  videoDimensionsChanged: (dimensions: Track.Dimensions, track?: any) => void;
472
+ videoPlaybackStarted: () => void;
473
+ videoPlaybackFailed: (error?: Error) => void;
453
474
  elementAttached: (element: HTMLMediaElement) => void;
454
475
  elementDetached: (element: HTMLMediaElement) => void;
455
476
  upstreamPaused: (track: any) => void;
@@ -14,6 +14,8 @@ export interface TrackPublishDefaults {
14
14
  *
15
15
  * You could customize specific encoding parameters of the backup track by
16
16
  * explicitly setting codec and encoding fields.
17
+ *
18
+ * Defaults to `true`
17
19
  */
18
20
  backupCodec?: true | false | { codec: BackupVideoCodec; encoding?: VideoEncoding };
19
21
 
@@ -1,6 +1,8 @@
1
+ import { TrackPublishedResponse } from '../../proto/livekit_rtc_pb';
1
2
  import { cloneDeep } from '../../utils/cloneDeep';
2
3
  import { isSafari, sleep } from '../utils';
3
4
  import { Track } from './Track';
5
+ import type { TrackPublication } from './TrackPublication';
4
6
  import {
5
7
  type AudioCaptureOptions,
6
8
  type CreateLocalTracksOptions,
@@ -190,3 +192,20 @@ export function mimeTypeToVideoCodecString(mimeType: string) {
190
192
  }
191
193
  return codec;
192
194
  }
195
+
196
+ export function getTrackPublicationInfo<T extends TrackPublication>(
197
+ tracks: T[],
198
+ ): TrackPublishedResponse[] {
199
+ const infos: TrackPublishedResponse[] = [];
200
+ tracks.forEach((track: TrackPublication) => {
201
+ if (track.track !== undefined) {
202
+ infos.push(
203
+ new TrackPublishedResponse({
204
+ cid: track.track.mediaStreamID,
205
+ track: track.trackInfo,
206
+ }),
207
+ );
208
+ }
209
+ });
210
+ return infos;
211
+ }
package/src/room/utils.ts CHANGED
@@ -2,6 +2,7 @@ import { ClientInfo, ClientInfo_SDK } from '../proto/livekit_models_pb';
2
2
  import type { DetectableBrowser } from '../utils/browserParser';
3
3
  import { getBrowser } from '../utils/browserParser';
4
4
  import { protocolVersion, version } from '../version';
5
+ import CriticalTimers from './timers';
5
6
  import type LocalAudioTrack from './track/LocalAudioTrack';
6
7
  import type RemoteAudioTrack from './track/RemoteAudioTrack';
7
8
  import { VideoCodec, videoCodecs } from './track/options';
@@ -21,7 +22,7 @@ export function unpackStreamId(packed: string): string[] {
21
22
  }
22
23
 
23
24
  export async function sleep(duration: number): Promise<void> {
24
- return new Promise((resolve) => setTimeout(resolve, duration));
25
+ return new Promise((resolve) => CriticalTimers.setTimeout(resolve, duration));
25
26
  }
26
27
 
27
28
  /** @internal */
package/src/test/mocks.ts CHANGED
@@ -8,7 +8,11 @@ vi.mock('../room/RTCEngine');
8
8
 
9
9
  // mock helpers for testing
10
10
 
11
- const mocks = {
11
+ const mocks: {
12
+ SignalClient: MockedClass<typeof SignalClient>;
13
+ RTCEngine: MockedClass<typeof RTCEngine>;
14
+ MockLocalVideoTrack: { stop: () => void };
15
+ } = {
12
16
  SignalClient: SignalClient as MockedClass<typeof SignalClient>,
13
17
  RTCEngine: RTCEngine as MockedClass<typeof RTCEngine>,
14
18
  MockLocalVideoTrack: {