livekit-client 1.12.3 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +83 -9
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +357 -97
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts +2 -5
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  12. package/dist/src/connectionHelper/checks/webrtc.d.ts.map +1 -1
  13. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +5 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/KeyProvider.d.ts +4 -2
  17. package/dist/src/e2ee/KeyProvider.d.ts.map +1 -1
  18. package/dist/src/e2ee/constants.d.ts +2 -0
  19. package/dist/src/e2ee/constants.d.ts.map +1 -1
  20. package/dist/src/e2ee/types.d.ts +7 -1
  21. package/dist/src/e2ee/types.d.ts.map +1 -1
  22. package/dist/src/e2ee/utils.d.ts +1 -0
  23. package/dist/src/e2ee/utils.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts +4 -2
  25. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  26. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  27. package/dist/src/e2ee/worker/SifGuard.d.ts +11 -0
  28. package/dist/src/e2ee/worker/SifGuard.d.ts.map +1 -0
  29. package/dist/src/options.d.ts +5 -0
  30. package/dist/src/options.d.ts.map +1 -1
  31. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  32. package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
  33. package/dist/src/room/DeviceManager.d.ts +1 -0
  34. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  35. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  36. package/dist/src/room/Room.d.ts +1 -1
  37. package/dist/src/room/Room.d.ts.map +1 -1
  38. package/dist/src/room/defaults.d.ts.map +1 -1
  39. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  40. package/dist/src/room/participant/Participant.d.ts +5 -0
  41. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  42. package/dist/src/room/participant/RemoteParticipant.d.ts +0 -5
  43. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  44. package/dist/src/room/timers.d.ts +2 -2
  45. package/dist/src/room/timers.d.ts.map +1 -1
  46. package/dist/src/room/track/LocalAudioTrack.d.ts +9 -1
  47. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  48. package/dist/src/room/track/LocalTrack.d.ts +3 -3
  49. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  50. package/dist/src/room/track/LocalVideoTrack.d.ts +6 -0
  51. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  52. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  53. package/dist/src/room/track/processor/types.d.ts +13 -2
  54. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  55. package/dist/src/room/types.d.ts +1 -1
  56. package/dist/src/room/types.d.ts.map +1 -1
  57. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -5
  58. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +5 -0
  59. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +4 -2
  60. package/dist/ts4.2/src/e2ee/constants.d.ts +2 -0
  61. package/dist/ts4.2/src/e2ee/types.d.ts +7 -1
  62. package/dist/ts4.2/src/e2ee/utils.d.ts +1 -0
  63. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +4 -2
  64. package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +11 -0
  65. package/dist/ts4.2/src/options.d.ts +5 -0
  66. package/dist/ts4.2/src/room/DeviceManager.d.ts +1 -0
  67. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  68. package/dist/ts4.2/src/room/participant/Participant.d.ts +5 -0
  69. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +0 -5
  70. package/dist/ts4.2/src/room/timers.d.ts +2 -2
  71. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +9 -1
  72. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
  73. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +6 -0
  74. package/dist/ts4.2/src/room/track/processor/types.d.ts +13 -2
  75. package/dist/ts4.2/src/room/types.d.ts +1 -1
  76. package/package.json +15 -16
  77. package/src/api/SignalClient.ts +13 -9
  78. package/src/connectionHelper/checks/turn.ts +1 -0
  79. package/src/connectionHelper/checks/webrtc.ts +9 -7
  80. package/src/connectionHelper/checks/websocket.ts +1 -0
  81. package/src/e2ee/E2eeManager.ts +27 -2
  82. package/src/e2ee/KeyProvider.ts +9 -4
  83. package/src/e2ee/constants.ts +3 -0
  84. package/src/e2ee/types.ts +9 -1
  85. package/src/e2ee/utils.ts +9 -0
  86. package/src/e2ee/worker/FrameCryptor.ts +46 -17
  87. package/src/e2ee/worker/ParticipantKeyHandler.ts +1 -0
  88. package/src/e2ee/worker/SifGuard.ts +47 -0
  89. package/src/e2ee/worker/e2ee.worker.ts +14 -0
  90. package/src/options.ts +6 -0
  91. package/src/proto/livekit_models_pb.ts +14 -0
  92. package/src/proto/livekit_rtc_pb.ts +14 -0
  93. package/src/room/DeviceManager.ts +7 -2
  94. package/src/room/RTCEngine.ts +3 -1
  95. package/src/room/Room.ts +27 -7
  96. package/src/room/defaults.ts +1 -0
  97. package/src/room/participant/LocalParticipant.ts +14 -2
  98. package/src/room/participant/Participant.ts +16 -0
  99. package/src/room/participant/RemoteParticipant.ts +0 -12
  100. package/src/room/track/LocalAudioTrack.ts +45 -0
  101. package/src/room/track/LocalTrack.ts +4 -4
  102. package/src/room/track/LocalVideoTrack.ts +39 -0
  103. package/src/room/track/RemoteAudioTrack.ts +9 -1
  104. package/src/room/track/RemoteTrackPublication.ts +2 -2
  105. package/src/room/track/processor/types.ts +17 -2
  106. package/src/room/types.ts +5 -1
