livekit-client 1.11.4 → 1.12.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +13 -1
- package/dist/livekit-client.e2ee.worker.js +2 -0
- package/dist/livekit-client.e2ee.worker.js.map +1 -0
- package/dist/livekit-client.e2ee.worker.mjs +1545 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -0
- package/dist/livekit-client.esm.mjs +4786 -4065
- 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 +4 -1
- 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/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -0
- package/dist/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/src/e2ee/KeyProvider.d.ts.map +1 -0
- package/dist/src/e2ee/constants.d.ts +14 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -0
- package/dist/src/e2ee/errors.d.ts +11 -0
- package/dist/src/e2ee/errors.d.ts.map +1 -0
- package/dist/src/e2ee/index.d.ts +4 -0
- package/dist/src/e2ee/index.d.ts.map +1 -0
- package/dist/src/e2ee/types.d.ts +129 -0
- package/dist/src/e2ee/types.d.ts.map +1 -0
- package/dist/src/e2ee/utils.d.ts +24 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +174 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/src/e2ee/worker/e2ee.worker.d.ts.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +5 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/proto/livekit_models.d.ts +2 -2
- package/dist/src/proto/livekit_models.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +3 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +17 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +14 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +6 -4
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +2 -2
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +9 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +2 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
- package/dist/src/utils/browserParser.d.ts +2 -0
- package/dist/src/utils/browserParser.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +4 -1
- package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +45 -0
- package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +42 -0
- package/dist/ts4.2/src/e2ee/constants.d.ts +14 -0
- package/dist/ts4.2/src/e2ee/errors.d.ts +11 -0
- package/dist/ts4.2/src/e2ee/index.d.ts +4 -0
- package/dist/ts4.2/src/e2ee/types.d.ts +129 -0
- package/dist/ts4.2/src/e2ee/utils.d.ts +24 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +174 -0
- package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +54 -0
- package/dist/ts4.2/src/e2ee/worker/e2ee.worker.d.ts +2 -0
- package/dist/ts4.2/src/index.d.ts +1 -0
- package/dist/ts4.2/src/logger.d.ts +4 -1
- package/dist/ts4.2/src/options.d.ts +5 -0
- package/dist/ts4.2/src/proto/livekit_models.d.ts +2 -2
- package/dist/ts4.2/src/room/PCTransport.d.ts +3 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +17 -3
- package/dist/ts4.2/src/room/Room.d.ts +10 -0
- package/dist/ts4.2/src/room/events.d.ts +14 -2
- package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +7 -2
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +6 -4
- package/dist/ts4.2/src/room/track/TrackPublication.d.ts +3 -0
- package/dist/ts4.2/src/room/track/options.d.ts +6 -6
- package/dist/ts4.2/src/room/track/utils.d.ts +9 -0
- package/dist/ts4.2/src/room/utils.d.ts +2 -0
- package/dist/ts4.2/src/utils/browserParser.d.ts +2 -0
- package/package.json +17 -7
- package/src/api/SignalClient.ts +28 -9
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/e2ee/E2eeManager.ts +374 -0
- package/src/e2ee/KeyProvider.ts +77 -0
- package/src/e2ee/constants.ts +40 -0
- package/src/e2ee/errors.ts +16 -0
- package/src/e2ee/index.ts +3 -0
- package/src/e2ee/types.ts +160 -0
- package/src/e2ee/utils.ts +127 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +21 -0
- package/src/e2ee/worker/FrameCryptor.ts +612 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +144 -0
- package/src/e2ee/worker/e2ee.worker.ts +223 -0
- package/src/e2ee/worker/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +10 -2
- package/src/options.ts +6 -0
- package/src/proto/livekit_models.ts +12 -12
- package/src/room/PCTransport.ts +39 -9
- package/src/room/RTCEngine.ts +127 -34
- package/src/room/Room.ts +94 -29
- package/src/room/defaults.ts +1 -1
- package/src/room/events.ts +14 -0
- package/src/room/participant/LocalParticipant.ts +52 -8
- package/src/room/participant/Participant.ts +4 -0
- package/src/room/participant/RemoteParticipant.ts +19 -15
- package/src/room/track/LocalTrack.ts +5 -4
- package/src/room/track/RemoteVideoTrack.ts +2 -2
- package/src/room/track/TrackPublication.ts +9 -1
- package/src/room/track/create.ts +9 -0
- package/src/room/track/options.ts +3 -2
- package/src/room/track/utils.ts +27 -0
- package/src/room/utils.ts +5 -0
- package/src/room/worker.d.ts +4 -0
- package/src/test/MockMediaStreamTrack.ts +1 -0
- package/src/utils/browserParser.ts +5 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
2
|
+
import { workerLogger } from '../../logger';
|
3
|
+
import { KEYRING_SIZE } from '../constants';
|
4
|
+
import type { KeyProviderOptions, KeySet, ParticipantKeyHandlerCallbacks } from '../types';
|
5
|
+
import { deriveKeys, importKey, ratchet } from '../utils';
|
6
|
+
|
7
|
+
// TODO ParticipantKeyHandlers currently don't get destroyed on participant disconnect
|
8
|
+
// we could do this by having a separate worker message on participant disconnected.
|
9
|
+
|
10
|
+
/**
|
11
|
+
* ParticipantKeyHandler is responsible for providing a cryptor instance with the
|
12
|
+
* en-/decryption key of a participant. It assumes that all tracks of a specific participant
|
13
|
+
* are encrypted with the same key.
|
14
|
+
* Additionally it exposes a method to ratchet a key which can be used by the cryptor either automatically
|
15
|
+
* if decryption fails or can be triggered manually on both sender and receiver side.
|
16
|
+
*
|
17
|
+
*/
|
18
|
+
export class ParticipantKeyHandler extends EventEmitter<ParticipantKeyHandlerCallbacks> {
|
19
|
+
private currentKeyIndex: number;
|
20
|
+
|
21
|
+
private cryptoKeyRing: Array<KeySet>;
|
22
|
+
|
23
|
+
private enabled: boolean;
|
24
|
+
|
25
|
+
private keyProviderOptions: KeyProviderOptions;
|
26
|
+
|
27
|
+
private ratchetPromiseMap: Map<number, Promise<CryptoKey>>;
|
28
|
+
|
29
|
+
private participantId: string | undefined;
|
30
|
+
|
31
|
+
hasValidKey: boolean;
|
32
|
+
|
33
|
+
constructor(
|
34
|
+
participantId: string | undefined,
|
35
|
+
isEnabled: boolean,
|
36
|
+
keyProviderOptions: KeyProviderOptions,
|
37
|
+
) {
|
38
|
+
super();
|
39
|
+
this.currentKeyIndex = 0;
|
40
|
+
this.cryptoKeyRing = new Array(KEYRING_SIZE);
|
41
|
+
this.enabled = isEnabled;
|
42
|
+
this.keyProviderOptions = keyProviderOptions;
|
43
|
+
this.ratchetPromiseMap = new Map();
|
44
|
+
this.participantId = participantId;
|
45
|
+
this.hasValidKey = false;
|
46
|
+
}
|
47
|
+
|
48
|
+
setEnabled(enabled: boolean) {
|
49
|
+
this.enabled = enabled;
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Ratchets the current key (or the one at keyIndex if provided) and
|
54
|
+
* returns the ratcheted material
|
55
|
+
* if `setKey` is true (default), it will also set the ratcheted key directly on the crypto key ring
|
56
|
+
* @param keyIndex
|
57
|
+
* @param setKey
|
58
|
+
*/
|
59
|
+
ratchetKey(keyIndex?: number, setKey = true): Promise<CryptoKey> {
|
60
|
+
const currentKeyIndex = (keyIndex ??= this.getCurrentKeyIndex());
|
61
|
+
|
62
|
+
const existingPromise = this.ratchetPromiseMap.get(currentKeyIndex);
|
63
|
+
if (typeof existingPromise !== 'undefined') {
|
64
|
+
return existingPromise;
|
65
|
+
}
|
66
|
+
const ratchetPromise = new Promise<CryptoKey>(async (resolve, reject) => {
|
67
|
+
try {
|
68
|
+
const currentMaterial = this.getKeySet(currentKeyIndex).material;
|
69
|
+
const newMaterial = await importKey(
|
70
|
+
await ratchet(currentMaterial, this.keyProviderOptions.ratchetSalt),
|
71
|
+
currentMaterial.algorithm.name,
|
72
|
+
'derive',
|
73
|
+
);
|
74
|
+
|
75
|
+
if (setKey) {
|
76
|
+
this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
|
77
|
+
}
|
78
|
+
this.emit('keyRatcheted', newMaterial, keyIndex, this.participantId);
|
79
|
+
resolve(newMaterial);
|
80
|
+
} catch (e) {
|
81
|
+
reject(e);
|
82
|
+
} finally {
|
83
|
+
this.ratchetPromiseMap.delete(currentKeyIndex);
|
84
|
+
}
|
85
|
+
});
|
86
|
+
this.ratchetPromiseMap.set(currentKeyIndex, ratchetPromise);
|
87
|
+
return ratchetPromise;
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* takes in a key material with `deriveBits` and `deriveKey` set as key usages
|
92
|
+
* and derives encryption keys from the material and sets it on the key ring buffer
|
93
|
+
* together with the material
|
94
|
+
* also resets the valid key property and updates the currentKeyIndex
|
95
|
+
*/
|
96
|
+
async setKey(material: CryptoKey, keyIndex = 0) {
|
97
|
+
await this.setKeyFromMaterial(material, keyIndex);
|
98
|
+
this.hasValidKey = true;
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* takes in a key material with `deriveBits` and `deriveKey` set as key usages
|
103
|
+
* and derives encryption keys from the material and sets it on the key ring buffer
|
104
|
+
* together with the material
|
105
|
+
* also updates the currentKeyIndex
|
106
|
+
*/
|
107
|
+
async setKeyFromMaterial(material: CryptoKey, keyIndex = 0, emitRatchetEvent = false) {
|
108
|
+
workerLogger.debug('setting new key');
|
109
|
+
if (keyIndex >= 0) {
|
110
|
+
this.currentKeyIndex = keyIndex % this.cryptoKeyRing.length;
|
111
|
+
}
|
112
|
+
const keySet = await deriveKeys(material, this.keyProviderOptions.ratchetSalt);
|
113
|
+
this.setKeySet(keySet, this.currentKeyIndex, emitRatchetEvent);
|
114
|
+
}
|
115
|
+
|
116
|
+
async setKeySet(keySet: KeySet, keyIndex: number, emitRatchetEvent = false) {
|
117
|
+
this.cryptoKeyRing[keyIndex % this.cryptoKeyRing.length] = keySet;
|
118
|
+
if (emitRatchetEvent) {
|
119
|
+
this.emit('keyRatcheted', keySet.material, keyIndex, this.participantId);
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
async setCurrentKeyIndex(index: number) {
|
124
|
+
this.currentKeyIndex = index % this.cryptoKeyRing.length;
|
125
|
+
this.hasValidKey = true;
|
126
|
+
}
|
127
|
+
|
128
|
+
isEnabled() {
|
129
|
+
return this.enabled;
|
130
|
+
}
|
131
|
+
|
132
|
+
getCurrentKeyIndex() {
|
133
|
+
return this.currentKeyIndex;
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* returns currently used KeySet or the one at `keyIndex` if provided
|
138
|
+
* @param keyIndex
|
139
|
+
* @returns
|
140
|
+
*/
|
141
|
+
getKeySet(keyIndex?: number) {
|
142
|
+
return this.cryptoKeyRing[keyIndex ?? this.currentKeyIndex];
|
143
|
+
}
|
144
|
+
}
|
@@ -0,0 +1,223 @@
|
|
1
|
+
import { workerLogger } from '../../logger';
|
2
|
+
import { KEY_PROVIDER_DEFAULTS } from '../constants';
|
3
|
+
import { CryptorErrorReason } from '../errors';
|
4
|
+
import type {
|
5
|
+
E2EEWorkerMessage,
|
6
|
+
EnableMessage,
|
7
|
+
ErrorMessage,
|
8
|
+
KeyProviderOptions,
|
9
|
+
RatchetMessage,
|
10
|
+
RatchetRequestMessage,
|
11
|
+
} from '../types';
|
12
|
+
import { FrameCryptor } from './FrameCryptor';
|
13
|
+
import { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
14
|
+
|
15
|
+
const participantCryptors: FrameCryptor[] = [];
|
16
|
+
const participantKeys: Map<string, ParticipantKeyHandler> = new Map();
|
17
|
+
|
18
|
+
let publishCryptors: FrameCryptor[] = [];
|
19
|
+
let publisherKeys: ParticipantKeyHandler;
|
20
|
+
|
21
|
+
let isEncryptionEnabled: boolean = false;
|
22
|
+
|
23
|
+
let useSharedKey: boolean = false;
|
24
|
+
|
25
|
+
let sharedKey: CryptoKey | undefined;
|
26
|
+
|
27
|
+
let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
|
28
|
+
|
29
|
+
workerLogger.setDefaultLevel('info');
|
30
|
+
|
31
|
+
onmessage = (ev) => {
|
32
|
+
const { kind, data }: E2EEWorkerMessage = ev.data;
|
33
|
+
|
34
|
+
switch (kind) {
|
35
|
+
case 'init':
|
36
|
+
workerLogger.info('worker initialized');
|
37
|
+
keyProviderOptions = data.keyProviderOptions;
|
38
|
+
useSharedKey = !!data.keyProviderOptions.sharedKey;
|
39
|
+
// acknowledge init successful
|
40
|
+
const enableMsg: EnableMessage = {
|
41
|
+
kind: 'enable',
|
42
|
+
data: { enabled: isEncryptionEnabled },
|
43
|
+
};
|
44
|
+
publisherKeys = new ParticipantKeyHandler(undefined, isEncryptionEnabled, keyProviderOptions);
|
45
|
+
publisherKeys.on('keyRatcheted', emitRatchetedKeys);
|
46
|
+
postMessage(enableMsg);
|
47
|
+
break;
|
48
|
+
case 'enable':
|
49
|
+
setEncryptionEnabled(data.enabled, data.participantId);
|
50
|
+
workerLogger.info('updated e2ee enabled status');
|
51
|
+
// acknowledge enable call successful
|
52
|
+
postMessage(ev.data);
|
53
|
+
break;
|
54
|
+
case 'decode':
|
55
|
+
let cryptor = getTrackCryptor(data.participantId, data.trackId);
|
56
|
+
cryptor.setupTransform(
|
57
|
+
kind,
|
58
|
+
data.readableStream,
|
59
|
+
data.writableStream,
|
60
|
+
data.trackId,
|
61
|
+
data.codec,
|
62
|
+
);
|
63
|
+
break;
|
64
|
+
case 'encode':
|
65
|
+
let pubCryptor = getPublisherCryptor(data.trackId);
|
66
|
+
pubCryptor.setupTransform(
|
67
|
+
kind,
|
68
|
+
data.readableStream,
|
69
|
+
data.writableStream,
|
70
|
+
data.trackId,
|
71
|
+
data.codec,
|
72
|
+
);
|
73
|
+
break;
|
74
|
+
case 'setKey':
|
75
|
+
if (useSharedKey) {
|
76
|
+
workerLogger.debug('set shared key');
|
77
|
+
setSharedKey(data.key, data.keyIndex);
|
78
|
+
} else if (data.participantId) {
|
79
|
+
getParticipantKeyHandler(data.participantId).setKey(data.key, data.keyIndex);
|
80
|
+
} else {
|
81
|
+
workerLogger.error('no participant Id was provided and shared key usage is disabled');
|
82
|
+
}
|
83
|
+
break;
|
84
|
+
case 'removeTransform':
|
85
|
+
unsetCryptorParticipant(data.trackId);
|
86
|
+
break;
|
87
|
+
case 'updateCodec':
|
88
|
+
getTrackCryptor(data.participantId, data.trackId).setVideoCodec(data.codec);
|
89
|
+
break;
|
90
|
+
case 'setRTPMap':
|
91
|
+
publishCryptors.forEach((cr) => {
|
92
|
+
cr.setRtpMap(data.map);
|
93
|
+
});
|
94
|
+
break;
|
95
|
+
case 'ratchetRequest':
|
96
|
+
handleRatchetRequest(data);
|
97
|
+
default:
|
98
|
+
break;
|
99
|
+
}
|
100
|
+
};
|
101
|
+
|
102
|
+
async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
|
103
|
+
const keyHandler = getParticipantKeyHandler(data.participantId);
|
104
|
+
await keyHandler.ratchetKey(data.keyIndex);
|
105
|
+
keyHandler.hasValidKey = true;
|
106
|
+
}
|
107
|
+
|
108
|
+
function getTrackCryptor(participantId: string, trackId: string) {
|
109
|
+
let cryptor = participantCryptors.find((c) => c.getTrackId() === trackId);
|
110
|
+
if (!cryptor) {
|
111
|
+
workerLogger.info('creating new cryptor for', { participantId });
|
112
|
+
if (!keyProviderOptions) {
|
113
|
+
throw Error('Missing keyProvider options');
|
114
|
+
}
|
115
|
+
cryptor = new FrameCryptor({
|
116
|
+
participantId,
|
117
|
+
keys: getParticipantKeyHandler(participantId),
|
118
|
+
keyProviderOptions,
|
119
|
+
});
|
120
|
+
|
121
|
+
setupCryptorErrorEvents(cryptor);
|
122
|
+
participantCryptors.push(cryptor);
|
123
|
+
} else if (participantId !== cryptor.getParticipantId()) {
|
124
|
+
// assign new participant id to track cryptor and pass in correct key handler
|
125
|
+
cryptor.setParticipant(participantId, getParticipantKeyHandler(participantId));
|
126
|
+
}
|
127
|
+
if (sharedKey) {
|
128
|
+
}
|
129
|
+
return cryptor;
|
130
|
+
}
|
131
|
+
|
132
|
+
function getParticipantKeyHandler(participantId?: string) {
|
133
|
+
if (!participantId) {
|
134
|
+
return publisherKeys!;
|
135
|
+
}
|
136
|
+
let keys = participantKeys.get(participantId);
|
137
|
+
if (!keys) {
|
138
|
+
keys = new ParticipantKeyHandler(participantId, true, keyProviderOptions);
|
139
|
+
if (sharedKey) {
|
140
|
+
keys.setKey(sharedKey);
|
141
|
+
}
|
142
|
+
participantKeys.set(participantId, keys);
|
143
|
+
}
|
144
|
+
return keys;
|
145
|
+
}
|
146
|
+
|
147
|
+
function unsetCryptorParticipant(trackId: string) {
|
148
|
+
participantCryptors.find((c) => c.getTrackId() === trackId)?.unsetParticipant();
|
149
|
+
}
|
150
|
+
|
151
|
+
function getPublisherCryptor(trackId: string) {
|
152
|
+
let publishCryptor = publishCryptors.find((cryptor) => cryptor.getTrackId() === trackId);
|
153
|
+
if (!publishCryptor) {
|
154
|
+
if (!keyProviderOptions) {
|
155
|
+
throw new TypeError('Missing keyProvider options');
|
156
|
+
}
|
157
|
+
publishCryptor = new FrameCryptor({
|
158
|
+
keys: publisherKeys!,
|
159
|
+
participantId: 'publisher',
|
160
|
+
keyProviderOptions,
|
161
|
+
});
|
162
|
+
setupCryptorErrorEvents(publishCryptor);
|
163
|
+
publishCryptors.push(publishCryptor);
|
164
|
+
}
|
165
|
+
return publishCryptor;
|
166
|
+
}
|
167
|
+
|
168
|
+
function setEncryptionEnabled(enable: boolean, participantId?: string) {
|
169
|
+
if (!participantId) {
|
170
|
+
isEncryptionEnabled = enable;
|
171
|
+
publisherKeys.setEnabled(enable);
|
172
|
+
} else {
|
173
|
+
getParticipantKeyHandler(participantId).setEnabled(enable);
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
function setSharedKey(key: CryptoKey, index?: number) {
|
178
|
+
workerLogger.debug('setting shared key');
|
179
|
+
sharedKey = key;
|
180
|
+
publisherKeys?.setKey(key, index);
|
181
|
+
for (const [, keyHandler] of participantKeys) {
|
182
|
+
keyHandler.setKey(key, index);
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
function setupCryptorErrorEvents(cryptor: FrameCryptor) {
|
187
|
+
cryptor.on('cryptorError', (error) => {
|
188
|
+
const msg: ErrorMessage = {
|
189
|
+
kind: 'error',
|
190
|
+
data: { error: new Error(`${CryptorErrorReason[error.reason]}: ${error.message}`) },
|
191
|
+
};
|
192
|
+
postMessage(msg);
|
193
|
+
});
|
194
|
+
}
|
195
|
+
|
196
|
+
function emitRatchetedKeys(material: CryptoKey, keyIndex?: number) {
|
197
|
+
const msg: RatchetMessage = {
|
198
|
+
kind: `ratchetKey`,
|
199
|
+
data: {
|
200
|
+
// participantId,
|
201
|
+
keyIndex,
|
202
|
+
material,
|
203
|
+
},
|
204
|
+
};
|
205
|
+
postMessage(msg);
|
206
|
+
}
|
207
|
+
|
208
|
+
// Operations using RTCRtpScriptTransform.
|
209
|
+
// @ts-ignore
|
210
|
+
if (self.RTCTransformEvent) {
|
211
|
+
workerLogger.debug('setup transform event');
|
212
|
+
// @ts-ignore
|
213
|
+
self.onrtctransform = (event) => {
|
214
|
+
const transformer = event.transformer;
|
215
|
+
workerLogger.debug('transformer', transformer);
|
216
|
+
transformer.handled = true;
|
217
|
+
const { kind, participantId, trackId, codec } = transformer.options;
|
218
|
+
const cryptor =
|
219
|
+
kind === 'encode' ? getPublisherCryptor(trackId) : getTrackCryptor(participantId, trackId);
|
220
|
+
workerLogger.debug('transform', { codec });
|
221
|
+
cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec);
|
222
|
+
};
|
223
|
+
}
|
package/src/index.ts
CHANGED
@@ -41,6 +41,7 @@ export { facingModeFromDeviceLabel, facingModeFromLocalTrack } from './room/trac
|
|
41
41
|
export * from './room/track/types';
|
42
42
|
export type { DataPublishOptions, SimulationScenario } from './room/types';
|
43
43
|
export * from './version';
|
44
|
+
export * from './e2ee';
|
44
45
|
export * from './room/track/processor/types';
|
45
46
|
export {
|
46
47
|
setLogLevel,
|
package/src/logger.ts
CHANGED
@@ -17,6 +17,7 @@ type StructuredLogger = {
|
|
17
17
|
info: (msg: string, context?: object) => void;
|
18
18
|
warn: (msg: string, context?: object) => void;
|
19
19
|
error: (msg: string, context?: object) => void;
|
20
|
+
setDefaultLevel: (level: log.LogLevelDesc) => void;
|
20
21
|
};
|
21
22
|
|
22
23
|
const livekitLogger = log.getLogger('livekit');
|
@@ -25,8 +26,13 @@ livekitLogger.setDefaultLevel(LogLevel.info);
|
|
25
26
|
|
26
27
|
export default livekitLogger as StructuredLogger;
|
27
28
|
|
28
|
-
export function setLogLevel(level: LogLevel | LogLevelString) {
|
29
|
-
|
29
|
+
export function setLogLevel(level: LogLevel | LogLevelString, loggerName?: 'livekit' | 'lk-e2ee') {
|
30
|
+
if (loggerName) {
|
31
|
+
log.getLogger(loggerName).setLevel(level);
|
32
|
+
}
|
33
|
+
for (const logger of Object.values(log.getLoggers())) {
|
34
|
+
logger.setLevel(level);
|
35
|
+
}
|
30
36
|
}
|
31
37
|
|
32
38
|
export type LogExtension = (level: LogLevel, msg: string, context?: object) => void;
|
@@ -54,3 +60,5 @@ export function setLogExtension(extension: LogExtension) {
|
|
54
60
|
};
|
55
61
|
livekitLogger.setLevel(livekitLogger.getLevel()); // Be sure to call setLevel method in order to apply plugin
|
56
62
|
}
|
63
|
+
|
64
|
+
export const workerLogger = log.getLogger('lk-e2ee') as StructuredLogger;
|
package/src/options.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import type { E2EEOptions } from './e2ee/types';
|
1
2
|
import type { ReconnectPolicy } from './room/ReconnectPolicy';
|
2
3
|
import type {
|
3
4
|
AudioCaptureOptions,
|
@@ -83,6 +84,11 @@ export interface InternalRoomOptions {
|
|
83
84
|
*/
|
84
85
|
|
85
86
|
expWebAudioMix: boolean | WebAudioSettings;
|
87
|
+
|
88
|
+
/**
|
89
|
+
* @experimental
|
90
|
+
*/
|
91
|
+
e2ee?: E2EEOptions;
|
86
92
|
}
|
87
93
|
|
88
94
|
/**
|
@@ -378,7 +378,7 @@ export function disconnectReasonToJSON(object: DisconnectReason): string {
|
|
378
378
|
}
|
379
379
|
|
380
380
|
export enum ReconnectReason {
|
381
|
-
|
381
|
+
RR_UNKNOWN = 0,
|
382
382
|
RR_SIGNAL_DISCONNECTED = 1,
|
383
383
|
RR_PUBLISHER_FAILED = 2,
|
384
384
|
RR_SUBSCRIBER_FAILED = 3,
|
@@ -389,8 +389,8 @@ export enum ReconnectReason {
|
|
389
389
|
export function reconnectReasonFromJSON(object: any): ReconnectReason {
|
390
390
|
switch (object) {
|
391
391
|
case 0:
|
392
|
-
case "
|
393
|
-
return ReconnectReason.
|
392
|
+
case "RR_UNKNOWN":
|
393
|
+
return ReconnectReason.RR_UNKNOWN;
|
394
394
|
case 1:
|
395
395
|
case "RR_SIGNAL_DISCONNECTED":
|
396
396
|
return ReconnectReason.RR_SIGNAL_DISCONNECTED;
|
@@ -412,8 +412,8 @@ export function reconnectReasonFromJSON(object: any): ReconnectReason {
|
|
412
412
|
|
413
413
|
export function reconnectReasonToJSON(object: ReconnectReason): string {
|
414
414
|
switch (object) {
|
415
|
-
case ReconnectReason.
|
416
|
-
return "
|
415
|
+
case ReconnectReason.RR_UNKNOWN:
|
416
|
+
return "RR_UNKNOWN";
|
417
417
|
case ReconnectReason.RR_SIGNAL_DISCONNECTED:
|
418
418
|
return "RR_SIGNAL_DISCONNECTED";
|
419
419
|
case ReconnectReason.RR_PUBLISHER_FAILED:
|
@@ -429,7 +429,7 @@ export function reconnectReasonToJSON(object: ReconnectReason): string {
|
|
429
429
|
}
|
430
430
|
|
431
431
|
export enum SubscriptionError {
|
432
|
-
|
432
|
+
SE_UNKNOWN = 0,
|
433
433
|
SE_CODEC_UNSUPPORTED = 1,
|
434
434
|
SE_TRACK_NOTFOUND = 2,
|
435
435
|
UNRECOGNIZED = -1,
|
@@ -438,8 +438,8 @@ export enum SubscriptionError {
|
|
438
438
|
export function subscriptionErrorFromJSON(object: any): SubscriptionError {
|
439
439
|
switch (object) {
|
440
440
|
case 0:
|
441
|
-
case "
|
442
|
-
return SubscriptionError.
|
441
|
+
case "SE_UNKNOWN":
|
442
|
+
return SubscriptionError.SE_UNKNOWN;
|
443
443
|
case 1:
|
444
444
|
case "SE_CODEC_UNSUPPORTED":
|
445
445
|
return SubscriptionError.SE_CODEC_UNSUPPORTED;
|
@@ -455,8 +455,8 @@ export function subscriptionErrorFromJSON(object: any): SubscriptionError {
|
|
455
455
|
|
456
456
|
export function subscriptionErrorToJSON(object: SubscriptionError): string {
|
457
457
|
switch (object) {
|
458
|
-
case SubscriptionError.
|
459
|
-
return "
|
458
|
+
case SubscriptionError.SE_UNKNOWN:
|
459
|
+
return "SE_UNKNOWN";
|
460
460
|
case SubscriptionError.SE_CODEC_UNSUPPORTED:
|
461
461
|
return "SE_CODEC_UNSUPPORTED";
|
462
462
|
case SubscriptionError.SE_TRACK_NOTFOUND:
|
@@ -3970,8 +3970,8 @@ function toTimestamp(date: Date): Timestamp {
|
|
3970
3970
|
}
|
3971
3971
|
|
3972
3972
|
function fromTimestamp(t: Timestamp): Date {
|
3973
|
-
let millis = t.seconds * 1_000;
|
3974
|
-
millis += t.nanos / 1_000_000;
|
3973
|
+
let millis = (t.seconds || 0) * 1_000;
|
3974
|
+
millis += (t.nanos || 0) / 1_000_000;
|
3975
3975
|
return new Date(millis);
|
3976
3976
|
}
|
3977
3977
|
|
package/src/room/PCTransport.ts
CHANGED
@@ -3,7 +3,7 @@ import { parse, write } from 'sdp-transform';
|
|
3
3
|
import type { MediaDescription } from 'sdp-transform';
|
4
4
|
import { debounce } from 'ts-debounce';
|
5
5
|
import log from '../logger';
|
6
|
-
import { NegotiationError } from './errors';
|
6
|
+
import { NegotiationError, UnexpectedConnectionState } from './errors';
|
7
7
|
import { ddExtensionURI, isChromiumBased, isSVCCodec } from './utils';
|
8
8
|
|
9
9
|
/** @internal */
|
@@ -25,11 +25,17 @@ const startBitrateForSVC = 0.7;
|
|
25
25
|
export const PCEvents = {
|
26
26
|
NegotiationStarted: 'negotiationStarted',
|
27
27
|
NegotiationComplete: 'negotiationComplete',
|
28
|
+
RTPVideoPayloadTypes: 'rtpVideoPayloadTypes',
|
28
29
|
} as const;
|
29
30
|
|
30
31
|
/** @internal */
|
31
32
|
export default class PCTransport extends EventEmitter {
|
32
|
-
|
33
|
+
private _pc: RTCPeerConnection | null;
|
34
|
+
|
35
|
+
public get pc() {
|
36
|
+
if (this._pc) return this._pc;
|
37
|
+
throw new UnexpectedConnectionState('Expected peer connection to be available');
|
38
|
+
}
|
33
39
|
|
34
40
|
pendingCandidates: RTCIceCandidateInit[] = [];
|
35
41
|
|
@@ -47,14 +53,17 @@ export default class PCTransport extends EventEmitter {
|
|
47
53
|
|
48
54
|
constructor(config?: RTCConfiguration, mediaConstraints: Record<string, unknown> = {}) {
|
49
55
|
super();
|
50
|
-
this.
|
56
|
+
this._pc = isChromiumBased()
|
51
57
|
? // @ts-expect-error chrome allows additional media constraints to be passed into the RTCPeerConnection constructor
|
52
58
|
new RTCPeerConnection(config, mediaConstraints)
|
53
59
|
: new RTCPeerConnection(config);
|
54
60
|
}
|
55
61
|
|
56
62
|
get isICEConnected(): boolean {
|
57
|
-
return
|
63
|
+
return (
|
64
|
+
this._pc !== null &&
|
65
|
+
(this.pc.iceConnectionState === 'connected' || this.pc.iceConnectionState === 'completed')
|
66
|
+
);
|
58
67
|
}
|
59
68
|
|
60
69
|
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
@@ -136,6 +145,14 @@ export default class PCTransport extends EventEmitter {
|
|
136
145
|
this.createAndSendOffer();
|
137
146
|
} else if (sd.type === 'answer') {
|
138
147
|
this.emit(PCEvents.NegotiationComplete);
|
148
|
+
if (sd.sdp) {
|
149
|
+
const sdpParsed = parse(sd.sdp);
|
150
|
+
sdpParsed.media.forEach((media) => {
|
151
|
+
if (media.type === 'video') {
|
152
|
+
this.emit(PCEvents.RTPVideoPayloadTypes, media.rtp);
|
153
|
+
}
|
154
|
+
});
|
155
|
+
}
|
139
156
|
}
|
140
157
|
}
|
141
158
|
|
@@ -163,7 +180,7 @@ export default class PCTransport extends EventEmitter {
|
|
163
180
|
this.restartingIce = true;
|
164
181
|
}
|
165
182
|
|
166
|
-
if (this.
|
183
|
+
if (this._pc && this._pc.signalingState === 'have-local-offer') {
|
167
184
|
// we're waiting for the peer to accept our offer, so we'll just wait
|
168
185
|
// the only exception to this is when ICE restart is needed
|
169
186
|
const currentSD = this.pc.remoteDescription;
|
@@ -175,7 +192,7 @@ export default class PCTransport extends EventEmitter {
|
|
175
192
|
this.renegotiate = true;
|
176
193
|
return;
|
177
194
|
}
|
178
|
-
} else if (this.
|
195
|
+
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
179
196
|
log.warn('could not createOffer with closed peer connection');
|
180
197
|
return;
|
181
198
|
}
|
@@ -258,9 +275,22 @@ export default class PCTransport extends EventEmitter {
|
|
258
275
|
}
|
259
276
|
|
260
277
|
close() {
|
261
|
-
this.
|
262
|
-
|
263
|
-
|
278
|
+
if (!this._pc) {
|
279
|
+
return;
|
280
|
+
}
|
281
|
+
this._pc.close();
|
282
|
+
this._pc.onconnectionstatechange = null;
|
283
|
+
this._pc.oniceconnectionstatechange = null;
|
284
|
+
this._pc.onicegatheringstatechange = null;
|
285
|
+
this._pc.ondatachannel = null;
|
286
|
+
this._pc.onnegotiationneeded = null;
|
287
|
+
this._pc.onsignalingstatechange = null;
|
288
|
+
this._pc.onicecandidate = null;
|
289
|
+
this._pc.ondatachannel = null;
|
290
|
+
this._pc.ontrack = null;
|
291
|
+
this._pc.onconnectionstatechange = null;
|
292
|
+
this._pc.oniceconnectionstatechange = null;
|
293
|
+
this._pc = null;
|
264
294
|
}
|
265
295
|
|
266
296
|
private async setMungedSDP(sd: RTCSessionDescriptionInit, munged?: string, remote?: boolean) {
|