livekit-client 2.0.3 → 2.0.5
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.
- 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
|
|