livekit-client 1.12.3 → 1.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) 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 +198 -107
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +515 -192
  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 +9 -3
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/KeyProvider.d.ts +10 -7
  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/events.d.ts +34 -0
  21. package/dist/src/e2ee/events.d.ts.map +1 -0
  22. package/dist/src/e2ee/index.d.ts +1 -0
  23. package/dist/src/e2ee/index.d.ts.map +1 -1
  24. package/dist/src/e2ee/types.d.ts +23 -33
  25. package/dist/src/e2ee/types.d.ts.map +1 -1
  26. package/dist/src/e2ee/utils.d.ts +1 -0
  27. package/dist/src/e2ee/utils.d.ts.map +1 -1
  28. package/dist/src/e2ee/worker/FrameCryptor.d.ts +18 -13
  29. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  30. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +6 -8
  31. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  32. package/dist/src/e2ee/worker/SifGuard.d.ts +11 -0
  33. package/dist/src/e2ee/worker/SifGuard.d.ts.map +1 -0
  34. package/dist/src/options.d.ts +5 -0
  35. package/dist/src/options.d.ts.map +1 -1
  36. package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
  37. package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
  38. package/dist/src/room/DeviceManager.d.ts +1 -0
  39. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  40. package/dist/src/room/PCTransport.d.ts.map +1 -1
  41. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  42. package/dist/src/room/Room.d.ts +1 -1
  43. package/dist/src/room/Room.d.ts.map +1 -1
  44. package/dist/src/room/defaults.d.ts.map +1 -1
  45. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  46. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  47. package/dist/src/room/participant/Participant.d.ts +5 -0
  48. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  49. package/dist/src/room/participant/RemoteParticipant.d.ts +0 -5
  50. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  51. package/dist/src/room/timers.d.ts +2 -2
  52. package/dist/src/room/timers.d.ts.map +1 -1
  53. package/dist/src/room/track/LocalAudioTrack.d.ts +9 -1
  54. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/LocalTrack.d.ts +3 -3
  56. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/LocalVideoTrack.d.ts +6 -0
  58. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  59. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  60. package/dist/src/room/track/processor/types.d.ts +14 -2
  61. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  62. package/dist/src/room/types.d.ts +1 -1
  63. package/dist/src/room/types.d.ts.map +1 -1
  64. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -5
  65. package/dist/ts4.2/src/e2ee/E2eeManager.d.ts +9 -3
  66. package/dist/ts4.2/src/e2ee/KeyProvider.d.ts +10 -7
  67. package/dist/ts4.2/src/e2ee/constants.d.ts +2 -0
  68. package/dist/ts4.2/src/e2ee/events.d.ts +34 -0
  69. package/dist/ts4.2/src/e2ee/index.d.ts +1 -0
  70. package/dist/ts4.2/src/e2ee/types.d.ts +23 -33
  71. package/dist/ts4.2/src/e2ee/utils.d.ts +1 -0
  72. package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +18 -13
  73. package/dist/ts4.2/src/e2ee/worker/ParticipantKeyHandler.d.ts +6 -8
  74. package/dist/ts4.2/src/e2ee/worker/SifGuard.d.ts +11 -0
  75. package/dist/ts4.2/src/options.d.ts +5 -0
  76. package/dist/ts4.2/src/room/DeviceManager.d.ts +1 -0
  77. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  78. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +1 -0
  79. package/dist/ts4.2/src/room/participant/Participant.d.ts +5 -0
  80. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +0 -5
  81. package/dist/ts4.2/src/room/timers.d.ts +2 -2
  82. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +9 -1
  83. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
  84. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +6 -0
  85. package/dist/ts4.2/src/room/track/processor/types.d.ts +14 -2
  86. package/dist/ts4.2/src/room/types.d.ts +1 -1
  87. package/package.json +15 -16
  88. package/src/api/SignalClient.ts +13 -9
  89. package/src/connectionHelper/checks/turn.ts +1 -0
  90. package/src/connectionHelper/checks/webrtc.ts +9 -7
  91. package/src/connectionHelper/checks/websocket.ts +1 -0
  92. package/src/e2ee/E2eeManager.ts +129 -76
  93. package/src/e2ee/KeyProvider.ts +31 -16
  94. package/src/e2ee/constants.ts +3 -0
  95. package/src/e2ee/events.ts +48 -0
  96. package/src/e2ee/index.ts +1 -0
  97. package/src/e2ee/types.ts +27 -41
  98. package/src/e2ee/utils.ts +9 -0
  99. package/src/e2ee/worker/FrameCryptor.ts +90 -47
  100. package/src/e2ee/worker/ParticipantKeyHandler.ts +25 -26
  101. package/src/e2ee/worker/SifGuard.ts +47 -0
  102. package/src/e2ee/worker/e2ee.worker.ts +75 -68
  103. package/src/options.ts +6 -0
  104. package/src/proto/livekit_models_pb.ts +14 -0
  105. package/src/proto/livekit_rtc_pb.ts +14 -0
  106. package/src/room/DeviceManager.ts +7 -2
  107. package/src/room/PCTransport.ts +12 -2
  108. package/src/room/RTCEngine.ts +3 -2
  109. package/src/room/Room.ts +47 -22
  110. package/src/room/defaults.ts +1 -0
  111. package/src/room/participant/LocalParticipant.ts +18 -2
  112. package/src/room/participant/Participant.ts +16 -0
  113. package/src/room/participant/RemoteParticipant.ts +0 -12
  114. package/src/room/track/LocalAudioTrack.ts +45 -0
  115. package/src/room/track/LocalTrack.ts +22 -14
  116. package/src/room/track/LocalVideoTrack.ts +39 -0
  117. package/src/room/track/RemoteAudioTrack.ts +9 -1
  118. package/src/room/track/RemoteTrackPublication.ts +2 -2
  119. package/src/room/track/facingMode.ts +1 -1
  120. package/src/room/track/processor/types.ts +18 -2
  121. package/src/room/types.ts +5 -1