@@ -3,7 +3,9 @@ import { VideoLayer, VideoQuality } from '../../proto/livekit_models_pb';
3
3
  import { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc_pb';
4
4
  import type { VideoSenderStats } from '../stats';
5
5
  import LocalTrack from './LocalTrack';
6
+ import { Track } from './Track';
6
7
  import type { VideoCaptureOptions, VideoCodec } from './options';
8
+ import type { TrackProcessor } from './processor/types';
7
9
  export declare class SimulcastTrackInfo {
8
10
  codec: VideoCodec;
9
11
  mediaStreamTrack: MediaStreamTrack;
@@ -28,12 +30,16 @@ export default class LocalVideoTrack extends LocalTrack {
28
30
  get isSimulcast(): boolean;
29
31
  startMonitor(signalClient: SignalClient): void;
30
32
  stop(): void;
33
+ pauseUpstream(): Promise<void>;
34
+ resumeUpstream(): Promise<void>;
31
35
  mute(): Promise<LocalVideoTrack>;
32
36
  unmute(): Promise<LocalVideoTrack>;
37
+ protected setTrackMuted(muted: boolean): void;
33
38
  getSenderStats(): Promise<VideoSenderStats[]>;
34
39
  setPublishingQuality(maxQuality: VideoQuality): void;
35
40
  setDeviceId(deviceId: ConstrainDOMString): Promise<boolean>;
36
41
  restartTrack(options?: VideoCaptureOptions): Promise<void>;
42
+ setProcessor(processor: TrackProcessor<Track.Kind>, showProcessedStreamLocally?: boolean): Promise<void>;
37
43
  addSimulcastTrack(codec: VideoCodec, encodings?: RTCRtpEncodingParameters[]): SimulcastTrackInfo;
38
44
  setSimulcastTrackSender(codec: VideoCodec, sender: RTCRtpSender): void;
39
45
  /**
@@ -10,9 +10,20 @@ export type ProcessorOptions<T extends Track.Kind> = {
10
10
  /**
11
11
  * @experimental
12
12
  */
13
- export interface TrackProcessor<T extends Track.Kind> {
13
+ export interface AudioProcessorOptions extends ProcessorOptions<Track.Kind.Audio> {
14
+ audioContext: AudioContext;
15
+ }
16
+ /**
17
+ * @experimental
18
+ */
19
+ export interface VideoProcessorOptions extends ProcessorOptions<Track.Kind.Video> {
20
+ }
21
+ /**
22
+ * @experimental
23
+ */
24
+ export interface TrackProcessor<T extends Track.Kind, U extends ProcessorOptions<T> = ProcessorOptions<T>> {
14
25
  name: string;
15
- init: (opts: ProcessorOptions<T>) => void;
26
+ init: (opts: U) => void;
16
27
  destroy: () => Promise<void>;
17
28
  processedTrack?: MediaStreamTrack;
18
29
  }
@@ -22,5 +22,5 @@ export type LiveKitReactNativeInfo = {
22
22
  platform: 'ios' | 'android' | 'windows' | 'macos' | 'web' | 'native';
23
23
  devicePixelRatio: number;
24
24
  };
25
- export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect';
25
+ export type SimulationScenario = 'signal-reconnect' | 'speaker' | 'node-failure' | 'server-leave' | 'migration' | 'resume-reconnect' | 'force-tcp' | 'force-tls' | 'full-reconnect' | 'subscriber-bandwidth';
26
26
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livekit-client",
3
- "version": "1.12.3",
3
+ "version": "1.13.0",
4
4
  "description": "JavaScript/TypeScript client SDK for LiveKit",
5
5
  "main": "./dist/livekit-client.umd.js",
6
6
  "unpkg": "./dist/livekit-client.umd.js",
@@ -61,10 +61,10 @@
61
61
  "webrtc-adapter": "^8.1.1"
62
62
  },
63
63
  "devDependencies": {
64
- "@babel/core": "7.22.1",
65
- "@babel/preset-env": "7.22.4",
64
+ "@babel/core": "7.22.9",
65
+ "@babel/preset-env": "7.22.9",
66
66
  "@bufbuild/protoc-gen-es": "^1.3.0",
67
- "@changesets/cli": "2.26.1",
67
+ "@changesets/cli": "2.26.2",
68
68
  "@livekit/changesets-changelog-github": "^0.0.4",
69
69
  "@rollup/plugin-babel": "6.0.3",
70
70
  "@rollup/plugin-commonjs": "24.1.0",
@@ -77,27 +77,26 @@
77
77
  "@types/events": "^3.0.0",
78
78
  "@types/sdp-transform": "2.4.6",
79
79
  "@types/ua-parser-js": "0.7.36",
80
- "@typescript-eslint/eslint-plugin": "5.59.8",
81
- "@typescript-eslint/parser": "5.59.8",
80
+ "@typescript-eslint/eslint-plugin": "5.62.0",
81
+ "@typescript-eslint/parser": "5.62.0",
82
82
  "downlevel-dts": "^0.11.0",
83
- "eslint": "8.42.0",
84
- "eslint-config-airbnb-typescript": "17.0.0",
85
- "eslint-config-prettier": "8.8.0",
83
+ "eslint": "8.46.0",
84
+ "eslint-config-airbnb-typescript": "17.1.0",
85
+ "eslint-config-prettier": "8.9.0",
86
86
  "eslint-plugin-ecmascript-compat": "^3.0.0",
87
- "eslint-plugin-import": "2.27.5",
87
+ "eslint-plugin-import": "2.28.0",
88
88
  "gh-pages": "5.0.0",
89
89
  "jsdom": "^22.1.0",
90
90
  "prettier": "^2.8.8",
91
- "rollup": "3.23.1",
91
+ "rollup": "3.27.0",
92
92
  "rollup-plugin-delete": "^2.0.0",
93
93
  "rollup-plugin-re": "1.0.7",
94
- "rollup-plugin-typescript2": "0.34.1",
94
+ "rollup-plugin-typescript2": "0.35.0",
95
95
  "size-limit": "^8.2.4",
96
- "ts-proto": "1.148.2",
97
96
  "typedoc": "0.24.8",
98
97
  "typedoc-plugin-no-inherit": "1.4.0",
99
- "typescript": "5.1.3",
100
- "vite": "4.3.9",
101
- "vitest": "^0.32.0"
98
+ "typescript": "5.1.6",
99
+ "vite": "4.4.7",
100
+ "vitest": "^0.33.0"
102
101
  }
103
102
  }
@@ -43,21 +43,13 @@ import { Mutex, getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../r
43
43
  import { AsyncQueue } from '../utils/AsyncQueue';
44
44
 
45
45
  // internal options
46
- interface ConnectOpts {
47
- autoSubscribe: boolean;
46
+ interface ConnectOpts extends SignalOptions {
48
47
  /** internal */
49
48
  reconnect?: boolean;
50
-
51
49
  /** internal */
52
50
  reconnectReason?: number;
53
-
54
51
  /** internal */
55
52
  sid?: string;
56
-
57
- /** @deprecated */
58
- publishOnly?: string;
59
-
60
- adaptiveStream?: boolean;
61
53
  }
62
54
 
63
55
  // public options
@@ -68,6 +60,7 @@ export interface SignalOptions {
68
60
  adaptiveStream?: boolean;
69
61
  maxRetries: number;
70
62
  e2eeEnabled: boolean;
63
+ websocketTimeout: number;
71
64
  }
72
65
 
73
66
  type SignalMessage = SignalRequest['message'];
@@ -224,9 +217,15 @@ export class SignalClient {
224
217
  return new Promise<JoinResponse | ReconnectResponse | void>(async (resolve, reject) => {
225
218
  const abortHandler = async () => {
226
219
  this.close();
220
+ clearTimeout(wsTimeout);
227
221
  reject(new ConnectionError('room connection has been cancelled (signal)'));
228
222
  };
229
223
 
224
+ const wsTimeout = setTimeout(() => {
225
+ this.close();
226
+ reject(new ConnectionError('room connection has timed out (signal)'));
227
+ }, opts.websocketTimeout);
228
+
230
229
  if (abortSignal?.aborted) {
231
230
  abortHandler();
232
231
  }
@@ -238,8 +237,13 @@ export class SignalClient {
238
237
  this.ws = new WebSocket(url + params);
239
238
  this.ws.binaryType = 'arraybuffer';
240
239
 
240
+ this.ws.onopen = () => {
241
+ clearTimeout(wsTimeout);
242
+ };
243
+
241
244
  this.ws.onerror = async (ev: Event) => {
242
245
  if (!this.isConnected) {
246
+ clearTimeout(wsTimeout);
243
247
  try {
244
248
  const resp = await fetch(`http${url.substring(2)}/validate${params}`);
245
249
  if (resp.status.toFixed(0).startsWith('4')) {
@@ -12,6 +12,7 @@ export class TURNCheck extends Checker {
12
12
  autoSubscribe: true,
13
13
  maxRetries: 0,
14
14
  e2eeEnabled: false,
15
+ websocketTimeout: 15_000,
15
16
  });
16
17
 
17
18
  let hasTLS = false;
@@ -1,3 +1,4 @@
1
+ import log from '../../logger';
1
2
  import { RoomEvent } from '../../room/events';
2
3
  import { Checker } from './Checker';
3
4
 
@@ -14,19 +15,20 @@ export class WebRTCCheck extends Checker {
14
15
 
15
16
  const candidates: RTCIceCandidate[] = [];
16
17
  this.room.engine.client.onTrickle = (sd, target) => {
17
- console.log('got candidate', sd);
18
18
  if (sd.candidate) {
19
19
  const candidate = new RTCIceCandidate(sd);
20
20
  candidates.push(candidate);
21
21
  let str = `${candidate.protocol} ${candidate.address}:${candidate.port} ${candidate.type}`;
22
- if (candidate.protocol === 'tcp' && candidate.tcpType === 'passive') {
23
- hasTcp = true;
24
- str += ' (active)';
25
- } else if (candidate.protocol === 'udp' && candidate.address) {
22
+ if (candidate.address) {
26
23
  if (isIPPrivate(candidate.address)) {
27
24
  str += ' (private)';
28
25
  } else {
29
- hasIpv4Udp = true;
26
+ if (candidate.protocol === 'tcp' && candidate.tcpType === 'passive') {
27
+ hasTcp = true;
28
+ str += ' (passive)';
29
+ } else if (candidate.protocol === 'udp') {
30
+ hasIpv4Udp = true;
31
+ }
30
32
  }
31
33
  }
32
34
  this.appendMessage(str);
@@ -48,7 +50,7 @@ export class WebRTCCheck extends Checker {
48
50
  });
49
51
  try {
50
52
  await this.connect();
51
- console.log('now the room is connected');
53
+ log.info('now the room is connected');
52
54
  } catch (err) {
53
55
  this.appendWarning('ports need to be open on firewall in order to connect.');
54
56
  throw err;
@@ -17,6 +17,7 @@ export class WebSocketCheck extends Checker {
17
17
  autoSubscribe: true,
18
18
  maxRetries: 0,
19
19
  e2eeEnabled: false,
20
+ websocketTimeout: 15_000,
20
21
  });
21
22
  this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
22
23
  if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {
@@ -25,6 +25,7 @@ import type {
25
25
  RatchetRequestMessage,
26
26
  RemoveTransformMessage,
27
27
  SetKeyMessage,
28
+ SifTrailerMessage,
28
29
  UpdateCodecMessage,
29
30
  } from './types';
30
31
  import { EncryptionEvent } from './types';
@@ -99,6 +100,17 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
99
100
  }
100
101
  }
101
102
 
103
+ /**
104
+ * @internal
105
+ */
106
+ setSifTrailer(trailer: Uint8Array) {
107
+ if (!trailer || trailer.length === 0) {
108
+ log.warn("ignoring server sent trailer as it's empty");
109
+ } else {
110
+ this.postSifTrailer(trailer);
111
+ }
112
+ }
113
+
102
114
  private onWorkerMessage = (ev: MessageEvent<E2EEWorkerMessage>) => {
103
115
  const { kind, data } = ev.data;
104
116
  switch (kind) {
@@ -233,6 +245,19 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
233
245
  this.worker.postMessage(msg);
234
246
  }
235
247
 
248
+ private postSifTrailer(trailer: Uint8Array) {
249
+ if (!this.worker) {
250
+ throw Error('could not post SIF trailer, worker is missing');
251
+ }
252
+ const msg: SifTrailerMessage = {
253
+ kind: 'setSifTrailer',
254
+ data: {
255
+ trailer,
256
+ },
257
+ };
258
+ this.worker.postMessage(msg);
259
+ }
260
+
236
261
  private setupE2EEReceiver(track: RemoteTrack, remoteId: string, trackInfo?: TrackInfo) {
237
262
  if (!track.receiver) {
238
263
  return;
@@ -342,7 +367,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
342
367
  }
343
368
 
344
369
  if (isScriptTransformSupported()) {
345
- log.warn('initialize script transform');
370
+ log.info('initialize script transform');
346
371
 
347
372
  const options = {
348
373
  kind: 'encode',
@@ -353,7 +378,7 @@ export class E2EEManager extends (EventEmitter as new () => TypedEventEmitter<E2
353
378
  // @ts-ignore
354
379
  sender.transform = new RTCRtpScriptTransform(this.worker, options);
355
380
  } else {
356
- log.warn('initialize encoded streams');
381
+ log.info('initialize encoded streams');
357
382
  // @ts-ignore
358
383
  const senderStreams = sender.createEncodedStreams();
359
384
  const msg: EncodeMessage = {
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
2
2
  import type TypedEventEmitter from 'typed-emitter';
3
3
  import { KEY_PROVIDER_DEFAULTS } from './constants';
4
4
  import type { KeyInfo, KeyProviderCallbacks, KeyProviderOptions } from './types';
5
- import { createKeyMaterialFromString } from './utils';
5
+ import { createKeyMaterialFromBuffer, createKeyMaterialFromString } from './utils';
6
6
 
7
7
  /**
8
8
  * @experimental
@@ -68,11 +68,16 @@ export class ExternalE2EEKeyProvider extends BaseKeyProvider {
68
68
  }
69
69
 
70
70
  /**
71
- * Accepts a passphrase that's used to create the crypto keys
71
+ * Accepts a passphrase that's used to create the crypto keys.
72
+ * When passing in a string, PBKDF2 is used.
73
+ * Also accepts an Array buffer of cryptographically random numbers that uses HKDF.
72
74
  * @param key
73
75
  */
74
- async setKey(key: string) {
75
- const derivedKey = await createKeyMaterialFromString(key);
76
+ async setKey(key: string | ArrayBuffer) {
77
+ const derivedKey =
78
+ typeof key === 'string'
79
+ ? await createKeyMaterialFromString(key)
80
+ : await createKeyMaterialFromBuffer(key);
76
81
  this.onSetEncryptionKey(derivedKey);
77
82
  }
78
83
  }
@@ -42,3 +42,6 @@ export const KEY_PROVIDER_DEFAULTS: KeyProviderOptions = {
42
42
  ratchetWindowSize: 8,
43
43
  failureTolerance: DECRYPTION_FAILURE_TOLERANCE,
44
44
  } as const;
45
+
46
+ export const MAX_SIF_COUNT = 100;
47
+ export const MAX_SIF_DURATION = 2000;
package/src/e2ee/types.ts CHANGED
@@ -31,6 +31,13 @@ export interface RTPVideoMapMessage extends BaseMessage {
31
31
  };
32
32
  }
33
33
 
34
+ export interface SifTrailerMessage extends BaseMessage {
35
+ kind: 'setSifTrailer';
36
+ data: {
37
+ trailer: Uint8Array;
38
+ };
39
+ }
40
+
34
41
  export interface EncodeMessage extends BaseMessage {
35
42
  kind: 'decode' | 'encode';
36
43
  data: {
@@ -102,7 +109,8 @@ export type E2EEWorkerMessage =
102
109
  | RTPVideoMapMessage
103
110
  | UpdateCodecMessage
104
111
  | RatchetRequestMessage
105
- | RatchetMessage;
112
+ | RatchetMessage
113
+ | SifTrailerMessage;
106
114
 
107
115
  export type KeySet = { material: CryptoKey; encryptionKey: CryptoKey };
108
116
 
package/src/e2ee/utils.ts CHANGED
@@ -56,6 +56,15 @@ export async function createKeyMaterialFromString(password: string) {
56
56
  return keyMaterial;
57
57
  }
58
58
 
59
+ export async function createKeyMaterialFromBuffer(cryptoBuffer: ArrayBuffer) {
60
+ const keyMaterial = await crypto.subtle.importKey('raw', cryptoBuffer, 'HKDF', false, [
61
+ 'deriveBits',
62
+ 'deriveKey',
63
+ ]);
64
+
65
+ return keyMaterial;
66
+ }
67
+
59
68
  function getAlgoOptions(algorithmName: string, salt: string) {
60
69
  const textEncoder = new TextEncoder();
61
70
  const encodedSalt = textEncoder.encode(salt);
@@ -15,6 +15,7 @@ import {
15
15
  } from '../types';
16
16
  import { deriveKeys, isVideoFrame } from '../utils';
17
17
  import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
18
+ import { SifGuard } from './SifGuard';
18
19
 
19
20
  export interface FrameCryptorConstructor {
20
21
  new (opts?: unknown): BaseFrameCryptor;
@@ -65,13 +66,15 @@ export class FrameCryptor extends BaseFrameCryptor {
65
66
  /**
66
67
  * used for detecting server injected unencrypted frames
67
68
  */
68
- private unencryptedFrameByteTrailer: Uint8Array;
69
+ private sifTrailer: Uint8Array;
70
+
71
+ private sifGuard: SifGuard;
69
72
 
70
73
  constructor(opts: {
71
74
  keys: ParticipantKeyHandler;
72
75
  participantId: string;
73
76
  keyProviderOptions: KeyProviderOptions;
74
- unencryptedFrameBytes?: Uint8Array;
77
+ sifTrailer?: Uint8Array;
75
78
  }) {
76
79
  super();
77
80
  this.sendCounts = new Map();
@@ -79,8 +82,8 @@ export class FrameCryptor extends BaseFrameCryptor {
79
82
  this.participantId = opts.participantId;
80
83
  this.rtpMap = new Map();
81
84
  this.keyProviderOptions = opts.keyProviderOptions;
82
- this.unencryptedFrameByteTrailer =
83
- opts.unencryptedFrameBytes ?? new TextEncoder().encode('LKROCKS');
85
+ this.sifTrailer = opts.sifTrailer ?? Uint8Array.from([]);
86
+ this.sifGuard = new SifGuard();
84
87
  }
85
88
 
86
89
  /**
@@ -92,6 +95,7 @@ export class FrameCryptor extends BaseFrameCryptor {
92
95
  setParticipant(id: string, keys: ParticipantKeyHandler) {
93
96
  this.participantId = id;
94
97
  this.keys = keys;
98
+ this.sifGuard.reset();
95
99
  }
96
100
 
97
101
  unsetParticipant() {
@@ -130,9 +134,10 @@ export class FrameCryptor extends BaseFrameCryptor {
130
134
  codec?: VideoCodec,
131
135
  ) {
132
136
  if (codec) {
133
- console.info('setting codec on cryptor to', codec);
137
+ workerLogger.info('setting codec on cryptor to', { codec });
134
138
  this.videoCodec = codec;
135
139
  }
140
+
136
141
  const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
137
142
  const transformStream = new TransformStream({
138
143
  transform: transformFn.bind(this),
@@ -142,7 +147,7 @@ export class FrameCryptor extends BaseFrameCryptor {
142
147
  .pipeThrough(transformStream)
143
148
  .pipeTo(writable)
144
149
  .catch((e) => {
145
- console.error(e);
150
+ workerLogger.warn(e);
146
151
  this.emit('cryptorError', e instanceof CryptorError ? e : new CryptorError(e.message));
147
152
  });
148
153
  this.trackId = trackId;
@@ -260,12 +265,24 @@ export class FrameCryptor extends BaseFrameCryptor {
260
265
  if (
261
266
  !this.keys.isEnabled() ||
262
267
  // skip for decryption for empty dtx frames
263
- encodedFrame.data.byteLength === 0 ||
264
- // skip decryption if frame is server injected
265
- isFrameServerInjected(encodedFrame.data, this.unencryptedFrameByteTrailer)
268
+ encodedFrame.data.byteLength === 0
266
269
  ) {
270
+ this.sifGuard.recordUserFrame();
267
271
  return controller.enqueue(encodedFrame);
268
272
  }
273
+
274
+ if (isFrameServerInjected(encodedFrame.data, this.sifTrailer)) {
275
+ this.sifGuard.recordSif();
276
+
277
+ if (this.sifGuard.isSifAllowed()) {
278
+ return controller.enqueue(encodedFrame);
279
+ } else {
280
+ workerLogger.warn('SIF limit reached, dropping frame');
281
+ return;
282
+ }
283
+ } else {
284
+ this.sifGuard.recordUserFrame();
285
+ }
269
286
  const data = new Uint8Array(encodedFrame.data);
270
287
  const keyIndex = data[encodedFrame.data.byteLength - 1];
271
288
 
@@ -293,9 +310,17 @@ export class FrameCryptor extends BaseFrameCryptor {
293
310
  workerLogger.warn('decoding frame failed', { error });
294
311
  }
295
312
  }
313
+ } else if (!this.keys.getKeySet(keyIndex) && this.keys.hasValidKey) {
314
+ // emit an error in case the key index is out of bounds but the key handler thinks we still have a valid key
315
+ workerLogger.warn('skipping decryption due to missing key at index');
316
+ this.emit(
317
+ CryptorEvent.Error,
318
+ new CryptorError(
319
+ `missing key at index for participant ${this.participantId}`,
320
+ CryptorErrorReason.MissingKey,
321
+ ),
322
+ );
296
323
  }
297
-
298
- return controller.enqueue(encodedFrame);
299
324
  }
300
325
 
301
326
  /**
@@ -398,12 +423,9 @@ export class FrameCryptor extends BaseFrameCryptor {
398
423
  }
399
424
 
400
425
  workerLogger.warn('maximum ratchet attempts exceeded, resetting key');
401
- this.emit(
402
- CryptorEvent.Error,
403
- new CryptorError(
404
- `valid key missing for participant ${this.participantId}`,
405
- CryptorErrorReason.MissingKey,
406
- ),
426
+ throw new CryptorError(
427
+ `valid key missing for participant ${this.participantId}`,
428
+ CryptorErrorReason.InvalidKey,
407
429
  );
408
430
  }
409
431
  } else {
@@ -513,6 +535,10 @@ export class FrameCryptor extends BaseFrameCryptor {
513
535
  const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
514
536
  return codec;
515
537
  }
538
+
539
+ setSifTrailer(trailer: Uint8Array) {
540
+ this.sifTrailer = trailer;
541
+ }
516
542
  }
517
543
 
518
544
  /**
@@ -605,6 +631,9 @@ export enum NALUType {
605
631
  * @internal
606
632
  */
607
633
  export function isFrameServerInjected(frameData: ArrayBuffer, trailerBytes: Uint8Array): boolean {
634
+ if (trailerBytes.byteLength === 0) {
635
+ return false;
636
+ }
608
637
  const frameTrailer = new Uint8Array(
609
638
  frameData.slice(frameData.byteLength - trailerBytes.byteLength),
610
639
  );
@@ -63,6 +63,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
63
63
  this.decryptionFailureCount += 1;
64
64
 
65
65
  if (this.decryptionFailureCount > this.keyProviderOptions.failureTolerance) {
66
+ workerLogger.warn(`key for ${this.participantId} is being marked as invalid`);
66
67
  this._hasValidKey = false;
67
68
  }
68
69
  }
@@ -0,0 +1,47 @@
1
+ import { MAX_SIF_COUNT, MAX_SIF_DURATION } from '../constants';
2
+
3
+ export class SifGuard {
4
+ private consecutiveSifCount = 0;
5
+
6
+ private sifSequenceStartedAt: number | undefined;
7
+
8
+ private lastSifReceivedAt: number = 0;
9
+
10
+ private userFramesSinceSif: number = 0;
11
+
12
+ recordSif() {
13
+ this.consecutiveSifCount += 1;
14
+ this.sifSequenceStartedAt ??= Date.now();
15
+ this.lastSifReceivedAt = Date.now();
16
+ }
17
+
18
+ recordUserFrame() {
19
+ if (this.sifSequenceStartedAt === undefined) {
20
+ return;
21
+ } else {
22
+ this.userFramesSinceSif += 1;
23
+ }
24
+ if (
25
+ // reset if we received more user frames than SIFs
26
+ this.userFramesSinceSif > this.consecutiveSifCount ||
27
+ // also reset if we got a new user frame and the latest SIF frame hasn't been updated in a while
28
+ Date.now() - this.lastSifReceivedAt > MAX_SIF_DURATION
29
+ ) {
30
+ this.reset();
31
+ }
32
+ }
33
+
34
+ isSifAllowed() {
35
+ return (
36
+ this.consecutiveSifCount < MAX_SIF_COUNT &&
37
+ (this.sifSequenceStartedAt === undefined ||
38
+ Date.now() - this.sifSequenceStartedAt < MAX_SIF_DURATION)
39
+ );
40
+ }
41
+
42
+ reset() {
43
+ this.userFramesSinceSif = 0;
44
+ this.consecutiveSifCount = 0;
45
+ this.sifSequenceStartedAt = undefined;
46
+ }
47
+ }
@@ -24,6 +24,8 @@ let useSharedKey: boolean = false;
24
24
 
25
25
  let sharedKey: CryptoKey | undefined;
26
26
 
27
+ let sifTrailer: Uint8Array | undefined;
28
+
27
29
  let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
28
30
 
29
31
  workerLogger.setDefaultLevel('info');
@@ -94,6 +96,10 @@ onmessage = (ev) => {
94
96
  break;
95
97
  case 'ratchetRequest':
96
98
  handleRatchetRequest(data);
99
+ break;
100
+ case 'setSifTrailer':
101
+ handleSifTrailer(data.trailer);
102
+ break;
97
103
  default:
98
104
  break;
99
105
  }
@@ -116,6 +122,7 @@ function getTrackCryptor(participantId: string, trackId: string) {
116
122
  participantId,
117
123
  keys: getParticipantKeyHandler(participantId),
118
124
  keyProviderOptions,
125
+ sifTrailer,
119
126
  });
120
127
 
121
128
  setupCryptorErrorEvents(cryptor);
@@ -205,6 +212,13 @@ function emitRatchetedKeys(material: CryptoKey, keyIndex?: number) {
205
212
  postMessage(msg);
206
213
  }
207
214
 
215
+ function handleSifTrailer(trailer: Uint8Array) {
216
+ sifTrailer = trailer;
217
+ participantCryptors.forEach((c) => {
218
+ c.setSifTrailer(trailer);
219
+ });
220
+ }
221
+
208
222
  // Operations using RTCRtpScriptTransform.
209
223
  // @ts-ignore
210
224
  if (self.RTCTransformEvent) {
package/src/options.ts CHANGED
@@ -31,6 +31,9 @@ export interface InternalRoomOptions {
31
31
  * enable Dynacast, off by default. With Dynacast dynamically pauses
32
32
  * video layers that are not being consumed by any subscribers, significantly
33
33
  * reducing publishing CPU and bandwidth usage.
34
+ *
35
+ * Dynacast will be enabled if SVC codecs (VP9/AV1) are used. Multi-codec simulcast
36
+ * requires dynacast
34
37
  */
35
38
  dynacast: boolean;
36
39
 
@@ -119,6 +122,9 @@ export interface InternalRoomConnectOptions {
119
122
 
120
123
  /** specifies how often an initial join connection is allowed to retry (only applicable if server is not reachable) */
121
124
  maxRetries: number;
125
+
126
+ /** amount of time for Websocket connection to be established, defaults to 15s */
127
+ websocketTimeout: number;
122
128
  }
123
129
 
124
130
  /**
@@ -1,3 +1,17 @@
1
+ // Copyright 2023 LiveKit, Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
1
15
  // @generated by protoc-gen-es v1.3.0 with parameter "target=ts"
2
16
  // @generated from file livekit_models.proto (package livekit, syntax proto3)
3
17
  /* eslint-disable */