@stream-io/video-client 1.57.0 → 1.59.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/e2ee-worker.browser.es.js +1342 -0
  3. package/dist/e2ee-worker.browser.es.js.map +1 -0
  4. package/dist/index.browser.es.js +1141 -117
  5. package/dist/index.browser.es.js.map +1 -1
  6. package/dist/index.cjs.js +1196 -115
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.es.js +1174 -117
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/src/Call.d.ts +66 -9
  12. package/dist/src/StreamVideoClient.d.ts +9 -1
  13. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  14. package/dist/src/devices/CameraManager.d.ts +8 -1
  15. package/dist/src/devices/DeviceManager.d.ts +2 -1
  16. package/dist/src/devices/MicrophoneManager.d.ts +8 -1
  17. package/dist/src/devices/SpeakerManager.d.ts +2 -1
  18. package/dist/src/events/participant.d.ts +10 -3
  19. package/dist/src/gen/coordinator/index.d.ts +1386 -240
  20. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  21. package/dist/src/helpers/MediaStreamRecorder.d.ts +26 -0
  22. package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
  23. package/dist/src/helpers/concurrency.d.ts +8 -0
  24. package/dist/src/helpers/ensureExhausted.d.ts +1 -1
  25. package/dist/src/helpers/loopback.d.ts +55 -0
  26. package/dist/src/permissions/PermissionsContext.d.ts +2 -2
  27. package/dist/src/reporting/ClientEventReporter.d.ts +0 -2
  28. package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
  29. package/dist/src/rtc/Subscriber.d.ts +5 -0
  30. package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
  31. package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
  32. package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
  33. package/dist/src/rtc/e2ee/events.d.ts +163 -0
  34. package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
  35. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
  36. package/dist/src/rtc/index.d.ts +1 -0
  37. package/dist/src/rtc/mediaEngine.d.ts +23 -0
  38. package/dist/src/rtc/types.d.ts +2 -0
  39. package/dist/src/store/CallState.d.ts +21 -0
  40. package/dist/src/types.d.ts +15 -6
  41. package/index.ts +5 -0
  42. package/package.json +1 -1
  43. package/src/Call.ts +311 -155
  44. package/src/StreamVideoClient.ts +35 -8
  45. package/src/__tests__/Call.lifecycle.test.ts +33 -2
  46. package/src/__tests__/Call.publishing.test.ts +42 -1
  47. package/src/__tests__/Call.test.ts +15 -0
  48. package/src/__tests__/StreamVideoClient.api.test.ts +33 -0
  49. package/src/__tests__/StreamVideoClient.rewatch.test.ts +213 -0
  50. package/src/__tests__/StreamVideoClient.ringing.test.ts +11 -4
  51. package/src/__tests__/StreamVideoClient.test.ts +14 -12
  52. package/src/coordinator/connection/connection.ts +6 -3
  53. package/src/devices/AudioDeviceManager.ts +6 -0
  54. package/src/devices/CameraManager.ts +67 -8
  55. package/src/devices/DeviceManager.ts +24 -4
  56. package/src/devices/MicrophoneManager.ts +66 -8
  57. package/src/devices/ScreenShareManager.ts +3 -0
  58. package/src/devices/SpeakerManager.ts +42 -12
  59. package/src/devices/__tests__/CameraManager.test.ts +27 -1
  60. package/src/devices/__tests__/DeviceManager.test.ts +49 -1
  61. package/src/devices/__tests__/MicrophoneManager.test.ts +23 -1
  62. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -20
  63. package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
  64. package/src/devices/__tests__/mocks.ts +36 -3
  65. package/src/events/__tests__/participant.test.ts +134 -1
  66. package/src/events/call.ts +6 -3
  67. package/src/events/callEventHandlers.ts +9 -3
  68. package/src/events/participant.ts +42 -7
  69. package/src/gen/coordinator/index.ts +1402 -236
  70. package/src/gen/video/sfu/models/models.ts +15 -0
  71. package/src/helpers/MediaStreamRecorder.ts +104 -0
  72. package/src/helpers/TypedEventEmitter.ts +71 -0
  73. package/src/helpers/__tests__/MediaStreamRecorder.test.ts +324 -0
  74. package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
  75. package/src/helpers/__tests__/concurrency.test.ts +74 -0
  76. package/src/helpers/__tests__/loopback.test.ts +367 -0
  77. package/src/helpers/concurrency.ts +24 -0
  78. package/src/helpers/ensureExhausted.ts +4 -2
  79. package/src/helpers/loopback.ts +200 -0
  80. package/src/permissions/PermissionsContext.ts +10 -5
  81. package/src/reporting/ClientEventReporter.ts +15 -28
  82. package/src/reporting/__tests__/ClientEventReporter.test.ts +3 -7
  83. package/src/rtc/BasePeerConnection.ts +12 -1
  84. package/src/rtc/Publisher.ts +8 -0
  85. package/src/rtc/Subscriber.ts +13 -0
  86. package/src/rtc/__tests__/Publisher.test.ts +101 -0
  87. package/src/rtc/__tests__/Subscriber.test.ts +190 -0
  88. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
  89. package/src/rtc/e2ee/E2EEManager.ts +12 -0
  90. package/src/rtc/e2ee/EncryptionManager.ts +336 -0
  91. package/src/rtc/e2ee/SPEC.md +586 -0
  92. package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
  93. package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
  94. package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
  95. package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
  96. package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
  97. package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
  98. package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
  99. package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
  100. package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
  101. package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
  102. package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
  103. package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
  104. package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
  105. package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
  106. package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
  107. package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
  108. package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
  109. package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
  110. package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
  111. package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
  112. package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
  113. package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
  114. package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
  115. package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
  116. package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
  117. package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
  118. package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
  119. package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
  120. package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
  121. package/src/rtc/e2ee/e2ee-worker.ts +27 -0
  122. package/src/rtc/e2ee/events.ts +179 -0
  123. package/src/rtc/e2ee/transformSupport.ts +35 -0
  124. package/src/rtc/helpers/rtcConfiguration.ts +4 -2
  125. package/src/rtc/index.ts +1 -0
  126. package/src/rtc/mediaEngine.ts +44 -0
  127. package/src/rtc/types.ts +2 -0
  128. package/src/store/CallState.ts +33 -1
  129. package/src/store/__tests__/CallState.test.ts +82 -1
  130. 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,26 @@
