livekit-client 0.18.4 → 1.0.0

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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/api/SignalClient.d.ts +2 -2
  3. package/dist/api/SignalClient.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -3
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/livekit-client.esm.mjs +263 -261
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.umd.js +1 -1
  9. package/dist/livekit-client.umd.js.map +1 -1
  10. package/dist/options.d.ts +1 -68
  11. package/dist/options.d.ts.map +1 -1
  12. package/dist/room/DeviceManager.d.ts.map +1 -1
  13. package/dist/room/RTCEngine.d.ts +3 -2
  14. package/dist/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/room/Room.d.ts +11 -7
  16. package/dist/room/Room.d.ts.map +1 -1
  17. package/dist/room/events.d.ts +6 -12
  18. package/dist/room/events.d.ts.map +1 -1
  19. package/dist/room/participant/LocalParticipant.d.ts.map +1 -1
  20. package/dist/room/participant/Participant.d.ts +0 -4
  21. package/dist/room/participant/Participant.d.ts.map +1 -1
  22. package/dist/room/participant/RemoteParticipant.d.ts +3 -2
  23. package/dist/room/participant/RemoteParticipant.d.ts.map +1 -1
  24. package/dist/room/participant/publishUtils.d.ts.map +1 -1
  25. package/dist/room/track/RemoteVideoTrack.d.ts +2 -0
  26. package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -1
  27. package/dist/room/track/Track.d.ts +7 -3
  28. package/dist/room/track/Track.d.ts.map +1 -1
  29. package/dist/room/track/defaults.d.ts.map +1 -1
  30. package/dist/room/track/options.d.ts +9 -30
  31. package/dist/room/track/options.d.ts.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.d.ts.map +1 -1
  34. package/package.json +3 -3
  35. package/src/api/SignalClient.ts +32 -7
  36. package/src/index.ts +2 -2
  37. package/src/options.ts +0 -82
  38. package/src/room/DeviceManager.ts +4 -1
  39. package/src/room/RTCEngine.ts +16 -7
  40. package/src/room/Room.ts +65 -41
  41. package/src/room/events.ts +7 -14
  42. package/src/room/participant/LocalParticipant.ts +37 -9
  43. package/src/room/participant/Participant.ts +0 -5
  44. package/src/room/participant/RemoteParticipant.ts +16 -5
  45. package/src/room/participant/publishUtils.test.ts +2 -2
  46. package/src/room/participant/publishUtils.ts +34 -5
  47. package/src/room/track/LocalAudioTrack.ts +2 -2
  48. package/src/room/track/LocalTrack.ts +17 -17
  49. package/src/room/track/LocalVideoTrack.ts +3 -3
  50. package/src/room/track/RemoteVideoTrack.ts +15 -2
  51. package/src/room/track/Track.ts +20 -13
  52. package/src/room/track/create.ts +1 -1
  53. package/src/room/track/defaults.ts +1 -2
  54. package/src/room/track/options.ts +12 -31
  55. package/src/room/track/utils.test.ts +6 -6
  56. package/src/version.ts +1 -1
  57. package/dist/connect.d.ts +0 -24
  58. package/dist/connect.d.ts.map +0 -1
  59. package/src/connect.ts +0 -98
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import LocalParticipant from './room/participant/LocalParticipant';
4
4
  import Participant, { ConnectionQuality } from './room/participant/Participant';
5
5
  import { ParticipantTrackPermission } from './room/participant/ParticipantTrackPermission';
6
6
  import RemoteParticipant from './room/participant/RemoteParticipant';
7
- import Room, { RoomState } from './room/Room';
7
+ import Room, { ConnectionState, RoomState } from './room/Room';
8
8
  import LocalAudioTrack from './room/track/LocalAudioTrack';
9
9
  import LocalTrack from './room/track/LocalTrack';
10
10
  import LocalTrackPublication from './room/track/LocalTrackPublication';
