livekit-client 2.0.3 → 2.0.5
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +11 -8
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +166 -77
- 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 +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +2 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -2
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/DeviceManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -2
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -2
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +4 -0
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +10 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/types.d.ts +4 -0
- package/dist/src/room/track/types.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +1 -1
- package/dist/ts4.2/src/e2ee/types.d.ts +2 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/ts4.2/src/index.d.ts +2 -2
- package/dist/ts4.2/src/logger.d.ts +4 -2
- package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -2
- package/dist/ts4.2/src/room/events.d.ts +2 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -2
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +4 -0
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -1
- package/dist/ts4.2/src/room/track/options.d.ts +10 -0
- package/dist/ts4.2/src/room/track/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +10 -5
- package/src/e2ee/E2eeManager.ts +2 -1
- package/src/e2ee/types.ts +2 -0
- package/src/e2ee/worker/FrameCryptor.ts +12 -6
- package/src/e2ee/worker/e2ee.worker.ts +1 -0
- package/src/index.ts +2 -1
- package/src/logger.ts +23 -18
- package/src/room/DeviceManager.ts +10 -1
- package/src/room/RTCEngine.ts +26 -8
- package/src/room/Room.ts +26 -2
- package/src/room/events.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +4 -4
- package/src/room/participant/Participant.ts +0 -2
- package/src/room/participant/RemoteParticipant.ts +8 -0
- package/src/room/track/LocalAudioTrack.ts +10 -0
- package/src/room/track/LocalTrack.ts +20 -3
- package/src/room/track/LocalVideoTrack.ts +15 -1
- package/src/room/track/options.ts +41 -8
- package/src/room/track/types.ts +5 -0
- package/src/room/track/utils.ts +18 -11
- package/src/room/utils.ts +3 -0
@@ -44,8 +44,7 @@ export default class Participant extends Participant_base {
|
|
44
44
|
protected log: StructuredLogger;
|
45
45
|
protected loggerOptions?: LoggerOptions;
|
46
46
|
protected get logContext(): {
|
47
|
-
|
48
|
-
participantId: string;
|
47
|
+
[x: string]: unknown;
|
49
48
|
};
|
50
49
|
get isEncrypted(): boolean;
|
51
50
|
get isAgent(): boolean;
|
@@ -16,6 +16,10 @@ export default class RemoteParticipant extends Participant {
|
|
16
16
|
private audioOutput?;
|
17
17
|
/** @internal */
|
18
18
|
static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant;
|
19
|
+
protected get logContext(): {
|
20
|
+
rpID: string;
|
21
|
+
remoteParticipant: string;
|
22
|
+
};
|
19
23
|
/** @internal */
|
20
24
|
constructor(signalClient: SignalClient, sid: string, identity?: string, name?: string, metadata?: string, loggerOptions?: LoggerOptions);
|
21
25
|
protected addTrackPublication(publication: RemoteTrackPublication): void;
|
@@ -3,6 +3,7 @@ import { Mutex } from '../utils';
|
|
3
3
|
import { Track } from './Track';
|
4
4
|
import type { VideoCodec } from './options';
|
5
5
|
import type { TrackProcessor } from './processor/types';
|
6
|
+
import type { ReplaceTrackOptions } from './types';
|
6
7
|
export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Kind> extends Track<TrackKind> {
|
7
8
|
/** @internal */
|
8
9
|
sender?: RTCRtpSender;
|
@@ -41,7 +42,8 @@ export default abstract class LocalTrack<TrackKind extends Track.Kind = Track.Ki
|
|
41
42
|
getDeviceId(): Promise<string | undefined>;
|
42
43
|
mute(): Promise<this>;
|
43
44
|
unmute(): Promise<this>;
|
44
|
-
replaceTrack(track: MediaStreamTrack,
|
45
|
+
replaceTrack(track: MediaStreamTrack, options?: ReplaceTrackOptions): Promise<typeof this>;
|
46
|
+
replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<typeof this>;
|
45
47
|
protected restart(constraints?: MediaTrackConstraints): Promise<this>;
|
46
48
|
protected setTrackMuted(muted: boolean): void;
|
47
49
|
protected get needsReAcquisition(): boolean;
|
@@ -220,10 +220,20 @@ export interface VideoEncoding {
|
|
220
220
|
maxFramerate?: number;
|
221
221
|
priority?: RTCPriorityType;
|
222
222
|
}
|
223
|
+
export interface VideoPresetOptions {
|
224
|
+
width: number;
|
225
|
+
height: number;
|
226
|
+
aspectRatio?: number;
|
227
|
+
maxBitrate: number;
|
228
|
+
maxFramerate?: number;
|
229
|
+
priority?: RTCPriorityType;
|
230
|
+
}
|
223
231
|
export declare class VideoPreset {
|
224
232
|
encoding: VideoEncoding;
|
225
233
|
width: number;
|
226
234
|
height: number;
|
235
|
+
aspectRatio?: number;
|
236
|
+
constructor(videoPresetOptions: VideoPresetOptions);
|
227
237
|
constructor(width: number, height: number, maxBitrate: number, maxFramerate?: number, priority?: RTCPriorityType);
|
228
238
|
get resolution(): VideoResolution;
|
229
239
|
}
|
package/package.json
CHANGED
package/src/api/SignalClient.ts
CHANGED
@@ -216,7 +216,7 @@ export class SignalClient {
|
|
216
216
|
token: string,
|
217
217
|
sid?: string,
|
218
218
|
reason?: ReconnectReason,
|
219
|
-
): Promise<ReconnectResponse |
|
219
|
+
): Promise<ReconnectResponse | undefined> {
|
220
220
|
if (!this.options) {
|
221
221
|
this.log.warn(
|
222
222
|
'attempted to reconnect without signal options being set, ignoring',
|
@@ -242,7 +242,7 @@ export class SignalClient {
|
|
242
242
|
token: string,
|
243
243
|
opts: ConnectOpts,
|
244
244
|
abortSignal?: AbortSignal,
|
245
|
-
): Promise<JoinResponse | ReconnectResponse |
|
245
|
+
): Promise<JoinResponse | ReconnectResponse | undefined> {
|
246
246
|
this.connectOptions = opts;
|
247
247
|
url = toWebsocketUrl(url);
|
248
248
|
// strip trailing slash
|
@@ -252,7 +252,7 @@ export class SignalClient {
|
|
252
252
|
const clientInfo = getClientInfo();
|
253
253
|
const params = createConnectionParams(token, clientInfo, opts);
|
254
254
|
|
255
|
-
return new Promise<JoinResponse | ReconnectResponse |
|
255
|
+
return new Promise<JoinResponse | ReconnectResponse | undefined>(async (resolve, reject) => {
|
256
256
|
const unlock = await this.connectionLock.lock();
|
257
257
|
try {
|
258
258
|
const abortHandler = async () => {
|
@@ -283,6 +283,7 @@ export class SignalClient {
|
|
283
283
|
|
284
284
|
this.ws.onerror = async (ev: Event) => {
|
285
285
|
if (this.state !== SignalConnectionState.CONNECTED) {
|
286
|
+
this.state = SignalConnectionState.DISCONNECTED;
|
286
287
|
clearTimeout(wsTimeout);
|
287
288
|
try {
|
288
289
|
const resp = await fetch(`http${url.substring(2)}/validate${params}`);
|
@@ -355,9 +356,13 @@ export class SignalClient {
|
|
355
356
|
abortSignal?.removeEventListener('abort', abortHandler);
|
356
357
|
this.startPingInterval();
|
357
358
|
if (resp.message?.case === 'reconnect') {
|
358
|
-
resolve(resp.message
|
359
|
+
resolve(resp.message.value);
|
359
360
|
} else {
|
360
|
-
|
361
|
+
this.log.debug(
|
362
|
+
'declaring signal reconnected without reconnect response received',
|
363
|
+
this.logContext,
|
364
|
+
);
|
365
|
+
resolve(undefined);
|
361
366
|
shouldProcessMessage = true;
|
362
367
|
}
|
363
368
|
} else if (this.isEstablishingConnection && resp.message.case === 'leave') {
|
package/src/e2ee/E2eeManager.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { EventEmitter } from 'events';
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
3
|
-
import log from '../logger';
|
3
|
+
import log, { LogLevel, workerLogger } from '../logger';
|
4
4
|
import { Encryption_Type, TrackInfo } from '../proto/livekit_models_pb';
|
5
5
|
import type RTCEngine from '../room/RTCEngine';
|
6
6
|
import type Room from '../room/Room';
|
@@ -68,6 +68,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
|
|
68
68
|
kind: 'init',
|
69
69
|
data: {
|
70
70
|
keyProviderOptions: this.keyProvider.getOptions(),
|
71
|
+
loglevel: workerLogger.getLevel() as LogLevel,
|
71
72
|
},
|
72
73
|
};
|
73
74
|
if (this.worker) {
|
package/src/e2ee/types.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import type { LogLevel } from '../logger';
|
1
2
|
import type { VideoCodec } from '../room/track/options';
|
2
3
|
import type { BaseKeyProvider } from './KeyProvider';
|
3
4
|
|
@@ -10,6 +11,7 @@ export interface InitMessage extends BaseMessage {
|
|
10
11
|
kind: 'init';
|
11
12
|
data: {
|
12
13
|
keyProviderOptions: KeyProviderOptions;
|
14
|
+
loglevel: LogLevel;
|
13
15
|
};
|
14
16
|
}
|
15
17
|
|
@@ -67,6 +67,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
67
67
|
|
68
68
|
private sifGuard: SifGuard;
|
69
69
|
|
70
|
+
private detectedCodec?: VideoCodec;
|
71
|
+
|
70
72
|
constructor(opts: {
|
71
73
|
keys: ParticipantKeyHandler;
|
72
74
|
participantIdentity: string;
|
@@ -85,8 +87,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
85
87
|
|
86
88
|
private get logContext() {
|
87
89
|
return {
|
88
|
-
|
89
|
-
|
90
|
+
participant: this.participantIdentity,
|
91
|
+
mediaTrackId: this.trackId,
|
90
92
|
fallbackCodec: this.videoCodec,
|
91
93
|
};
|
92
94
|
}
|
@@ -229,7 +231,6 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
229
231
|
encodedFrame.timestamp,
|
230
232
|
);
|
231
233
|
let frameInfo = this.getUnencryptedBytes(encodedFrame);
|
232
|
-
workerLogger.debug('frameInfo for encoded frame', { ...frameInfo, ...this.logContext });
|
233
234
|
|
234
235
|
// Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
235
236
|
const frameHeader = new Uint8Array(encodedFrame.data, 0, frameInfo.unencryptedBytes);
|
@@ -334,7 +335,6 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
334
335
|
const decodedFrame = await this.decryptFrame(encodedFrame, keyIndex);
|
335
336
|
this.keys.decryptionSuccess();
|
336
337
|
if (decodedFrame) {
|
337
|
-
workerLogger.debug('enqueue decrypted frame', this.logContext);
|
338
338
|
return controller.enqueue(decodedFrame);
|
339
339
|
}
|
340
340
|
} catch (error) {
|
@@ -375,7 +375,6 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
375
375
|
throw new TypeError(`no encryption key found for decryption of ${this.participantIdentity}`);
|
376
376
|
}
|
377
377
|
let frameInfo = this.getUnencryptedBytes(encodedFrame);
|
378
|
-
workerLogger.debug('frameInfo for decoded frame', { ...frameInfo, ...this.logContext });
|
379
378
|
|
380
379
|
// Construct frame trailer. Similar to the frame header described in
|
381
380
|
// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
@@ -534,6 +533,14 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
534
533
|
var frameInfo = { unencryptedBytes: 0, isH264: false };
|
535
534
|
if (isVideoFrame(frame)) {
|
536
535
|
let detectedCodec = this.getVideoCodec(frame) ?? this.videoCodec;
|
536
|
+
if (detectedCodec !== this.detectedCodec) {
|
537
|
+
workerLogger.debug('detected different codec', {
|
538
|
+
detectedCodec,
|
539
|
+
oldCodec: this.detectedCodec,
|
540
|
+
...this.logContext,
|
541
|
+
});
|
542
|
+
this.detectedCodec = detectedCodec;
|
543
|
+
}
|
537
544
|
|
538
545
|
if (detectedCodec === 'av1' || detectedCodec === 'vp9') {
|
539
546
|
throw new Error(`${detectedCodec} is not yet supported for end to end encryption`);
|
@@ -591,7 +598,6 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
591
598
|
// @ts-expect-error payloadType is not yet part of the typescript definition and currently not supported in Safari
|
592
599
|
const payloadType = frame.getMetadata().payloadType;
|
593
600
|
const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
|
594
|
-
workerLogger.debug('reading codec from frame', { codec, ...this.logContext });
|
595
601
|
return codec;
|
596
602
|
}
|
597
603
|
}
|
package/src/index.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { LogLevel, getLogger, setLogExtension, setLogLevel } from './logger';
|
1
|
+
import { LogLevel, LoggerNames, getLogger, setLogExtension, setLogLevel } from './logger';
|
2
2
|
import { DataPacket_Kind, DisconnectReason } from './proto/livekit_models_pb';
|
3
3
|
import DefaultReconnectPolicy from './room/DefaultReconnectPolicy';
|
4
4
|
import Room, { ConnectionState } from './room/Room';
|
@@ -55,6 +55,7 @@ export {
|
|
55
55
|
supportsVP9,
|
56
56
|
createAudioAnalyser,
|
57
57
|
LogLevel,
|
58
|
+
LoggerNames,
|
58
59
|
getLogger,
|
59
60
|
Room,
|
60
61
|
ConnectionState,
|
package/src/logger.ts
CHANGED
@@ -24,16 +24,19 @@ export enum LoggerNames {
|
|
24
24
|
|
25
25
|
type LogLevelString = keyof typeof LogLevel;
|
26
26
|
|
27
|
-
export type StructuredLogger = {
|
27
|
+
export type StructuredLogger = log.Logger & {
|
28
28
|
trace: (msg: string, context?: object) => void;
|
29
29
|
debug: (msg: string, context?: object) => void;
|
30
30
|
info: (msg: string, context?: object) => void;
|
31
31
|
warn: (msg: string, context?: object) => void;
|
32
32
|
error: (msg: string, context?: object) => void;
|
33
33
|
setDefaultLevel: (level: log.LogLevelDesc) => void;
|
34
|
+
setLevel: (level: log.LogLevelDesc) => void;
|
35
|
+
getLevel: () => number;
|
34
36
|
};
|
35
37
|
|
36
38
|
let livekitLogger = log.getLogger('livekit');
|
39
|
+
const livekitLoggers = Object.values(LoggerNames).map((name) => log.getLogger(name));
|
37
40
|
|
38
41
|
livekitLogger.setDefaultLevel(LogLevel.info);
|
39
42
|
|
@@ -52,9 +55,7 @@ export function setLogLevel(level: LogLevel | LogLevelString, loggerName?: Logge
|
|
52
55
|
if (loggerName) {
|
53
56
|
log.getLogger(loggerName).setLevel(level);
|
54
57
|
}
|
55
|
-
for (const logger of
|
56
|
-
.filter(([logrName]) => logrName.startsWith('livekit'))
|
57
|
-
.map(([, logr]) => logr)) {
|
58
|
+
for (const logger of livekitLoggers) {
|
58
59
|
logger.setLevel(level);
|
59
60
|
}
|
60
61
|
}
|
@@ -65,24 +66,28 @@ export type LogExtension = (level: LogLevel, msg: string, context?: object) => v
|
|
65
66
|
* use this to hook into the logging function to allow sending internal livekit logs to third party services
|
66
67
|
* if set, the browser logs will lose their stacktrace information (see https://github.com/pimterry/loglevel#writing-plugins)
|
67
68
|
*/
|
68
|
-
export function setLogExtension(extension: LogExtension, logger
|
69
|
-
const
|
69
|
+
export function setLogExtension(extension: LogExtension, logger?: StructuredLogger) {
|
70
|
+
const loggers = logger ? [logger] : livekitLoggers;
|
70
71
|
|
71
|
-
|
72
|
-
const
|
72
|
+
loggers.forEach((logR) => {
|
73
|
+
const originalFactory = logR.methodFactory;
|
73
74
|
|
74
|
-
|
75
|
-
|
75
|
+
logR.methodFactory = (methodName, configLevel, loggerName) => {
|
76
|
+
const rawMethod = originalFactory(methodName, configLevel, loggerName);
|
76
77
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
const logLevel = LogLevel[methodName as LogLevelString];
|
79
|
+
const needLog = logLevel >= configLevel && logLevel < LogLevel.silent;
|
80
|
+
|
81
|
+
return (msg, context?: [msg: string, context: object]) => {
|
82
|
+
if (context) rawMethod(msg, context);
|
83
|
+
else rawMethod(msg);
|
84
|
+
if (needLog) {
|
85
|
+
extension(logLevel, msg, context);
|
86
|
+
}
|
87
|
+
};
|
83
88
|
};
|
84
|
-
|
85
|
-
|
89
|
+
logR.setLevel(logR.getLevel());
|
90
|
+
});
|
86
91
|
}
|
87
92
|
|
88
93
|
export const workerLogger = log.getLogger('lk-e2ee') as StructuredLogger;
|
@@ -80,7 +80,16 @@ export default class DeviceManager {
|
|
80
80
|
// device has been chosen
|
81
81
|
const devices = await this.getDevices(kind);
|
82
82
|
|
83
|
-
|
83
|
+
// `default` devices will have the same groupId as the entry with the actual device id so we store the counts for each group id
|
84
|
+
const groupIdCounts = new Map(devices.map((d) => [d.groupId, 0]));
|
85
|
+
|
86
|
+
devices.forEach((d) => groupIdCounts.set(d.groupId, (groupIdCounts.get(d.groupId) ?? 0) + 1));
|
87
|
+
|
88
|
+
const device = devices.find(
|
89
|
+
(d) =>
|
90
|
+
(groupId === d.groupId || (groupIdCounts.get(d.groupId) ?? 0) > 1) &&
|
91
|
+
d.deviceId !== defaultId,
|
92
|
+
);
|
84
93
|
|
85
94
|
return device?.deviceId;
|
86
95
|
}
|
package/src/room/RTCEngine.ts
CHANGED
@@ -205,8 +205,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
205
205
|
get logContext() {
|
206
206
|
return {
|
207
207
|
room: this.latestJoinResponse?.room?.name,
|
208
|
-
|
209
|
-
|
208
|
+
roomID: this.latestJoinResponse?.room?.sid,
|
209
|
+
participant: this.latestJoinResponse?.participant?.identity,
|
210
|
+
pID: this.latestJoinResponse?.participant?.sid,
|
210
211
|
};
|
211
212
|
}
|
212
213
|
|
@@ -418,6 +419,19 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
418
419
|
);
|
419
420
|
}
|
420
421
|
}
|
422
|
+
|
423
|
+
// detect cases where both signal client and peer connection are severed and assume that user has lost network connection
|
424
|
+
const isSignalSevered =
|
425
|
+
this.client.isDisconnected ||
|
426
|
+
this.client.currentState === SignalConnectionState.RECONNECTING;
|
427
|
+
const isPCSevered = [
|
428
|
+
PCTransportState.FAILED,
|
429
|
+
PCTransportState.CLOSING,
|
430
|
+
PCTransportState.CLOSED,
|
431
|
+
].includes(connectionState);
|
432
|
+
if (isSignalSevered && isPCSevered && !this._isClosed) {
|
433
|
+
this.emit(EngineEvent.Offline);
|
434
|
+
}
|
421
435
|
};
|
422
436
|
this.pcManager.onTrack = (ev: RTCTrackEvent) => {
|
423
437
|
this.emit(EngineEvent.MediaTrackAdded, ev.track, ev.streams[0], ev.receiver);
|
@@ -992,14 +1006,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
992
1006
|
|
993
1007
|
this.log.info(`resuming signal connection, attempt ${this.reconnectAttempts}`, this.logContext);
|
994
1008
|
this.emit(EngineEvent.Resuming);
|
995
|
-
|
1009
|
+
let res: ReconnectResponse | undefined;
|
996
1010
|
try {
|
997
1011
|
this.setupSignalClientCallbacks();
|
998
|
-
|
999
|
-
if (res) {
|
1000
|
-
const rtcConfig = this.makeRTCConfiguration(res);
|
1001
|
-
this.pcManager.updateConfiguration(rtcConfig);
|
1002
|
-
}
|
1012
|
+
res = await this.client.reconnect(this.url, this.token, this.participantSid, reason);
|
1003
1013
|
} catch (error) {
|
1004
1014
|
let message = '';
|
1005
1015
|
if (error instanceof Error) {
|
@@ -1016,6 +1026,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1016
1026
|
}
|
1017
1027
|
this.emit(EngineEvent.SignalResumed);
|
1018
1028
|
|
1029
|
+
if (res) {
|
1030
|
+
const rtcConfig = this.makeRTCConfiguration(res);
|
1031
|
+
this.pcManager.updateConfiguration(rtcConfig);
|
1032
|
+
} else {
|
1033
|
+
this.log.warn('Did not receive reconnect response', this.logContext);
|
1034
|
+
}
|
1035
|
+
|
1019
1036
|
if (this.shouldFailNext) {
|
1020
1037
|
this.shouldFailNext = false;
|
1021
1038
|
throw new Error('simulated failure');
|
@@ -1400,4 +1417,5 @@ export type EngineEventCallbacks = {
|
|
1400
1417
|
subscribedQualityUpdate: (update: SubscribedQualityUpdate) => void;
|
1401
1418
|
localTrackUnpublished: (unpublishedResponse: TrackUnpublishedResponse) => void;
|
1402
1419
|
remoteMute: (trackSid: string, muted: boolean) => void;
|
1420
|
+
offline: () => void;
|
1403
1421
|
};
|
package/src/room/Room.ts
CHANGED
@@ -69,7 +69,9 @@ import {
|
|
69
69
|
Mutex,
|
70
70
|
createDummyVideoStreamTrack,
|
71
71
|
getEmptyAudioStreamTrack,
|
72
|
+
isBrowserSupported,
|
72
73
|
isCloud,
|
74
|
+
isReactNative,
|
73
75
|
isWeb,
|
74
76
|
supportsSetSinkId,
|
75
77
|
toHttpUrl,
|
@@ -247,8 +249,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
247
249
|
private get logContext() {
|
248
250
|
return {
|
249
251
|
room: this.name,
|
250
|
-
|
251
|
-
|
252
|
+
roomID: this.roomInfo?.sid,
|
253
|
+
participant: this.localParticipant.identity,
|
254
|
+
pID: this.localParticipant.sid,
|
252
255
|
};
|
253
256
|
}
|
254
257
|
|
@@ -349,6 +352,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
349
352
|
})
|
350
353
|
.on(EngineEvent.Restarting, this.handleRestarting)
|
351
354
|
.on(EngineEvent.SignalRestarted, this.handleSignalRestarted)
|
355
|
+
.on(EngineEvent.Offline, () => {
|
356
|
+
if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
|
357
|
+
this.emit(RoomEvent.Reconnecting);
|
358
|
+
}
|
359
|
+
})
|
352
360
|
.on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
|
353
361
|
this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
|
354
362
|
});
|
@@ -410,6 +418,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
410
418
|
}
|
411
419
|
|
412
420
|
connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
|
421
|
+
if (!isBrowserSupported()) {
|
422
|
+
if (isReactNative()) {
|
423
|
+
throw Error("WebRTC isn't detected, have you called registerGlobals?");
|
424
|
+
} else {
|
425
|
+
throw Error(
|
426
|
+
"LiveKit doesn't seem to be supported on this browser. Try to update your browser and make sure no browser extensions are disabling webRTC.",
|
427
|
+
);
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
413
431
|
// In case a disconnect called happened right before the connect call, make sure the disconnect is completed first by awaiting its lock
|
414
432
|
const unlockDisconnect = await this.disconnectLock.lock();
|
415
433
|
|
@@ -865,6 +883,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
865
883
|
}
|
866
884
|
|
867
885
|
private onPageLeave = async () => {
|
886
|
+
this.log.info('Page leave detected, disconnecting', this.logContext);
|
868
887
|
await this.disconnect();
|
869
888
|
};
|
870
889
|
|
@@ -1042,6 +1061,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1042
1061
|
) {
|
1043
1062
|
throw new Error('cannot switch audio output, setSinkId not supported');
|
1044
1063
|
}
|
1064
|
+
if (this.options.webAudioMix) {
|
1065
|
+
// setting `default` for web audio output doesn't work, so we need to normalize the id before
|
1066
|
+
deviceId =
|
1067
|
+
(await DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) ?? '';
|
1068
|
+
}
|
1045
1069
|
this.options.audioOutput ??= {};
|
1046
1070
|
const prevDeviceId = this.options.audioOutput.deviceId;
|
1047
1071
|
this.options.audioOutput.deviceId = deviceId;
|
package/src/room/events.ts
CHANGED
@@ -513,6 +513,10 @@ export default class LocalParticipant extends Participant {
|
|
513
513
|
track: LocalTrack | MediaStreamTrack,
|
514
514
|
options?: TrackPublishOptions,
|
515
515
|
): Promise<LocalTrackPublication> {
|
516
|
+
if (track instanceof LocalAudioTrack) {
|
517
|
+
track.setAudioContext(this.audioContext);
|
518
|
+
}
|
519
|
+
|
516
520
|
await this.reconnectFuture?.promise;
|
517
521
|
if (track instanceof LocalTrack && this.pendingPublishPromises.has(track)) {
|
518
522
|
await this.pendingPublishPromises.get(track);
|
@@ -566,10 +570,6 @@ export default class LocalParticipant extends Participant {
|
|
566
570
|
});
|
567
571
|
}
|
568
572
|
|
569
|
-
if (track instanceof LocalAudioTrack) {
|
570
|
-
track.setAudioContext(this.audioContext);
|
571
|
-
}
|
572
|
-
|
573
573
|
// is it already published? if so skip
|
574
574
|
let existingPublication: LocalTrackPublication | undefined;
|
575
575
|
this.trackPublications.forEach((publication) => {
|
@@ -33,6 +33,14 @@ export default class RemoteParticipant extends Participant {
|
|
33
33
|
return new RemoteParticipant(signalClient, pi.sid, pi.identity, pi.name, pi.metadata);
|
34
34
|
}
|
35
35
|
|
36
|
+
protected get logContext() {
|
37
|
+
return {
|
38
|
+
...super.logContext,
|
39
|
+
rpID: this.sid,
|
40
|
+
remoteParticipant: this.identity,
|
41
|
+
};
|
42
|
+
}
|
43
|
+
|
36
44
|
/** @internal */
|
37
45
|
constructor(
|
38
46
|
signalClient: SignalClient,
|
@@ -51,6 +51,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
51
51
|
async mute(): Promise<typeof this> {
|
52
52
|
const unlock = await this.muteLock.lock();
|
53
53
|
try {
|
54
|
+
if (this.isMuted) {
|
55
|
+
this.log.debug('Track already muted', this.logContext);
|
56
|
+
return this;
|
57
|
+
}
|
58
|
+
|
54
59
|
// disabled special handling as it will cause BT headsets to switch communication modes
|
55
60
|
if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
|
56
61
|
this.log.debug('stopping mic track', this.logContext);
|
@@ -67,6 +72,11 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
67
72
|
async unmute(): Promise<typeof this> {
|
68
73
|
const unlock = await this.muteLock.lock();
|
69
74
|
try {
|
75
|
+
if (!this.isMuted) {
|
76
|
+
this.log.debug('Track already unmuted', this.logContext);
|
77
|
+
return this;
|
78
|
+
}
|
79
|
+
|
70
80
|
const deviceHasChanged =
|
71
81
|
this._constraints.deviceId &&
|
72
82
|
this._mediaStreamTrack.getSettings().deviceId !==
|
@@ -8,6 +8,7 @@ import { Mutex, compareVersions, isMobile, sleep } from '../utils';
|
|
8
8
|
import { Track, attachToElement, detachTrack } from './Track';
|
9
9
|
import type { VideoCodec } from './options';
|
10
10
|
import type { TrackProcessor } from './processor/types';
|
11
|
+
import type { ReplaceTrackOptions } from './types';
|
11
12
|
|
12
13
|
const defaultDimensionsTimeout = 1000;
|
13
14
|
|
@@ -224,18 +225,34 @@ export default abstract class LocalTrack<
|
|
224
225
|
return this;
|
225
226
|
}
|
226
227
|
|
227
|
-
async replaceTrack(track: MediaStreamTrack,
|
228
|
+
async replaceTrack(track: MediaStreamTrack, options?: ReplaceTrackOptions): Promise<typeof this>;
|
229
|
+
async replaceTrack(track: MediaStreamTrack, userProvidedTrack?: boolean): Promise<typeof this>;
|
230
|
+
async replaceTrack(
|
231
|
+
track: MediaStreamTrack,
|
232
|
+
userProvidedOrOptions: boolean | ReplaceTrackOptions | undefined,
|
233
|
+
) {
|
228
234
|
if (!this.sender) {
|
229
235
|
throw new TrackInvalidError('unable to replace an unpublished track');
|
230
236
|
}
|
231
237
|
|
238
|
+
let userProvidedTrack: boolean | undefined;
|
239
|
+
let stopProcessor: boolean | undefined;
|
240
|
+
|
241
|
+
if (typeof userProvidedOrOptions === 'boolean') {
|
242
|
+
userProvidedTrack = userProvidedOrOptions;
|
243
|
+
} else if (userProvidedOrOptions !== undefined) {
|
244
|
+
userProvidedTrack = userProvidedOrOptions.userProvidedTrack;
|
245
|
+
stopProcessor = userProvidedOrOptions.stopProcessor;
|
246
|
+
}
|
247
|
+
|
248
|
+
this.providedByUser = userProvidedTrack ?? true;
|
249
|
+
|
232
250
|
this.log.debug('replace MediaStreamTrack', this.logContext);
|
233
251
|
await this.setMediaStreamTrack(track);
|
234
252
|
// this must be synced *after* setting mediaStreamTrack above, since it relies
|
235
253
|
// on the previous state in order to cleanup
|
236
|
-
this.providedByUser = userProvidedTrack;
|
237
254
|
|
238
|
-
if (this.processor) {
|
255
|
+
if (stopProcessor && this.processor) {
|
239
256
|
await this.stopProcessor();
|
240
257
|
}
|
241
258
|
return this;
|
@@ -118,6 +118,11 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
118
118
|
async mute(): Promise<typeof this> {
|
119
119
|
const unlock = await this.muteLock.lock();
|
120
120
|
try {
|
121
|
+
if (this.isMuted) {
|
122
|
+
this.log.debug('Track already muted', this.logContext);
|
123
|
+
return this;
|
124
|
+
}
|
125
|
+
|
121
126
|
if (this.source === Track.Source.Camera && !this.isUserProvided) {
|
122
127
|
this.log.debug('stopping camera track', this.logContext);
|
123
128
|
// also stop the track, so that camera indicator is turned off
|
@@ -133,6 +138,11 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
|
|
133
138
|
async unmute(): Promise<typeof this> {
|
134
139
|
const unlock = await this.muteLock.lock();
|
135
140
|
try {
|
141
|
+
if (!this.isMuted) {
|
142
|
+
this.log.debug('Track already unmuted', this.logContext);
|
143
|
+
return this;
|
144
|
+
}
|
145
|
+
|
136
146
|
if (this.source === Track.Source.Camera && !this.isUserProvided) {
|
137
147
|
this.log.debug('reacquiring camera track', this.logContext);
|
138
148
|
await this.restartTrack();
|
@@ -414,7 +424,11 @@ async function setPublishingLayersForSender(
|
|
414
424
|
}
|
415
425
|
|
416
426
|
if (encodings.length !== senderEncodings.length) {
|
417
|
-
log.warn('cannot set publishing layers, encodings mismatch'
|
427
|
+
log.warn('cannot set publishing layers, encodings mismatch', {
|
428
|
+
...logContext,
|
429
|
+
encodings,
|
430
|
+
senderEncodings,
|
431
|
+
});
|
418
432
|
return;
|
419
433
|
}
|
420
434
|
|