1
+ export type MediaStreamRecorderOptions = {
2
+ /** Auto-stop after this many milliseconds. */
3
+ maxDurationMs?: number;
4
+ };
5
+ export type MediaRecordingResult = {
6
+ blob: Blob;
7
+ };
8
+ /** Single-use `MediaRecorder` wrapper that returns an in-memory blob. */
9
+ export declare class MediaStreamRecorder {
10
+ private recording;
11
+ private logger;
12
+ private chunks;
13
+ private timeoutId;
14
+ private recorder;
15
+ private readonly tracks;
16
+ private readonly maxDurationMs;
17
+ constructor(stream: MediaStream, options?: MediaStreamRecorderOptions);
18
+ /** Starts recording and resolves when recording stops. */
19
+ start: () => Promise<MediaRecordingResult | null>;
20
+ /** Stops recording early. */
21
+ stop: () => void;
22
+ private isSettled;
23
+ private detach;
24
+ private handleStop;
25
+ private handleError;
26
+ }
@@ -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
+ }
@@ -27,6 +27,14 @@ export declare const withoutConcurrency: <T>(tag: TagKey, cb: () => Promise<T>)
27
27
  * it's up to the function to decide how to react to cancelation.
28
28
  */
29
29
  export declare const withCancellation: <T>(tag: TagKey, cb: (signal: AbortSignal) => Promise<T | "canceled">) => Promise<T | "canceled">;
30
+ /**
31
+ * Wraps an async function so concurrent calls share the in-flight promise
32
+ * instead of queueing another execution.
33
+ *
34
+ * @param cb Async function to wrap.
35
+ * @returns Wrapped async function.
36
+ */
37
+ export declare function singleFlight<P extends unknown[], T>(cb: (...args: P) => Promise<T>): (...args: P) => Promise<T>;
30
38
  export declare function hasPending(tag: TagKey): boolean;
31
39
  export declare function settled(tag: TagKey): Promise<void>;
32
40
  export {};
