livekit-client 1.4.4 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.esm.mjs +2478 -5368
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +3 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/ConnectionCheck.d.ts +25 -0
- package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/Checker.d.ts +59 -0
- package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/publishAudio.d.ts +6 -0
- package/dist/src/connectionHelper/checks/publishAudio.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/publishVideo.d.ts +6 -0
- package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/reconnect.d.ts +6 -0
- package/dist/src/connectionHelper/checks/reconnect.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/turn.d.ts +6 -0
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/webrtc.d.ts +6 -0
- package/dist/src/connectionHelper/checks/webrtc.d.ts.map +1 -0
- package/dist/src/connectionHelper/checks/websocket.d.ts +6 -0
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -3
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +4 -1
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/google/protobuf/timestamp.d.ts +4 -4
- package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +4 -4
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +12 -4
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/DeviceManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +27 -4
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +9 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrackPublication.d.ts +2 -0
- package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +2 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +1 -1
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +3 -3
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/types.d.ts +3 -3
- package/dist/src/room/track/types.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +13 -0
- package/dist/src/room/types.d.ts.map +1 -0
- package/dist/src/room/utils.d.ts +44 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +86 -0
- package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +25 -0
- package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +59 -0
- package/dist/ts4.2/src/connectionHelper/checks/publishAudio.d.ts +6 -0
- package/dist/ts4.2/src/connectionHelper/checks/publishVideo.d.ts +6 -0
- package/dist/ts4.2/src/connectionHelper/checks/reconnect.d.ts +6 -0
- package/dist/ts4.2/src/connectionHelper/checks/turn.d.ts +6 -0
- package/dist/ts4.2/src/connectionHelper/checks/webrtc.d.ts +6 -0
- package/dist/ts4.2/src/connectionHelper/checks/websocket.d.ts +6 -0
- package/dist/ts4.2/src/index.d.ts +31 -0
- package/dist/ts4.2/src/logger.d.ts +26 -0
- package/dist/ts4.2/src/options.d.ts +94 -0
- package/dist/ts4.2/src/proto/google/protobuf/timestamp.d.ts +141 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +1421 -0
- package/dist/ts4.2/src/proto/livekit_rtc.d.ts +7122 -0
- package/dist/ts4.2/src/room/DefaultReconnectPolicy.d.ts +8 -0
- package/dist/ts4.2/src/room/DeviceManager.d.ts +9 -0
- package/dist/ts4.2/src/room/PCTransport.d.ts +33 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +97 -0
- package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +23 -0
- package/dist/ts4.2/src/room/Room.d.ts +220 -0
- package/dist/ts4.2/src/room/defaults.d.ts +8 -0
- package/dist/ts4.2/src/room/errors.d.ts +39 -0
- package/dist/ts4.2/src/room/events.d.ts +426 -0
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +141 -0
- package/dist/ts4.2/src/room/participant/Participant.d.ts +92 -0
- package/dist/ts4.2/src/room/participant/ParticipantTrackPermission.d.ts +26 -0
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +52 -0
- package/dist/ts4.2/src/room/participant/publishUtils.d.ts +19 -0
- package/dist/ts4.2/src/room/stats.d.ts +67 -0
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +25 -0
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +42 -0
- package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +38 -0
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +53 -0
- package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +53 -0
- package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +15 -0
- package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +61 -0
- package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +52 -0
- package/dist/ts4.2/src/room/track/Track.d.ts +122 -0
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +68 -0
- package/dist/ts4.2/src/room/track/create.d.ts +24 -0
- package/dist/ts4.2/src/room/track/options.d.ts +241 -0
- package/dist/ts4.2/src/room/track/types.d.ts +23 -0
- package/dist/ts4.2/src/room/track/utils.d.ts +14 -0
- package/dist/ts4.2/src/room/types.d.ts +13 -0
- package/dist/ts4.2/src/room/utils.d.ts +79 -0
- package/dist/ts4.2/src/test/MockMediaStreamTrack.d.ts +26 -0
- package/dist/ts4.2/src/test/mocks.d.ts +11 -0
- package/dist/ts4.2/src/version.d.ts +3 -0
- package/package.json +32 -22
- package/src/api/SignalClient.ts +41 -17
- package/src/connectionHelper/ConnectionCheck.ts +90 -0
- package/src/connectionHelper/checks/Checker.ts +164 -0
- package/src/connectionHelper/checks/publishAudio.ts +33 -0
- package/src/connectionHelper/checks/publishVideo.ts +33 -0
- package/src/connectionHelper/checks/reconnect.ts +45 -0
- package/src/connectionHelper/checks/turn.ts +53 -0
- package/src/connectionHelper/checks/webrtc.ts +18 -0
- package/src/connectionHelper/checks/websocket.ts +22 -0
- package/src/index.ts +8 -1
- package/src/options.ts +5 -1
- package/src/proto/livekit_rtc.ts +12 -1
- package/src/room/DeviceManager.ts +0 -17
- package/src/room/RTCEngine.ts +35 -26
- package/src/room/Room.ts +231 -63
- package/src/room/events.ts +9 -0
- package/src/room/participant/LocalParticipant.ts +18 -11
- package/src/room/participant/publishUtils.ts +1 -1
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +4 -0
- package/src/room/track/LocalVideoTrack.ts +1 -1
- package/src/room/track/RemoteTrackPublication.ts +20 -0
- package/src/room/track/RemoteVideoTrack.ts +4 -0
- package/src/room/track/Track.ts +1 -0
- package/src/room/types.ts +12 -0
- package/src/room/utils.ts +150 -12
package/src/room/RTCEngine.ts
CHANGED
@@ -38,6 +38,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
|
|
38
38
|
import { Track } from './track/Track';
|
39
39
|
import {
|
40
40
|
isWeb,
|
41
|
+
Mutex,
|
41
42
|
sleep,
|
42
43
|
supportsAddTrack,
|
43
44
|
supportsSetCodecPreferences,
|
@@ -130,12 +131,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
130
131
|
/** specifies how often an initial join connection is allowed to retry */
|
131
132
|
private maxJoinAttempts: number = 1;
|
132
133
|
|
134
|
+
private closingLock: Mutex;
|
135
|
+
|
133
136
|
constructor(private options: InternalRoomOptions) {
|
134
137
|
super();
|
135
138
|
this.client = new SignalClient();
|
136
139
|
this.client.signalLatency = this.options.expSignalLatency;
|
137
140
|
this.reconnectPolicy = this.options.reconnectPolicy;
|
138
141
|
this.registerOnLineListener();
|
142
|
+
this.closingLock = new Mutex();
|
139
143
|
}
|
140
144
|
|
141
145
|
async join(
|
@@ -179,30 +183,35 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
179
183
|
}
|
180
184
|
}
|
181
185
|
|
182
|
-
close() {
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
this.
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
186
|
+
async close() {
|
187
|
+
const unlock = await this.closingLock.lock();
|
188
|
+
try {
|
189
|
+
this._isClosed = true;
|
190
|
+
this.removeAllListeners();
|
191
|
+
this.deregisterOnLineListener();
|
192
|
+
this.clearPendingReconnect();
|
193
|
+
if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
|
194
|
+
this.publisher.pc.getSenders().forEach((sender) => {
|
195
|
+
try {
|
196
|
+
// TODO: react-native-webrtc doesn't have removeTrack yet.
|
197
|
+
if (this.publisher?.pc.removeTrack) {
|
198
|
+
this.publisher?.pc.removeTrack(sender);
|
199
|
+
}
|
200
|
+
} catch (e) {
|
201
|
+
log.warn('could not removeTrack', { error: e });
|
193
202
|
}
|
194
|
-
}
|
195
|
-
|
196
|
-
|
197
|
-
}
|
198
|
-
this.
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
this.
|
203
|
-
|
203
|
+
});
|
204
|
+
this.publisher.close();
|
205
|
+
this.publisher = undefined;
|
206
|
+
}
|
207
|
+
if (this.subscriber) {
|
208
|
+
this.subscriber.close();
|
209
|
+
this.subscriber = undefined;
|
210
|
+
}
|
211
|
+
await this.client.close();
|
212
|
+
} finally {
|
213
|
+
unlock();
|
204
214
|
}
|
205
|
-
this.client.close();
|
206
215
|
}
|
207
216
|
|
208
217
|
addTrack(req: AddTrackRequest): Promise<TrackInfo> {
|
@@ -335,7 +344,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
335
344
|
const shouldEmit = this.pcState === PCState.New;
|
336
345
|
this.pcState = PCState.Connected;
|
337
346
|
if (shouldEmit) {
|
338
|
-
this.emit(EngineEvent.Connected);
|
347
|
+
this.emit(EngineEvent.Connected, joinResponse);
|
339
348
|
}
|
340
349
|
} else if (primaryPC.connectionState === 'failed') {
|
341
350
|
// on Safari, PeerConnection will switch to 'disconnected' during renegotiation
|
@@ -784,9 +793,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
784
793
|
}
|
785
794
|
|
786
795
|
if (this.client.isConnected) {
|
787
|
-
this.client.sendLeave();
|
796
|
+
await this.client.sendLeave();
|
788
797
|
}
|
789
|
-
this.client.close();
|
798
|
+
await this.client.close();
|
790
799
|
this.primaryPC = undefined;
|
791
800
|
this.publisher?.close();
|
792
801
|
this.publisher = undefined;
|
@@ -1040,7 +1049,7 @@ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | unde
|
|
1040
1049
|
class SignalReconnectError extends Error {}
|
1041
1050
|
|
1042
1051
|
export type EngineEventCallbacks = {
|
1043
|
-
connected: () => void;
|
1052
|
+
connected: (joinResp: JoinResponse) => void;
|
1044
1053
|
disconnected: (reason?: DisconnectReason) => void;
|
1045
1054
|
resuming: () => void;
|
1046
1055
|
resumed: () => void;
|
package/src/room/Room.ts
CHANGED
@@ -17,6 +17,9 @@ import {
|
|
17
17
|
Room as RoomModel,
|
18
18
|
ServerInfo,
|
19
19
|
SpeakerInfo,
|
20
|
+
TrackInfo,
|
21
|
+
TrackSource,
|
22
|
+
TrackType,
|
20
23
|
UserPacket,
|
21
24
|
} from '../proto/livekit_models';
|
22
25
|
import {
|
@@ -42,7 +45,7 @@ import type { ConnectionQuality } from './participant/Participant';
|
|
42
45
|
import RemoteParticipant from './participant/RemoteParticipant';
|
43
46
|
import RTCEngine from './RTCEngine';
|
44
47
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
45
|
-
import
|
48
|
+
import LocalTrackPublication from './track/LocalTrackPublication';
|
46
49
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
47
50
|
import type RemoteTrack from './track/RemoteTrack';
|
48
51
|
import RemoteTrackPublication from './track/RemoteTrackPublication';
|
@@ -50,7 +53,16 @@ import { Track } from './track/Track';
|
|
50
53
|
import type { TrackPublication } from './track/TrackPublication';
|
51
54
|
import type { AdaptiveStreamSettings } from './track/types';
|
52
55
|
import { getNewAudioContext } from './track/utils';
|
53
|
-
import {
|
56
|
+
import type { SimulationOptions } from './types';
|
57
|
+
import {
|
58
|
+
Future,
|
59
|
+
createDummyVideoStreamTrack,
|
60
|
+
getEmptyAudioStreamTrack,
|
61
|
+
isWeb,
|
62
|
+
Mutex,
|
63
|
+
supportsSetSinkId,
|
64
|
+
unpackStreamId,
|
65
|
+
} from './utils';
|
54
66
|
|
55
67
|
export enum ConnectionState {
|
56
68
|
Disconnected = 'disconnected',
|
@@ -101,6 +113,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
101
113
|
/** options of room */
|
102
114
|
options: InternalRoomOptions;
|
103
115
|
|
116
|
+
private _isRecording: boolean = false;
|
117
|
+
|
104
118
|
private identityToSid: Map<string, string>;
|
105
119
|
|
106
120
|
/** connect options of room */
|
@@ -116,6 +130,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
116
130
|
/** future holding client initiated connection attempt */
|
117
131
|
private connectFuture?: Future<void>;
|
118
132
|
|
133
|
+
private disconnectLock: Mutex;
|
134
|
+
|
119
135
|
/**
|
120
136
|
* Creates a new Room, the primary construct for a LiveKit session.
|
121
137
|
* @param options
|
@@ -142,6 +158,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
142
158
|
|
143
159
|
this.maybeCreateEngine();
|
144
160
|
|
161
|
+
this.disconnectLock = new Mutex();
|
162
|
+
|
145
163
|
this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
|
146
164
|
}
|
147
165
|
|
@@ -301,18 +319,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
301
319
|
|
302
320
|
this.localParticipant.updateInfo(pi);
|
303
321
|
// forward metadata changed for the local participant
|
304
|
-
this.
|
305
|
-
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
306
|
-
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
307
|
-
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
308
|
-
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
309
|
-
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
310
|
-
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
311
|
-
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
312
|
-
.on(
|
313
|
-
ParticipantEvent.ParticipantPermissionsChanged,
|
314
|
-
this.onLocalParticipantPermissionsChanged,
|
315
|
-
);
|
322
|
+
this.setupLocalParticipantEvents();
|
316
323
|
|
317
324
|
// populate remote participants, these should not trigger new events
|
318
325
|
joinResponse.otherParticipants.forEach((info) => {
|
@@ -332,11 +339,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
332
339
|
this.name = joinResponse.room!.name;
|
333
340
|
this.sid = joinResponse.room!.sid;
|
334
341
|
this.metadata = joinResponse.room!.metadata;
|
342
|
+
if (this._isRecording !== joinResponse.room!.activeRecording) {
|
343
|
+
this._isRecording = joinResponse.room!.activeRecording;
|
344
|
+
this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
|
345
|
+
}
|
335
346
|
this.emit(RoomEvent.SignalConnected);
|
336
347
|
} catch (err) {
|
337
348
|
this.recreateEngine();
|
338
349
|
this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
|
339
|
-
|
350
|
+
let errorMessage = '';
|
351
|
+
if (err instanceof Error) {
|
352
|
+
errorMessage = err.message;
|
353
|
+
log.debug(`error trying to establish signal connection`, { error: err });
|
354
|
+
}
|
355
|
+
reject(new ConnectionError(`could not establish signal connection: ${errorMessage}`));
|
340
356
|
return;
|
341
357
|
}
|
342
358
|
|
@@ -383,26 +399,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
383
399
|
* disconnects the room, emits [[RoomEvent.Disconnected]]
|
384
400
|
*/
|
385
401
|
disconnect = async (stopTracks = true) => {
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
+
const unlock = await this.disconnectLock.lock();
|
403
|
+
try {
|
404
|
+
if (this.state === ConnectionState.Disconnected) {
|
405
|
+
log.debug('already disconnected');
|
406
|
+
return;
|
407
|
+
}
|
408
|
+
log.info('disconnect from room', { identity: this.localParticipant.identity });
|
409
|
+
if (
|
410
|
+
this.state === ConnectionState.Connecting ||
|
411
|
+
this.state === ConnectionState.Reconnecting
|
412
|
+
) {
|
413
|
+
// try aborting pending connection attempt
|
414
|
+
log.warn('abort connection attempt');
|
415
|
+
this.abortController?.abort();
|
416
|
+
// in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
|
417
|
+
this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
|
418
|
+
this.connectFuture = undefined;
|
419
|
+
}
|
420
|
+
// send leave
|
421
|
+
if (this.engine?.client.isConnected) {
|
422
|
+
await this.engine.client.sendLeave();
|
423
|
+
}
|
424
|
+
// close engine (also closes client)
|
425
|
+
if (this.engine) {
|
426
|
+
await this.engine.close();
|
427
|
+
}
|
428
|
+
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
429
|
+
/* @ts-ignore */
|
430
|
+
this.engine = undefined;
|
431
|
+
} finally {
|
432
|
+
unlock();
|
402
433
|
}
|
403
|
-
this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
|
404
|
-
/* @ts-ignore */
|
405
|
-
this.engine = undefined;
|
406
434
|
};
|
407
435
|
|
408
436
|
/**
|
@@ -424,15 +452,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
424
452
|
this.connectFuture = undefined;
|
425
453
|
}
|
426
454
|
|
455
|
+
/**
|
456
|
+
* if the current room has a participant with `recorder: true` in its JWT grant
|
457
|
+
**/
|
458
|
+
get isRecording() {
|
459
|
+
return this._isRecording;
|
460
|
+
}
|
461
|
+
|
427
462
|
/**
|
428
463
|
* @internal for testing
|
429
464
|
*/
|
430
|
-
simulateScenario(scenario: string) {
|
465
|
+
async simulateScenario(scenario: string) {
|
431
466
|
let postAction = () => {};
|
432
467
|
let req: SimulateScenario | undefined;
|
433
468
|
switch (scenario) {
|
434
469
|
case 'signal-reconnect':
|
435
|
-
this.engine.client.close();
|
470
|
+
await this.engine.client.close();
|
436
471
|
if (this.engine.client.onClose) {
|
437
472
|
this.engine.client.onClose('simulate disconnect');
|
438
473
|
}
|
@@ -495,8 +530,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
495
530
|
}
|
496
531
|
}
|
497
532
|
|
498
|
-
private onBeforeUnload = () => {
|
499
|
-
this.disconnect();
|
533
|
+
private onBeforeUnload = async () => {
|
534
|
+
await this.disconnect();
|
500
535
|
};
|
501
536
|
|
502
537
|
/**
|
@@ -507,7 +542,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
507
542
|
* - `getUserMedia`
|
508
543
|
*/
|
509
544
|
async startAudio() {
|
510
|
-
this.acquireAudioContext();
|
545
|
+
await this.acquireAudioContext();
|
511
546
|
|
512
547
|
const elements: Array<HTMLMediaElement> = [];
|
513
548
|
this.participants.forEach((p) => {
|
@@ -537,7 +572,18 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
537
572
|
}
|
538
573
|
|
539
574
|
/**
|
540
|
-
*
|
575
|
+
* Returns the active audio output device used in this room.
|
576
|
+
*
|
577
|
+
* Note: to get the active `audioinput` or `videoinput` use [[LocalTrack.getDeviceId()]]
|
578
|
+
*
|
579
|
+
* @return the previously successfully set audio output device ID or an empty string if the default device is used.
|
580
|
+
*/
|
581
|
+
getActiveAudioOutputDevice(): string {
|
582
|
+
return this.options.audioOutput?.deviceId ?? '';
|
583
|
+
}
|
584
|
+
|
585
|
+
/**
|
586
|
+
* Switches all active devices used in this room to the given device.
|
541
587
|
*
|
542
588
|
* Note: setting AudioOutput is not supported on some browsers. See [setSinkId](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility)
|
543
589
|
*
|
@@ -579,16 +625,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
579
625
|
this.options.audioOutput ??= {};
|
580
626
|
const prevDeviceId = this.options.audioOutput.deviceId;
|
581
627
|
this.options.audioOutput.deviceId = deviceId;
|
582
|
-
const promises: Promise<void>[] = [];
|
583
|
-
this.participants.forEach((p) => {
|
584
|
-
promises.push(
|
585
|
-
p.setAudioOutput({
|
586
|
-
deviceId,
|
587
|
-
}),
|
588
|
-
);
|
589
|
-
});
|
590
628
|
try {
|
591
|
-
await Promise.all(
|
629
|
+
await Promise.all(
|
630
|
+
Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
|
631
|
+
);
|
592
632
|
} catch (e) {
|
593
633
|
this.options.audioOutput.deviceId = prevDeviceId;
|
594
634
|
throw e;
|
@@ -596,6 +636,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
596
636
|
}
|
597
637
|
}
|
598
638
|
|
639
|
+
private setupLocalParticipantEvents() {
|
640
|
+
this.localParticipant
|
641
|
+
.on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
642
|
+
.on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
643
|
+
.on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
644
|
+
.on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
645
|
+
.on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
646
|
+
.on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
647
|
+
.on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
648
|
+
.on(
|
649
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
650
|
+
this.onLocalParticipantPermissionsChanged,
|
651
|
+
);
|
652
|
+
}
|
653
|
+
|
599
654
|
private recreateEngine() {
|
600
655
|
this.engine?.close();
|
601
656
|
/* @ts-ignore */
|
@@ -760,7 +815,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
760
815
|
|
761
816
|
this.participants.clear();
|
762
817
|
this.activeSpeakers = [];
|
763
|
-
if (this.audioContext) {
|
818
|
+
if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
|
764
819
|
this.audioContext.close();
|
765
820
|
this.audioContext = undefined;
|
766
821
|
}
|
@@ -950,8 +1005,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
950
1005
|
};
|
951
1006
|
|
952
1007
|
private handleRoomUpdate = (r: RoomModel) => {
|
953
|
-
this.
|
954
|
-
|
1008
|
+
if (this._isRecording !== r.activeRecording) {
|
1009
|
+
this._isRecording = r.activeRecording;
|
1010
|
+
this.emit(RoomEvent.RecordingStatusChanged, r.activeRecording);
|
1011
|
+
}
|
1012
|
+
if (this.metadata !== r.metadata) {
|
1013
|
+
this.metadata = r.metadata;
|
1014
|
+
this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
|
1015
|
+
}
|
955
1016
|
};
|
956
1017
|
|
957
1018
|
private handleConnectionQualityUpdate = (update: ConnectionQualityUpdate) => {
|
@@ -967,18 +1028,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
967
1028
|
});
|
968
1029
|
};
|
969
1030
|
|
970
|
-
private acquireAudioContext() {
|
971
|
-
if (
|
972
|
-
this.
|
1031
|
+
private async acquireAudioContext() {
|
1032
|
+
if (
|
1033
|
+
typeof this.options.expWebAudioMix !== 'boolean' &&
|
1034
|
+
this.options.expWebAudioMix.audioContext
|
1035
|
+
) {
|
1036
|
+
// override audio context with custom audio context if supplied by user
|
1037
|
+
this.audioContext = this.options.expWebAudioMix.audioContext;
|
1038
|
+
await this.audioContext.resume();
|
1039
|
+
} else {
|
1040
|
+
// by using an AudioContext, it reduces lag on audio elements
|
1041
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
1042
|
+
this.audioContext = getNewAudioContext() ?? undefined;
|
973
1043
|
}
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
if (ctx) {
|
978
|
-
this.audioContext = ctx;
|
979
|
-
if (this.options.expWebAudioMix) {
|
980
|
-
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
981
|
-
}
|
1044
|
+
|
1045
|
+
if (this.options.expWebAudioMix) {
|
1046
|
+
this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
|
982
1047
|
}
|
983
1048
|
}
|
984
1049
|
|
@@ -1193,6 +1258,108 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1193
1258
|
this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
|
1194
1259
|
};
|
1195
1260
|
|
1261
|
+
/**
|
1262
|
+
* Allows to populate a room with simulated participants.
|
1263
|
+
* No actual connection to a server will be established, all state is
|
1264
|
+
* @experimental
|
1265
|
+
*/
|
1266
|
+
simulateParticipants(options: SimulationOptions) {
|
1267
|
+
const publishOptions = {
|
1268
|
+
audio: true,
|
1269
|
+
video: true,
|
1270
|
+
...options.publish,
|
1271
|
+
};
|
1272
|
+
const participantOptions = {
|
1273
|
+
count: 9,
|
1274
|
+
audio: false,
|
1275
|
+
video: true,
|
1276
|
+
aspectRatios: [1.66, 1.7, 1.3],
|
1277
|
+
...options.participants,
|
1278
|
+
};
|
1279
|
+
this.handleDisconnect();
|
1280
|
+
this.name = 'simulated-room';
|
1281
|
+
this.localParticipant.identity = 'simulated-local';
|
1282
|
+
this.localParticipant.name = 'simulated-local';
|
1283
|
+
this.setupLocalParticipantEvents();
|
1284
|
+
this.emit(RoomEvent.SignalConnected);
|
1285
|
+
this.emit(RoomEvent.Connected);
|
1286
|
+
this.setAndEmitConnectionState(ConnectionState.Connected);
|
1287
|
+
if (publishOptions.video) {
|
1288
|
+
const camPub = new LocalTrackPublication(
|
1289
|
+
Track.Kind.Video,
|
1290
|
+
TrackInfo.fromPartial({
|
1291
|
+
source: TrackSource.CAMERA,
|
1292
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1293
|
+
type: TrackType.AUDIO,
|
1294
|
+
name: 'video-dummy',
|
1295
|
+
}),
|
1296
|
+
new LocalVideoTrack(
|
1297
|
+
createDummyVideoStreamTrack(
|
1298
|
+
160 * participantOptions.aspectRatios[0] ?? 1,
|
1299
|
+
160,
|
1300
|
+
true,
|
1301
|
+
true,
|
1302
|
+
),
|
1303
|
+
),
|
1304
|
+
);
|
1305
|
+
// @ts-ignore
|
1306
|
+
this.localParticipant.addTrackPublication(camPub);
|
1307
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, camPub);
|
1308
|
+
}
|
1309
|
+
if (publishOptions.audio) {
|
1310
|
+
const audioPub = new LocalTrackPublication(
|
1311
|
+
Track.Kind.Audio,
|
1312
|
+
TrackInfo.fromPartial({
|
1313
|
+
source: TrackSource.MICROPHONE,
|
1314
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1315
|
+
type: TrackType.AUDIO,
|
1316
|
+
}),
|
1317
|
+
new LocalAudioTrack(getEmptyAudioStreamTrack()),
|
1318
|
+
);
|
1319
|
+
// @ts-ignore
|
1320
|
+
this.localParticipant.addTrackPublication(audioPub);
|
1321
|
+
this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, audioPub);
|
1322
|
+
}
|
1323
|
+
|
1324
|
+
for (let i = 0; i < participantOptions.count - 1; i += 1) {
|
1325
|
+
let info: ParticipantInfo = ParticipantInfo.fromPartial({
|
1326
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1327
|
+
identity: `simulated-${i}`,
|
1328
|
+
state: ParticipantInfo_State.ACTIVE,
|
1329
|
+
tracks: [],
|
1330
|
+
joinedAt: Date.now(),
|
1331
|
+
});
|
1332
|
+
const p = this.getOrCreateParticipant(info.identity, info);
|
1333
|
+
if (participantOptions.video) {
|
1334
|
+
const dummyVideo = createDummyVideoStreamTrack(
|
1335
|
+
160 * participantOptions.aspectRatios[i % participantOptions.aspectRatios.length] ?? 1,
|
1336
|
+
160,
|
1337
|
+
false,
|
1338
|
+
true,
|
1339
|
+
);
|
1340
|
+
const videoTrack = TrackInfo.fromPartial({
|
1341
|
+
source: TrackSource.CAMERA,
|
1342
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1343
|
+
type: TrackType.AUDIO,
|
1344
|
+
});
|
1345
|
+
p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
|
1346
|
+
info.tracks = [...info.tracks, videoTrack];
|
1347
|
+
}
|
1348
|
+
if (participantOptions.audio) {
|
1349
|
+
const dummyTrack = getEmptyAudioStreamTrack();
|
1350
|
+
const audioTrack = TrackInfo.fromPartial({
|
1351
|
+
source: TrackSource.MICROPHONE,
|
1352
|
+
sid: Math.floor(Math.random() * 10_000).toString(),
|
1353
|
+
type: TrackType.AUDIO,
|
1354
|
+
});
|
1355
|
+
p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
|
1356
|
+
info.tracks = [...info.tracks, audioTrack];
|
1357
|
+
}
|
1358
|
+
|
1359
|
+
p.updateInfo(info);
|
1360
|
+
}
|
1361
|
+
}
|
1362
|
+
|
1196
1363
|
// /** @internal */
|
1197
1364
|
emit<E extends keyof RoomEventCallbacks>(
|
1198
1365
|
event: E,
|
@@ -1273,4 +1440,5 @@ export type RoomEventCallbacks = {
|
|
1273
1440
|
) => void;
|
1274
1441
|
audioPlaybackChanged: (playing: boolean) => void;
|
1275
1442
|
signalConnected: () => void;
|
1443
|
+
recordingStatusChanged: (recording: boolean) => void;
|
1276
1444
|
};
|
package/src/room/events.ts
CHANGED
@@ -250,6 +250,11 @@ export enum RoomEvent {
|
|
250
250
|
* Signal connected, can publish tracks.
|
251
251
|
*/
|
252
252
|
SignalConnected = 'signalConnected',
|
253
|
+
|
254
|
+
/**
|
255
|
+
* Recording of a room has started/stopped.
|
256
|
+
*/
|
257
|
+
RecordingStatusChanged = 'recordingStatusChanged',
|
253
258
|
}
|
254
259
|
|
255
260
|
export enum ParticipantEvent {
|
@@ -422,6 +427,10 @@ export enum TrackEvent {
|
|
422
427
|
Message = 'message',
|
423
428
|
Muted = 'muted',
|
424
429
|
Unmuted = 'unmuted',
|
430
|
+
/**
|
431
|
+
* Only fires on LocalTracks
|
432
|
+
*/
|
433
|
+
Restarted = 'restarted',
|
425
434
|
Ended = 'ended',
|
426
435
|
Subscribed = 'subscribed',
|
427
436
|
Unsubscribed = 'unsubscribed',
|
@@ -34,7 +34,7 @@ import {
|
|
34
34
|
} from '../track/options';
|
35
35
|
import { Track } from '../track/Track';
|
36
36
|
import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
|
37
|
-
import { isFireFox, isWeb, supportsAV1 } from '../utils';
|
37
|
+
import { isFireFox, isSafari, isWeb, supportsAV1 } from '../utils';
|
38
38
|
import Participant from './Participant';
|
39
39
|
import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
|
40
40
|
import {
|
@@ -243,6 +243,7 @@ export default class LocalParticipant extends Participant {
|
|
243
243
|
}
|
244
244
|
const publishPromises: Array<Promise<LocalTrackPublication>> = [];
|
245
245
|
for (const localTrack of localTracks) {
|
246
|
+
log.info('publishing track', { localTrack });
|
246
247
|
publishPromises.push(this.publishTrack(localTrack, publishOptions));
|
247
248
|
}
|
248
249
|
const publishedTracks = await Promise.all(publishPromises);
|
@@ -374,14 +375,20 @@ export default class LocalParticipant extends Participant {
|
|
374
375
|
|
375
376
|
let videoConstraints: MediaTrackConstraints | boolean = true;
|
376
377
|
if (options.resolution) {
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
378
|
+
if (isSafari()) {
|
379
|
+
videoConstraints = {
|
380
|
+
width: { max: options.resolution.width },
|
381
|
+
height: { max: options.resolution.height },
|
382
|
+
frameRate: options.resolution.frameRate,
|
383
|
+
};
|
384
|
+
} else {
|
385
|
+
videoConstraints = {
|
386
|
+
width: { ideal: options.resolution.width },
|
387
|
+
height: { ideal: options.resolution.height },
|
388
|
+
frameRate: options.resolution.frameRate,
|
389
|
+
};
|
390
|
+
}
|
382
391
|
}
|
383
|
-
// typescript definition is missing getDisplayMedia: https://github.com/microsoft/TypeScript/issues/33232
|
384
|
-
// @ts-ignore
|
385
392
|
const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({
|
386
393
|
audio: options.audio ?? false,
|
387
394
|
video: videoConstraints,
|
@@ -957,7 +964,7 @@ export default class LocalParticipant extends Participant {
|
|
957
964
|
});
|
958
965
|
this.unpublishTrack(track);
|
959
966
|
} else if (track.isUserProvided) {
|
960
|
-
await track.
|
967
|
+
await track.mute();
|
961
968
|
} else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
|
962
969
|
try {
|
963
970
|
if (isWeb()) {
|
@@ -986,8 +993,8 @@ export default class LocalParticipant extends Participant {
|
|
986
993
|
log.debug('track ended, attempting to use a different device');
|
987
994
|
await track.restartTrack();
|
988
995
|
} catch (e) {
|
989
|
-
log.warn(`could not restart track,
|
990
|
-
await track.
|
996
|
+
log.warn(`could not restart track, muting instead`);
|
997
|
+
await track.mute();
|
991
998
|
}
|
992
999
|
}
|
993
1000
|
};
|
@@ -305,7 +305,7 @@ function encodingsFromPresets(
|
|
305
305
|
const rid = videoRids[idx];
|
306
306
|
encodings.push({
|
307
307
|
rid,
|
308
|
-
scaleResolutionDownBy: size / Math.min(preset.width, preset.height),
|
308
|
+
scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
|
309
309
|
maxBitrate: preset.encoding.maxBitrate,
|
310
310
|
/* @ts-ignore */
|
311
311
|
maxFramerate: preset.encoding.maxFramerate,
|
@@ -121,6 +121,9 @@ export default abstract class LocalTrack extends Track {
|
|
121
121
|
}
|
122
122
|
this._mediaStreamTrack = track;
|
123
123
|
|
124
|
+
// sync muted state with the enabled state of the newly provided track
|
125
|
+
this._mediaStreamTrack.enabled = !this.isMuted;
|
126
|
+
|
124
127
|
await this.resumeUpstream();
|
125
128
|
|
126
129
|
this.attachedElements.forEach((el) => {
|
@@ -180,6 +183,7 @@ export default abstract class LocalTrack extends Track {
|
|
180
183
|
|
181
184
|
this.mediaStream = mediaStream;
|
182
185
|
this.constraints = constraints;
|
186
|
+
this.emit(TrackEvent.Restarted, this);
|
183
187
|
return this;
|
184
188
|
}
|
185
189
|
|