livekit-client 2.22.1 → 2.22.3
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 +663 -476
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +480 -157
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +8 -1
- package/dist/livekit-client.fm.worker.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.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +33 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/constants.d.ts +5 -0
- package/dist/src/e2ee/constants.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +24 -3
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
- package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +43 -19
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +12 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +16 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +1 -1
- package/dist/src/room/utils.d.ts +34 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/utils.d.ts +1 -0
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +33 -0
- package/dist/ts4.2/e2ee/constants.d.ts +5 -0
- package/dist/ts4.2/e2ee/types.d.ts +24 -3
- package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +43 -19
- package/dist/ts4.2/logger.d.ts +4 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +12 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +1 -0
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +17 -17
- package/dist/ts4.2/room/participant/publishUtils.d.ts +16 -0
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/ts4.2/room/track/options.d.ts +1 -1
- package/dist/ts4.2/room/utils.d.ts +34 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +2 -1
- package/src/api/WebSocketStream.ts +3 -8
- package/src/api/utils.ts +10 -0
- package/src/e2ee/E2eeManager.test.ts +196 -0
- package/src/e2ee/E2eeManager.ts +150 -31
- package/src/e2ee/constants.ts +6 -0
- package/src/e2ee/subscriberBlackScreen.test.ts +544 -0
- package/src/e2ee/types.ts +28 -3
- package/src/e2ee/worker/DataCryptor.ts +2 -1
- package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
- package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
- package/src/e2ee/worker/FrameCryptor.race.test.ts +9 -26
- package/src/e2ee/worker/FrameCryptor.test.ts +0 -1
- package/src/e2ee/worker/FrameCryptor.ts +202 -119
- package/src/e2ee/worker/e2ee.worker.ts +72 -17
- package/src/logger.ts +22 -0
- package/src/room/PCTransportManager.test.ts +35 -0
- package/src/room/PCTransportManager.ts +12 -4
- package/src/room/RTCEngine.ts +28 -7
- package/src/room/Room.ts +1 -1
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +171 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +17 -18
- package/src/room/data-stream/incoming/StreamReader.ts +20 -50
- package/src/room/participant/LocalParticipant.ts +30 -14
- package/src/room/participant/publishUtils.test.ts +133 -0
- package/src/room/participant/publishUtils.ts +54 -19
- package/src/room/track/LocalVideoTrack.ts +15 -5
- package/src/room/track/options.ts +1 -1
- package/src/room/utils.test.ts +87 -0
- package/src/room/utils.ts +59 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { LogLevel, getWorkerLogLevelListenerCount, setLogLevel, workerLogger } from '../logger';
|
|
3
|
+
import Room from '../room/Room';
|
|
4
|
+
import { E2EEManager } from './E2eeManager';
|
|
5
|
+
import { BaseKeyProvider } from './KeyProvider';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Install just enough of the DOM to let isE2EESupported() return true so
|
|
9
|
+
* setup() doesn't throw.
|
|
10
|
+
*/
|
|
11
|
+
function installE2EEShims() {
|
|
12
|
+
const w = window as unknown as Record<string, any>;
|
|
13
|
+
if (typeof w.RTCRtpSender === 'undefined') {
|
|
14
|
+
w.RTCRtpSender = class {};
|
|
15
|
+
}
|
|
16
|
+
w.RTCRtpSender.prototype.createEncodedStreams = () => {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class FakeWorker {
|
|
20
|
+
postMessage = vi.fn();
|
|
21
|
+
|
|
22
|
+
onmessage: unknown = null;
|
|
23
|
+
|
|
24
|
+
onerror: unknown = null;
|
|
25
|
+
|
|
26
|
+
levelMessages(): LogLevel[] {
|
|
27
|
+
return this.postMessage.mock.calls
|
|
28
|
+
.map(([m]) => m)
|
|
29
|
+
.filter((m: any) => m?.kind === 'setLogLevel')
|
|
30
|
+
.map((m: any) => m.data.level);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeManager() {
|
|
35
|
+
installE2EEShims();
|
|
36
|
+
const room = new Room();
|
|
37
|
+
const worker = new FakeWorker();
|
|
38
|
+
const manager = new E2EEManager(
|
|
39
|
+
{ keyProvider: new BaseKeyProvider({ sharedKey: true }), worker: worker as unknown as Worker },
|
|
40
|
+
false,
|
|
41
|
+
);
|
|
42
|
+
return { room, worker, manager };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('E2EEManager log-level listener lifecycle', () => {
|
|
46
|
+
const startingLevel = workerLogger.getLevel();
|
|
47
|
+
const startingCount = getWorkerLogLevelListenerCount();
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
setLogLevel(startingLevel);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('forwards level changes to the worker while subscribed', () => {
|
|
54
|
+
const { room, worker, manager } = makeManager();
|
|
55
|
+
manager.setup(room);
|
|
56
|
+
worker.postMessage.mockClear();
|
|
57
|
+
|
|
58
|
+
setLogLevel(LogLevel.debug);
|
|
59
|
+
|
|
60
|
+
expect(worker.levelMessages()).toEqual([LogLevel.debug]);
|
|
61
|
+
manager.dispose();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('dispose() removes the listener and stops forwarding', () => {
|
|
65
|
+
const { room, worker, manager } = makeManager();
|
|
66
|
+
manager.setup(room);
|
|
67
|
+
manager.dispose();
|
|
68
|
+
worker.postMessage.mockClear();
|
|
69
|
+
|
|
70
|
+
setLogLevel(LogLevel.warn);
|
|
71
|
+
|
|
72
|
+
expect(worker.postMessage).not.toHaveBeenCalled();
|
|
73
|
+
expect(getWorkerLogLevelListenerCount()).toBe(startingCount);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('re-setup with a new room does not stack listeners', () => {
|
|
77
|
+
const { worker, manager } = makeManager();
|
|
78
|
+
const roomA = new Room();
|
|
79
|
+
const roomB = new Room();
|
|
80
|
+
manager.setup(roomA);
|
|
81
|
+
const countAfterFirst = getWorkerLogLevelListenerCount();
|
|
82
|
+
manager.setup(roomB);
|
|
83
|
+
expect(getWorkerLogLevelListenerCount()).toBe(countAfterFirst);
|
|
84
|
+
|
|
85
|
+
worker.postMessage.mockClear();
|
|
86
|
+
setLogLevel(LogLevel.debug);
|
|
87
|
+
expect(worker.levelMessages()).toEqual([LogLevel.debug]); // exactly one delivery
|
|
88
|
+
|
|
89
|
+
manager.dispose();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('dispose() is idempotent', () => {
|
|
93
|
+
const { room, manager } = makeManager();
|
|
94
|
+
manager.setup(room);
|
|
95
|
+
manager.dispose();
|
|
96
|
+
manager.dispose();
|
|
97
|
+
expect(getWorkerLogLevelListenerCount()).toBe(startingCount);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('dispose() rejects pending encrypt/decrypt futures and clears both maps', async () => {
|
|
101
|
+
const { room, manager } = makeManager();
|
|
102
|
+
manager.setup(room);
|
|
103
|
+
|
|
104
|
+
const encrypting = manager.encryptData(new Uint8Array([1, 2, 3]) as any);
|
|
105
|
+
const decrypting = manager.handleEncryptedData(
|
|
106
|
+
new Uint8Array([4, 5, 6]) as any,
|
|
107
|
+
new Uint8Array([7, 8, 9]) as any,
|
|
108
|
+
'peer',
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const priv = manager as unknown as {
|
|
113
|
+
encryptDataRequests: Map<string, unknown>;
|
|
114
|
+
decryptDataRequests: Map<string, unknown>;
|
|
115
|
+
};
|
|
116
|
+
expect(priv.encryptDataRequests.size).toBe(1);
|
|
117
|
+
expect(priv.decryptDataRequests.size).toBe(1);
|
|
118
|
+
|
|
119
|
+
manager.dispose();
|
|
120
|
+
|
|
121
|
+
await expect(encrypting).rejects.toThrow(/disposed/);
|
|
122
|
+
await expect(decrypting).rejects.toThrow(/disposed/);
|
|
123
|
+
expect(priv.encryptDataRequests.size).toBe(0);
|
|
124
|
+
expect(priv.decryptDataRequests.size).toBe(0);
|
|
125
|
+
|
|
126
|
+
// Second dispose while maps are empty must not throw.
|
|
127
|
+
expect(() => manager.dispose()).not.toThrow();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* GC-path test. Flaky by construction — FinalizationRegistry callbacks are
|
|
133
|
+
* best-effort. Skipped unless vitest is run with `--expose-gc`:
|
|
134
|
+
*
|
|
135
|
+
* NODE_OPTIONS="--expose-gc" pnpm exec vitest run src/e2ee/E2eeManager.test.ts
|
|
136
|
+
*
|
|
137
|
+
* Deliberately bypasses `manager.setup(room)`. `new Room()` on its own is not
|
|
138
|
+
* collectable in this test environment (device-change listeners, timers), and
|
|
139
|
+
* that leak is not what this test is about — it would only mask what we
|
|
140
|
+
* actually want to verify: that the log-level listener wiring holds nothing
|
|
141
|
+
* strongly.
|
|
142
|
+
*/
|
|
143
|
+
describe('E2EEManager GC cleanup', () => {
|
|
144
|
+
const startingLevel = workerLogger.getLevel();
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
installE2EEShims();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
setLogLevel(startingLevel);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it.skipIf(!(globalThis as any).gc)(
|
|
155
|
+
'releases the log-level listener when the manager is garbage collected',
|
|
156
|
+
async () => {
|
|
157
|
+
const before = getWorkerLogLevelListenerCount();
|
|
158
|
+
|
|
159
|
+
// Construct + subscribe in an IIFE so nothing lives on the test's stack.
|
|
160
|
+
// Direct call to the private subscription — no Room, no leaky graph.
|
|
161
|
+
const managerRef = ((): WeakRef<E2EEManager> => {
|
|
162
|
+
const worker = new FakeWorker();
|
|
163
|
+
const manager = new E2EEManager(
|
|
164
|
+
{
|
|
165
|
+
keyProvider: new BaseKeyProvider({ sharedKey: true }),
|
|
166
|
+
worker: worker as unknown as Worker,
|
|
167
|
+
},
|
|
168
|
+
false,
|
|
169
|
+
);
|
|
170
|
+
(manager as unknown as { subscribeToLogLevelChanges(): void }).subscribeToLogLevelChanges();
|
|
171
|
+
expect(getWorkerLogLevelListenerCount()).toBe(before + 1);
|
|
172
|
+
return new WeakRef(manager);
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
// Full major GC + macrotask yield in a loop, with allocation pressure to
|
|
176
|
+
// force the major sweep FinalizationRegistry needs.
|
|
177
|
+
//
|
|
178
|
+
// Crucial: do NOT call `managerRef.deref()` inside the loop. Per spec,
|
|
179
|
+
// `WeakRef.prototype.deref` keeps the referent alive until the end of the
|
|
180
|
+
// current job — calling it in the check would pin the manager forever.
|
|
181
|
+
// Read the listener count (which does not touch the referent) instead.
|
|
182
|
+
const gc = (globalThis as any).gc as (opts?: { type?: 'major'; execution?: 'sync' }) => void;
|
|
183
|
+
for (let i = 0; i < 50; i++) {
|
|
184
|
+
// eslint-disable-next-line no-void
|
|
185
|
+
void new Array(100_000).fill({ i });
|
|
186
|
+
gc({ type: 'major', execution: 'sync' });
|
|
187
|
+
await new Promise((r) => setImmediate(r));
|
|
188
|
+
if (getWorkerLogLevelListenerCount() === before) break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Diagnostic: separate "manager wasn't collected" from "FR didn't fire".
|
|
192
|
+
expect(managerRef.deref(), 'manager was not collected — strong ref leaked').toBeUndefined();
|
|
193
|
+
expect(getWorkerLogLevelListenerCount()).toBe(before);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
});
|
package/src/e2ee/E2eeManager.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from 'events';
|
|
|
3
3
|
import type TypedEventEmitter from 'typed-emitter';
|
|
4
4
|
import type { FrameMetadata } from '../frameMetadata/types';
|
|
5
5
|
import { hasFrameMetadataPublishOptions } from '../frameMetadata/utils';
|
|
6
|
-
import
|
|
6
|
+
import { LogLevel, LoggerNames, getLogger, onWorkerLogLevelChanged, workerLogger } from '../logger';
|
|
7
7
|
import type RTCEngine from '../room/RTCEngine';
|
|
8
8
|
import type Room from '../room/Room';
|
|
9
9
|
import { ConnectionState } from '../room/Room';
|
|
@@ -12,6 +12,7 @@ import { EngineEvent, ParticipantEvent, RoomEvent } from '../room/events';
|
|
|
12
12
|
import type RemoteTrack from '../room/track/RemoteTrack';
|
|
13
13
|
import RemoteVideoTrack from '../room/track/RemoteVideoTrack';
|
|
14
14
|
import type { Track } from '../room/track/Track';
|
|
15
|
+
import type { TrackPublication } from '../room/track/TrackPublication';
|
|
15
16
|
import type { TrackPublishOptions, VideoCodec } from '../room/track/options';
|
|
16
17
|
import { mimeTypeToVideoCodecString } from '../room/track/utils';
|
|
17
18
|
import {
|
|
@@ -23,7 +24,8 @@ import {
|
|
|
23
24
|
} from '../room/utils';
|
|
24
25
|
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
25
26
|
import type { BaseKeyProvider } from './KeyProvider';
|
|
26
|
-
import { E2EE_FLAG } from './constants';
|
|
27
|
+
import { E2EE_FLAG, E2EE_TRACK_ID } from './constants';
|
|
28
|
+
import { CryptorError, CryptorErrorReason } from './errors';
|
|
27
29
|
import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
|
|
28
30
|
import type {
|
|
29
31
|
DecryptDataRequestMessage,
|
|
@@ -61,6 +63,7 @@ export interface BaseE2EEManager {
|
|
|
61
63
|
keyIndex: number,
|
|
62
64
|
): Promise<DecryptDataResponseMessage['data']>;
|
|
63
65
|
on<E extends keyof E2EEManagerCallbacks>(event: E, listener: E2EEManagerCallbacks[E]): this;
|
|
66
|
+
dispose?(): void;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -86,6 +89,29 @@ export class E2EEManager
|
|
|
86
89
|
|
|
87
90
|
private dataChannelEncryptionEnabled: boolean;
|
|
88
91
|
|
|
92
|
+
private unsubscribeLogLevel?: () => void;
|
|
93
|
+
|
|
94
|
+
private log = getLogger(LoggerNames.E2EE, () => this.logContext);
|
|
95
|
+
|
|
96
|
+
get logContext() {
|
|
97
|
+
return {
|
|
98
|
+
room: this.room?.name,
|
|
99
|
+
participant: this.room?.localParticipant.identity,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Runs a cleanup callback once this manager is garbage collected. Lets the
|
|
105
|
+
* log-level listener (held in a module-global Set on the main-thread logger)
|
|
106
|
+
* fall out of scope even when the consumer forgets to call `dispose()`.
|
|
107
|
+
*/
|
|
108
|
+
private static disposeRegistry =
|
|
109
|
+
typeof FinalizationRegistry !== 'undefined' &&
|
|
110
|
+
typeof WeakRef !== 'undefined' &&
|
|
111
|
+
new FinalizationRegistry((cleanup: () => void) => {
|
|
112
|
+
cleanup();
|
|
113
|
+
});
|
|
114
|
+
|
|
89
115
|
constructor(options: E2EEManagerOptions, dcEncryptionEnabled: boolean) {
|
|
90
116
|
super();
|
|
91
117
|
this.keyProvider = options.keyProvider;
|
|
@@ -111,7 +137,7 @@ export class E2EEManager
|
|
|
111
137
|
'tried to setup end-to-end encryption on an unsupported browser',
|
|
112
138
|
);
|
|
113
139
|
}
|
|
114
|
-
log.info('setting up e2ee');
|
|
140
|
+
this.log.info('setting up e2ee');
|
|
115
141
|
if (room !== this.room) {
|
|
116
142
|
this.room = room;
|
|
117
143
|
this.setupEventListeners(room, this.keyProvider);
|
|
@@ -124,19 +150,89 @@ export class E2EEManager
|
|
|
124
150
|
},
|
|
125
151
|
};
|
|
126
152
|
if (this.worker) {
|
|
127
|
-
log.info(`initializing worker`, { worker: this.worker });
|
|
153
|
+
this.log.info(`initializing worker`, { worker: this.worker });
|
|
128
154
|
this.worker.onmessage = this.onWorkerMessage;
|
|
129
155
|
this.worker.onerror = this.onWorkerError;
|
|
130
156
|
this.worker.postMessage(msg);
|
|
157
|
+
this.subscribeToLogLevelChanges();
|
|
131
158
|
}
|
|
132
159
|
}
|
|
133
160
|
}
|
|
134
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Subscribe the current worker to main-thread `workerLogger` level changes,
|
|
164
|
+
* without strongly retaining `this` or `this.worker` from the module-global
|
|
165
|
+
* listener Set on the logger. See {@link disposeRegistry}.
|
|
166
|
+
*/
|
|
167
|
+
private subscribeToLogLevelChanges() {
|
|
168
|
+
// Guard against duplicate registration on re-setup.
|
|
169
|
+
this.unsubscribeLogLevel?.();
|
|
170
|
+
|
|
171
|
+
let unsub: (() => void) | undefined;
|
|
172
|
+
if (E2EEManager.disposeRegistry) {
|
|
173
|
+
// Modern engines: hold the worker weakly so the module-global listener Set
|
|
174
|
+
// on the logger can't retain this manager, and clean up the entry on GC.
|
|
175
|
+
const workerRef = new WeakRef(this.worker);
|
|
176
|
+
unsub = onWorkerLogLevelChanged((level) => {
|
|
177
|
+
const worker = workerRef.deref();
|
|
178
|
+
if (!worker) {
|
|
179
|
+
unsub?.();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
worker.postMessage({ kind: 'setLogLevel', data: { level } });
|
|
183
|
+
});
|
|
184
|
+
E2EEManager.disposeRegistry.register(this, unsub, this);
|
|
185
|
+
} else {
|
|
186
|
+
// Safari <14.1 and similar: no WeakRef. Fall back to a strong reference;
|
|
187
|
+
// the leak lives until the consumer calls `dispose()`.
|
|
188
|
+
const worker = this.worker;
|
|
189
|
+
unsub = onWorkerLogLevelChanged((level) => {
|
|
190
|
+
worker.postMessage({ kind: 'setLogLevel', data: { level } });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
this.unsubscribeLogLevel = unsub;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @internal
|
|
198
|
+
* Release the log-level subscription, reject any pending encrypt/decrypt
|
|
199
|
+
* futures, and detach the worker message handlers. The worker itself is
|
|
200
|
+
* caller-owned and is not terminated. Idempotent.
|
|
201
|
+
*/
|
|
202
|
+
dispose() {
|
|
203
|
+
this.unsubscribeLogLevel?.();
|
|
204
|
+
this.unsubscribeLogLevel = undefined;
|
|
205
|
+
if (E2EEManager.disposeRegistry) {
|
|
206
|
+
E2EEManager.disposeRegistry.unregister(this);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Reject pending futures BEFORE detaching worker handlers, so any late
|
|
210
|
+
// response can't resolve one after we've cut the pipe. Each future's
|
|
211
|
+
// `onFinally` deletes its own map entry, so both maps drain themselves.
|
|
212
|
+
// Snapshot before iterating in case a rejection handler mutates the map.
|
|
213
|
+
const disposalError = new CryptorError(
|
|
214
|
+
'E2EEManager disposed',
|
|
215
|
+
CryptorErrorReason.InternalError,
|
|
216
|
+
);
|
|
217
|
+
for (const future of [...this.encryptDataRequests.values()]) {
|
|
218
|
+
future.reject?.(disposalError);
|
|
219
|
+
}
|
|
220
|
+
for (const future of [...this.decryptDataRequests.values()]) {
|
|
221
|
+
future.reject?.(disposalError);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (this.worker) {
|
|
225
|
+
this.worker.onmessage = null;
|
|
226
|
+
this.worker.onerror = null;
|
|
227
|
+
}
|
|
228
|
+
this.removeAllListeners();
|
|
229
|
+
}
|
|
230
|
+
|
|
135
231
|
/**
|
|
136
232
|
* @internal
|
|
137
233
|
*/
|
|
138
234
|
setParticipantCryptorEnabled(enabled: boolean, participantIdentity: string) {
|
|
139
|
-
log.debug(`set e2ee to ${enabled} for participant ${participantIdentity}`);
|
|
235
|
+
this.log.debug(`set e2ee to ${enabled} for participant ${participantIdentity}`);
|
|
140
236
|
this.postEnable(enabled, participantIdentity);
|
|
141
237
|
}
|
|
142
238
|
|
|
@@ -145,7 +241,7 @@ export class E2EEManager
|
|
|
145
241
|
*/
|
|
146
242
|
setSifTrailer(trailer: NonSharedUint8Array) {
|
|
147
243
|
if (!trailer || trailer.length === 0) {
|
|
148
|
-
log.warn("ignoring server sent trailer as it's empty");
|
|
244
|
+
this.log.warn("ignoring server sent trailer as it's empty");
|
|
149
245
|
} else {
|
|
150
246
|
this.postSifTrailer(trailer);
|
|
151
247
|
}
|
|
@@ -155,25 +251,23 @@ export class E2EEManager
|
|
|
155
251
|
const { kind, data } = ev.data;
|
|
156
252
|
switch (kind) {
|
|
157
253
|
case 'error':
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
// Reject the corresponding future
|
|
254
|
+
// If error has uuid, it's from an async operation (encrypt/decrypt).
|
|
255
|
+
// Reject the corresponding future and let the caller decide how to log/handle;
|
|
256
|
+
// logging here would duplicate whatever the caller does.
|
|
162
257
|
if (data.uuid) {
|
|
163
258
|
const decryptFuture = this.decryptDataRequests.get(data.uuid);
|
|
164
259
|
if (decryptFuture?.reject) {
|
|
165
260
|
decryptFuture.reject(data.error);
|
|
166
|
-
break;
|
|
261
|
+
break;
|
|
167
262
|
}
|
|
168
263
|
|
|
169
264
|
const encryptFuture = this.encryptDataRequests.get(data.uuid);
|
|
170
265
|
if (encryptFuture?.reject) {
|
|
171
266
|
encryptFuture.reject(data.error);
|
|
172
|
-
break;
|
|
267
|
+
break;
|
|
173
268
|
}
|
|
174
269
|
}
|
|
175
|
-
|
|
176
|
-
// Emit general error event for unhandled errors
|
|
270
|
+
this.log.error(data.error.message);
|
|
177
271
|
this.emit(EncryptionEvent.EncryptionError, data.error, data.participantIdentity);
|
|
178
272
|
break;
|
|
179
273
|
case 'initAck':
|
|
@@ -234,13 +328,16 @@ export class E2EEManager
|
|
|
234
328
|
case 'packetTrailerMetadata':
|
|
235
329
|
this.handleFrameMetadata(data.trackId, data.rtpTimestamp, data.ssrc, data.metadata);
|
|
236
330
|
break;
|
|
331
|
+
case 'log':
|
|
332
|
+
workerLogger[data.level](data.msg, data.context);
|
|
333
|
+
break;
|
|
237
334
|
default:
|
|
238
335
|
break;
|
|
239
336
|
}
|
|
240
337
|
};
|
|
241
338
|
|
|
242
339
|
private onWorkerError = (ev: ErrorEvent) => {
|
|
243
|
-
log.error('e2ee worker encountered an error:', { error: ev.error });
|
|
340
|
+
this.log.error('e2ee worker encountered an error:', { error: ev.error });
|
|
244
341
|
this.emit(EncryptionEvent.EncryptionError, ev.error, undefined);
|
|
245
342
|
};
|
|
246
343
|
|
|
@@ -279,20 +376,14 @@ export class E2EEManager
|
|
|
279
376
|
|
|
280
377
|
private setupEventListeners(room: Room, keyProvider: BaseKeyProvider) {
|
|
281
378
|
room.on(RoomEvent.TrackPublished, (pub, participant) =>
|
|
282
|
-
this.
|
|
283
|
-
pub.trackInfo!.encryption !== Encryption_Type.NONE,
|
|
284
|
-
participant.identity,
|
|
285
|
-
),
|
|
379
|
+
this.setParticipantCryptorEnabledForPublication(pub, participant.identity),
|
|
286
380
|
);
|
|
287
381
|
room
|
|
288
382
|
.on(RoomEvent.ConnectionStateChanged, (state) => {
|
|
289
383
|
if (state === ConnectionState.Connected) {
|
|
290
384
|
room.remoteParticipants.forEach((participant) => {
|
|
291
385
|
participant.trackPublications.forEach((pub) => {
|
|
292
|
-
this.
|
|
293
|
-
pub.trackInfo!.encryption !== Encryption_Type.NONE,
|
|
294
|
-
participant.identity,
|
|
295
|
-
);
|
|
386
|
+
this.setParticipantCryptorEnabledForPublication(pub, participant.identity);
|
|
296
387
|
});
|
|
297
388
|
});
|
|
298
389
|
}
|
|
@@ -482,6 +573,28 @@ export class E2EEManager
|
|
|
482
573
|
this.worker.postMessage(msg);
|
|
483
574
|
}
|
|
484
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Derive the participant's encryption state from one of its publications.
|
|
578
|
+
* Publications without trackInfo are skipped rather than thrown on: throwing
|
|
579
|
+
* inside the forEach above would leave every remaining participant without an
|
|
580
|
+
* encryption state, and a cryptor with unknown state can no longer decrypt.
|
|
581
|
+
*/
|
|
582
|
+
private setParticipantCryptorEnabledForPublication(
|
|
583
|
+
pub: TrackPublication,
|
|
584
|
+
participantIdentity: string,
|
|
585
|
+
) {
|
|
586
|
+
if (!pub.trackInfo) {
|
|
587
|
+
this.log.warn('skipping e2ee enabled update for publication without trackInfo', {
|
|
588
|
+
trackSid: pub.trackSid,
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.setParticipantCryptorEnabled(
|
|
593
|
+
pub.trackInfo.encryption !== Encryption_Type.NONE,
|
|
594
|
+
participantIdentity,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
485
598
|
private setupE2EEReceiver(track: RemoteTrack, remoteId: string, trackInfo?: TrackInfo) {
|
|
486
599
|
if (!track.receiver) {
|
|
487
600
|
return;
|
|
@@ -504,7 +617,7 @@ export class E2EEManager
|
|
|
504
617
|
|
|
505
618
|
private setupE2EESender(track: Track, sender: RTCRtpSender) {
|
|
506
619
|
if (!isLocalTrack(track) || !sender) {
|
|
507
|
-
if (!sender) log.warn('early return because sender is not ready');
|
|
620
|
+
if (!sender) this.log.warn('early return because sender is not ready');
|
|
508
621
|
return;
|
|
509
622
|
}
|
|
510
623
|
this.handleSender(
|
|
@@ -541,21 +654,27 @@ export class E2EEManager
|
|
|
541
654
|
codec,
|
|
542
655
|
hasPacketTrailer,
|
|
543
656
|
};
|
|
544
|
-
// @ts-ignore
|
|
545
657
|
receiver.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
546
658
|
} else {
|
|
547
|
-
if (E2EE_FLAG in receiver &&
|
|
548
|
-
//
|
|
659
|
+
if (E2EE_FLAG in receiver && E2EE_TRACK_ID in receiver) {
|
|
660
|
+
// The transceiver is being reused for a new track. We cannot set up a new
|
|
661
|
+
// pipeline from here: this receiver's encoded streams were transferred to
|
|
662
|
+
// the worker on first setup and a transferred stream can't be transferred
|
|
663
|
+
// again, while createEncodedStreams() may only be called once per receiver.
|
|
664
|
+
// So hand the worker the new track/participant and let it re-point (and if
|
|
665
|
+
// necessary rebuild) the cryptor that still owns those streams.
|
|
549
666
|
const msg: UpdateCodecMessage = {
|
|
550
667
|
kind: 'updateCodec',
|
|
551
668
|
data: {
|
|
552
669
|
trackId,
|
|
670
|
+
previousTrackId: receiver[E2EE_TRACK_ID] as string,
|
|
553
671
|
codec,
|
|
554
672
|
participantIdentity,
|
|
555
673
|
hasPacketTrailer,
|
|
556
674
|
},
|
|
557
675
|
};
|
|
558
676
|
this.worker.postMessage(msg);
|
|
677
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
559
678
|
return;
|
|
560
679
|
}
|
|
561
680
|
// @ts-ignore
|
|
@@ -582,7 +701,6 @@ export class E2EEManager
|
|
|
582
701
|
trackId: trackId,
|
|
583
702
|
codec,
|
|
584
703
|
participantIdentity: participantIdentity,
|
|
585
|
-
isReuse: E2EE_FLAG in receiver,
|
|
586
704
|
hasPacketTrailer,
|
|
587
705
|
},
|
|
588
706
|
};
|
|
@@ -591,6 +709,8 @@ export class E2EEManager
|
|
|
591
709
|
|
|
592
710
|
// @ts-ignore
|
|
593
711
|
receiver[E2EE_FLAG] = true;
|
|
712
|
+
// @ts-ignore
|
|
713
|
+
receiver[E2EE_TRACK_ID] = trackId;
|
|
594
714
|
}
|
|
595
715
|
|
|
596
716
|
/**
|
|
@@ -613,7 +733,7 @@ export class E2EEManager
|
|
|
613
733
|
}
|
|
614
734
|
|
|
615
735
|
if (isScriptTransformSupportedForWorker()) {
|
|
616
|
-
log.info('initialize script transform');
|
|
736
|
+
this.log.info('initialize script transform');
|
|
617
737
|
const options: ScriptTransformOptions = {
|
|
618
738
|
kind: 'encode',
|
|
619
739
|
participantIdentity: this.room.localParticipant.identity,
|
|
@@ -625,7 +745,7 @@ export class E2EEManager
|
|
|
625
745
|
// @ts-ignore
|
|
626
746
|
sender.transform = new RTCRtpScriptTransform(this.worker, options);
|
|
627
747
|
} else {
|
|
628
|
-
log.info('initialize encoded streams');
|
|
748
|
+
this.log.info('initialize encoded streams');
|
|
629
749
|
// @ts-ignore
|
|
630
750
|
const senderStreams = sender.createEncodedStreams();
|
|
631
751
|
const msg: EncodeMessage = {
|
|
@@ -636,7 +756,6 @@ export class E2EEManager
|
|
|
636
756
|
codec,
|
|
637
757
|
trackId,
|
|
638
758
|
participantIdentity: this.room.localParticipant.identity,
|
|
639
|
-
isReuse: false,
|
|
640
759
|
hasPacketTrailer: hasFrameMetadataPublishOptions(frameMetadata),
|
|
641
760
|
packetTrailer: frameMetadata,
|
|
642
761
|
},
|
package/src/e2ee/constants.ts
CHANGED
|
@@ -29,6 +29,12 @@ export const IV_LENGTH = 12;
|
|
|
29
29
|
// flag set to indicate that e2ee has been setup for sender/receiver;
|
|
30
30
|
export const E2EE_FLAG = 'lk_e2ee';
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* trackId a receiver's/sender's encoded stream pipeline was last set up for.
|
|
34
|
+
* Needed to recover the right cryptor when a transceiver gets reused.
|
|
35
|
+
*/
|
|
36
|
+
export const E2EE_TRACK_ID = 'lk_e2ee_track_id';
|
|
37
|
+
|
|
32
38
|
export const SALT = 'LKFrameEncryptionKey';
|
|
33
39
|
|
|
34
40
|
export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
|