@@ -2,7 +2,8 @@ import { EventEmitter } from 'events';
2
2
  import type TypedEventEmitter from 'typed-emitter';
3
3
  import { workerLogger } from '../../logger';
4
4
  import { KEYRING_SIZE } from '../constants';
5
- import type { KeyProviderOptions, KeySet, ParticipantKeyHandlerCallbacks } from '../types';
5
+ import { KeyHandlerEvent, type ParticipantKeyHandlerCallbacks } from '../events';
6
+ import type { KeyProviderOptions, KeySet } from '../types';
6
7
  import { deriveKeys, importKey, ratchet } from '../utils';
7
8
 
8
9
  // TODO ParticipantKeyHandlers currently don't get destroyed on participant disconnect
@@ -19,15 +20,13 @@ import { deriveKeys, importKey, ratchet } from '../utils';
19
20
  export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEventEmitter<ParticipantKeyHandlerCallbacks>) {
20
21
  private currentKeyIndex: number;
21
22
 
22
- private cryptoKeyRing: Array<KeySet>;
23
-
24
- private enabled: boolean;
23
+ private cryptoKeyRing: Array<KeySet | undefined>;
25
24
 
26
25
  private keyProviderOptions: KeyProviderOptions;
27
26
 
28
27
  private ratchetPromiseMap: Map<number, Promise<CryptoKey>>;
29
28
 
30
- private participantId: string | undefined;
29
+ private participantIdentity: string;
31
30
 
32
31
  private decryptionFailureCount = 0;
33
32
 
@@ -37,25 +36,16 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
37
36
  return this._hasValidKey;
38
37
  }
39
38
 
