livekit-client 0.18.5 → 1.0.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 (60) 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 +3 -4
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/livekit-client.esm.mjs +257 -254
  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/track/RemoteVideoTrack.d.ts +24 -1
  25. package/dist/room/track/RemoteVideoTrack.d.ts.map +1 -1
  26. package/dist/room/track/Track.d.ts +2 -1
  27. package/dist/room/track/Track.d.ts.map +1 -1
  28. package/dist/room/track/options.d.ts +0 -30
  29. package/dist/room/track/options.d.ts.map +1 -1
  30. package/dist/room/track/types.d.ts +5 -0
  31. package/dist/room/track/types.d.ts.map +1 -1
  32. package/dist/test/MockMediaStreamTrack.d.ts +26 -0
  33. package/dist/test/MockMediaStreamTrack.d.ts.map +1 -0
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.d.ts.map +1 -1
  36. package/package.json +4 -3
  37. package/src/api/SignalClient.ts +32 -7
  38. package/src/index.ts +4 -3
  39. package/src/options.ts +0 -82
  40. package/src/room/DeviceManager.ts +4 -1
  41. package/src/room/RTCEngine.ts +16 -7
  42. package/src/room/Room.ts +66 -42
  43. package/src/room/events.ts +7 -14
  44. package/src/room/participant/LocalParticipant.ts +4 -0
  45. package/src/room/participant/Participant.ts +0 -5
  46. package/src/room/participant/RemoteParticipant.ts +16 -5
  47. package/src/room/participant/publishUtils.test.ts +2 -2
  48. package/src/room/track/LocalVideoTrack.ts +1 -1
  49. package/src/room/track/RemoteVideoTrack.test.ts +149 -0
  50. package/src/room/track/RemoteVideoTrack.ts +118 -39
  51. package/src/room/track/Track.ts +18 -2
  52. package/src/room/track/create.ts +1 -1
  53. package/src/room/track/options.ts +1 -31
  54. package/src/room/track/types.ts +5 -0
  55. package/src/room/track/utils.test.ts +6 -6
  56. package/src/test/MockMediaStreamTrack.ts +83 -0
  57. package/src/version.ts +1 -1
  58. package/dist/connect.d.ts +0 -24
  59. package/dist/connect.d.ts.map +0 -1
  60. package/src/connect.ts +0 -98
@@ -123,15 +123,25 @@ export class SignalClient {
123
123
  this.requestQueue = new Queue();
124
124
  }
125
125
 
126
- async join(url: string, token: string, opts?: SignalOptions): Promise<JoinResponse> {
126
+ async join(
127
+ url: string,
128
+ token: string,
129
+ opts?: SignalOptions,
130
+ abortSignal?: AbortSignal,
131
+ ): Promise<JoinResponse> {
127
132
  // during a full reconnect, we'd want to start the sequence even if currently
128
133
  // connected
129
134
  this.isConnected = false;
130
- const res = await this.connect(url, token, {
131
- autoSubscribe: opts?.autoSubscribe,
132
- publishOnly: opts?.publishOnly,
133
- adaptiveStream: opts?.adaptiveStream,
134
- });
135
+ const res = await this.connect(
136
+ url,
137
+ token,
138
+ {
139
+ autoSubscribe: opts?.autoSubscribe,
140
+ publishOnly: opts?.publishOnly,
141
+ adaptiveStream: opts?.adaptiveStream,
142
+ },
143
+ abortSignal,
144
+ );
135
145
  return res as JoinResponse;
136
146
  }
137
147
 
@@ -142,7 +152,12 @@ export class SignalClient {
142
152
  });
143
153
  }
144
154
 
145
- connect(url: string, token: string, opts: ConnectOpts): Promise<JoinResponse | void> {
155
+ connect(
156
+ url: string,
157
+ token: string,
158
+ opts: ConnectOpts,
159
+ abortSignal?: AbortSignal,
160
+ ): Promise<JoinResponse | void> {
146
161
  if (url.startsWith('http')) {
147
162
  url = url.replace('http', 'ws');
148
163
  }
@@ -154,6 +169,15 @@ export class SignalClient {
154
169
  const params = createConnectionParams(token, clientInfo, opts);
155
170
 
156
171
  return new Promise<JoinResponse | void>((resolve, reject) => {
172
+ const abortHandler = () => {
173
+ ws.close();
174
+ this.close();
175
+ reject(new ConnectionError('room connection has been cancelled'));
176
+ };
177
+ if (abortSignal?.aborted) {
178
+ abortHandler();
179
+ }
180
+ abortSignal?.addEventListener('abort', abortHandler);
157
181
  log.debug(`connecting to ${url + params}`);
158
182
  this.ws = undefined;
159
183
  const ws = new WebSocket(url + params);
@@ -204,6 +228,7 @@ export class SignalClient {
204
228
  // handle join message only
205
229
  if (msg.join) {
206
230
  this.isConnected = true;
231
+ abortSignal?.removeEventListener('abort', abortHandler);
207
232
  resolve(msg.join);
208
233
  } else {
209
234
  reject(new ConnectionError('did not receive join response'));
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';
@@ -12,10 +12,9 @@ import LocalVideoTrack from './room/track/LocalVideoTrack';
12
12
  import RemoteAudioTrack from './room/track/RemoteAudioTrack';
13
13
  import RemoteTrack from './room/track/RemoteTrack';
14
14
  import RemoteTrackPublication from './room/track/RemoteTrackPublication';
15
- import RemoteVideoTrack from './room/track/RemoteVideoTrack';
15
+ import RemoteVideoTrack, { ElementInfo } 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,
@@ -46,4 +46,5 @@ export {
46
46
  TrackPublication,
47
47
  VideoQuality,
48
48
  ConnectionQuality,
49
+ ElementInfo,
49
50
  };
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);
@@ -535,7 +560,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
535
560
 
536
561
  this.localParticipant.tracks.forEach((pub) => {
537
562
  if (pub.track) {
538
- this.localParticipant.unpublishTrack(pub.track);
563
+ this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
539
564
  }
540
565
  if (shouldStopTracks) {
541
566
  pub.track?.detach();
@@ -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.
@@ -436,6 +436,10 @@ export default class LocalParticipant extends Participant {
436
436
  ];
437
437
  }
438
438
 
439
+ if (!this.engine || this.engine.isClosed) {
440
+ throw new UnexpectedConnectionState('cannot publish track when not connected');
441
+ }
442
+
439
443
  const ti = await this.engine.addTrack(req);
440
444
  const publication = new LocalTrackPublication(track.kind, ti, track);
441
445
  track.sid = ti.sid;
@@ -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;