@@ -1 +1 @@
1
- export declare const ensureExhausted: (x: never, message: string) => void;
1
+ export declare const ensureExhausted: (x: never, message?: string) => void;
@@ -0,0 +1,55 @@
1
+ import type { Call } from '../Call';
2
+ import type { StreamVideoParticipant } from '../types';
3
+ /** Default loopback stream wait timeout. */
4
+ export declare const LOOPBACK_STREAMS_WAIT_TIMEOUT_MS: number;
5
+ /** Default loopback recording duration. */
6
+ export declare const DEFAULT_LOOPBACK_RECORDING_DURATION_MS: number;
7
+ /** Shortest permitted loopback recording duration. */
8
+ export declare const MIN_LOOPBACK_RECORDING_DURATION_MS: number;
9
+ /** Longest permitted loopback recording duration. */
10
+ export declare const MAX_LOOPBACK_RECORDING_DURATION_MS: number;
11
+ /** SFU-echoed streams on the local participant. */
12
+ export type LoopbackStreams = {
13
+ loopbackVideoStream?: MediaStream;
14
+ loopbackAudioStream?: MediaStream;
15
+ };
16
+ /** SFU-echoed tracks on the local participant. */
17
+ export type LoopbackTracks = {
18
+ audioTrack?: MediaStreamTrack;
19
+ videoTrack?: MediaStreamTrack;
20
+ };
21
+ /** Thrown when loopback tracks do not arrive in time. */
22
+ export declare class LoopbackStreamsTimeoutError extends Error {
23
+ constructor();
24
+ }
25
+ /**
26
+ * Returns the streams on the given participant that the SFU echoed back to us,
27
+ * ignoring the local capture streams the publishing path writes to the same
28
+ * fields.
29
+ */
30
+ export declare const getLoopbackStreams: (call: Call | undefined, participant: StreamVideoParticipant | undefined) => LoopbackStreams;
31
+ /**
32
+ * Returns loopback tracks when all required tracks are present.
33
+ *
34
+ * Prefer {@link waitForLoopbackStreams}, which observes call state until the
35
+ * tracks arrive. This is the synchronous snapshot it is built from.
36
+ *
37
+ * @internal
38
+ */
39
+ export declare const getLoopbackTracks: (call: Call | undefined, participant: StreamVideoParticipant | undefined, includeVideo: boolean) => LoopbackTracks | undefined;
40
+ export type WaitForLoopbackStreamsOptions = {
41
+ /** Whether video is required. Audio is always required. */
42
+ includeVideo: boolean;
43
+ /** Aborts the wait. */
44
+ signal: AbortSignal;
45
+ /** Defaults to {@link LOOPBACK_STREAMS_WAIT_TIMEOUT_MS}. */
46
+ timeoutMs?: number;
47
+ };
48
+ /** Waits for the requested loopback tracks. */
49
+ export declare const waitForLoopbackStreams: (call: Call, opts: WaitForLoopbackStreamsOptions) => Promise<LoopbackTracks | null>;
50
+ /** Enables loopback audio while `fn` runs, then restores it. */
51
+ export declare const withLoopbackAudioEnabled: <T>(tracks: LoopbackTracks, fn: () => Promise<T>) => Promise<T>;
52
+ /** Enables loopback audio and returns a restore function. */
53
+ export declare const enableLoopbackAudio: (tracks: LoopbackTracks) => (() => void);
54
+ /** Clamps and rounds a loopback recording duration. */
55
+ export declare const clampLoopbackRecordingDuration: (durationMs: number) => number;
@@ -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: OwnCapability, settings?: CallSettingsResponse | undefined) => boolean;
44
+ canRequest: (permission: RequestPermissionRequestPermissionsEnum, settings?: CallSettingsResponse | undefined) => boolean;
45
45
  }
@@ -12,7 +12,6 @@ export type CallReportContext = {
12
12
  callId: string;
13
13
  getSfuId: () => string;
14
14
  getCallSessionId: () => string;
15
- getUserSessionId: () => string;
16
15
  };
17
16
  export type ClientEventReporterOptions = {
18
17
  streamClient: StreamClient;
@@ -79,7 +78,6 @@ export declare class ClientEventReporter {
79
78
  private emitPeerConnectionSuccess;
80
79
  private emitPeerConnectionFailure;
81
80
  private getSfuId;
82
- private getUserSessionId;
83
81
  private sessionIdField;
84
82
  private buildCommon;
85
83
  private send;
@@ -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.
@@ -29,6 +29,11 @@ export declare class Subscriber extends BasePeerConnection {
29
29
  * Restarts the ICE connection and renegotiates with the SFU.
30
30
  */
31
31
  restartIce: () => Promise<void>;
32
+ /**
33
+ * Whether the given stream is one the SFU echoed back to us
34
+ * (`allowOwnTracksLoopback`) rather than a local capture stream.
35
+ */
36
+ isSelfSubscribedStream: (stream: MediaStream | undefined) => boolean;
32
37
  private handleOnTrack;
33
38
  private setRemoteTrackInterrupted;
34
39
  private negotiate;
@@ -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 { ICEServer } from '../../gen/coordinator';
2
- export declare const toRtcConfiguration: (config: ICEServer[]) => RTCConfiguration;
1
+ import { ICEServerResponse } from '../../gen/coordinator';
2
+ export declare const toRtcConfiguration: (config: ICEServerResponse[]) => RTCConfiguration;
@@ -1,4 +1,5 @@
1
1
  export * from './codecs';
2
+ export * from './mediaEngine';
2
3
  export * from './Dispatcher';
3
4
  export * from './NegotiationError';
4
5
  export * from './IceTrickleBuffer';
@@ -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;
@@ -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;