@@ -15,7 +15,6 @@ import RemoteTrackPublication from './room/track/RemoteTrackPublication';
15
15
  import RemoteVideoTrack from './room/track/RemoteVideoTrack';
16
16
  import { TrackPublication } from './room/track/TrackPublication';
17
17
 
18
- export * from './connect';
19
18
  export * from './options';
20
19
  export * from './room/errors';
21
20
  export * from './room/events';
@@ -29,6 +28,7 @@ export {
29
28
  setLogExtension,
30
29
  LogLevel,
31
30
  Room,
31
+ ConnectionState,
32
32
  RoomState,
33
33
  DataPacket_Kind,
34
34
  Participant,
package/src/options.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { LogLevel } from './logger';
2
1
  import {
3
2
  AudioCaptureOptions,
4
- CreateLocalTracksOptions,
5
3
  TrackPublishDefaults,
6
4
  VideoCaptureOptions,
7
5
  } from './room/track/options';
@@ -74,83 +72,3 @@ export interface RoomConnectOptions {
74
72
  */
75
73
  publishOnly?: string;
76
74
  }
77
-
78
- /**
79
- * @deprecated use new Room([[RoomOptions]]) and room.connect([[RoomConnectOptions]]) instead
80
- *
81
- * if video or audio tracks are created as part of [[connect]], it'll automatically
82
- * publish those tracks to the room.
83
- */
84
- export interface ConnectOptions extends CreateLocalTracksOptions {
85
- /** autosubscribe to room tracks upon connect, defaults to true */
86
- autoSubscribe?: boolean;
87
-
88
- /**
89
- * publish only mode
90
- */
91
- publishOnly?: string;
92
-
93
- /**
94
- * see [[RoomOptions.adaptiveStream]]
95
- */
96
- adaptiveStream?: AdaptiveStreamSettings | boolean;
97
-
98
- /**
99
- * alias for adaptiveStream
100
- * @deprecated
101
- */
102
- autoManageVideo?: boolean;
103
-
104
- /**
105
- * see [[RoomOptions.dynacast]]
106
- */
107
- dynacast?: boolean;
108
-
109
- /** configures LiveKit internal log level */
110
- logLevel?: LogLevel;
111
-
112
- /**
113
- * set ICE servers. When deployed correctly, LiveKit automatically uses the built-in TURN servers
114
- */
115
- iceServers?: RTCIceServer[];
116
-
117
- /**
118
- * use to override any RTCConfiguration options.
119
- */
120
- rtcConfig?: RTCConfiguration;
121
-
122
- /**
123
- * capture and publish audio track on connect, defaults to false
124
- *
125
- * If this option is used, you will not be notified if user denies capture permission.
126
- */
127
- audio?: boolean;
128
-
129
- /**
130
- * capture and publish video track on connect, defaults to false
131
- *
132
- * If this option is used, you will not be notified if user denies capture permission.
133
- */
134
- video?: boolean;
135
-
136
- /**
137
- * default options to use when capturing user's audio
138
- */
139
- audioCaptureDefaults?: AudioCaptureOptions;
140
-
141
- /**
142
- * default options to use when capturing user's video
143
- */
144
- videoCaptureDefaults?: VideoCaptureOptions;
145
-
146
- /**
147
- * default options to use when publishing tracks
148
- */
149
- publishDefaults?: TrackPublishDefaults;
150
-
151
- /**
152
- * should local tracks be stopped when they are unpublished. defaults to true
153
- * set this to false if you would prefer to clean up unpublished local tracks manually.
154
- */
155
- stopLocalTrackOnUnpublish?: boolean;
156
- }
@@ -32,8 +32,11 @@ export default class DeviceManager {
32
32
  video: kind !== 'audioinput' && kind !== 'audiooutput',
33
33
  audio: kind !== 'videoinput',
34
34
  };
35
- await navigator.mediaDevices.getUserMedia(permissionsToAcquire);
35
+ const stream = await navigator.mediaDevices.getUserMedia(permissionsToAcquire);
36
36
  devices = await navigator.mediaDevices.enumerateDevices();
37
+ stream.getTracks().forEach((track) => {
38
+ track.stop();
39
+ });
37
40
  }
