@stream-io/video-client 1.57.0 → 1.58.0
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/CHANGELOG.md +13 -0
- package/dist/e2ee-worker.browser.es.js +1342 -0
- package/dist/e2ee-worker.browser.es.js.map +1 -0
- package/dist/index.browser.es.js +831 -55
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +875 -54
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +864 -55
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +52 -6
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/devices/CameraManager.d.ts +6 -0
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/devices/MicrophoneManager.d.ts +6 -0
- package/dist/src/devices/SpeakerManager.d.ts +2 -1
- package/dist/src/events/participant.d.ts +10 -3
- package/dist/src/gen/coordinator/index.d.ts +1363 -243
- package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
- package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
- package/dist/src/helpers/ensureExhausted.d.ts +1 -1
- package/dist/src/permissions/PermissionsContext.d.ts +2 -2
- package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
- package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
- package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
- package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
- package/dist/src/rtc/e2ee/events.d.ts +163 -0
- package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/mediaEngine.d.ts +23 -0
- package/dist/src/rtc/types.d.ts +2 -0
- package/dist/src/store/CallState.d.ts +21 -0
- package/dist/src/types.d.ts +15 -6
- package/index.ts +3 -0
- package/package.json +1 -1
- package/src/Call.ts +160 -26
- package/src/__tests__/Call.test.ts +15 -0
- package/src/coordinator/connection/connection.ts +6 -3
- package/src/devices/AudioDeviceManager.ts +6 -0
- package/src/devices/CameraManager.ts +51 -4
- package/src/devices/DeviceManager.ts +17 -0
- package/src/devices/MicrophoneManager.ts +55 -5
- package/src/devices/ScreenShareManager.ts +3 -0
- package/src/devices/SpeakerManager.ts +42 -12
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
- package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
- package/src/devices/__tests__/mocks.ts +36 -3
- package/src/events/__tests__/participant.test.ts +134 -1
- package/src/events/callEventHandlers.ts +9 -3
- package/src/events/participant.ts +42 -7
- package/src/gen/coordinator/index.ts +1384 -244
- package/src/gen/video/sfu/models/models.ts +15 -0
- package/src/helpers/TypedEventEmitter.ts +71 -0
- package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
- package/src/helpers/ensureExhausted.ts +4 -2
- package/src/permissions/PermissionsContext.ts +10 -5
- package/src/rtc/BasePeerConnection.ts +12 -1
- package/src/rtc/Publisher.ts +8 -0
- package/src/rtc/Subscriber.ts +5 -0
- package/src/rtc/__tests__/Publisher.test.ts +101 -0
- package/src/rtc/__tests__/Subscriber.test.ts +89 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
- package/src/rtc/e2ee/E2EEManager.ts +12 -0
- package/src/rtc/e2ee/EncryptionManager.ts +336 -0
- package/src/rtc/e2ee/SPEC.md +586 -0
- package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
- package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
- package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
- package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
- package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
- package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
- package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
- package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
- package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
- package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
- package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
- package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
- package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
- package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
- package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
- package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
- package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
- package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
- package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
- package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
- package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
- package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
- package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
- package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
- package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
- package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
- package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
- package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
- package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
- package/src/rtc/e2ee/e2ee-worker.ts +27 -0
- package/src/rtc/e2ee/events.ts +179 -0
- package/src/rtc/e2ee/transformSupport.ts +35 -0
- package/src/rtc/helpers/rtcConfiguration.ts +4 -2
- package/src/rtc/index.ts +1 -0
- package/src/rtc/mediaEngine.ts +44 -0
- package/src/rtc/types.ts +2 -0
- package/src/store/CallState.ts +33 -1
- package/src/store/__tests__/CallState.test.ts +82 -1
- package/src/types.ts +23 -5
|
@@ -37,6 +37,14 @@ export interface CallState {
|
|
|
37
37
|
* @generated from protobuf field: repeated stream.video.sfu.models.Pin pins = 4;
|
|
38
38
|
*/
|
|
39
39
|
pins: Pin[];
|
|
40
|
+
/**
|
|
41
|
+
* e2ee_enabled is true when the call uses end-to-end encryption. Clients
|
|
42
|
+
* must enable their frame encryptor; the SFU forwards encrypted frames
|
|
43
|
+
* opaquely and server-side recording/transcription/broadcasting are disabled.
|
|
44
|
+
*
|
|
45
|
+
* @generated from protobuf field: bool e2ee_enabled = 5 [json_name = "e2eeEnabled"];
|
|
46
|
+
*/
|
|
47
|
+
e2EeEnabled: boolean;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* @generated from protobuf message stream.video.sfu.models.ParticipantCount
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ScopedLogger } from '../logger';
|
|
2
|
+
export type EventMap = Record<string, unknown>;
|
|
3
|
+
export type Listener<P> = (payload: P) => void | Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Tiny, type-safe event emitter.
|
|
6
|
+
*
|
|
7
|
+
* Usage styles:
|
|
8
|
+
* - compose: `private readonly events = new TypedEventEmitter<MyMap>()`
|
|
9
|
+
* - extend: `class Foo extends TypedEventEmitter<MyMap> {}`
|
|
10
|
+
*
|
|
11
|
+
* Listener exceptions (sync throw or rejected promise) are caught and logged,
|
|
12
|
+
* so one bad listener cannot break dispatch for the rest.
|
|
13
|
+
*/
|
|
14
|
+
export declare class TypedEventEmitter<M extends EventMap> {
|
|
15
|
+
protected readonly logger: ScopedLogger;
|
|
16
|
+
private readonly byEvent;
|
|
17
|
+
constructor(logger?: ScopedLogger | string);
|
|
18
|
+
on: <E extends keyof M>(event: E, fn: Listener<M[E]>) => (() => void);
|
|
19
|
+
off: <E extends keyof M>(event: E, fn: Listener<M[E]>) => void;
|
|
20
|
+
emit: <E extends keyof M>(event: E, payload: M[E]) => void;
|
|
21
|
+
removeAllListeners: (event?: keyof M) => void;
|
|
22
|
+
private invoke;
|
|
23
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ensureExhausted: (x: never, message
|
|
1
|
+
export declare const ensureExhausted: (x: never, message?: string) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CallSettingsResponse, OwnCapability } from '../gen/coordinator';
|
|
1
|
+
import { CallSettingsResponse, OwnCapability, RequestPermissionRequestPermissionsEnum } from '../gen/coordinator';
|
|
2
2
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
3
3
|
/**
|
|
4
4
|
* Stores the permissions for the current user and exposes
|
|
@@ -41,5 +41,5 @@ export declare class PermissionsContext {
|
|
|
41
41
|
* @param permission the permission to check for.
|
|
42
42
|
* @param settings the call settings to check against (optional).
|
|
43
43
|
*/
|
|
44
|
-
canRequest: (permission:
|
|
44
|
+
canRequest: (permission: RequestPermissionRequestPermissionsEnum, settings?: CallSettingsResponse | undefined) => boolean;
|
|
45
45
|
}
|
|
@@ -2,6 +2,7 @@ import { ScopedLogger } from '../logger';
|
|
|
2
2
|
import type { CallEventListener } from '../coordinator/connection/types';
|
|
3
3
|
import { CallState } from '../store';
|
|
4
4
|
import { PeerType, TrackType } from '../gen/video/sfu/models/models';
|
|
5
|
+
import type { E2EEManager } from './e2ee/E2EEManager';
|
|
5
6
|
import { StreamSfuClient } from '../StreamSfuClient';
|
|
6
7
|
import { AllSfuEvents, Dispatcher } from './Dispatcher';
|
|
7
8
|
import { StatsTracer, Tracer } from '../stats';
|
|
@@ -18,6 +19,7 @@ export declare abstract class BasePeerConnection {
|
|
|
18
19
|
protected readonly state: CallState;
|
|
19
20
|
protected readonly dispatcher: Dispatcher;
|
|
20
21
|
protected readonly clientPublishOptions?: ClientPublishOptions;
|
|
22
|
+
protected readonly e2ee?: E2EEManager;
|
|
21
23
|
protected tag: string;
|
|
22
24
|
protected sfuClient: StreamSfuClient;
|
|
23
25
|
protected onReconnectionNeeded?: OnReconnectionNeeded;
|
|
@@ -39,7 +41,7 @@ export declare abstract class BasePeerConnection {
|
|
|
39
41
|
/**
|
|
40
42
|
* Constructs a new `BasePeerConnection` instance.
|
|
41
43
|
*/
|
|
42
|
-
protected constructor(peerType: PeerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay, statsTimestampDriftThresholdMs, }: BasePeerConnectionOpts);
|
|
44
|
+
protected constructor(peerType: PeerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, e2ee, iceRestartDelay, statsTimestampDriftThresholdMs, }: BasePeerConnectionOpts);
|
|
43
45
|
private createPeerConnection;
|
|
44
46
|
/**
|
|
45
47
|
* Disposes the `RTCPeerConnection` instance.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the RTC layer needs to attach E2EE to a track. {@link EncryptionManager}
|
|
3
|
+
* is the built-in AES-GCM implementation, but `Call.setE2EEManager` takes any:
|
|
4
|
+
* an integrator can plug in another scheme, RFC 9605 SFrame for example, by
|
|
5
|
+
* attaching their own encoded transform in these two methods.
|
|
6
|
+
*/
|
|
7
|
+
export interface E2EEManager {
|
|
8
|
+
/** `trackType` only groups perf stats, keeping a camera and screen share apart. */
|
|
9
|
+
encrypt(sender: RTCRtpSender, codec?: string, trackType?: string): void;
|
|
10
|
+
/** `trackType` only groups perf stats, keeping a peer's audio and video apart. */
|
|
11
|
+
decrypt(receiver: RTCRtpReceiver, userId: string, trackType?: string): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { TypedEventEmitter } from '../../helpers/TypedEventEmitter';
|
|
2
|
+
import type { E2EEEventMap } from './events';
|
|
3
|
+
import type { E2EEManager } from './E2EEManager';
|
|
4
|
+
export type { E2EEEventMap, DecryptionFailedEvent, DecryptionResumedEvent, DecryptionStalledEvent, EncryptionFailedEvent, KeyStateReport, MissingKeyEvent, PerfReport, TrackPerf, UnencryptedFrameEvent, UnsupportedVersionEvent, } from './events';
|
|
5
|
+
/**
|
|
6
|
+
* - `'AES-128-GCM'` (default): 16-byte keys, per BSI TR-02102-1 §3.2.
|
|
7
|
+
* - `'AES-256-GCM'`: 32-byte keys. For a compliance reviewer that requires
|
|
8
|
+
* 256-bit strength, such as a KBV Anlage 31b certifier.
|
|
9
|
+
*/
|
|
10
|
+
export type E2EEAlgorithm = 'AES-128-GCM' | 'AES-256-GCM';
|
|
11
|
+
/** Options for {@link EncryptionManager.create}. */
|
|
12
|
+
export type EncryptionManagerOptions = {
|
|
13
|
+
algorithm?: E2EEAlgorithm;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Distributes keys to the E2EE Web Worker and attaches encrypt/decrypt
|
|
17
|
+
* transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
18
|
+
*/
|
|
19
|
+
export declare class EncryptionManager extends TypedEventEmitter<E2EEEventMap> implements E2EEManager {
|
|
20
|
+
private readonly algorithm;
|
|
21
|
+
private readonly transform;
|
|
22
|
+
private disposed;
|
|
23
|
+
private piped?;
|
|
24
|
+
private readonly userId;
|
|
25
|
+
private readonly worker;
|
|
26
|
+
private readonly workerUrl;
|
|
27
|
+
private constructor();
|
|
28
|
+
/**
|
|
29
|
+
* Whether this browser has WebRTC Encoded Transforms, which E2EE requires.
|
|
30
|
+
* Use it to guard UI, or to avoid calling {@link create} where it would throw.
|
|
31
|
+
*/
|
|
32
|
+
static isSupported: () => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Create an EncryptionManager instance and initialize the E2EE Web Worker.
|
|
35
|
+
*
|
|
36
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
37
|
+
* @param options - the create options.
|
|
38
|
+
* @throws {Error} If the browser lacks Encoded Transforms.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* if (EncryptionManager.isSupported()) {
|
|
43
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
44
|
+
* call.setE2EEManager(e2ee);
|
|
45
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
static create: (userId: string, options?: EncryptionManagerOptions) => Promise<EncryptionManager>;
|
|
49
|
+
/**
|
|
50
|
+
* {@link dispose} terminates the worker, so `postMessage` becomes a silent
|
|
51
|
+
* no-op and an attached transform points at a dead worker: frames would stall
|
|
52
|
+
* forever with no error and no event. Throwing is also fail-closed, since a
|
|
53
|
+
* caller that swallows it still publishes nothing rather than cleartext.
|
|
54
|
+
*/
|
|
55
|
+
private assertUsable;
|
|
56
|
+
/**
|
|
57
|
+
* Terminate the worker and release all resources.
|
|
58
|
+
*
|
|
59
|
+
* The manager is unusable afterwards and every other method throws. Call
|
|
60
|
+
* {@link create} for a new one. Safe to call more than once.
|
|
61
|
+
*/
|
|
62
|
+
dispose: () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Set a per-user AES-GCM encryption key in the worker's key store.
|
|
65
|
+
*
|
|
66
|
+
* Use it when each participant has their own key from a central authority.
|
|
67
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
68
|
+
*
|
|
69
|
+
* @param userId - The key owner.
|
|
70
|
+
* @param keyIndex - Increases with each rotation.
|
|
71
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
72
|
+
*/
|
|
73
|
+
setKey: (userId: string, keyIndex: number, rawKey: ArrayBuffer) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
76
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
77
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
78
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
79
|
+
*
|
|
80
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
81
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
82
|
+
*/
|
|
83
|
+
setSharedKey: (keyIndex: number, rawKey: ArrayBuffer) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Remove one shared-key epoch from the worker's receive key ring.
|
|
86
|
+
*
|
|
87
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
88
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
89
|
+
*
|
|
90
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
91
|
+
*/
|
|
92
|
+
removeSharedKey: (keyIndex: number) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
95
|
+
*
|
|
96
|
+
* @param userId - The key owner.
|
|
97
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
98
|
+
*/
|
|
99
|
+
removeKey: (userId: string, keyIndex: number) => void;
|
|
100
|
+
/**
|
|
101
|
+
* Drop every key a user holds, revoking their ability to decrypt later
|
|
102
|
+
* frames. Call it when a participant leaves.
|
|
103
|
+
*
|
|
104
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
105
|
+
*/
|
|
106
|
+
removeAllKeys: (userId: string) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Called by the Publisher when it adds a transceiver.
|
|
109
|
+
*
|
|
110
|
+
* @param sender - The sender to encrypt.
|
|
111
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
112
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
encrypt: (sender: RTCRtpSender, codec?: string, trackType?: string) => void;
|
|
116
|
+
/**
|
|
117
|
+
* Called by the Subscriber when a remote track arrives.
|
|
118
|
+
*
|
|
119
|
+
* @param receiver - The receiver to decrypt.
|
|
120
|
+
* @param userId - The remote user, for key lookup in the worker.
|
|
121
|
+
* @param trackType - Optional label; only groups perf stats.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
decrypt: (receiver: RTCRtpReceiver, userId: string, trackType?: string) => void;
|
|
125
|
+
/** Pipe through the worker's transform, tracking targets to avoid double-piping. */
|
|
126
|
+
private pipe;
|
|
127
|
+
/**
|
|
128
|
+
* Toggle periodic performance reporting from the E2EE worker.
|
|
129
|
+
*
|
|
130
|
+
* While on, the worker emits `e2ee.perf_report` once per second with per-track
|
|
131
|
+
* FPS and crypto timings. Useful for debugging throughput.
|
|
132
|
+
*/
|
|
133
|
+
enablePerformanceReporting: (enabled: boolean) => void;
|
|
134
|
+
/**
|
|
135
|
+
* Request a snapshot of the worker's keys. It arrives later as the
|
|
136
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
137
|
+
*/
|
|
138
|
+
requestKeyState: () => void;
|
|
139
|
+
private handleWorkerMessage;
|
|
140
|
+
private handleWorkerError;
|
|
141
|
+
private validateKeyLength;
|
|
142
|
+
/**
|
|
143
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
144
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
145
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
146
|
+
*/
|
|
147
|
+
private validateKeyIndex;
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2EE worker. The build populates it.
|
|
3
|
+
*
|
|
4
|
+
* The actual implementation lives in `e2ee-worker/e2ee-worker-impl.ts` and its
|
|
5
|
+
* sibling modules (`constants.ts`, `codec.ts`, `crypto.ts`, `utils.ts`,
|
|
6
|
+
* `types.ts`), all written as real TypeScript with full type checking
|
|
7
|
+
* and import support.
|
|
8
|
+
*
|
|
9
|
+
* ## How it works
|
|
10
|
+
*
|
|
11
|
+
* 1. You edit files in `e2ee-worker/` as normal TypeScript
|
|
12
|
+
* 2. `yarn build` runs Rollup, which invokes `rollup-plugin-inline-worker`
|
|
13
|
+
* 3. The plugin bundles `e2ee-worker/e2ee-worker-impl.ts` (and its imports)
|
|
14
|
+
* into a single function via a nested Rollup + TypeScript build
|
|
15
|
+
* 4. The function replaces this file's export at build time
|
|
16
|
+
* 5. `EncryptionManager.create()` creates a Worker from `e2eeWorker.toString()`
|
|
17
|
+
*
|
|
18
|
+
* In tests, this module is mocked (see `__tests__/EncryptionManager.test.ts`),
|
|
19
|
+
* so the placeholder is never evaluated.
|
|
20
|
+
*
|
|
21
|
+
* @see e2ee-worker/e2ee-worker-impl.ts - the entry point
|
|
22
|
+
* @see ../../plugins/rollup-plugin-inline-worker.ts - the Rollup plugin
|
|
23
|
+
* @see EncryptionManager.ts - consumes e2eeWorker
|
|
24
|
+
*/
|
|
25
|
+
export declare function e2eeWorker(): void;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throughput sample for one track. `userId` is the local sender on encode, the
|
|
3
|
+
* remote sender on decode. The (userId, trackType) pair keeps a peer's audio
|
|
4
|
+
* and video reported apart instead of summed.
|
|
5
|
+
*/
|
|
6
|
+
export type TrackPerf = {
|
|
7
|
+
userId: string;
|
|
8
|
+
trackType: string;
|
|
9
|
+
fps: number;
|
|
10
|
+
maxCryptoMs: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Encode entries carry the `codec` the publisher knows; decode entries cannot,
|
|
14
|
+
* since a remote sender's codec is not reliably known locally.
|
|
15
|
+
*/
|
|
16
|
+
export type PerfReport = {
|
|
17
|
+
encode: (TrackPerf & {
|
|
18
|
+
codec: string;
|
|
19
|
+
})[];
|
|
20
|
+
decode: TrackPerf[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Fired when a needed key is not held. The `keyIndex` tells the two cases apart:
|
|
24
|
+
*
|
|
25
|
+
* - Without it, the local encoder has no key and drops every outgoing frame.
|
|
26
|
+
* The host never provided a key, or a key import failed.
|
|
27
|
+
* - With it, a remote sender's frame named a key this peer does not hold, so
|
|
28
|
+
* the frame was dropped. This is normal while a key is in flight, or while a
|
|
29
|
+
* rotation propagates.
|
|
30
|
+
*/
|
|
31
|
+
export type MissingKeyEvent = {
|
|
32
|
+
/** The local user, or the remote sender. */
|
|
33
|
+
userId: string;
|
|
34
|
+
/** Decode direction only; absent when the local encoder has no key. */
|
|
35
|
+
keyIndex?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Decode direction only, which reports per track: a peer's tracks can sit on
|
|
38
|
+
* different key epochs, so one may stall while another plays. The encode
|
|
39
|
+
* direction stops every outgoing track at once and is reported per user.
|
|
40
|
+
*/
|
|
41
|
+
trackType?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A remote frame arrived unencrypted and went to the decoder as-is. Expected
|
|
45
|
+
* when the call's mode is `available` and that peer publishes plain; where every
|
|
46
|
+
* peer must encrypt, it means media renders without authentication.
|
|
47
|
+
*/
|
|
48
|
+
export type UnencryptedFrameEvent = {
|
|
49
|
+
userId: string;
|
|
50
|
+
trackType?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A remote frame carried this format's framing but a `version` this build
|
|
54
|
+
* cannot decrypt, so it was dropped.
|
|
55
|
+
*
|
|
56
|
+
* **This client is the one that needs updating**, not the peer: the peer wrote
|
|
57
|
+
* a newer format than this SDK implements. Prompt the local user to update the
|
|
58
|
+
* app; no key changes anything.
|
|
59
|
+
*/
|
|
60
|
+
export type UnsupportedVersionEvent = {
|
|
61
|
+
userId: string;
|
|
62
|
+
trackType?: string;
|
|
63
|
+
/** The framing version read from the frame. */
|
|
64
|
+
version: number;
|
|
65
|
+
};
|
|
66
|
+
/** The worker could not decrypt a remote frame. Throttled per track. */
|
|
67
|
+
export type DecryptionFailedEvent = {
|
|
68
|
+
userId: string;
|
|
69
|
+
trackType?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Fired when a track decrypts again after a reported failure. See
|
|
73
|
+
* {@link E2EEEventMap} for how it pairs with `e2ee.decryption_failed`.
|
|
74
|
+
*/
|
|
75
|
+
export type DecryptionResumedEvent = {
|
|
76
|
+
userId: string;
|
|
77
|
+
trackType?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* An outgoing frame could not be encrypted, so that track publishes nothing.
|
|
81
|
+
* Latched per track, so a permanently failing track reports once.
|
|
82
|
+
*/
|
|
83
|
+
export type EncryptionFailedEvent = {
|
|
84
|
+
userId: string;
|
|
85
|
+
/** Only this track is affected; the sender's others keep publishing. */
|
|
86
|
+
trackType?: string;
|
|
87
|
+
/** Short, human-readable reason. */
|
|
88
|
+
reason: string;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Fired when a remote track passes the internal failure tolerance: decryption
|
|
92
|
+
* has failed on that many consecutive frames, so the track renders nothing until
|
|
93
|
+
* it recovers. The cause is not known here: a key mismatch is the common one,
|
|
94
|
+
* but a tampered or truncated frame looks the same to the decryptor.
|
|
95
|
+
*/
|
|
96
|
+
export type DecryptionStalledEvent = {
|
|
97
|
+
userId: string;
|
|
98
|
+
/** The keyIndex that crossed the tolerance. */
|
|
99
|
+
keyIndex: number;
|
|
100
|
+
trackType?: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Answer to {@link EncryptionManager.requestKeyState}. `fingerprint` is hex of
|
|
104
|
+
* the first 8 bytes of SHA-256(rawKey): not reversible, so safe to log. Key
|
|
105
|
+
* material is never returned.
|
|
106
|
+
*/
|
|
107
|
+
export type KeyStateReport = {
|
|
108
|
+
perUserKeys: Array<{
|
|
109
|
+
userId: string;
|
|
110
|
+
keyIndex: number;
|
|
111
|
+
fingerprint: string;
|
|
112
|
+
}>;
|
|
113
|
+
/** Every shared receive epoch, including the one currently used to encode. */
|
|
114
|
+
sharedKeys: Array<{
|
|
115
|
+
keyIndex: number;
|
|
116
|
+
fingerprint: string;
|
|
117
|
+
isActive: boolean;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Events that the E2EE {@link EncryptionManager} emits.
|
|
122
|
+
*
|
|
123
|
+
* Subscribe with `manager.on(eventName, handler)`. To unsubscribe, call the
|
|
124
|
+
* function it returns, or `manager.off(eventName, handler)`.
|
|
125
|
+
*
|
|
126
|
+
* Every name follows the `e2ee.<snake_case>` convention. That makes them easy
|
|
127
|
+
* to grep, and keeps them distinct from SFU and coordinator events.
|
|
128
|
+
*/
|
|
129
|
+
export type E2EEEventMap = {
|
|
130
|
+
/** Key mismatch, rotation in progress, or a tampered frame. */
|
|
131
|
+
'e2ee.decryption_failed': DecryptionFailedEvent;
|
|
132
|
+
/**
|
|
133
|
+
* Pairs one-to-one with `e2ee.decryption_failed` and is never throttled, so a
|
|
134
|
+
* host can drive its UI from the pair alone. Also fires for a track that
|
|
135
|
+
* recovers on a new keyIndex.
|
|
136
|
+
*/
|
|
137
|
+
'e2ee.decryption_resumed': DecryptionResumedEvent;
|
|
138
|
+
/**
|
|
139
|
+
* Consecutive decrypt failures on one track crossed the internal tolerance.
|
|
140
|
+
* Fires once per (userId, keyIndex) entering that state, and
|
|
141
|
+
* `e2ee.decryption_resumed` is what clears it.
|
|
142
|
+
*/
|
|
143
|
+
'e2ee.decryption_stalled': DecryptionStalledEvent;
|
|
144
|
+
/** That track is publishing nothing. Latched, so it reports once. */
|
|
145
|
+
'e2ee.encryption_failed': EncryptionFailedEvent;
|
|
146
|
+
/**
|
|
147
|
+
* The host must set or distribute a key. Distinct from
|
|
148
|
+
* `e2ee.encryption_failed`, where a key was present but the crypto threw.
|
|
149
|
+
* Throttled, and stops once the key arrives.
|
|
150
|
+
*/
|
|
151
|
+
'e2ee.missing_key': MissingKeyEvent;
|
|
152
|
+
'e2ee.unencrypted_frame': UnencryptedFrameEvent;
|
|
153
|
+
/**
|
|
154
|
+
* A peer is publishing a framing version this build cannot read, so its
|
|
155
|
+
* frames are dropped. Throttled per track; surface it as a prompt to update
|
|
156
|
+
* **this** client.
|
|
157
|
+
*/
|
|
158
|
+
'e2ee.unsupported_version': UnsupportedVersionEvent;
|
|
159
|
+
/** Once per second while {@link EncryptionManager.enablePerformanceReporting} is on. */
|
|
160
|
+
'e2ee.perf_report': PerfReport;
|
|
161
|
+
/** Answer to {@link EncryptionManager.requestKeyState}. */
|
|
162
|
+
'e2ee.key_state': KeyStateReport;
|
|
163
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection and selection policy for the two WebRTC Encoded Transform APIs.
|
|
3
|
+
* Internal: which one the SDK attaches is an RTC-layer detail. Consumers want
|
|
4
|
+
* `EncryptionManager.isSupported` instead.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Chrome only, and the reason an RTCPeerConnection carrying E2EE needs the
|
|
8
|
+
* non-standard `encodedInsertableStreams` flag.
|
|
9
|
+
*/
|
|
10
|
+
export declare const hasInsertableStreams: () => boolean;
|
|
11
|
+
/** Whether the standard `RTCRtpScriptTransform` API exists. */
|
|
12
|
+
export declare const hasScriptTransform: () => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Which Encoded Transform API to attach E2EE with here.
|
|
15
|
+
*
|
|
16
|
+
* - `'insertable'`: legacy Insertable Streams. Used on Chrome, where
|
|
17
|
+
* `RTCRtpScriptTransform` is still unreliable for E2EE.
|
|
18
|
+
* - `'script'`: the standard API. Used everywhere else.
|
|
19
|
+
* - `undefined`: neither exists, so E2EE cannot run.
|
|
20
|
+
*/
|
|
21
|
+
export declare const preferredTransform: () => "script" | "insertable" | undefined;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare const toRtcConfiguration: (config:
|
|
1
|
+
import { ICEServerResponse } from '../../gen/coordinator';
|
|
2
|
+
export declare const toRtcConfiguration: (config: ICEServerResponse[]) => RTCConfiguration;
|
package/dist/src/rtc/index.d.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-call media engine. On React Native it owns the call's native
|
|
3
|
+
* `PeerConnectionFactory` (built with the call's audio configuration); on web it
|
|
4
|
+
* is a no-op. Capture (`getUserMedia`/`getDisplayMedia`) and peer-connection creation
|
|
5
|
+
* always go through the WebRTC globals, which resolve to this factory while it
|
|
6
|
+
* is the live call factory — so the engine only needs to manage its lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export interface CallMediaEngine {
|
|
11
|
+
dispose(): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a per-call {@link CallMediaEngine}. Registered once at SDK startup
|
|
15
|
+
* via {@link setCallMediaEngineProvider}. May return the engine synchronously
|
|
16
|
+
* (the default globals engine) or asynchronously (React Native, where allocating
|
|
17
|
+
* the native per-call factory is an async bridge call).
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export type CallMediaEngineProvider = () => CallMediaEngine | Promise<CallMediaEngine>;
|
|
22
|
+
export declare const setCallMediaEngineProvider: (newProvider: CallMediaEngineProvider) => void;
|
|
23
|
+
export declare const getCallMediaEngineProvider: () => CallMediaEngineProvider;
|
package/dist/src/rtc/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { CallState } from '../store';
|
|
|
4
4
|
import { Dispatcher } from './Dispatcher';
|
|
5
5
|
import type { OptimalVideoLayer } from './layers';
|
|
6
6
|
import type { ClientPublishOptions } from '../types';
|
|
7
|
+
import type { E2EEManager } from './e2ee/E2EEManager';
|
|
7
8
|
import type { VideoSender } from '../gen/video/sfu/event/events';
|
|
8
9
|
/**
|
|
9
10
|
* Canonical reasons the SDK uses to trigger a reconnection. Free-form strings
|
|
@@ -81,6 +82,7 @@ export type BasePeerConnectionOpts = {
|
|
|
81
82
|
iceRestartDelay?: number;
|
|
82
83
|
clientPublishOptions?: ClientPublishOptions;
|
|
83
84
|
statsTimestampDriftThresholdMs?: number;
|
|
85
|
+
e2ee?: E2EEManager;
|
|
84
86
|
};
|
|
85
87
|
export type TrackPublishOptions = {
|
|
86
88
|
audioBitrateProfile?: AudioBitrateProfile;
|
|
@@ -12,6 +12,7 @@ type OrphanedTrack = {
|
|
|
12
12
|
trackLookupPrefix: string;
|
|
13
13
|
trackType: TrackType;
|
|
14
14
|
track: MediaStream;
|
|
15
|
+
receiver?: RTCRtpReceiver;
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
18
|
* Holds the state of the current call.
|
|
@@ -35,6 +36,7 @@ export declare class CallState {
|
|
|
35
36
|
private settingsSubject;
|
|
36
37
|
private transcribingSubject;
|
|
37
38
|
private captioningSubject;
|
|
39
|
+
private e2eeEnabledSubject;
|
|
38
40
|
private endedBySubject;
|
|
39
41
|
private thumbnailsSubject;
|
|
40
42
|
private membersSubject;
|
|
@@ -188,6 +190,11 @@ export declare class CallState {
|
|
|
188
190
|
* Will provide the closed captioning state of this call.
|
|
189
191
|
*/
|
|
190
192
|
captioning$: Observable<boolean>;
|
|
193
|
+
/**
|
|
194
|
+
* Whether end-to-end encryption is active for this call, as reported by the
|
|
195
|
+
* SFU in the join response.
|
|
196
|
+
*/
|
|
197
|
+
e2eeEnabled$: Observable<boolean>;
|
|
191
198
|
/**
|
|
192
199
|
* Will provide the user who ended this call.
|
|
193
200
|
*/
|
|
@@ -476,6 +483,10 @@ export declare class CallState {
|
|
|
476
483
|
* Will provide the transcribing state of this call.
|
|
477
484
|
*/
|
|
478
485
|
get transcribing(): boolean;
|
|
486
|
+
/**
|
|
487
|
+
* Whether end-to-end encryption is active for this call.
|
|
488
|
+
*/
|
|
489
|
+
get e2eeEnabled(): boolean;
|
|
479
490
|
/**
|
|
480
491
|
* Will provide the user who ended this call.
|
|
481
492
|
*/
|
|
@@ -571,6 +582,16 @@ export declare class CallState {
|
|
|
571
582
|
* @param id the ID of the orphaned track to remove.
|
|
572
583
|
*/
|
|
573
584
|
removeOrphanedTrack: (id: string) => void;
|
|
585
|
+
/**
|
|
586
|
+
* Drops every orphaned track. Call this when the peer connections that own
|
|
587
|
+
* the stored receivers go away (full leave, reconnect, or migration):
|
|
588
|
+
* `pc.close()` does not raise the track `ended` event, so the per-track
|
|
589
|
+
* cleanup never fires and the receivers + their closed PCs would otherwise
|
|
590
|
+
* leak for the call's lifetime.
|
|
591
|
+
*
|
|
592
|
+
* @internal
|
|
593
|
+
*/
|
|
594
|
+
removeAllOrphanedTracks: () => void;
|
|
574
595
|
/**
|
|
575
596
|
* Takes all orphaned tracks with the given track lookup prefix.
|
|
576
597
|
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Participant, TrackType, VideoDimension } from './gen/video/sfu/models/models';
|
|
2
|
-
import type { AudioSettingsRequestDefaultDeviceEnum, CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability,
|
|
2
|
+
import type { AudioSettingsRequestDefaultDeviceEnum, CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability, VideoReactionResponse, StartRecordingRequest, StartRecordingResponse } from './gen/coordinator';
|
|
3
3
|
import type { StreamClient } from './coordinator/connection/client';
|
|
4
4
|
import type { ClientEventReporter } from './reporting';
|
|
5
5
|
import type { RejectReason, StreamClientOptions, TokenProvider, User } from './coordinator/connection/types';
|
|
@@ -7,7 +7,7 @@ import type { Comparator } from './sorting';
|
|
|
7
7
|
import type { StreamVideoWriteableStateStore } from './store';
|
|
8
8
|
import { AxiosError } from 'axios';
|
|
9
9
|
import type { Call } from './Call';
|
|
10
|
-
export type StreamReaction = Pick<
|
|
10
|
+
export type StreamReaction = Pick<VideoReactionResponse, 'type' | 'emoji_code' | 'custom'>;
|
|
11
11
|
export declare enum VisibilityState {
|
|
12
12
|
UNKNOWN = "UNKNOWN",
|
|
13
13
|
VISIBLE = "VISIBLE",
|
|
@@ -344,8 +344,15 @@ type StreamRNVideoSDKCallManagerRingingParams = {
|
|
|
344
344
|
isRingingTypeCall: boolean;
|
|
345
345
|
};
|
|
346
346
|
type StreamRNVideoSDKCallManagerSetupParams = StreamRNVideoSDKCallManagerRingingParams & {
|
|
347
|
+
cid: string;
|
|
347
348
|
defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
|
|
348
349
|
};
|
|
350
|
+
type StreamRNVideoSDKCallManagerStartParams = StreamRNVideoSDKCallManagerRingingParams & {
|
|
351
|
+
cid: string;
|
|
352
|
+
};
|
|
353
|
+
type StreamRNVideoSDKCallManagerStopParams = StreamRNVideoSDKCallManagerRingingParams & {
|
|
354
|
+
shouldStopCallManager: boolean;
|
|
355
|
+
};
|
|
349
356
|
type StreamRNVideoSDKEndCallReason =
|
|
350
357
|
/** Call ended by the local user (e.g., hanging up). */
|
|
351
358
|
'local'
|
|
@@ -371,6 +378,8 @@ type StreamRNVideoSDKCallingX = {
|
|
|
371
378
|
joinCall: (call: Call, activeCalls: Call[]) => Promise<void>;
|
|
372
379
|
endCall: (call: Call, reason?: StreamRNVideoSDKEndCallReason) => Promise<void>;
|
|
373
380
|
registerOutgoingCall: (call: Call) => Promise<void>;
|
|
381
|
+
wireAudioEngineSubscription: () => void;
|
|
382
|
+
unwireAudioEngineSubscription: () => void;
|
|
374
383
|
};
|
|
375
384
|
export type StreamRNVideoSDKGlobals = {
|
|
376
385
|
callingX: StreamRNVideoSDKCallingX;
|
|
@@ -378,15 +387,15 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
378
387
|
/**
|
|
379
388
|
* Sets up the in call manager.
|
|
380
389
|
*/
|
|
381
|
-
setup({ defaultDevice, isRingingTypeCall, }: StreamRNVideoSDKCallManagerSetupParams): void;
|
|
390
|
+
setup({ defaultDevice, isRingingTypeCall, cid, }: StreamRNVideoSDKCallManagerSetupParams): void;
|
|
382
391
|
/**
|
|
383
392
|
* Starts the in call manager.
|
|
384
393
|
*/
|
|
385
|
-
start({ isRingingTypeCall, }:
|
|
394
|
+
start({ isRingingTypeCall, cid, }: StreamRNVideoSDKCallManagerStartParams): void;
|
|
386
395
|
/**
|
|
387
396
|
* Stops the in call manager.
|
|
388
397
|
*/
|
|
389
|
-
stop({ isRingingTypeCall }:
|
|
398
|
+
stop({ isRingingTypeCall, shouldStopCallManager, }: StreamRNVideoSDKCallManagerStopParams): void;
|
|
390
399
|
/**
|
|
391
400
|
* iOS-only. Keeps the audio engine's microphone-input (voice-processing)
|
|
392
401
|
* chain prepared while the mic is muted, so the `AVAudioEngine` stays full-duplex
|
|
@@ -423,5 +432,5 @@ declare global {
|
|
|
423
432
|
/**
|
|
424
433
|
* The options to pass to {@link Call.join} method.
|
|
425
434
|
*/
|
|
426
|
-
export type JoinCallData = Omit<JoinCallRequest, 'location'>;
|
|
435
|
+
export type JoinCallData = Omit<JoinCallRequest, 'location' | 'e2ee'>;
|
|
427
436
|
export { AxiosError };
|
package/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './src/stats/types';
|
|
|
11
11
|
|
|
12
12
|
export * from './src/Call';
|
|
13
13
|
export * from './src/CallType';
|
|
14
|
+
export * from './src/rtc/mediaEngine';
|
|
14
15
|
export * from './src/StreamVideoClient';
|
|
15
16
|
export * from './src/StreamSfuClient';
|
|
16
17
|
export * from './src/devices';
|
|
@@ -23,6 +24,8 @@ export * from './src/helpers/DynascaleManager';
|
|
|
23
24
|
export * from './src/helpers/ViewportTracker';
|
|
24
25
|
export * from './src/helpers/sound-detector';
|
|
25
26
|
export * from './src/helpers/participantUtils';
|
|
27
|
+
export * from './src/rtc/e2ee/E2EEManager';
|
|
28
|
+
export * from './src/rtc/e2ee/EncryptionManager';
|
|
26
29
|
export * as Browsers from './src/helpers/browsers';
|
|
27
30
|
|
|
28
31
|
export * from './src/logger';
|