40
- constructor(
41
- participantId: string | undefined,
42
- isEnabled: boolean,
43
- keyProviderOptions: KeyProviderOptions,
44
- ) {
39
+ constructor(participantIdentity: string, keyProviderOptions: KeyProviderOptions) {
45
40
  super();
46
41
  this.currentKeyIndex = 0;
47
- this.cryptoKeyRing = new Array(KEYRING_SIZE);
48
- this.enabled = isEnabled;
42
+ this.cryptoKeyRing = new Array(KEYRING_SIZE).fill(undefined);
49
43
  this.keyProviderOptions = keyProviderOptions;
50
44
  this.ratchetPromiseMap = new Map();
51
- this.participantId = participantId;
45
+ this.participantIdentity = participantIdentity;
52
46
  this.resetKeyStatus();
53
47
  }
54
48
 
55
- setEnabled(enabled: boolean) {
56
- this.enabled = enabled;
57
- }
58
-
59
49
  decryptionFailure() {
60
50
  if (this.keyProviderOptions.failureTolerance < 0) {
61
51
  return;
@@ -63,6 +53,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
63
53
  this.decryptionFailureCount += 1;
64
54
 
65
55
  if (this.decryptionFailureCount > this.keyProviderOptions.failureTolerance) {
56
+ workerLogger.warn(`key for ${this.participantIdentity} is being marked as invalid`);
66
57
  this._hasValidKey = false;
67
58
  }
68
59
  }
@@ -88,7 +79,7 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
88
79
  * @param setKey
89
80
  */
90
81
  ratchetKey(keyIndex?: number, setKey = true): Promise<CryptoKey> {
91
- const currentKeyIndex = (keyIndex ??= this.getCurrentKeyIndex());
82
+ const currentKeyIndex = keyIndex ?? this.getCurrentKeyIndex();
92
83
 
93
84
  const existingPromise = this.ratchetPromiseMap.get(currentKeyIndex);
94
85
  if (typeof existingPromise !== 'undefined') {
@@ -96,7 +87,13 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
96
87
  }
97
88
  const ratchetPromise = new Promise<CryptoKey>(async (resolve, reject) => {
98
89
  try {
99
- const currentMaterial = this.getKeySet(currentKeyIndex).material;
90
+ const keySet = this.getKeySet(currentKeyIndex);
91
+ if (!keySet) {
92
+ throw new TypeError(
93
+ `Cannot ratchet key without a valid keyset of participant ${this.participantIdentity}`,
94
+ );
95
+ }
96
+ const currentMaterial = keySet.material;
100
97
  const newMaterial = await importKey(
101
98
  await ratchet(currentMaterial, this.keyProviderOptions.ratchetSalt),
102
99
  currentMaterial.algorithm.name,
@@ -105,8 +102,13 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
105
102
 
106
103
  if (setKey) {
107
104
  this.setKeyFromMaterial(newMaterial, currentKeyIndex, true);
105
+ this.emit(
106
+ KeyHandlerEvent.KeyRatcheted,
107
+ newMaterial,
108
+ this.participantIdentity,
109
+ currentKeyIndex,
110
+ );
108
111
  }
109
- this.emit('keyRatcheted', newMaterial, keyIndex, this.participantId);
110
112
  resolve(newMaterial);
111
113
  } catch (e) {
112
114
  reject(e);
@@ -144,10 +146,11 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
144
146
  this.setKeySet(keySet, this.currentKeyIndex, emitRatchetEvent);
145
147
  }
146
148
 
147
- async setKeySet(keySet: KeySet, keyIndex: number, emitRatchetEvent = false) {
149
+ setKeySet(keySet: KeySet, keyIndex: number, emitRatchetEvent = false) {
148
150
  this.cryptoKeyRing[keyIndex % this.cryptoKeyRing.length] = keySet;
151
+
149
152
  if (emitRatchetEvent) {
150
- this.emit('keyRatcheted', keySet.material, keyIndex, this.participantId);
153
+ this.emit(KeyHandlerEvent.KeyRatcheted, keySet.material, this.participantIdentity, keyIndex);
151
154
  }
152
155
  }
153
156
 
@@ -156,10 +159,6 @@ export class ParticipantKeyHandler extends (EventEmitter as new () => TypedEvent
156
159
  this.resetKeyStatus();
157
160
  }
158
161
 
159
- isEnabled() {
160
- return this.enabled;
161
- }
162
-
163
162
  getCurrentKeyIndex() {
164
163
  return this.currentKeyIndex;
165
164
  }
@@ -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
+ }
@@ -1,22 +1,21 @@
1
1
  import { workerLogger } from '../../logger';
2
2
  import { KEY_PROVIDER_DEFAULTS } from '../constants';
3
3
  import { CryptorErrorReason } from '../errors';
4
+ import { CryptorEvent, KeyHandlerEvent } from '../events';
4
5
  import type {
5
6
  E2EEWorkerMessage,
6
- EnableMessage,
7
7
  ErrorMessage,
8
+ InitAck,
8
9
  KeyProviderOptions,
9
10
  RatchetMessage,
10
11
  RatchetRequestMessage,
11
12
  } from '../types';
12
- import { FrameCryptor } from './FrameCryptor';
13
+ import { FrameCryptor, encryptionEnabledMap } from './FrameCryptor';
13
14
  import { ParticipantKeyHandler } from './ParticipantKeyHandler';
14
15
 
15
16
  const participantCryptors: FrameCryptor[] = [];
16
17
  const participantKeys: Map<string, ParticipantKeyHandler> = new Map();
17
-
18
- let publishCryptors: FrameCryptor[] = [];
19
- let publisherKeys: ParticipantKeyHandler;
18
+ let sharedKeyHandler: ParticipantKeyHandler | undefined;
20
19
 
21
20
  let isEncryptionEnabled: boolean = false;
22
21
 
@@ -24,6 +23,8 @@ let useSharedKey: boolean = false;
24
23
 
25
24
  let sharedKey: CryptoKey | undefined;
26
25
 
26
+ let sifTrailer: Uint8Array | undefined;
27
+
27
28
  let keyProviderOptions: KeyProviderOptions = KEY_PROVIDER_DEFAULTS;
28
29
 
29
30
  workerLogger.setDefaultLevel('info');
@@ -37,22 +38,20 @@ onmessage = (ev) => {
37
38
  keyProviderOptions = data.keyProviderOptions;
38
39
  useSharedKey = !!data.keyProviderOptions.sharedKey;
39
40
  // acknowledge init successful
40
- const enableMsg: EnableMessage = {
41
- kind: 'enable',
41
+ const ackMsg: InitAck = {
42
+ kind: 'initAck',
42
43
  data: { enabled: isEncryptionEnabled },
43
44
  };
44
- publisherKeys = new ParticipantKeyHandler(undefined, isEncryptionEnabled, keyProviderOptions);
45
- publisherKeys.on('keyRatcheted', emitRatchetedKeys);
46
- postMessage(enableMsg);
45
+ postMessage(ackMsg);
47
46
  break;
48
47
  case 'enable':
49
- setEncryptionEnabled(data.enabled, data.participantId);
48
+ setEncryptionEnabled(data.enabled, data.participantIdentity);
50
49
  workerLogger.info('updated e2ee enabled status');
51
50
  // acknowledge enable call successful
52
51
  postMessage(ev.data);
53
52
  break;
54
53
  case 'decode':
55
- let cryptor = getTrackCryptor(data.participantId, data.trackId);
54
+ let cryptor = getTrackCryptor(data.participantIdentity, data.trackId);
56
55
  cryptor.setupTransform(
57
56
  kind,
58
57
  data.readableStream,
@@ -62,7 +61,7 @@ onmessage = (ev) => {
62
61
  );
63
62
  break;
64
63
  case 'encode':
65
- let pubCryptor = getPublisherCryptor(data.trackId);
64
+ let pubCryptor = getTrackCryptor(data.participantIdentity, data.trackId);
66
65
  pubCryptor.setupTransform(
67
66
  kind,
68
67
  data.readableStream,
@@ -73,10 +72,11 @@ onmessage = (ev) => {
73
72
  break;
74
73
  case 'setKey':
75
74
  if (useSharedKey) {
76
- workerLogger.debug('set shared key');
75
+ workerLogger.warn('set shared key');
77
76
  setSharedKey(data.key, data.keyIndex);
78
- } else if (data.participantId) {
79
- getParticipantKeyHandler(data.participantId).setKey(data.key, data.keyIndex);
77
+ } else if (data.participantIdentity) {
78
+ workerLogger.warn(`set participant sender key ${data.participantIdentity}`);
79
+ getParticipantKeyHandler(data.participantIdentity).setKey(data.key, data.keyIndex);
80
80
  } else {
81
81
  workerLogger.error('no participant Id was provided and shared key usage is disabled');
82
82
  }
@@ -85,106 +85,107 @@ onmessage = (ev) => {
85
85
  unsetCryptorParticipant(data.trackId);
86
86
  break;
87
87
  case 'updateCodec':
88
- getTrackCryptor(data.participantId, data.trackId).setVideoCodec(data.codec);
88
+ getTrackCryptor(data.participantIdentity, data.trackId).setVideoCodec(data.codec);
89
89
  break;
90
90
  case 'setRTPMap':
91
- publishCryptors.forEach((cr) => {
92
- cr.setRtpMap(data.map);
91
+ // this is only used for the local participant
92
+ participantCryptors.forEach((cr) => {
93
+ if (cr.getParticipantIdentity() === data.participantIdentity) {
94
+ cr.setRtpMap(data.map);
95
+ }
93
96
  });
94
97
  break;
95
98
  case 'ratchetRequest':
96
99
  handleRatchetRequest(data);
100
+ break;
101
+ case 'setSifTrailer':
102
+ handleSifTrailer(data.trailer);
103
+ break;
97
104
  default:
98
105
  break;
99
106
  }
100
107
  };
101
108
 
102
109
  async function handleRatchetRequest(data: RatchetRequestMessage['data']) {
103
- const keyHandler = getParticipantKeyHandler(data.participantId);
104
- await keyHandler.ratchetKey(data.keyIndex);
105
- keyHandler.resetKeyStatus();
110
+ if (useSharedKey) {
111
+ const keyHandler = getSharedKeyHandler();
112
+ await keyHandler.ratchetKey(data.keyIndex);
113
+ keyHandler.resetKeyStatus();
114
+ } else if (data.participantIdentity) {
115
+ const keyHandler = getParticipantKeyHandler(data.participantIdentity);
116
+ await keyHandler.ratchetKey(data.keyIndex);
117
+ keyHandler.resetKeyStatus();
118
+ } else {
119
+ workerLogger.error(
120
+ 'no participant Id was provided for ratchet request and shared key usage is disabled',
121
+ );
122
+ }
106
123
  }
107
124
 
108
- function getTrackCryptor(participantId: string, trackId: string) {
125
+ function getTrackCryptor(participantIdentity: string, trackId: string) {
109
126
  let cryptor = participantCryptors.find((c) => c.getTrackId() === trackId);
110
127
  if (!cryptor) {
111
- workerLogger.info('creating new cryptor for', { participantId });
128
+ workerLogger.info('creating new cryptor for', { participantIdentity });
112
129
  if (!keyProviderOptions) {
113
130
  throw Error('Missing keyProvider options');
114
131
  }
115
132
  cryptor = new FrameCryptor({
116
- participantId,
117
- keys: getParticipantKeyHandler(participantId),
133
+ participantIdentity,
134
+ keys: getParticipantKeyHandler(participantIdentity),
118
135
  keyProviderOptions,
136
+ sifTrailer,
119
137
  });
120
138
 
121
139
  setupCryptorErrorEvents(cryptor);
122
140
  participantCryptors.push(cryptor);
123
- } else if (participantId !== cryptor.getParticipantId()) {
141
+ } else if (participantIdentity !== cryptor.getParticipantIdentity()) {
124
142
  // assign new participant id to track cryptor and pass in correct key handler
125
- cryptor.setParticipant(participantId, getParticipantKeyHandler(participantId));
143
+ cryptor.setParticipant(participantIdentity, getParticipantKeyHandler(participantIdentity));
126
144
  }
127
145
  if (sharedKey) {
128
146
  }
129
147
  return cryptor;
130
148
  }
131
149
 
132
- function getParticipantKeyHandler(participantId?: string) {
133
- if (!participantId) {
134
- return publisherKeys!;
150
+ function getParticipantKeyHandler(participantIdentity: string) {
151
+ if (useSharedKey) {
152
+ return getSharedKeyHandler();
135
153
  }
136
- let keys = participantKeys.get(participantId);
154
+ let keys = participantKeys.get(participantIdentity);
137
155
  if (!keys) {
138
- keys = new ParticipantKeyHandler(participantId, true, keyProviderOptions);
156
+ keys = new ParticipantKeyHandler(participantIdentity, keyProviderOptions);
139
157
  if (sharedKey) {
140
158
  keys.setKey(sharedKey);
141
159
  }
142
- participantKeys.set(participantId, keys);
160
+ keys.on(KeyHandlerEvent.KeyRatcheted, emitRatchetedKeys);
161
+ participantKeys.set(participantIdentity, keys);
143
162
  }
144
163
  return keys;
145
164
  }
146
165
 
147
- function unsetCryptorParticipant(trackId: string) {
148
- participantCryptors.find((c) => c.getTrackId() === trackId)?.unsetParticipant();
166
+ function getSharedKeyHandler() {
167
+ if (!sharedKeyHandler) {
168
+ sharedKeyHandler = new ParticipantKeyHandler('shared-key', keyProviderOptions);
169
+ }
170
+ return sharedKeyHandler;
149
171
  }
150
172
 
151
- function getPublisherCryptor(trackId: string) {
152
- let publishCryptor = publishCryptors.find((cryptor) => cryptor.getTrackId() === trackId);
153
- if (!publishCryptor) {
154
- if (!keyProviderOptions) {
155
- throw new TypeError('Missing keyProvider options');
156
- }
157
- publishCryptor = new FrameCryptor({
158
- keys: publisherKeys!,
159
- participantId: 'publisher',
160
- keyProviderOptions,
161
- });
162
- setupCryptorErrorEvents(publishCryptor);
163
- publishCryptors.push(publishCryptor);
164
- }
165
- return publishCryptor;
173
+ function unsetCryptorParticipant(trackId: string) {
174
+ participantCryptors.find((c) => c.getTrackId() === trackId)?.unsetParticipant();
166
175
  }
167
176
 
168
- function setEncryptionEnabled(enable: boolean, participantId?: string) {
169
- if (!participantId) {
170
- isEncryptionEnabled = enable;
171
- publisherKeys.setEnabled(enable);
172
- } else {
173
- getParticipantKeyHandler(participantId).setEnabled(enable);
174
- }
177
+ function setEncryptionEnabled(enable: boolean, participantIdentity: string) {
178
+ encryptionEnabledMap.set(participantIdentity, enable);
175
179
  }
176
180
 
177
181
  function setSharedKey(key: CryptoKey, index?: number) {
178
182
  workerLogger.debug('setting shared key');
179
183
  sharedKey = key;
180
- publisherKeys?.setKey(key, index);
181
- for (const [, keyHandler] of participantKeys) {
182
- keyHandler.setKey(key, index);
183
- }
184
+ getSharedKeyHandler().setKey(key, index);
184
185
  }
185
186
 
186
187
  function setupCryptorErrorEvents(cryptor: FrameCryptor) {
187
- cryptor.on('cryptorError', (error) => {
188
+ cryptor.on(CryptorEvent.Error, (error) => {
188
189
  const msg: ErrorMessage = {
189
190
  kind: 'error',
190
191
  data: { error: new Error(`${CryptorErrorReason[error.reason]}: ${error.message}`) },
@@ -193,11 +194,11 @@ function setupCryptorErrorEvents(cryptor: FrameCryptor) {
193
194
  });
194
195
  }
195
196
 
196
- function emitRatchetedKeys(material: CryptoKey, keyIndex?: number) {
197
+ function emitRatchetedKeys(material: CryptoKey, participantIdentity: string, keyIndex?: number) {
197
198
  const msg: RatchetMessage = {
198
199
  kind: `ratchetKey`,
199
200
  data: {
200
- // participantId,
201
+ participantIdentity,
201
202
  keyIndex,
202
203
  material,
203
204
  },
@@ -205,6 +206,13 @@ function emitRatchetedKeys(material: CryptoKey, keyIndex?: number) {
205
206
  postMessage(msg);
206
207
  }
207
208
 
209
+ function handleSifTrailer(trailer: Uint8Array) {
210
+ sifTrailer = trailer;
211
+ participantCryptors.forEach((c) => {
212
+ c.setSifTrailer(trailer);
213
+ });
214
+ }
215
+
208
216
  // Operations using RTCRtpScriptTransform.
209
217
  // @ts-ignore
210
218
  if (self.RTCTransformEvent) {
@@ -214,9 +222,8 @@ if (self.RTCTransformEvent) {
214
222
  const transformer = event.transformer;
215
223
  workerLogger.debug('transformer', transformer);
216
224
  transformer.handled = true;
217
- const { kind, participantId, trackId, codec } = transformer.options;
218
- const cryptor =
219
- kind === 'encode' ? getPublisherCryptor(trackId) : getTrackCryptor(participantId, trackId);
225
+ const { kind, participantIdentity, trackId, codec } = transformer.options;
226
+ const cryptor = getTrackCryptor(participantIdentity, trackId);
220
227
  workerLogger.debug('transform', { codec });
221
228
  cryptor.setupTransform(kind, transformer.readable, transformer.writable, trackId, codec);
222
229
  };
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 */
@@ -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_rtc.proto (package livekit, syntax proto3)
3
17
  /* eslint-disable */
@@ -37,9 +37,8 @@ export default class DeviceManager {
37
37
 
38
38
  if (
39
39
  requestPermissions &&
40
- kind &&
41
40
  // for safari we need to skip this check, as otherwise it will re-acquire user media and fail on iOS https://bugs.webkit.org/show_bug.cgi?id=179363
42
- (!DeviceManager.userMediaPromiseMap.get(kind) || !isSafari())
41
+ !(isSafari() && this.hasDeviceInUse(kind))
43
42
  ) {
44
43
  const isDummyDeviceOrEmpty =
45
44
  devices.length === 0 ||
@@ -85,4 +84,10 @@ export default class DeviceManager {
85
84
 
86
85
  return device?.deviceId;
87
86
  }
87
+
88
+ private hasDeviceInUse(kind?: MediaDeviceKind): boolean {
89
+ return kind
90
+ ? DeviceManager.userMediaPromiseMap.has(kind)
91
+ : DeviceManager.userMediaPromiseMap.size > 0;
92
+ }
88
93
  }
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
- import { parse, write } from 'sdp-transform';
3
2
  import type { MediaDescription } from 'sdp-transform';
3
+ import { parse, write } from 'sdp-transform';
4
4
  import { debounce } from 'ts-debounce';
5
5
  import log from '../logger';
6
6
  import { NegotiationError, UnexpectedConnectionState } from './errors';
@@ -16,7 +16,7 @@ interface TrackBitrateInfo {
16
16
 
17
17
  /* The svc codec (av1/vp9) would use a very low bitrate at the begining and
18
18
  increase slowly by the bandwidth estimator until it reach the target bitrate. The
19
- process commonly cost more than 10 seconds cause subscriber will get blur video at
19
+ process commonly cost more than 10 seconds cause subscriber will get blur video at
20
20
  the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
21
21
  eliminate this issue.
22
22
  */
@@ -308,6 +308,7 @@ export default class PCTransport extends EventEmitter {
308
308
  } catch (e) {
309
309
  log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
310
310
  error: e,
311
+ sdp: munged,
311
312
  });
312
313
  sd.sdp = originalSdp;
313
314
  }
@@ -328,6 +329,15 @@ export default class PCTransport extends EventEmitter {
328
329
  } else if (typeof e === 'string') {
329
330
  msg = e;
330
331
  }
332
+
333
+ const fields: any = {
334
+ error: msg,
335
+ sdp: sd.sdp,
336
+ };
337
+ if (!remote && this.pc.remoteDescription) {
338
+ fields.remoteSdp = this.pc.remoteDescription;
339
+ }
340
+ log.error(`unable to set ${sd.type}`, fields);
331
341
  throw new NegotiationError(msg);
332
342
  }
333
343
  }
@@ -191,8 +191,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
191
191
  this.url = url;
192
192
  this.token = token;
193
193
  this.signalOpts = opts;
194
+ this.maxJoinAttempts = opts.maxRetries;
194
195
  try {
195
196
  this.joinAttempts += 1;
197
+
198
+ this.setupSignalClientCallbacks();
196
199
  const joinResponse = await this.client.join(url, token, opts, abortSignal);
197
200
  this._isClosed = false;
198
201
  this.latestJoinResponse = joinResponse;
@@ -206,10 +209,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
206
209
  if (!this.subscriberPrimary) {
207
210
  this.negotiate();
208
211
  }
209
- this.setupSignalClientCallbacks();
210
212
 
211
213
  this.clientConfiguration = joinResponse.clientConfiguration;
212
-
213
214
  return joinResponse;
214
215
  } catch (e) {
215
216
  if (e instanceof ConnectionError) {