38
41
  }
39
42
  if (kind) {
@@ -48,6 +48,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
48
48
 
49
49
  rtcConfig: RTCConfiguration = {};
50
50
 
51
+ get isClosed() {
52
+ return this._isClosed;
53
+ }
54
+
51
55
  private lossyDC?: RTCDataChannel;
52
56
 
53
57
  // @ts-ignore noUnusedLocals
@@ -64,7 +68,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
64
68
 
65
69
  private pcState: PCState = PCState.New;
66
70
 
67
- private isClosed: boolean = true;
71
+ private _isClosed: boolean = true;
68
72
 
69
73
  private pendingTrackResolvers: { [key: string]: (info: TrackInfo) => void } = {};
70
74
 
@@ -94,13 +98,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
94
98
  this.client = new SignalClient();
95
99
  }
96
100
 
97
- async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
101
+ async join(
102
+ url: string,
103
+ token: string,
104
+ opts?: SignalOptions,
105
+ abortSignal?: AbortSignal,
106
+ ): Promise<JoinResponse> {
98
107
  this.url = url;
99
108
  this.token = token;
100
109
  this.signalOpts = opts;
101
110
 
102
- const joinResponse = await this.client.join(url, token, opts);
103
- this.isClosed = false;
111
+ const joinResponse = await this.client.join(url, token, opts, abortSignal);
112
+ this._isClosed = false;
104
113
 
105
114
  this.subscriberPrimary = joinResponse.subscriberPrimary;
106
115
  if (!this.publisher) {
@@ -117,7 +126,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
117
126
  }
118
127
 
119
128
  close() {
120
- this.isClosed = true;
129
+ this._isClosed = true;
121
130
 
122
131
  this.removeAllListeners();
123
132
  if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
@@ -403,7 +412,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
403
412
  // continues to work, we can reconnect to websocket to continue the session
404
413
  // after a number of retries, we'll close and give up permanently
405
414
  private handleDisconnect = (connection: string) => {
406
- if (this.isClosed) {
415
+ if (this._isClosed) {
407
416
  return;
408
417
  }
409
418
  log.debug(`${connection} disconnected`);
@@ -414,7 +423,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
414
423
 
415
424
  const delay = this.reconnectAttempts * this.reconnectAttempts * 300;
416
425
  setTimeout(async () => {
417
- if (this.isClosed) {
426
+ if (this._isClosed) {
418
427
  return;
419
428
  }
420
429
  if (
package/src/room/Room.ts CHANGED
@@ -35,12 +35,16 @@ import { AdaptiveStreamSettings, RemoteTrack } from './track/types';
35
35
  import { getNewAudioContext } from './track/utils';
36
36
  import { isWeb, unpackStreamId } from './utils';
37
37
 
38
- export enum RoomState {
38
+ export enum ConnectionState {
39
39
  Disconnected = 'disconnected',
40
+ Connecting = 'connecting',
40
41
  Connected = 'connected',
41
42
  Reconnecting = 'reconnecting',
42
43
  }
43
44
 
45
+ /** @deprecated RoomState has been renamed to [[ConnectionState]] */
46
+ export const RoomState = ConnectionState;
47
+
44
48
  /**
45
49
  * In LiveKit, a room is the logical grouping for a list of participants.
46
50
  * Participants in a room can publish tracks, and subscribe to others' tracks.
@@ -50,7 +54,7 @@ export enum RoomState {
50
54
  * @noInheritDoc
51
55
  */
52
56
  class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>) {
53
- state: RoomState = RoomState.Disconnected;
57
+ state: ConnectionState = ConnectionState.Disconnected;
54
58
 
55
59
  /** map of sid: [[RemoteParticipant]] */
56
60
  participants: Map<string, RemoteParticipant>;
@@ -87,6 +91,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
87
91
 
88
92
  private audioContext?: AudioContext;
89
93
 
94
+ /** used for aborting pending connections to a LiveKit server */
95
+ private abortController?: AbortController;
96
+
90
97
  /**
91
98
  * Creates a new Room, the primary construct for a LiveKit session.
92
99
  * @param options
@@ -150,18 +157,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
150
157
  .on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
151
158
  .on(EngineEvent.DataPacketReceived, this.handleDataPacket)
152
159
  .on(EngineEvent.Resuming, () => {
153
- this.state = RoomState.Reconnecting;
160
+ this.setAndEmitConnectionState(ConnectionState.Reconnecting);
154
161
  this.emit(RoomEvent.Reconnecting);
155
- this.emit(RoomEvent.StateChanged, this.state);
156
162
  })
157
163
  .on(EngineEvent.Resumed, () => {
158
- this.state = RoomState.Connected;
164
+ this.setAndEmitConnectionState(ConnectionState.Connected);
159
165
  this.emit(RoomEvent.Reconnected);
160
- this.emit(RoomEvent.StateChanged, this.state);
161
166
  this.updateSubscriptions();
162
167
  })
163
168
  .on(EngineEvent.SignalResumed, () => {
164
- if (this.state === RoomState.Reconnecting) {
169
+ if (this.state === ConnectionState.Reconnecting) {
165
170
  this.sendSyncState();
166
171
  }
167
172
  })
@@ -186,11 +191,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
186
191
 
187
192
  connect = async (url: string, token: string, opts?: RoomConnectOptions) => {
188
193
  // guard against calling connect
189
- if (this.state !== RoomState.Disconnected) {
194
+ if (this.state !== ConnectionState.Disconnected) {
190
195
  log.warn(`already connected to room ${this.name}`);
191
196
  return;
192
197
  }
193
198
 
199
+ this.setAndEmitConnectionState(ConnectionState.Connecting);
200
+
201
+ if (!this.abortController || this.abortController.signal.aborted) {
202
+ this.abortController = new AbortController();
203
+ }
204
+
194
205
  // recreate engine if previously disconnected
195
206
  this.createEngine();
196
207
 
@@ -203,12 +214,17 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
203
214
  this.connOptions = opts;
204
215
 
205
216
  try {
206
- const joinResponse = await this.engine.join(url, token, {
207
- autoSubscribe: opts?.autoSubscribe,
208
- publishOnly: opts?.publishOnly,
209
- adaptiveStream:
210
- typeof this.options?.adaptiveStream === 'object' ? true : this.options?.adaptiveStream,
211
- });
217
+ const joinResponse = await this.engine.join(
218
+ url,
219
+ token,
220
+ {
221
+ autoSubscribe: opts?.autoSubscribe,
222
+ publishOnly: opts?.publishOnly,
223
+ adaptiveStream:
224
+ typeof this.options?.adaptiveStream === 'object' ? true : this.options?.adaptiveStream,
225
+ },
226
+ this.abortController.signal,
227
+ );
212
228
  log.debug(
213
229
  `connected to Livekit Server version: ${joinResponse.serverVersion}, region: ${joinResponse.serverRegion}`,
214
230
  );
@@ -223,7 +239,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
223
239
  this.options.dynacast = false;
224
240
  }
225
241
 
226
- this.state = RoomState.Connected;
227
242
  const pi = joinResponse.participant!;
228
243
 
229
244
  this.localParticipant.sid = pi.sid;
@@ -232,9 +247,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
232
247
  this.localParticipant.updateInfo(pi);
233
248
  // forward metadata changed for the local participant
234
249
  this.localParticipant
235
- .on(ParticipantEvent.MetadataChanged, (metadata: string | undefined) => {
236
- this.emit(RoomEvent.MetadataChanged, metadata, this.localParticipant);
237
- })
238
250
  .on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
239
251
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
240
252
  })
@@ -275,9 +287,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
275
287
  this.name = joinResponse.room!.name;
276
288
  this.sid = joinResponse.room!.sid;
277
289
  this.metadata = joinResponse.room!.metadata;
278
- this.emit(RoomEvent.StateChanged, this.state);
279
290
  } catch (err) {
280
291
  this.engine.close();
292
+ this.setAndEmitConnectionState(ConnectionState.Disconnected);
281
293
  throw err;
282
294
  }
283
295
 
@@ -286,18 +298,30 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
286
298
  const connectTimeout = setTimeout(() => {
287
299
  // timeout
288
300
  this.engine.close();
301
+ this.setAndEmitConnectionState(ConnectionState.Disconnected);
289
302
  reject(new ConnectionError('could not connect after timeout'));
290
303
  }, maxICEConnectTimeout);
304
+ const abortHandler = () => {
305
+ log.warn('closing engine');
306
+ clearTimeout(connectTimeout);
307
+ this.engine.close();
308
+ this.setAndEmitConnectionState(ConnectionState.Disconnected);
309
+ reject(new ConnectionError('room connection has been cancelled'));
310
+ };
311
+ if (this.abortController?.signal.aborted) {
312
+ abortHandler();
313
+ }
314
+ this.abortController?.signal.addEventListener('abort', abortHandler);
291
315
 
292
316
  this.engine.once(EngineEvent.Connected, () => {
293
317
  clearTimeout(connectTimeout);
294
-
318
+ this.abortController?.signal.removeEventListener('abort', abortHandler);
295
319
  // also hook unload event
296
320
  if (isWeb()) {
297
321
  window.addEventListener('beforeunload', this.onBeforeUnload);
298
322
  navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
299
323
  }
300
-
324
+ this.setAndEmitConnectionState(ConnectionState.Connected);
301
325
  resolve(this);
302
326
  });
303
327
  });
@@ -307,6 +331,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
307
331
  * disconnects the room, emits [[RoomEvent.Disconnected]]
308
332
  */
309
333
  disconnect = (stopTracks = true) => {
334
+ if (this.state === ConnectionState.Connecting) {
335
+ // try aborting pending connection attempt
336
+ log.warn('abort connection attempt');
337
+ this.abortController?.abort();
338
+ return;
339
+ }
310
340
  // send leave
311
341
  if (this.engine) {
312
342
  this.engine.client.sendLeave();
@@ -482,9 +512,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
482
512
  }
483
513
 
484
514
  private handleRestarting = () => {
485
- this.state = RoomState.Reconnecting;
515
+ this.setAndEmitConnectionState(ConnectionState.Reconnecting);
486
516
  this.emit(RoomEvent.Reconnecting);
487
- this.emit(RoomEvent.StateChanged, this.state);
488
517
 
489
518
  // also unwind existing participants & existing subscriptions
490
519
  for (const p of this.participants.values()) {
@@ -494,9 +523,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
494
523
 
495
524
  private handleRestarted = async (joinResponse: JoinResponse) => {
496
525
  log.debug(`reconnected to server region ${joinResponse.serverRegion}`);
497
- this.state = RoomState.Connected;
526
+ this.setAndEmitConnectionState(ConnectionState.Connected);
498
527
  this.emit(RoomEvent.Reconnected);
499
- this.emit(RoomEvent.StateChanged, this.state);
500
528
 
501
529
  // rehydrate participants
502
530
  if (joinResponse.participant) {
@@ -524,9 +552,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
524
552
  };
525
553
 
526
554
  private handleDisconnect(shouldStopTracks = true) {
527
- if (this.state === RoomState.Disconnected) {
528
- return;
529
- }
530
555
  this.participants.forEach((p) => {
531
556
  p.tracks.forEach((pub) => {
532
557
  p.unpublishTrack(pub.trackSid);
@@ -553,9 +578,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
553
578
  window.removeEventListener('beforeunload', this.onBeforeUnload);
554
579
  navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
555
580
  }
556
- this.state = RoomState.Disconnected;
581
+ this.setAndEmitConnectionState(ConnectionState.Disconnected);
557
582
  this.emit(RoomEvent.Disconnected);
558
- this.emit(RoomEvent.StateChanged, this.state);
559
583
  }
560
584
 
561
585
  private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
@@ -821,9 +845,6 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
821
845
  .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
822
846
  this.emit(RoomEvent.TrackUnmuted, pub, participant);
823
847
  })
824
- .on(ParticipantEvent.MetadataChanged, (metadata: string | undefined) => {
825
- this.emit(RoomEvent.MetadataChanged, metadata, participant);
826
- })
827
848
  .on(ParticipantEvent.ParticipantMetadataChanged, (metadata: string | undefined) => {
828
849
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, participant);
829
850
  })
@@ -892,6 +913,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
892
913
  }
893
914
  }
894
915
 
916
+ private setAndEmitConnectionState(state: ConnectionState) {
917
+ if (state === this.state) {
918
+ return;
919
+ }
920
+ this.state = state;
921
+ this.emit(RoomEvent.ConnectionStateChanged, this.state);
922
+ }
923
+
895
924
  // /** @internal */
896
925
  emit<E extends keyof RoomEventCallbacks>(
897
926
  event: E,
@@ -908,7 +937,9 @@ export type RoomEventCallbacks = {
908
937
  reconnecting: () => void;
909
938
  reconnected: () => void;
910
939
  disconnected: () => void;
911
- stateChanged: (state: RoomState) => void;
940
+ /** @deprecated stateChanged has been renamed to connectionStateChanged */
941
+ stateChanged: (state: ConnectionState) => void;
942
+ connectionStateChanged: (state: ConnectionState) => void;
912
943
  mediaDevicesChanged: () => void;
913
944
  participantConnected: (participant: RemoteParticipant) => void;
914
945
  participantDisconnected: (participant: RemoteParticipant) => void;
@@ -932,13 +963,6 @@ export type RoomEventCallbacks = {
932
963
  publication: LocalTrackPublication,
933
964
  participant: LocalParticipant,
934
965
  ) => void;
935
- /**
936
- * @deprecated use [[participantMetadataChanged]] instead
937
- */
938
- metadataChanged: (
939
- metadata: string | undefined,
940
- participant?: RemoteParticipant | LocalParticipant,
941
- ) => void;
942
966
  participantMetadataChanged: (
943
967
  metadata: string | undefined,
944
968
  participant: RemoteParticipant | LocalParticipant,
@@ -29,9 +29,14 @@ export enum RoomEvent {
29
29
  /**
30
30
  * Whenever the connection state of the room changes
31
31
  *
32
- * args: ([[RoomState]])
32
+ * args: ([[ConnectionState]])
33
33
  */
34
- StateChanged = 'stateChanged',
34
+ ConnectionStateChanged = 'connectionStateChanged',
35
+
36
+ /**
37
+ * @deprecated StateChanged has been renamed to ConnectionStateChanged
38
+ */
39
+ StateChanged = 'connectionStateChanged',
35
40
 
36
41
  /**
37
42
  * When input or output devices on the machine have changed.
@@ -139,12 +144,6 @@ export enum RoomEvent {
139
144
  */
140
145
  ActiveSpeakersChanged = 'activeSpeakersChanged',
141
146
 
142
- /**
143
- * @deprecated Use ParticipantMetadataChanged instead
144
- * @internal
145
- */
146
- MetadataChanged = 'metadataChanged',
147
-
148
147
  /**
149
148
  * Participant metadata is a simple way for app-specific state to be pushed to
150
149
  * all users.
@@ -308,12 +307,6 @@ export enum ParticipantEvent {
308
307
  */
309
308
  LocalTrackUnpublished = 'localTrackUnpublished',
310
309
 
311
- /**
312
- * @deprecated Use ParticipantMetadataChanged instead
313
- * @internal
314
- */
315
- MetadataChanged = 'metadataChanged',
316
-
317
310
  /**
318
311
  * Participant metadata is a simple way for app-specific state to be pushed to
319
312
  * all users.
@@ -409,6 +409,18 @@ export default class LocalParticipant extends Participant {
409
409
  // width and height should be defined for video
410
410
  req.width = width ?? 0;
411
411
  req.height = height ?? 0;
412
+ // for svc codecs, disable simulcast and enable scalability L3T3
413
+ // by default
414
+ if (track instanceof LocalVideoTrack) {
415
+ if (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1') {
416
+ opts.simulcast = false;
417
+ opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
418
+ } else {
419
+ // other codecs, unset scalability
420
+ opts.scalabilityMode = undefined;
421
+ }
422
+ }
423
+
412
424
  encodings = computeVideoEncodings(
413
425
  track.source === Track.Source.ScreenShare,
414
426
  width,
@@ -424,6 +436,10 @@ export default class LocalParticipant extends Participant {
424
436
  ];
425
437
  }
426
438
 
439
+ if (!this.engine || this.engine.isClosed) {
440
+ throw new UnexpectedConnectionState('cannot publish track when not connected');
441
+ }
442
+
427
443
  const ti = await this.engine.addTrack(req);
428
444
  const publication = new LocalTrackPublication(track.kind, ti, track);
429
445
  track.sid = ti.sid;
@@ -440,6 +456,9 @@ export default class LocalParticipant extends Participant {
440
456
  track.mediaStreamTrack,
441
457
  transceiverInit,
442
458
  );
459
+ if (opts.videoCodec) {
460
+ this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
461
+ }
443
462
  this.engine.negotiate();
444
463
 
445
464
  // store RTPSender
@@ -449,10 +468,6 @@ export default class LocalParticipant extends Participant {
449
468
  } else if (track instanceof LocalAudioTrack) {
450
469
  track.startMonitor();
451
470
  }
452
-
453
- if (opts.videoCodec) {
454
- this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
455
- }
456
471
  this.addTrackPublication(publication);
457
472
 
458
473
  // send event for publication
@@ -699,21 +714,34 @@ export default class LocalParticipant extends Participant {
699
714
  }
700
715
  const cap = RTCRtpSender.getCapabilities(kind);
701
716
  if (!cap) return;
702
- const selected = cap.codecs.find((c) => {
717
+ let selected: RTCRtpCodecCapability | undefined;
718
+ const codecs: RTCRtpCodecCapability[] = [];
719
+ cap.codecs.forEach((c) => {
703
720
  const codec = c.mimeType.toLowerCase();
704
721
  const matchesVideoCodec = codec === `video/${videoCodec}`;
705
722
 
723
+ if (selected !== undefined) {
724
+ codecs.push(c);
725
+ return;
726
+ }
706
727
  // for h264 codecs that have sdpFmtpLine available, use only if the
707
728
  // profile-level-id is 42e01f for cross-browser compatibility
708
729
  if (videoCodec === 'h264' && c.sdpFmtpLine) {
709
- return matchesVideoCodec && c.sdpFmtpLine.includes('profile-level-id=42e01f');
730
+ if (matchesVideoCodec && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
731
+ selected = c;
732
+ return;
733
+ }
710
734
  }
711
-
712
- return matchesVideoCodec || codec === 'audio/opus';
735
+ if (matchesVideoCodec || codec === 'audio/opus') {
736
+ selected = c;
737
+ return;
738
+ }
739
+ codecs.push(c);
713
740
  });
714
741
  if (selected && 'setCodecPreferences' in transceiver) {
715
742
  // @ts-ignore
716
- transceiver.setCodecPreferences([selected]);
743
+ codecs.unshift(selected);
744
+ transceiver.setCodecPreferences(codecs);
717
745
  }
718
746
  }
719
747
 
@@ -188,7 +188,6 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
188
188
  this.metadata = md;
189
189
 
190
190
  if (changed) {
191
- this.emit(ParticipantEvent.MetadataChanged, prevMetadata);
192
191
  this.emit(ParticipantEvent.ParticipantMetadataChanged, prevMetadata);
193
192
  }
194
193
  }
@@ -266,10 +265,6 @@ export type ParticipantEventCallbacks = {
266
265
  trackUnmuted: (publication: TrackPublication) => void;
267
266
  localTrackPublished: (publication: LocalTrackPublication) => void;
268
267
  localTrackUnpublished: (publication: LocalTrackPublication) => void;
269
- /**
270
- * @deprecated use [[participantMetadataChanged]] instead
271
- */
272
- metadataChanged: (prevMetadata: string | undefined, participant?: any) => void;
273
268
  participantMetadataChanged: (prevMetadata: string | undefined, participant?: any) => void;
274
269
  dataReceived: (payload: Uint8Array, kind: DataPacket_Kind) => void;
275
270
  isSpeakingChanged: (speaking: boolean) => void;
@@ -19,6 +19,8 @@ export default class RemoteParticipant extends Participant {
19
19
 
20
20
  signalClient: SignalClient;
21
21
 
22
+ private volume?: number;
23
+
22
24
  /** @internal */
23
25
  static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant {
24
26
  const rp = new RemoteParticipant(signalClient, pi.sid, pi.identity);
@@ -68,24 +70,26 @@ export default class RemoteParticipant extends Participant {
68
70
  }
69
71
 
70
72
  /**
71
- * sets the volume on the participant's microphone track if it exists.
73
+ * sets the volume on the participant's microphone track
74
+ * if no track exists the volume will be applied when the microphone track is added
72
75
  */
73
76
  setVolume(volume: number) {
77
+ this.volume = volume;
74
78
  const audioPublication = this.getTrack(Track.Source.Microphone);
75
- if (audioPublication) {
79
+ if (audioPublication && audioPublication.track) {
76
80
  (audioPublication.track as RemoteAudioTrack).setVolume(volume);
77
81
  }
78
82
  }
79
83
 
80
84
  /**
81
85
  * gets the volume on the participant's microphone track
82
- * returns undefined if no microphone track exists
83
86
  */
84
87
  getVolume() {
85
88
  const audioPublication = this.getTrack(Track.Source.Microphone);
86
- if (audioPublication) {
89
+ if (audioPublication && audioPublication.track) {
87
90
  return (audioPublication.track as RemoteAudioTrack).getVolume();
88
91
  }
92
+ return this.volume;
89
93
  }
90
94
 
91
95
  /** @internal */
@@ -153,7 +157,14 @@ export default class RemoteParticipant extends Participant {
153
157
  track.start();
154
158
 
155
159
  publication.setTrack(track);
156
-
160
+ // set participant volume on new microphone tracks
161
+ if (
162
+ this.volume !== undefined &&
163
+ track instanceof RemoteAudioTrack &&
164
+ track.source === Track.Source.Microphone
165
+ ) {
166
+ track.setVolume(this.volume);
167
+ }
157
168
  this.emit(ParticipantEvent.TrackSubscribed, track, publication);
158
169
 
159
170
  return publication;
@@ -28,11 +28,11 @@ describe('presetsForResolution', () => {
28
28
 
29
29
  describe('determineAppropriateEncoding', () => {
30
30
  it('uses higher encoding', () => {
31
- expect(determineAppropriateEncoding(false, 600, 300)).toEqual(VideoPresets.vga.encoding);
31
+ expect(determineAppropriateEncoding(false, 600, 300)).toEqual(VideoPresets.h360.encoding);
32
32
  });
33
33
 
34
34
  it('handles portrait', () => {
35
- expect(determineAppropriateEncoding(false, 300, 600)).toEqual(VideoPresets.vga.encoding);
35
+ expect(determineAppropriateEncoding(false, 300, 600)).toEqual(VideoPresets.h360.encoding);
36
36
  });
37
37
  });
38
38