livekit-client 1.12.3 → 1.13.0
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 +83 -9
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +357 -97
- 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 +2 -5
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/webrtc.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +5 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/KeyProvider.d.ts +4 -2
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts +2 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +7 -1
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +4 -2
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/e2ee/worker/SifGuard.d.ts +11 -0
- package/dist/src/e2ee/worker/SifGuard.d.ts.map +1 -0
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
- package/dist/src/room/DeviceManager.d.ts +1 -0
- package/dist/src/room/DeviceManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +5 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +0 -5
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +2 -2
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +9 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +6 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +13 -2
- package/dist/src/room/track/processor/types.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -1
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +2 -5
- package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +5 -0
- package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +4 -2
- package/dist/ts4.2/src/e2ee/constants.d.ts +2 -0
- package/dist/ts4.2/src/e2ee/types.d.ts +7 -1
- package/dist/ts4.2/src/e2ee/utils.d.ts +1 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +4 -2
- package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +11 -0
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/room/DeviceManager.d.ts +1 -0
- package/dist/ts4.2/src/room/Room.d.ts +1 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +5 -0
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +0 -5
- package/dist/ts4.2/src/room/timers.d.ts +2 -2
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +9 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +6 -0
- package/dist/ts4.2/src/room/track/processor/types.d.ts +13 -2
- package/dist/ts4.2/src/room/types.d.ts +1 -1
- package/package.json +15 -16
- package/src/api/SignalClient.ts +13 -9
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/webrtc.ts +9 -7
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/e2ee/E2eeManager.ts +27 -2
- package/src/e2ee/KeyProvider.ts +9 -4
- package/src/e2ee/constants.ts +3 -0
- package/src/e2ee/types.ts +9 -1
- package/src/e2ee/utils.ts +9 -0
- package/src/e2ee/worker/FrameCryptor.ts +46 -17
- package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -0
- package/src/e2ee/worker/SifGuard.ts +47 -0
- package/src/e2ee/worker/e2ee.worker.ts +14 -0
- package/src/options.ts +6 -0
- package/src/proto/livekit_models_pb.ts +14 -0
- package/src/proto/livekit_rtc_pb.ts +14 -0
- package/src/room/DeviceManager.ts +7 -2
- package/src/room/RTCEngine.ts +3 -1
- package/src/room/Room.ts +27 -7
- package/src/room/defaults.ts +1 -0
- package/src/room/participant/LocalParticipant.ts +14 -2
- package/src/room/participant/Participant.ts +16 -0
- package/src/room/participant/RemoteParticipant.ts +0 -12
- package/src/room/track/LocalAudioTrack.ts +45 -0
- package/src/room/track/LocalTrack.ts +4 -4
- package/src/room/track/LocalVideoTrack.ts +39 -0
- package/src/room/track/RemoteAudioTrack.ts +9 -1
- package/src/room/track/RemoteTrackPublication.ts +2 -2
- package/src/room/track/processor/types.ts +17 -2
- package/src/room/types.ts +5 -1
@@ -3,7 +3,9 @@ import { VideoLayer, VideoQuality } from '../../proto/livekit_models_pb';
|
|
3
3
|
import { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc_pb';
|
4
4
|
import type { VideoSenderStats } from '../stats';
|
5
5
|
import LocalTrack from './LocalTrack';
|
6
|
+
import { Track } from './Track';
|
6
7
|
import type { VideoCaptureOptions, VideoCodec } from './options';
|
8
|
+
import type { TrackProcessor } from './processor/types';
|
7
9
|
export declare class SimulcastTrackInfo {
|
8
10
|
codec: VideoCodec;
|
9
11
|
mediaStreamTrack: MediaStreamTrack;
|
@@ -28,12 +30,16 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
28
30
|
get isSimulcast(): boolean;
|
29
31
|
startMonitor(signalClient: SignalClient): void;
|
30
32
|
stop(): void;
|
33
|
+
pauseUpstream(): Promise<void>;
|
34
|
+
resumeUpstream(): Promise<void>;
|
31
35
|
mute(): Promise<LocalVideoTrack>;
|
32
36
|
unmute(): Promise<LocalVideoTrack>;
|
37
|
+
protected setTrackMuted(muted: boolean): void;
|
33
38
|
getSenderStats(): Promise<VideoSenderStats[]>;
|
34
39
|
setPublishingQuality(maxQuality: VideoQuality): void;
|
35
40
|
setDeviceId(deviceId: ConstrainDOMString): Promise<boolean>;
|
36
41
|
restartTrack(options?: VideoCaptureOptions): Promise<void>;
|
42
|
+
setProcessor(processor: TrackProcessor<Track.Kind>, showProcessedStreamLocally?: boolean): Promise<void>;
|
37
43
|
addSimulcastTrack(codec: VideoCodec, encodings?: RTCRtpEncodingParameters[]): SimulcastTrackInfo;
|
38
44
|
setSimulcastTrackSender(codec: VideoCodec, sender: RTCRtpSender): void;
|
39
45
|
/**
|
@@ -10,9 +10,20 @@ export type ProcessorOptions<T extends Track.Kind> = {
|
|
10
10
|
/**
|
11
11
|
* @experimental
|
12
12
|
*/
|
13
|
-
export interface
|
13
|
+
export interface AudioProcessorOptions extends ProcessorOptions<Track.Kind.Audio> {
|
14
|
+
audioContext: AudioContext;
|
15
|
+
}
|
16
|
+
/**
|
17
|
+
* @experimental
|
18
|
+
*/
|
19
|
+
export interface VideoProcessorOptions extends ProcessorOptions<Track.Kind.Video> {
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* @experimental
|
23
|
+
*/
|
24
|
+
export interface TrackProcessor<T extends Track.Kind, U extends ProcessorOptions<T> = ProcessorOptions<T>> {
|
14
25
|
name: string;
|
15
|
-
init: (opts:
|
26
|
+
init: (opts: U) => void;
|
16
27
|
destroy: () => Promise<void>;
|
17
28
|
processedTrack?: MediaStreamTrack;
|
18
29
|
}
|
@@ -22,5 +22,5 @@ export type LiveKitReactNativeInfo = {
|
|
22
22
|
platform: 'ios' | 'android' | 'windows' | 'macos' | 'web' | 'native';
|
23
23
|
devicePixelRatio: number;
|
24
24
|
};
|
25
|
-
export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect';
|
25
|
+
export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect' | 'subscriber-bandwidth';
|
26
26
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "livekit-client",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.13.0",
|
4
4
|
"description": "JavaScript/TypeScript client SDK for LiveKit",
|
5
5
|
"main": "./dist/livekit-client.umd.js",
|
6
6
|
"unpkg": "./dist/livekit-client.umd.js",
|
@@ -61,10 +61,10 @@
|
|
61
61
|
"webrtc-adapter": "^8.1.1"
|
62
62
|
},
|
63
63
|
"devDependencies": {
|
64
|
-
"@babel/core": "7.22.
|
65
|
-
"@babel/preset-env": "7.22.
|
64
|
+
"@babel/core": "7.22.9",
|
65
|
+
"@babel/preset-env": "7.22.9",
|
66
66
|
"@bufbuild/protoc-gen-es": "^1.3.0",
|
67
|
-
"@changesets/cli": "2.26.
|
67
|
+
"@changesets/cli": "2.26.2",
|
68
68
|
"@livekit/changesets-changelog-github": "^0.0.4",
|
69
69
|
"@rollup/plugin-babel": "6.0.3",
|
70
70
|
"@rollup/plugin-commonjs": "24.1.0",
|
@@ -77,27 +77,26 @@
|
|
77
77
|
"@types/events": "^3.0.0",
|
78
78
|
"@types/sdp-transform": "2.4.6",
|
79
79
|
"@types/ua-parser-js": "0.7.36",
|
80
|
-
"@typescript-eslint/eslint-plugin": "5.
|
81
|
-
"@typescript-eslint/parser": "5.
|
80
|
+
"@typescript-eslint/eslint-plugin": "5.62.0",
|
81
|
+
"@typescript-eslint/parser": "5.62.0",
|
82
82
|
"downlevel-dts": "^0.11.0",
|
83
|
-
"eslint": "8.
|
84
|
-
"eslint-config-airbnb-typescript": "17.
|
85
|
-
"eslint-config-prettier": "8.
|
83
|
+
"eslint": "8.46.0",
|
84
|
+
"eslint-config-airbnb-typescript": "17.1.0",
|
85
|
+
"eslint-config-prettier": "8.9.0",
|
86
86
|
"eslint-plugin-ecmascript-compat": "^3.0.0",
|
87
|
-
"eslint-plugin-import": "2.
|
87
|
+
"eslint-plugin-import": "2.28.0",
|
88
88
|
"gh-pages": "5.0.0",
|
89
89
|
"jsdom": "^22.1.0",
|
90
90
|
"prettier": "^2.8.8",
|
91
|
-
"rollup": "3.
|
91
|
+
"rollup": "3.27.0",
|
92
92
|
"rollup-plugin-delete": "^2.0.0",
|
93
93
|
"rollup-plugin-re": "1.0.7",
|
94
|
-
"rollup-plugin-typescript2": "0.
|
94
|
+
"rollup-plugin-typescript2": "0.35.0",
|
95
95
|
"size-limit": "^8.2.4",
|
96
|
-
"ts-proto": "1.148.2",
|
97
96
|
"typedoc": "0.24.8",
|
98
97
|
"typedoc-plugin-no-inherit": "1.4.0",
|
99
|
-
"typescript": "5.1.
|
100
|
-
"vite": "4.
|
101
|
-
"vitest": "^0.
|
98
|
+
"typescript": "5.1.6",
|
99
|
+
"vite": "4.4.7",
|
100
|
+
"vitest": "^0.33.0"
|
102
101
|
}
|
103
102
|
}
|
package/src/api/SignalClient.ts
CHANGED
@@ -43,21 +43,13 @@ import { Mutex, getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../r
|
|
43
43
|
import { AsyncQueue } from '../utils/AsyncQueue';
|
44
44
|
|
45
45
|
// internal options
|
46
|
-
interface ConnectOpts {
|
47
|
-
autoSubscribe: boolean;
|
46
|
+
interface ConnectOpts extends SignalOptions {
|
48
47
|
/** internal */
|
49
48
|
reconnect?: boolean;
|
50
|
-
|
51
49
|
/** internal */
|
52
50
|
reconnectReason?: number;
|
53
|
-
|
54
51
|
/** internal */
|
55
52
|
sid?: string;
|
56
|
-
|
57
|
-
/** @deprecated */
|
58
|
-
publishOnly?: string;
|
59
|
-
|
60
|
-
adaptiveStream?: boolean;
|
61
53
|
}
|
62
54
|
|
63
55
|
// public options
|
@@ -68,6 +60,7 @@ export interface SignalOptions {
|
|
68
60
|
adaptiveStream?: boolean;
|
69
61
|
maxRetries: number;
|
70
62
|
e2eeEnabled: boolean;
|
63
|
+
websocketTimeout: number;
|
71
64
|
}
|
72
65
|
|
73
66
|
type SignalMessage = SignalRequest['message'];
|
@@ -224,9 +217,15 @@ export class SignalClient {
|
|
224
217
|
return new Promise<JoinResponse | ReconnectResponse | void>(async (resolve, reject) => {
|
225
218
|
const abortHandler = async () => {
|
226
219
|
this.close();
|
220
|
+
clearTimeout(wsTimeout);
|
227
221
|
reject(new ConnectionError('room connection has been cancelled (signal)'));
|
228
222
|
};
|
229
223
|
|
224
|
+
const wsTimeout = setTimeout(() => {
|
225
|
+
this.close();
|
226
|
+
reject(new ConnectionError('room connection has timed out (signal)'));
|
227
|
+
}, opts.websocketTimeout);
|
228
|
+
|
230
229
|
if (abortSignal?.aborted) {
|
231
230
|
abortHandler();
|
232
231
|
}
|
@@ -238,8 +237,13 @@ export class SignalClient {
|
|
238
237
|
this.ws = new WebSocket(url + params);
|
239
238
|
this.ws.binaryType = 'arraybuffer';
|
240
239
|
|
240
|
+
this.ws.onopen = () => {
|
241
|
+
clearTimeout(wsTimeout);
|
242
|
+
};
|
243
|
+
|
241
244
|
this.ws.onerror = async (ev: Event) => {
|
242
245
|
if (!this.isConnected) {
|
246
|
+
clearTimeout(wsTimeout);
|
243
247
|
try {
|
244
248
|
const resp = await fetch(`http${url.substring(2)}/validate${params}`);
|
245
249
|
if (resp.status.toFixed(0).startsWith('4')) {
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import log from '../../logger';
|
1
2
|
import { RoomEvent } from '../../room/events';
|
2
3
|
import { Checker } from './Checker';
|
3
4
|
|
@@ -14,19 +15,20 @@ export class WebRTCCheck extends Checker {
|
|
14
15
|
|
15
16
|
const candidates: RTCIceCandidate[] = [];
|
16
17
|
this.room.engine.client.onTrickle = (sd, target) => {
|
17
|
-
console.log('got candidate', sd);
|
18
18
|
if (sd.candidate) {
|
19
19
|
const candidate = new RTCIceCandidate(sd);
|
20
20
|
candidates.push(candidate);
|
21
21
|
let str = `${candidate.protocol} ${candidate.address}:${candidate.port} ${candidate.type}`;
|
22
|
-
if (candidate.
|
23
|
-
hasTcp = true;
|
24
|
-
str += ' (active)';
|
25
|
-
} else if (candidate.protocol === 'udp' && candidate.address) {
|
22
|
+
if (candidate.address) {
|
26
23
|
if (isIPPrivate(candidate.address)) {
|
27
24
|
str += ' (private)';
|
28
25
|
} else {
|
29
|
-
|
26
|
+
if (candidate.protocol === 'tcp' && candidate.tcpType === 'passive') {
|
27
|
+
hasTcp = true;
|
28
|
+
str += ' (passive)';
|
29
|
+
} else if (candidate.protocol === 'udp') {
|
30
|
+
hasIpv4Udp = true;
|
31
|
+
}
|
30
32
|
}
|
31
33
|
}
|
32
34
|
this.appendMessage(str);
|
@@ -48,7 +50,7 @@ export class WebRTCCheck extends Checker {
|
|
48
50
|
});
|
49
51
|
try {
|
50
52
|
await this.connect();
|
51
|
-
|
53
|
+
log.info('now the room is connected');
|
52
54
|
} catch (err) {
|
53
55
|
this.appendWarning('ports need to be open on firewall in order to connect.');
|
54
56
|
throw err;
|
@@ -17,6 +17,7 @@ export class WebSocketCheck extends Checker {
|
|
17
17
|
autoSubscribe: true,
|
18
18
|
maxRetries: 0,
|
19
19
|
e2eeEnabled: false,
|
20
|
+
websocketTimeout: 15_000,
|
20
21
|
});
|
21
22
|
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
|
22
23
|
if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
|
package/src/e2ee/E2eeManager.ts
CHANGED
@@ -25,6 +25,7 @@ import type {
|
|
25
25
|
RatchetRequestMessage,
|
26
26
|
RemoveTransformMessage,
|
27
27
|
SetKeyMessage,
|
28
|
+
SifTrailerMessage,
|
28
29
|
UpdateCodecMessage,
|
29
30
|
} from './types';
|
30
31
|
import { EncryptionEvent } from './types';
|
@@ -99,6 +100,17 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
|
|
99
100
|
}
|
100
101
|
}
|
101
102
|
|
103
|
+
/**
|
104
|
+
* @internal
|
105
|
+
*/
|
106
|
+
setSifTrailer(trailer: Uint8Array) {
|
107
|
+
if (!trailer || trailer.length === 0) {
|
108
|
+
log.warn("ignoring server sent trailer as it's empty");
|
109
|
+
} else {
|
110
|
+
this.postSifTrailer(trailer);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
|
102
114
|
private onWorkerMessage = (ev: MessageEvent<E2EEWorkerMessage>) => {
|
103
115
|
const { kind, data } = ev.data;
|
104
116
|
switch (kind) {
|
@@ -233,6 +245,19 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
|
|
233
245
|
this.worker.postMessage(msg);
|
234
246
|
}
|
235
247
|
|
248
|
+
private postSifTrailer(trailer: Uint8Array) {
|
249
|
+
if (!this.worker) {
|
250
|
+
throw Error('could not post SIF trailer, worker is missing');
|
251
|
+
}
|
252
|
+
const msg: SifTrailerMessage = {
|
253
|
+
kind: 'setSifTrailer',
|
254
|
+
data: {
|
255
|
+
trailer,
|
256
|
+
},
|
257
|
+
};
|
258
|
+
this.worker.postMessage(msg);
|
259
|
+
}
|
260
|
+
|
236
261
|
private setupE2EEReceiver(track: RemoteTrack, remoteId: string, trackInfo?: TrackInfo) {
|
237
262
|
if (!track.receiver) {
|
238
263
|
return;
|
@@ -342,7 +367,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
|
|
342
367
|
}
|
343
368
|
|
344
369
|
if (isScriptTransformSupported()) {
|
345
|
-
log.
|
370
|
+
log.info('initialize script transform');
|
346
371
|
|
347
372
|
const options = {
|
348
373
|
kind: 'encode',
|
@@ -353,7 +378,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
|
|
353
378
|
// @ts-ignore
|
354
379
|
sender.transform = new RTCRtpScriptTransform(this.worker, options);
|
355
380
|
} else {
|
356
|
-
log.
|
381
|
+
log.info('initialize encoded streams');
|
357
382
|
// @ts-ignore
|
358
383
|
const senderStreams = sender.createEncodedStreams();
|
359
384
|
const msg: EncodeMessage = {
|
package/src/e2ee/KeyProvider.ts
CHANGED
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
2
2
|
import type TypedEventEmitter from 'typed-emitter';
|
3
3
|
import { KEY_PROVIDER_DEFAULTS } from './constants';
|
4
4
|
import type { KeyInfo, KeyProviderCallbacks, KeyProviderOptions } from './types';
|
5
|
-
import { createKeyMaterialFromString } from './utils';
|
5
|
+
import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './utils';
|
6
6
|
|
7
7
|
/**
|
8
8
|
* @experimental
|
@@ -68,11 +68,16 @@ export class ExternalE2EEKeyProvider extends BaseKeyProvider {
|
|
68
68
|
}
|
69
69
|
|
70
70
|
/**
|
71
|
-
* Accepts a passphrase that's used to create the crypto keys
|
71
|
+
* Accepts a passphrase that's used to create the crypto keys.
|
72
|
+
* When passing in a string, PBKDF2 is used.
|
73
|
+
* Also accepts an Array buffer of cryptographically random numbers that uses HKDF.
|
72
74
|
* @param key
|
73
75
|
*/
|
74
|
-
async setKey(key: string) {
|
75
|
-
const derivedKey =
|
76
|
+
async setKey(key: string | ArrayBuffer) {
|
77
|
+
const derivedKey =
|
78
|
+
typeof key === 'string'
|
79
|
+
? await createKeyMaterialFromString(key)
|
80
|
+
: await createKeyMaterialFromBuffer(key);
|
76
81
|
this.onSetEncryptionKey(derivedKey);
|
77
82
|
}
|
78
83
|
}
|
package/src/e2ee/constants.ts
CHANGED
package/src/e2ee/types.ts
CHANGED
@@ -31,6 +31,13 @@ export interface RTPVideoMapMessage extends BaseMessage {
|
|
31
31
|
};
|
32
32
|
}
|
33
33
|
|
34
|
+
export interface SifTrailerMessage extends BaseMessage {
|
35
|
+
kind: 'setSifTrailer';
|
36
|
+
data: {
|
37
|
+
trailer: Uint8Array;
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
34
41
|
export interface EncodeMessage extends BaseMessage {
|
35
42
|
kind: 'decode' | 'encode';
|
36
43
|
data: {
|
@@ -102,7 +109,8 @@ export type E2EEWorkerMessage =
|
|
102
109
|
| RTPVideoMapMessage
|
103
110
|
| UpdateCodecMessage
|
104
111
|
| RatchetRequestMessage
|
105
|
-
| RatchetMessage
|
112
|
+
| RatchetMessage
|
113
|
+
| SifTrailerMessage;
|
106
114
|
|
107
115
|
export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
|
108
116
|
|
package/src/e2ee/utils.ts
CHANGED
@@ -56,6 +56,15 @@ export async function createKeyMaterialFromString(password: string) {
|
|
56
56
|
return keyMaterial;
|
57
57
|
}
|
58
58
|
|
59
|
+
export async function createKeyMaterialFromBuffer(cryptoBuffer: ArrayBuffer) {
|
60
|
+
const keyMaterial = await crypto.subtle.importKey('raw', cryptoBuffer, 'HKDF', false, [
|
61
|
+
'deriveBits',
|
62
|
+
'deriveKey',
|
63
|
+
]);
|
64
|
+
|
65
|
+
return keyMaterial;
|
66
|
+
}
|
67
|
+
|
59
68
|
function getAlgoOptions(algorithmName: string, salt: string) {
|
60
69
|
const textEncoder = new TextEncoder();
|
61
70
|
const encodedSalt = textEncoder.encode(salt);
|
@@ -15,6 +15,7 @@ import {
|
|
15
15
|
} from '../types';
|
16
16
|
import { deriveKeys, isVideoFrame } from '../utils';
|
17
17
|
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
18
|
+
import { SifGuard } from './SifGuard';
|
18
19
|
|
19
20
|
export interface FrameCryptorConstructor {
|
20
21
|
new (opts?: unknown): BaseFrameCryptor;
|
@@ -65,13 +66,15 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
65
66
|
/**
|
66
67
|
* used for detecting server injected unencrypted frames
|
67
68
|
*/
|
68
|
-
private
|
69
|
+
private sifTrailer: Uint8Array;
|
70
|
+
|
71
|
+
private sifGuard: SifGuard;
|
69
72
|
|
70
73
|
constructor(opts: {
|
71
74
|
keys: ParticipantKeyHandler;
|
72
75
|
participantId: string;
|
73
76
|
keyProviderOptions: KeyProviderOptions;
|
74
|
-
|
77
|
+
sifTrailer?: Uint8Array;
|
75
78
|
}) {
|
76
79
|
super();
|
77
80
|
this.sendCounts = new Map();
|
@@ -79,8 +82,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
79
82
|
this.participantId = opts.participantId;
|
80
83
|
this.rtpMap = new Map();
|
81
84
|
this.keyProviderOptions = opts.keyProviderOptions;
|
82
|
-
this.
|
83
|
-
|
85
|
+
this.sifTrailer = opts.sifTrailer ?? Uint8Array.from([]);
|
86
|
+
this.sifGuard = new SifGuard();
|
84
87
|
}
|
85
88
|
|
86
89
|
/**
|
@@ -92,6 +95,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
92
95
|
setParticipant(id: string, keys: ParticipantKeyHandler) {
|
93
96
|
this.participantId = id;
|
94
97
|
this.keys = keys;
|
98
|
+
this.sifGuard.reset();
|
95
99
|
}
|
96
100
|
|
97
101
|
unsetParticipant() {
|
@@ -130,9 +134,10 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
130
134
|
codec?: VideoCodec,
|
131
135
|
) {
|
132
136
|
if (codec) {
|
133
|
-
|
137
|
+
workerLogger.info('setting codec on cryptor to', { codec });
|
134
138
|
this.videoCodec = codec;
|
135
139
|
}
|
140
|
+
|
136
141
|
const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
|
137
142
|
const transformStream = new TransformStream({
|
138
143
|
transform: transformFn.bind(this),
|
@@ -142,7 +147,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
142
147
|
.pipeThrough(transformStream)
|
143
148
|
.pipeTo(writable)
|
144
149
|
.catch((e) => {
|
145
|
-
|
150
|
+
workerLogger.warn(e);
|
146
151
|
this.emit('cryptorError', e instanceof CryptorError ? e : new CryptorError(e.message));
|
147
152
|
});
|
148
153
|
this.trackId = trackId;
|
@@ -260,12 +265,24 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
260
265
|
if (
|
261
266
|
!this.keys.isEnabled() ||
|
262
267
|
// skip for decryption for empty dtx frames
|
263
|
-
encodedFrame.data.byteLength === 0
|
264
|
-
// skip decryption if frame is server injected
|
265
|
-
isFrameServerInjected(encodedFrame.data, this.unencryptedFrameByteTrailer)
|
268
|
+
encodedFrame.data.byteLength === 0
|
266
269
|
) {
|
270
|
+
this.sifGuard.recordUserFrame();
|
267
271
|
return controller.enqueue(encodedFrame);
|
268
272
|
}
|
273
|
+
|
274
|
+
if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
|
275
|
+
this.sifGuard.recordSif();
|
276
|
+
|
277
|
+
if (this.sifGuard.isSifAllowed()) {
|
278
|
+
return controller.enqueue(encodedFrame);
|
279
|
+
} else {
|
280
|
+
workerLogger.warn('SIF limit reached, dropping frame');
|
281
|
+
return;
|
282
|
+
}
|
283
|
+
} else {
|
284
|
+
this.sifGuard.recordUserFrame();
|
285
|
+
}
|
269
286
|
const data = new Uint8Array(encodedFrame.data);
|
270
287
|
const keyIndex = data[encodedFrame.data.byteLength - 1];
|
271
288
|
|
@@ -293,9 +310,17 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
293
310
|
workerLogger.warn('decoding frame failed', { error });
|
294
311
|
}
|
295
312
|
}
|
313
|
+
} else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
|
314
|
+
// emit an error in case the key index is out of bounds but the key handler thinks we still have a valid key
|
315
|
+
workerLogger.warn('skipping decryption due to missing key at index');
|
316
|
+
this.emit(
|
317
|
+
CryptorEvent.Error,
|
318
|
+
new CryptorError(
|
319
|
+
`missing key at index for participant ${this.participantId}`,
|
320
|
+
CryptorErrorReason.MissingKey,
|
321
|
+
),
|
322
|
+
);
|
296
323
|
}
|
297
|
-
|
298
|
-
return controller.enqueue(encodedFrame);
|
299
324
|
}
|
300
325
|
|
301
326
|
/**
|
@@ -398,12 +423,9 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
398
423
|
}
|
399
424
|
|
400
425
|
workerLogger.warn('maximum ratchet attempts exceeded, resetting key');
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
`valid key missing for participant ${this.participantId}`,
|
405
|
-
CryptorErrorReason.MissingKey,
|
406
|
-
),
|
426
|
+
throw new CryptorError(
|
427
|
+
`valid key missing for participant ${this.participantId}`,
|
428
|
+
CryptorErrorReason.InvalidKey,
|
407
429
|
);
|
408
430
|
}
|
409
431
|
} else {
|
@@ -513,6 +535,10 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
513
535
|
const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
|
514
536
|
return codec;
|
515
537
|
}
|
538
|
+
|
539
|
+
setSifTrailer(trailer: Uint8Array) {
|
540
|
+
this.sifTrailer = trailer;
|
541
|
+
}
|
516
542
|
}
|
517
543
|
|
518
544
|
/**
|
@@ -605,6 +631,9 @@ export enum NALUType {
|
|
605
631
|
* @internal
|
606
632
|
*/
|
607
633
|
export function isFrameServerInjected(frameData: ArrayBuffer, trailerBytes: Uint8Array): boolean {
|
634
|
+
if (trailerBytes.byteLength === 0) {
|
635
|
+
return false;
|
636
|
+
}
|
608
637
|
const frameTrailer = new Uint8Array(
|
609
638
|
frameData.slice(frameData.byteLength - trailerBytes.byteLength),
|
610
639
|
);
|
@@ -63,6 +63,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
|
|
63
63
|
this.decryptionFailureCount += 1;
|
64
64
|
|
65
65
|
if (this.decryptionFailureCount > this.keyProviderOptions.failureTolerance) {
|
66
|
+
workerLogger.warn(`key for ${this.participantId} is being marked as invalid`);
|
66
67
|
this._hasValidKey = false;
|
67
68
|
}
|
68
69
|
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { MAX_SIF_COUNT, MAX_SIF_DURATION } from '../constants';
|
2
|
+
|
3
|
+
export class SifGuard {
|
4
|
+
private consecutiveSifCount = 0;
|
5
|
+
|
6
|
+
private sifSequenceStartedAt: number | undefined;
|
7
|
+
|
8
|
+
private lastSifReceivedAt: number = 0;
|
9
|
+
|
10
|
+
private userFramesSinceSif: number = 0;
|
11
|
+
|
12
|
+
recordSif() {
|
13
|
+
this.consecutiveSifCount += 1;
|
14
|
+
this.sifSequenceStartedAt ??= Date.now();
|
15
|
+
this.lastSifReceivedAt = Date.now();
|
16
|
+
}
|
17
|
+
|
18
|
+
recordUserFrame() {
|
19
|
+
if (this.sifSequenceStartedAt === undefined) {
|
20
|
+
return;
|
21
|
+
} else {
|
22
|
+
this.userFramesSinceSif += 1;
|
23
|
+
}
|
24
|
+
if (
|
25
|
+
// reset if we received more user frames than SIFs
|
26
|
+
this.userFramesSinceSif > this.consecutiveSifCount ||
|
27
|
+
// also reset if we got a new user frame and the latest SIF frame hasn't been updated in a while
|
28
|
+
Date.now() - this.lastSifReceivedAt > MAX_SIF_DURATION
|
29
|
+
) {
|
30
|
+
this.reset();
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
isSifAllowed() {
|
35
|
+
return (
|
36
|
+
this.consecutiveSifCount < MAX_SIF_COUNT &&
|
37
|
+
(this.sifSequenceStartedAt === undefined ||
|
38
|
+
Date.now() - this.sifSequenceStartedAt < MAX_SIF_DURATION)
|
39
|
+
);
|
40
|
+
}
|
41
|
+
|
42
|
+
reset() {
|
43
|
+
this.userFramesSinceSif = 0;
|
44
|
+
this.consecutiveSifCount = 0;
|
45
|
+
this.sifSequenceStartedAt = undefined;
|
46
|
+
}
|
47
|
+
}
|
@@ -24,6 +24,8 @@ let useSharedKey: boolean = false;
|
|
24
24
|
|
25
25
|
let sharedKey: CryptoKey | undefined;
|
26
26
|
|
27
|
+
let sifTrailer: Uint8Array | undefined;
|
28
|
+
|
27
29
|
let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
|
28
30
|
|
29
31
|
workerLogger.setDefaultLevel('info');
|
@@ -94,6 +96,10 @@ onmessage = (ev) => {
|
|
94
96
|
break;
|
95
97
|
case 'ratchetRequest':
|
96
98
|
handleRatchetRequest(data);
|
99
|
+
break;
|
100
|
+
case 'setSifTrailer':
|
101
|
+
handleSifTrailer(data.trailer);
|
102
|
+
break;
|
97
103
|
default:
|
98
104
|
break;
|
99
105
|
}
|
@@ -116,6 +122,7 @@ function getTrackCryptor(participantId: string, trackId: string) {
|
|
116
122
|
participantId,
|
117
123
|
keys: getParticipantKeyHandler(participantId),
|
118
124
|
keyProviderOptions,
|
125
|
+
sifTrailer,
|
119
126
|
});
|
120
127
|
|
121
128
|
setupCryptorErrorEvents(cryptor);
|
@@ -205,6 +212,13 @@ function emitRatchetedKeys(material: CryptoKey, keyIndex?: number) {
|
|
205
212
|
postMessage(msg);
|
206
213
|
}
|
207
214
|
|
215
|
+
function handleSifTrailer(trailer: Uint8Array) {
|
216
|
+
sifTrailer = trailer;
|
217
|
+
participantCryptors.forEach((c) => {
|
218
|
+
c.setSifTrailer(trailer);
|
219
|
+
});
|
220
|
+
}
|
221
|
+
|
208
222
|
// Operations using RTCRtpScriptTransform.
|
209
223
|
// @ts-ignore
|
210
224
|
if (self.RTCTransformEvent) {
|
package/src/options.ts
CHANGED
@@ -31,6 +31,9 @@ export interface InternalRoomOptions {
|
|
31
31
|
* enable Dynacast, off by default. With Dynacast dynamically pauses
|
32
32
|
* video layers that are not being consumed by any subscribers, significantly
|
33
33
|
* reducing publishing CPU and bandwidth usage.
|
34
|
+
*
|
35
|
+
* Dynacast will be enabled if SVC codecs (VP9/AV1) are used. Multi-codec simulcast
|
36
|
+
* requires dynacast
|
34
37
|
*/
|
35
38
|
dynacast: boolean;
|
36
39
|
|
@@ -119,6 +122,9 @@ export interface InternalRoomConnectOptions {
|
|
119
122
|
|
120
123
|
/** specifies how often an initial join connection is allowed to retry (only applicable if server is not reachable) */
|
121
124
|
maxRetries: number;
|
125
|
+
|
126
|
+
/** amount of time for Websocket connection to be established, defaults to 15s */
|
127
|
+
websocketTimeout: number;
|
122
128
|
}
|
123
129
|
|
124
130
|
/**
|
@@ -1,3 +1,17 @@
|
|
1
|
+
// Copyright 2023 LiveKit, Inc.
|
2
|
+
//
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
// you may not use this file except in compliance with the License.
|
5
|
+
// You may obtain a copy of the License at
|
6
|
+
//
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
//
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
// See the License for the specific language governing permissions and
|
13
|
+
// limitations under the License.
|
14
|
+
|
1
15
|
// @generated by protoc-gen-es v1.3.0 with parameter "target=ts"
|
2
16
|
// @generated from file livekit_models.proto (package livekit, syntax proto3)
|
3
17
|
/* eslint-disable */
|