@stream-io/video-react-native-sdk 1.45.1 → 1.46.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 +36 -2
- package/android/build.gradle +7 -1
- package/dist/commonjs/components/Call/CallContent/CallContent.js +1 -1
- package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +78 -7
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +3 -0
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/commonjs/components/Call/RingingCallContent/OutgoingCall.js +4 -12
- package/dist/commonjs/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
- package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +27 -4
- package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
- package/dist/commonjs/index.js +14 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/modules/encryption/EncryptionManager.js +305 -0
- package/dist/commonjs/modules/encryption/EncryptionManager.js.map +1 -0
- package/dist/commonjs/modules/encryption/eventMapping.js +143 -0
- package/dist/commonjs/modules/encryption/eventMapping.js.map +1 -0
- package/dist/commonjs/modules/encryption/index.js +13 -0
- package/dist/commonjs/modules/encryption/index.js.map +1 -0
- package/dist/commonjs/modules/encryption/parity.js +6 -0
- package/dist/commonjs/modules/encryption/parity.js.map +1 -0
- package/dist/commonjs/utils/StreamVideoRN/index.js +23 -0
- package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +29 -4
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/utils/internal/iosPipTrack.js +41 -0
- package/dist/commonjs/utils/internal/iosPipTrack.js.map +1 -0
- package/dist/commonjs/utils/internal/registerSDKGlobals.js +6 -0
- package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
- package/dist/commonjs/utils/internal/ringingCallLifecycle.js +142 -0
- package/dist/commonjs/utils/internal/ringingCallLifecycle.js.map +1 -0
- package/dist/commonjs/utils/push/internal/utils.js +4 -0
- package/dist/commonjs/utils/push/internal/utils.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/module/components/Call/CallContent/CallContent.js +1 -1
- package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +80 -9
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js +3 -0
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/module/components/Call/RingingCallContent/OutgoingCall.js +4 -12
- package/dist/module/components/Call/RingingCallContent/OutgoingCall.js.map +1 -1
- package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js +28 -5
- package/dist/module/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.js.map +1 -1
- package/dist/module/index.js +5 -0
- package/dist/module/index.js.map +1 -1
- package/dist/module/modules/encryption/EncryptionManager.js +301 -0
- package/dist/module/modules/encryption/EncryptionManager.js.map +1 -0
- package/dist/module/modules/encryption/eventMapping.js +135 -0
- package/dist/module/modules/encryption/eventMapping.js.map +1 -0
- package/dist/module/modules/encryption/index.js +4 -0
- package/dist/module/modules/encryption/index.js.map +1 -0
- package/dist/module/modules/encryption/parity.js +4 -0
- package/dist/module/modules/encryption/parity.js.map +1 -0
- package/dist/module/utils/StreamVideoRN/index.js +23 -0
- package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
- package/dist/module/utils/internal/callingx/callingx.js +29 -4
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/utils/internal/iosPipTrack.js +35 -0
- package/dist/module/utils/internal/iosPipTrack.js.map +1 -0
- package/dist/module/utils/internal/registerSDKGlobals.js +6 -0
- package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
- package/dist/module/utils/internal/ringingCallLifecycle.js +134 -0
- package/dist/module/utils/internal/ringingCallLifecycle.js.map +1 -0
- package/dist/module/utils/push/internal/utils.js +4 -0
- package/dist/module/utils/push/internal/utils.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +12 -0
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
- package/dist/typescript/components/Call/RingingCallContent/OutgoingCall.d.ts.map +1 -1
- package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts +11 -0
- package/dist/typescript/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.d.ts.map +1 -1
- package/dist/typescript/index.d.ts +1 -0
- package/dist/typescript/index.d.ts.map +1 -1
- package/dist/typescript/modules/encryption/EncryptionManager.d.ts +190 -0
- package/dist/typescript/modules/encryption/EncryptionManager.d.ts.map +1 -0
- package/dist/typescript/modules/encryption/eventMapping.d.ts +38 -0
- package/dist/typescript/modules/encryption/eventMapping.d.ts.map +1 -0
- package/dist/typescript/modules/encryption/index.d.ts +2 -0
- package/dist/typescript/modules/encryption/index.d.ts.map +1 -0
- package/dist/typescript/modules/encryption/parity.d.ts +55 -0
- package/dist/typescript/modules/encryption/parity.d.ts.map +1 -0
- package/dist/typescript/utils/StreamVideoRN/index.d.ts +20 -1
- package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
- package/dist/typescript/utils/StreamVideoRN/types.d.ts +67 -0
- package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
- package/dist/typescript/utils/internal/callingx/callingx.d.ts +1 -1
- package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +1 -1
- package/dist/typescript/utils/internal/iosPipTrack.d.ts +9 -0
- package/dist/typescript/utils/internal/iosPipTrack.d.ts.map +1 -0
- package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
- package/dist/typescript/utils/internal/ringingCallLifecycle.d.ts +27 -0
- package/dist/typescript/utils/internal/ringingCallLifecycle.d.ts.map +1 -0
- package/dist/typescript/utils/push/internal/utils.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/ios/RTCViewPip.swift +33 -8
- package/ios/RTCViewPipManager.mm +1 -0
- package/package.json +8 -8
- package/src/components/Call/CallContent/CallContent.tsx +1 -1
- package/src/components/Call/CallContent/RTCViewPipIOS.tsx +92 -10
- package/src/components/Call/CallContent/RTCViewPipNative.tsx +13 -0
- package/src/components/Call/RingingCallContent/OutgoingCall.tsx +4 -12
- package/src/components/Participant/ParticipantView/VideoRenderer/TrackSubscriber.tsx +58 -5
- package/src/index.ts +5 -0
- package/src/modules/encryption/EncryptionManager.ts +360 -0
- package/src/modules/encryption/eventMapping.ts +151 -0
- package/src/modules/encryption/index.ts +1 -0
- package/src/modules/encryption/parity.ts +83 -0
- package/src/utils/StreamVideoRN/index.ts +24 -1
- package/src/utils/StreamVideoRN/types.ts +68 -0
- package/src/utils/internal/callingx/callingx.ts +37 -2
- package/src/utils/internal/iosPipTrack.ts +39 -0
- package/src/utils/internal/registerSDKGlobals.ts +2 -0
- package/src/utils/internal/ringingCallLifecycle.ts +148 -0
- package/src/utils/push/internal/utils.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RTCEncryptionManager,
|
|
3
|
+
type RTCEncryptionEventData,
|
|
4
|
+
type RTCEncryptionEventType,
|
|
5
|
+
} from '@stream-io/react-native-webrtc';
|
|
6
|
+
import {
|
|
7
|
+
TypedEventEmitter,
|
|
8
|
+
videoLoggerSystem,
|
|
9
|
+
type E2EEAlgorithm,
|
|
10
|
+
type E2EEEventMap,
|
|
11
|
+
type E2EEManager,
|
|
12
|
+
type EncryptionManagerOptions,
|
|
13
|
+
type Listener,
|
|
14
|
+
type ScopedLogger,
|
|
15
|
+
} from '@stream-io/video-client';
|
|
16
|
+
import {
|
|
17
|
+
algorithmToNative,
|
|
18
|
+
E2EE_EVENT_TYPES,
|
|
19
|
+
mapNativeEvent,
|
|
20
|
+
trackTypeToNative,
|
|
21
|
+
} from './eventMapping';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Distributes keys to the native encryption manager and attaches
|
|
25
|
+
* encrypt/decrypt transforms to RTCRtpSenders and RTCRtpReceivers.
|
|
26
|
+
*
|
|
27
|
+
* The public surface matches the web
|
|
28
|
+
* {@link https://github.com/GetStream/stream-video-js/blob/main/packages/client/src/rtc/e2ee/EncryptionManager.ts | EncryptionManager}
|
|
29
|
+
* method for method, so host code is portable between the two SDKs.
|
|
30
|
+
*
|
|
31
|
+
* The crypto itself lives in the WebRTC binary, not here: frames never cross
|
|
32
|
+
* the React Native bridge. This class carries key and attach commands over it,
|
|
33
|
+
* and diagnostic events back.
|
|
34
|
+
*
|
|
35
|
+
* Like the keys it installs, the manager is application-owned: create one per
|
|
36
|
+
* call, attach it before {@link https://github.com/GetStream/stream-video-js/blob/main/packages/client/src/Call.ts | Call}`.join()`,
|
|
37
|
+
* and {@link EncryptionManager.dispose | dispose} it when that call is done.
|
|
38
|
+
* Nothing in the SDK will do it for you, and on React Native the leak is native
|
|
39
|
+
* rather than collectable.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* if (EncryptionManager.isSupported()) {
|
|
44
|
+
* const e2ee = await EncryptionManager.create(call.currentUserId);
|
|
45
|
+
* e2ee.setSharedKey(0, keyBytes);
|
|
46
|
+
* call.setE2EEManager(e2ee); // must happen before call.join()
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export class EncryptionManager implements E2EEManager {
|
|
51
|
+
private readonly algorithm: E2EEAlgorithm;
|
|
52
|
+
private readonly native: RTCEncryptionManager;
|
|
53
|
+
private readonly events: TypedEventEmitter<E2EEEventMap>;
|
|
54
|
+
private readonly logger: ScopedLogger;
|
|
55
|
+
private readonly bridged: Array<
|
|
56
|
+
[RTCEncryptionEventType, (data: RTCEncryptionEventData) => void]
|
|
57
|
+
> = [];
|
|
58
|
+
private disposed = false;
|
|
59
|
+
|
|
60
|
+
private constructor(native: RTCEncryptionManager, algorithm: E2EEAlgorithm) {
|
|
61
|
+
this.native = native;
|
|
62
|
+
this.algorithm = algorithm;
|
|
63
|
+
this.logger = videoLoggerSystem.getLogger('EncryptionManager');
|
|
64
|
+
this.events = new TypedEventEmitter<E2EEEventMap>(this.logger);
|
|
65
|
+
// Native dispatches per event name, so the one handler is registered under
|
|
66
|
+
// each of them, and each registration has to be undone on dispose.
|
|
67
|
+
for (const type of E2EE_EVENT_TYPES) {
|
|
68
|
+
this.native.on(type, this.handleNativeEvent);
|
|
69
|
+
this.bridged.push([type, this.handleNativeEvent]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private handleNativeEvent = (data: RTCEncryptionEventData) => {
|
|
74
|
+
const event = mapNativeEvent(data);
|
|
75
|
+
if (!event) return;
|
|
76
|
+
this.events.emit(event.type, event.payload);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether E2EE can run here. Use it to guard UI, or to avoid calling
|
|
81
|
+
* {@link create} where it would throw.
|
|
82
|
+
*
|
|
83
|
+
* `false` means the installed `@stream-io/react-native-webrtc` predates E2EE
|
|
84
|
+
* support.
|
|
85
|
+
*/
|
|
86
|
+
static isSupported = (): boolean =>
|
|
87
|
+
RTCEncryptionManager?.isSupported() ?? false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create an EncryptionManager and initialize the underlying native manager.
|
|
91
|
+
*
|
|
92
|
+
* Attach it with `call.setE2EEManager()` **before** `call.join()`: the join
|
|
93
|
+
* request carries the E2EE flag, and the peer connections are built with the
|
|
94
|
+
* transforms in place.
|
|
95
|
+
*
|
|
96
|
+
* Resolves rather than returning directly because every Stream SDK exposes
|
|
97
|
+
* this as a promise; nothing here is actually asynchronous, since a blocking
|
|
98
|
+
* native create is what keeps a sender from ever existing unencrypted.
|
|
99
|
+
*
|
|
100
|
+
* @param userId - The local user's ID, normally `call.currentUserId`.
|
|
101
|
+
* @param options - the create options.
|
|
102
|
+
* @throws {Error} If E2EE is unavailable. It never degrades to plaintext.
|
|
103
|
+
*/
|
|
104
|
+
static create = async (
|
|
105
|
+
userId: string,
|
|
106
|
+
options?: EncryptionManagerOptions,
|
|
107
|
+
): Promise<EncryptionManager> => {
|
|
108
|
+
if (!RTCEncryptionManager) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'E2EE requires a version of @stream-io/react-native-webrtc that supports RTCEncryptionManager',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!RTCEncryptionManager.isSupported()) {
|
|
114
|
+
throw new Error('E2EE is not supported on this device');
|
|
115
|
+
}
|
|
116
|
+
const algorithm = options?.algorithm ?? 'AES-128-GCM';
|
|
117
|
+
const native = RTCEncryptionManager.create(userId, {
|
|
118
|
+
algorithm: algorithmToNative(algorithm),
|
|
119
|
+
});
|
|
120
|
+
return new EncryptionManager(native, algorithm);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Subscribe to an E2EE event.
|
|
125
|
+
*
|
|
126
|
+
* @returns a function that unsubscribes the listener.
|
|
127
|
+
*/
|
|
128
|
+
on = <E extends keyof E2EEEventMap>(
|
|
129
|
+
event: E,
|
|
130
|
+
fn: Listener<E2EEEventMap[E]>,
|
|
131
|
+
): (() => void) => this.events.on(event, fn);
|
|
132
|
+
|
|
133
|
+
/** Unsubscribe a listener registered with {@link on}. */
|
|
134
|
+
off = <E extends keyof E2EEEventMap>(
|
|
135
|
+
event: E,
|
|
136
|
+
fn: Listener<E2EEEventMap[E]>,
|
|
137
|
+
): void => this.events.off(event, fn);
|
|
138
|
+
|
|
139
|
+
/** Drop every listener, or every listener of one event. */
|
|
140
|
+
removeAllListeners = (event?: keyof E2EEEventMap): void =>
|
|
141
|
+
this.events.removeAllListeners(event);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set a per-user AES-GCM encryption key in the native key store.
|
|
145
|
+
*
|
|
146
|
+
* Use it when each participant has their own key from a central authority.
|
|
147
|
+
* The receiver picks the right one by the `keyIndex` in the frame trailer.
|
|
148
|
+
*
|
|
149
|
+
* @param userId - The key owner.
|
|
150
|
+
* @param keyIndex - Increases with each rotation.
|
|
151
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
152
|
+
*/
|
|
153
|
+
setKey = (userId: string, keyIndex: number, rawKey: ArrayBuffer): void => {
|
|
154
|
+
this.assertUsable();
|
|
155
|
+
this.validateKeyIndex(keyIndex);
|
|
156
|
+
this.validateKeyLength(rawKey);
|
|
157
|
+
this.native.setKey(userId, keyIndex, this.copyKey(rawKey));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fallback key for any user without a per-user key. The simplest E2EE mode:
|
|
162
|
+
* one key for everyone, usually passphrase-derived, no distribution needed.
|
|
163
|
+
* Setting an epoch makes it active for encryption while older epochs remain
|
|
164
|
+
* available to decrypt in-flight frames until {@link removeSharedKey}.
|
|
165
|
+
*
|
|
166
|
+
* @param keyIndex - An integer 0-255, since one trailer byte carries it.
|
|
167
|
+
* @param rawKey - 16 bytes for AES-128-GCM, 32 for AES-256-GCM.
|
|
168
|
+
*/
|
|
169
|
+
setSharedKey = (keyIndex: number, rawKey: ArrayBuffer): void => {
|
|
170
|
+
this.assertUsable();
|
|
171
|
+
this.validateKeyIndex(keyIndex);
|
|
172
|
+
this.validateKeyLength(rawKey);
|
|
173
|
+
this.native.setSharedKey(keyIndex, this.copyKey(rawKey));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Retire one of a user's key epochs, leaving their other epochs usable.
|
|
178
|
+
*
|
|
179
|
+
* @param userId - The key owner.
|
|
180
|
+
* @param keyIndex - The exact epoch to remove. Absent epochs are a no-op.
|
|
181
|
+
*/
|
|
182
|
+
removeKey = (userId: string, keyIndex: number): void => {
|
|
183
|
+
this.assertUsable();
|
|
184
|
+
this.validateKeyIndex(keyIndex);
|
|
185
|
+
this.native.removeKey(userId, keyIndex);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Drop every key a user holds from the local key store, so later frames of
|
|
190
|
+
* theirs no longer decrypt.
|
|
191
|
+
*
|
|
192
|
+
* This is local cleanup, not remote revocation: it cannot stop that
|
|
193
|
+
* participant from decrypting anything, and a shared key still decrypts them.
|
|
194
|
+
* Revoking access means withholding future keys, which the application owns.
|
|
195
|
+
*
|
|
196
|
+
* To retire one rotated epoch instead, use {@link removeKey}.
|
|
197
|
+
*/
|
|
198
|
+
removeAllKeys = (userId: string): void => {
|
|
199
|
+
this.assertUsable();
|
|
200
|
+
this.native.removeAllKeys(userId);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove one shared-key epoch from the native receive key ring.
|
|
205
|
+
*
|
|
206
|
+
* If this is the active epoch, shared-key encryption stops until
|
|
207
|
+
* {@link setSharedKey} succeeds again. An older epoch is not reactivated.
|
|
208
|
+
*
|
|
209
|
+
* @param keyIndex - The exact shared-key epoch to remove.
|
|
210
|
+
*/
|
|
211
|
+
removeSharedKey = (keyIndex: number): void => {
|
|
212
|
+
this.assertUsable();
|
|
213
|
+
this.validateKeyIndex(keyIndex);
|
|
214
|
+
this.native.removeSharedKey(keyIndex);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Called by the Publisher when it adds a transceiver.
|
|
219
|
+
*
|
|
220
|
+
* Synchronous all the way to the native `SetFrameTransformer` call: were it
|
|
221
|
+
* async, the sender would exist before its transform did, which is a
|
|
222
|
+
* plaintext window. A failure throws instead of publishing cleartext.
|
|
223
|
+
*
|
|
224
|
+
* @param sender - The sender to encrypt.
|
|
225
|
+
* @param codec - Codec name, e.g. 'vp8', selecting the clear-byte rules.
|
|
226
|
+
* @param trackType - `TrackType` enum name, e.g. 'SCREEN_SHARE_AUDIO'.
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
229
|
+
encrypt = (
|
|
230
|
+
sender: RTCRtpSender,
|
|
231
|
+
codec?: string,
|
|
232
|
+
trackType?: string,
|
|
233
|
+
): void => {
|
|
234
|
+
this.assertUsable();
|
|
235
|
+
this.native.encrypt(sender as never, codec, trackTypeToNative(trackType));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Called by the Subscriber when a remote track arrives.
|
|
240
|
+
*
|
|
241
|
+
* @param receiver - The receiver to decrypt.
|
|
242
|
+
* @param userId - The remote user, for key lookup.
|
|
243
|
+
* @param trackType - `TrackType` enum name, e.g. 'SCREEN_SHARE_AUDIO'.
|
|
244
|
+
* @internal
|
|
245
|
+
*/
|
|
246
|
+
decrypt = (
|
|
247
|
+
receiver: RTCRtpReceiver,
|
|
248
|
+
userId: string,
|
|
249
|
+
trackType?: string,
|
|
250
|
+
): void => {
|
|
251
|
+
this.assertUsable();
|
|
252
|
+
this.native.decrypt(
|
|
253
|
+
receiver as never,
|
|
254
|
+
userId,
|
|
255
|
+
trackTypeToNative(trackType),
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Toggle periodic performance reporting.
|
|
261
|
+
*
|
|
262
|
+
* While on, `e2ee.perf_report` is emitted once per second with per-track FPS
|
|
263
|
+
* and crypto timings. Useful for debugging throughput.
|
|
264
|
+
*/
|
|
265
|
+
enablePerformanceReporting = (enabled: boolean): void => {
|
|
266
|
+
this.assertUsable();
|
|
267
|
+
// Observational, so it is the one native call that need not block. Kept
|
|
268
|
+
// void-returning to match the web manager.
|
|
269
|
+
this.native
|
|
270
|
+
.enablePerformanceReporting(enabled)
|
|
271
|
+
.catch((err) =>
|
|
272
|
+
this.logger.warn('Failed to toggle performance reporting', err),
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Request a snapshot of the installed keys. It arrives later as the
|
|
278
|
+
* `e2ee.key_state` event, listing fingerprints only, never key material.
|
|
279
|
+
*/
|
|
280
|
+
requestKeyState = (): void => {
|
|
281
|
+
this.assertUsable();
|
|
282
|
+
this.native
|
|
283
|
+
.requestKeyState()
|
|
284
|
+
.catch((err) => this.logger.warn('Failed to request key state', err));
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Release the native manager and all resources.
|
|
289
|
+
*
|
|
290
|
+
* **The application owns this object's lifetime — the SDK never disposes it.**
|
|
291
|
+
* Nothing releases the native manager when a peer connection closes, and there
|
|
292
|
+
* is no detach API, so an undisposed manager keeps its transforms and its key
|
|
293
|
+
* material alive for the lifetime of the process. Dispose it together with the
|
|
294
|
+
* call instance whose flow has ended; a later flow gets a fresh call and a
|
|
295
|
+
* fresh manager (see `Call.setE2EEManager`).
|
|
296
|
+
*
|
|
297
|
+
* In-flight frames are dropped rather than drained.
|
|
298
|
+
*
|
|
299
|
+
* The manager is unusable afterwards and every key or attach method throws.
|
|
300
|
+
* Call {@link create} for a new one. Safe to call more than once.
|
|
301
|
+
*/
|
|
302
|
+
dispose = (): void => {
|
|
303
|
+
if (this.disposed) return;
|
|
304
|
+
this.disposed = true;
|
|
305
|
+
for (const [type, handler] of this.bridged) {
|
|
306
|
+
this.native.off(type, handler);
|
|
307
|
+
}
|
|
308
|
+
this.bridged.length = 0;
|
|
309
|
+
try {
|
|
310
|
+
this.native.dispose();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
// Local cleanup still has to finish, or listeners outlive the manager.
|
|
313
|
+
this.logger.warn('Failed to dispose the native encryption manager', err);
|
|
314
|
+
}
|
|
315
|
+
this.events.removeAllListeners();
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* {@link dispose} releases the native manager, so an attached transform would
|
|
320
|
+
* point at nothing: frames would stall with no error and no event. Throwing
|
|
321
|
+
* is also fail-closed, since a caller that swallows it still publishes
|
|
322
|
+
* nothing rather than cleartext.
|
|
323
|
+
*/
|
|
324
|
+
private assertUsable = () => {
|
|
325
|
+
if (this.disposed) throw new Error(`EncryptionManager is disposed`);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The caller keeps ownership of its buffer and may re-import the same bytes,
|
|
330
|
+
* so hand native a copy rather than a view onto memory that can change.
|
|
331
|
+
*/
|
|
332
|
+
private copyKey = (rawKey: ArrayBuffer): Uint8Array => {
|
|
333
|
+
const copy = new Uint8Array(rawKey.byteLength);
|
|
334
|
+
copy.set(new Uint8Array(rawKey));
|
|
335
|
+
return copy;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
private validateKeyLength = (rawKey: ArrayBuffer) => {
|
|
339
|
+
const is256 = this.algorithm === 'AES-256-GCM';
|
|
340
|
+
const expected = is256 ? 32 : 16;
|
|
341
|
+
if (rawKey.byteLength !== expected) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Key must be exactly ${expected} bytes (${is256 ? 'AES-256' : 'AES-128'})`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* One trailer byte carries the keyIndex. A larger value would truncate to
|
|
350
|
+
* `keyIndex & 0xFF`, so the receiver would look up the wrong key and fail
|
|
351
|
+
* every decrypt. Reject it rather than ship a silently broken key epoch.
|
|
352
|
+
*/
|
|
353
|
+
private validateKeyIndex = (keyIndex: number) => {
|
|
354
|
+
if (!Number.isInteger(keyIndex) || keyIndex < 0 || keyIndex > 255) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`keyIndex must be an integer between 0 and 255, got ${keyIndex}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RTCEncryptionAlgorithm,
|
|
3
|
+
RTCEncryptionTrackType,
|
|
4
|
+
type RTCEncryptionEventData,
|
|
5
|
+
type RTCEncryptionTrackPerf,
|
|
6
|
+
} from '@stream-io/react-native-webrtc';
|
|
7
|
+
import type {
|
|
8
|
+
E2EEAlgorithm,
|
|
9
|
+
E2EEEventMap,
|
|
10
|
+
TrackPerf,
|
|
11
|
+
} from '@stream-io/video-client';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The core RTC layer labels tracks with the `TrackType` enum *name*
|
|
15
|
+
* (`SfuModels.TrackType[trackType]`), while the native manager takes a numeric
|
|
16
|
+
* enum. Screen-share audio is deliberately its own value: native keeps replay
|
|
17
|
+
* state per (userId, trackType), so folding it into `AUDIO` mis-groups it.
|
|
18
|
+
*
|
|
19
|
+
* These read the native enums inside the function body on purpose. This module
|
|
20
|
+
* is reachable from the SDK's public entry point, so an app whose
|
|
21
|
+
* `@stream-io/react-native-webrtc` predates E2EE would crash on import if the
|
|
22
|
+
* enums were dereferenced at module scope - even if it never touches E2EE.
|
|
23
|
+
*/
|
|
24
|
+
export const trackTypeToNative = (
|
|
25
|
+
trackType?: string,
|
|
26
|
+
): RTCEncryptionTrackType | undefined => {
|
|
27
|
+
switch (trackType) {
|
|
28
|
+
case 'AUDIO':
|
|
29
|
+
return RTCEncryptionTrackType.AUDIO;
|
|
30
|
+
case 'VIDEO':
|
|
31
|
+
return RTCEncryptionTrackType.VIDEO;
|
|
32
|
+
case 'SCREEN_SHARE':
|
|
33
|
+
return RTCEncryptionTrackType.SCREEN_SHARE;
|
|
34
|
+
case 'SCREEN_SHARE_AUDIO':
|
|
35
|
+
return RTCEncryptionTrackType.SCREEN_SHARE_AUDIO;
|
|
36
|
+
default:
|
|
37
|
+
// Native infers audio vs video from the sender rather than pinning a
|
|
38
|
+
// wrong value.
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Map a native track type back to the name the web events carry. */
|
|
44
|
+
export const trackTypeFromNative = (
|
|
45
|
+
trackType?: RTCEncryptionTrackType | number,
|
|
46
|
+
): string | undefined => {
|
|
47
|
+
switch (trackType) {
|
|
48
|
+
case RTCEncryptionTrackType.AUDIO:
|
|
49
|
+
return 'AUDIO';
|
|
50
|
+
case RTCEncryptionTrackType.VIDEO:
|
|
51
|
+
return 'VIDEO';
|
|
52
|
+
case RTCEncryptionTrackType.SCREEN_SHARE:
|
|
53
|
+
return 'SCREEN_SHARE';
|
|
54
|
+
case RTCEncryptionTrackType.SCREEN_SHARE_AUDIO:
|
|
55
|
+
return 'SCREEN_SHARE_AUDIO';
|
|
56
|
+
default:
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Map the public algorithm name to the native enum. */
|
|
62
|
+
export const algorithmToNative = (
|
|
63
|
+
algorithm: E2EEAlgorithm,
|
|
64
|
+
): RTCEncryptionAlgorithm =>
|
|
65
|
+
algorithm === 'AES-256-GCM'
|
|
66
|
+
? RTCEncryptionAlgorithm.AES_256_GCM
|
|
67
|
+
: RTCEncryptionAlgorithm.AES_128_GCM;
|
|
68
|
+
|
|
69
|
+
const toTrackPerf = (row: RTCEncryptionTrackPerf): TrackPerf => ({
|
|
70
|
+
userId: row.userId,
|
|
71
|
+
trackType: trackTypeFromNative(row.trackType) ?? '',
|
|
72
|
+
fps: row.fps,
|
|
73
|
+
maxCryptoMs: row.maxCryptoMs,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Translate a native event into the payload the web manager emits, so a host
|
|
78
|
+
* can write one set of handlers for both platforms.
|
|
79
|
+
*
|
|
80
|
+
* Two things have to be reshaped rather than passed through: native track types
|
|
81
|
+
* are numeric, and native flattens `managerId`/`type`/`userId` alongside the
|
|
82
|
+
* payload of every event, including the two (`perf_report`, `key_state`) that
|
|
83
|
+
* are not about a single user. Fields the web types require but native leaves
|
|
84
|
+
* optional get an explicit default, so a handler never reads `undefined` where
|
|
85
|
+
* its type promises a value.
|
|
86
|
+
*
|
|
87
|
+
* @returns the mapped event, or `undefined` for an unrecognized event name.
|
|
88
|
+
*/
|
|
89
|
+
export const mapNativeEvent = <E extends keyof E2EEEventMap>(
|
|
90
|
+
event: RTCEncryptionEventData,
|
|
91
|
+
): { type: E; payload: E2EEEventMap[E] } | undefined => {
|
|
92
|
+
const { type, userId, trackType, keyIndex, version, reason } = event;
|
|
93
|
+
const mapped = (payload: E2EEEventMap[keyof E2EEEventMap]) =>
|
|
94
|
+
({ type, payload }) as { type: E; payload: E2EEEventMap[E] };
|
|
95
|
+
|
|
96
|
+
switch (type) {
|
|
97
|
+
case 'e2ee.decryption_failed':
|
|
98
|
+
case 'e2ee.decryption_resumed':
|
|
99
|
+
case 'e2ee.unencrypted_frame':
|
|
100
|
+
return mapped({ userId, trackType: trackTypeFromNative(trackType) });
|
|
101
|
+
case 'e2ee.missing_key':
|
|
102
|
+
return mapped({
|
|
103
|
+
userId,
|
|
104
|
+
keyIndex,
|
|
105
|
+
trackType: trackTypeFromNative(trackType),
|
|
106
|
+
});
|
|
107
|
+
case 'e2ee.decryption_stalled':
|
|
108
|
+
return mapped({
|
|
109
|
+
userId,
|
|
110
|
+
keyIndex: keyIndex ?? 0,
|
|
111
|
+
trackType: trackTypeFromNative(trackType),
|
|
112
|
+
});
|
|
113
|
+
case 'e2ee.encryption_failed':
|
|
114
|
+
return mapped({
|
|
115
|
+
userId,
|
|
116
|
+
trackType: trackTypeFromNative(trackType),
|
|
117
|
+
reason: reason ?? '',
|
|
118
|
+
});
|
|
119
|
+
case 'e2ee.unsupported_version':
|
|
120
|
+
return mapped({
|
|
121
|
+
userId,
|
|
122
|
+
trackType: trackTypeFromNative(trackType),
|
|
123
|
+
version: version ?? 0,
|
|
124
|
+
});
|
|
125
|
+
case 'e2ee.perf_report':
|
|
126
|
+
return mapped({
|
|
127
|
+
encode: (event.encode ?? []).map((row) => ({
|
|
128
|
+
...toTrackPerf(row),
|
|
129
|
+
codec: row.codec ?? '',
|
|
130
|
+
})),
|
|
131
|
+
decode: (event.decode ?? []).map(toTrackPerf),
|
|
132
|
+
});
|
|
133
|
+
case 'e2ee.key_state':
|
|
134
|
+
return mapped(event.keyState ?? { perUserKeys: [], sharedKeys: [] });
|
|
135
|
+
default:
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Every event the native manager emits, i.e. what the bridge subscribes to. */
|
|
141
|
+
export const E2EE_EVENT_TYPES = [
|
|
142
|
+
'e2ee.decryption_failed',
|
|
143
|
+
'e2ee.decryption_resumed',
|
|
144
|
+
'e2ee.decryption_stalled',
|
|
145
|
+
'e2ee.encryption_failed',
|
|
146
|
+
'e2ee.missing_key',
|
|
147
|
+
'e2ee.unencrypted_frame',
|
|
148
|
+
'e2ee.unsupported_version',
|
|
149
|
+
'e2ee.perf_report',
|
|
150
|
+
'e2ee.key_state',
|
|
151
|
+
] as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { EncryptionManager } from './EncryptionManager';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time proof that the React Native {@link EncryptionManager} exposes the
|
|
3
|
+
* same public surface as the web one in `@stream-io/video-client`.
|
|
4
|
+
*
|
|
5
|
+
* This file has no runtime output and is imported by nothing. It exists because
|
|
6
|
+
* the SDK's entry point deliberately *shadows* the client's `EncryptionManager`
|
|
7
|
+
* export with this platform's implementation: a host writing against the
|
|
8
|
+
* documented API gets whichever class its platform ships, so the two surfaces
|
|
9
|
+
* diverging would be a silent break for that host rather than a build error.
|
|
10
|
+
* Both directions are asserted, so adding a method to either manager fails the
|
|
11
|
+
* build until the other one follows. Keep it that way, or delete both managers'
|
|
12
|
+
* claim to a shared API.
|
|
13
|
+
*
|
|
14
|
+
* `test:types` and the `tsc` pass in `bob build` both cover `src`, so CI runs
|
|
15
|
+
* this whether or not anything imports it.
|
|
16
|
+
*/
|
|
17
|
+
import type { EncryptionManager as WebEncryptionManager } from '@stream-io/video-client';
|
|
18
|
+
import type { EncryptionManager } from './EncryptionManager';
|
|
19
|
+
|
|
20
|
+
/** Fails to compile unless `T` is exactly `true`. */
|
|
21
|
+
type AssertTrue<T extends true> = T;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Public members only.
|
|
25
|
+
*
|
|
26
|
+
* A class type carrying `private` members is nominal, so the two managers can
|
|
27
|
+
* never be compared as classes however identical their APIs. `keyof` yields
|
|
28
|
+
* public keys alone, which is also precisely the surface a host can touch.
|
|
29
|
+
*/
|
|
30
|
+
type PublicSurface<T> = { [K in keyof T]: T[K] };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `emit` is public on the web manager only because it extends the shared
|
|
34
|
+
* `TypedEventEmitter`. Dispatching a forged E2EE event is not part of the
|
|
35
|
+
* documented API — `SPEC.md` §2 lists `on`/`off` and nothing else — so it is
|
|
36
|
+
* excluded here rather than mirrored on this side.
|
|
37
|
+
*/
|
|
38
|
+
type WebSurface = Omit<PublicSurface<WebEncryptionManager>, 'emit'>;
|
|
39
|
+
type NativeSurface = PublicSurface<EncryptionManager>;
|
|
40
|
+
|
|
41
|
+
/** Every documented web member exists here, with a compatible signature. */
|
|
42
|
+
export type NativeCoversWeb = AssertTrue<
|
|
43
|
+
NativeSurface extends WebSurface ? true : false
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ...and nothing extra. A React Native-only method would compile fine but make
|
|
48
|
+
* host code silently unportable, which is the failure this file exists to catch.
|
|
49
|
+
*/
|
|
50
|
+
export type WebCoversNative = AssertTrue<
|
|
51
|
+
WebSurface extends NativeSurface ? true : false
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The statics are checked by signature rather than by assignability: both
|
|
56
|
+
* `create`s return their own class, so comparing them whole would only ever
|
|
57
|
+
* restate that the two classes are nominally distinct.
|
|
58
|
+
*/
|
|
59
|
+
export type CreateAcceptsSameArguments = AssertTrue<
|
|
60
|
+
Parameters<typeof EncryptionManager.create> extends Parameters<
|
|
61
|
+
typeof WebEncryptionManager.create
|
|
62
|
+
>
|
|
63
|
+
? Parameters<typeof WebEncryptionManager.create> extends Parameters<
|
|
64
|
+
typeof EncryptionManager.create
|
|
65
|
+
>
|
|
66
|
+
? true
|
|
67
|
+
: false
|
|
68
|
+
: false
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
export type CreateResolvesAManager = AssertTrue<
|
|
72
|
+
Awaited<ReturnType<typeof EncryptionManager.create>> extends NativeSurface
|
|
73
|
+
? true
|
|
74
|
+
: false
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
export type IsSupportedMatches = AssertTrue<
|
|
78
|
+
typeof EncryptionManager.isSupported extends typeof WebEncryptionManager.isSupported
|
|
79
|
+
? typeof WebEncryptionManager.isSupported extends typeof EncryptionManager.isSupported
|
|
80
|
+
? true
|
|
81
|
+
: false
|
|
82
|
+
: false
|
|
83
|
+
>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { StreamVideoConfig } from './types';
|
|
1
|
+
import type { RingingCallLifecycleHooks, StreamVideoConfig } from './types';
|
|
2
|
+
import { setRingingCallLifecycleHooks as storeRingingCallLifecycleHooks } from '../internal/ringingCallLifecycle';
|
|
2
3
|
import pushLogoutCallbacks from '../internal/pushLogoutCallback';
|
|
3
4
|
import { setupIosVoipPushEvents } from '../push/setupIosVoipPushEvents';
|
|
4
5
|
import { setupAndroidPushEvents } from '../push/setupAndroidPushEvents';
|
|
@@ -137,6 +138,28 @@ export class StreamVideoRN {
|
|
|
137
138
|
setupAndroidPushEvents(pushConfig);
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Register hooks that run around a ringing call's join, for every ringing path:
|
|
143
|
+
* accepted from the CallKit/Telecom UI, accepted inside the app, and outgoing.
|
|
144
|
+
*
|
|
145
|
+
* Call this at your application's entry point, alongside
|
|
146
|
+
* {@link StreamVideoRN.setPushConfig} and **outside** the React tree. A call
|
|
147
|
+
* accepted from a push notification can be created and joined before any
|
|
148
|
+
* component mounts, so hooks registered from inside React would be missed on
|
|
149
|
+
* exactly the path that most needs them.
|
|
150
|
+
*
|
|
151
|
+
* Calling it again replaces the previously registered hooks.
|
|
152
|
+
*
|
|
153
|
+
* @example // in index.js
|
|
154
|
+
* StreamVideoRN.setRingingCallLifecycleHooks({
|
|
155
|
+
* onBeforeCallJoin: (call) => attachE2EEIfConfigured(call),
|
|
156
|
+
* onAfterCallLeave: (call) => disposeE2EEManager(call),
|
|
157
|
+
* });
|
|
158
|
+
*/
|
|
159
|
+
static setRingingCallLifecycleHooks = (hooks: RingingCallLifecycleHooks) => {
|
|
160
|
+
storeRingingCallLifecycleHooks(hooks);
|
|
161
|
+
};
|
|
162
|
+
|
|
140
163
|
static getConfig() {
|
|
141
164
|
return this.config;
|
|
142
165
|
}
|