livekit-client 2.15.7 → 2.15.9

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 (196) 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 +253 -118
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2442 -323
  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 +31 -2
  10. package/dist/src/api/SignalClient.d.ts.map +1 -1
  11. package/dist/src/api/WebSocketStream.d.ts +29 -0
  12. package/dist/src/api/WebSocketStream.d.ts.map +1 -0
  13. package/dist/src/api/utils.d.ts +2 -0
  14. package/dist/src/api/utils.d.ts.map +1 -1
  15. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  16. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
  17. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
  18. package/dist/src/e2ee/E2eeManager.d.ts +16 -2
  19. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  20. package/dist/src/e2ee/types.d.ts +35 -1
  21. package/dist/src/e2ee/types.d.ts.map +1 -1
  22. package/dist/src/e2ee/utils.d.ts +2 -0
  23. package/dist/src/e2ee/utils.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/DataCryptor.d.ts +15 -0
  25. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +3 -2
  27. package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
  28. package/dist/src/e2ee/worker/sifPayload.d.ts +6 -6
  29. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  30. package/dist/src/index.d.ts +5 -3
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/logger.d.ts +1 -0
  33. package/dist/src/logger.d.ts.map +1 -1
  34. package/dist/src/options.d.ts +10 -2
  35. package/dist/src/options.d.ts.map +1 -1
  36. package/dist/src/room/PCTransport.d.ts +1 -0
  37. package/dist/src/room/PCTransport.d.ts.map +1 -1
  38. package/dist/src/room/PCTransportManager.d.ts +6 -4
  39. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  40. package/dist/src/room/RTCEngine.d.ts +6 -3
  41. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  42. package/dist/src/room/Room.d.ts +3 -2
  43. package/dist/src/room/Room.d.ts.map +1 -1
  44. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -2
  45. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  46. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  47. package/dist/src/room/defaults.d.ts.map +1 -1
  48. package/dist/src/room/errors.d.ts +2 -1
  49. package/dist/src/room/errors.d.ts.map +1 -1
  50. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  51. package/dist/src/room/participant/Participant.d.ts +2 -2
  52. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  53. package/dist/src/room/token-source/TokenSource.d.ts +70 -0
  54. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -0
  55. package/dist/src/room/token-source/types.d.ts +68 -0
  56. package/dist/src/room/token-source/types.d.ts.map +1 -0
  57. package/dist/src/room/token-source/utils.d.ts +5 -0
  58. package/dist/src/room/token-source/utils.d.ts.map +1 -0
  59. package/dist/src/room/track/LocalTrack.d.ts +1 -1
  60. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  61. package/dist/src/room/track/options.d.ts +7 -3
  62. package/dist/src/room/track/options.d.ts.map +1 -1
  63. package/dist/src/room/track/utils.d.ts.map +1 -1
  64. package/dist/src/room/types.d.ts +1 -0
  65. package/dist/src/room/types.d.ts.map +1 -1
  66. package/dist/src/room/utils.d.ts +8 -1
  67. package/dist/src/room/utils.d.ts.map +1 -1
  68. package/dist/src/utils/camelToSnakeCase.d.ts +8 -0
  69. package/dist/src/utils/camelToSnakeCase.d.ts.map +1 -0
  70. package/dist/ts4.2/{src/api → api}/SignalClient.d.ts +31 -2
  71. package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
  72. package/dist/ts4.2/{src/api → api}/utils.d.ts +2 -0
  73. package/dist/ts4.2/{src/e2ee → e2ee}/E2eeManager.d.ts +16 -2
  74. package/dist/ts4.2/{src/e2ee → e2ee}/types.d.ts +35 -1
  75. package/dist/ts4.2/{src/e2ee → e2ee}/utils.d.ts +3 -0
  76. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +15 -0
  77. package/dist/ts4.2/{src/e2ee → e2ee}/worker/ParticipantKeyHandler.d.ts +3 -2
  78. package/dist/ts4.2/{src/e2ee → e2ee}/worker/sifPayload.d.ts +6 -6
  79. package/dist/ts4.2/{src/index.d.ts → index.d.ts} +5 -3
  80. package/dist/ts4.2/{src/logger.d.ts → logger.d.ts} +1 -0
  81. package/dist/ts4.2/{src/options.d.ts → options.d.ts} +10 -2
  82. package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +1 -0
  83. package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +6 -4
  84. package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +6 -3
  85. package/dist/ts4.2/{src/room → room}/Room.d.ts +3 -2
  86. package/dist/ts4.2/{src/room → room}/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -1
  87. package/dist/ts4.2/{src/room → room}/errors.d.ts +2 -1
  88. package/dist/ts4.2/{src/room → room}/participant/Participant.d.ts +2 -2
  89. package/dist/ts4.2/room/token-source/TokenSource.d.ts +71 -0
  90. package/dist/ts4.2/room/token-source/types.d.ts +68 -0
  91. package/dist/ts4.2/room/token-source/utils.d.ts +5 -0
  92. package/dist/ts4.2/{src/room → room}/track/LocalTrack.d.ts +1 -1
  93. package/dist/ts4.2/{src/room → room}/track/options.d.ts +10 -3
  94. package/dist/ts4.2/{src/room → room}/types.d.ts +1 -0
  95. package/dist/ts4.2/{src/room → room}/utils.d.ts +8 -1
  96. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
  97. package/package.json +11 -10
  98. package/src/api/SignalClient.test.ts +688 -0
  99. package/src/api/SignalClient.ts +308 -161
  100. package/src/api/WebSocketStream.test.ts +625 -0
  101. package/src/api/WebSocketStream.ts +118 -0
  102. package/src/api/utils.ts +10 -0
  103. package/src/connectionHelper/checks/publishVideo.ts +5 -0
  104. package/src/connectionHelper/checks/turn.ts +1 -0
  105. package/src/connectionHelper/checks/webrtc.ts +1 -1
  106. package/src/connectionHelper/checks/websocket.ts +1 -0
  107. package/src/e2ee/E2eeManager.ts +94 -2
  108. package/src/e2ee/types.ts +44 -1
  109. package/src/e2ee/utils.ts +16 -0
  110. package/src/e2ee/worker/DataCryptor.test.ts +271 -0
  111. package/src/e2ee/worker/DataCryptor.ts +147 -0
  112. package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -3
  113. package/src/e2ee/worker/e2ee.worker.ts +47 -0
  114. package/src/e2ee/worker/sifPayload.ts +10 -6
  115. package/src/index.ts +16 -1
  116. package/src/logger.ts +1 -0
  117. package/src/options.ts +15 -2
  118. package/src/room/PCTransport.ts +7 -3
  119. package/src/room/PCTransportManager.ts +39 -35
  120. package/src/room/RTCEngine.ts +109 -22
  121. package/src/room/Room.ts +43 -18
  122. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
  123. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
  124. package/src/room/defaults.ts +1 -0
  125. package/src/room/errors.ts +3 -0
  126. package/src/room/participant/LocalParticipant.ts +8 -6
  127. package/src/room/participant/Participant.ts +6 -1
  128. package/src/room/token-source/TokenSource.ts +285 -0
  129. package/src/room/token-source/types.ts +84 -0
  130. package/src/room/token-source/utils.test.ts +63 -0
  131. package/src/room/token-source/utils.ts +40 -0
  132. package/src/room/track/LocalAudioTrack.ts +1 -1
  133. package/src/room/track/LocalTrack.ts +1 -1
  134. package/src/room/track/options.ts +12 -4
  135. package/src/room/track/utils.ts +10 -2
  136. package/src/room/types.ts +1 -0
  137. package/src/room/utils.ts +37 -4
  138. package/src/utils/camelToSnakeCase.ts +16 -0
  139. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/ConnectionCheck.d.ts +0 -0
  140. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/Checker.d.ts +0 -0
  141. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/cloudRegion.d.ts +0 -0
  142. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/connectionProtocol.d.ts +0 -0
  143. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishAudio.d.ts +0 -0
  144. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishVideo.d.ts +0 -0
  145. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/reconnect.d.ts +0 -0
  146. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/turn.d.ts +0 -0
  147. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/webrtc.d.ts +0 -0
  148. /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/websocket.d.ts +0 -0
  149. /package/dist/ts4.2/{src/e2ee → e2ee}/KeyProvider.d.ts +0 -0
  150. /package/dist/ts4.2/{src/e2ee → e2ee}/constants.d.ts +0 -0
  151. /package/dist/ts4.2/{src/e2ee → e2ee}/errors.d.ts +0 -0
  152. /package/dist/ts4.2/{src/e2ee → e2ee}/events.d.ts +0 -0
  153. /package/dist/ts4.2/{src/e2ee → e2ee}/index.d.ts +0 -0
  154. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/FrameCryptor.d.ts +0 -0
  155. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/e2ee.worker.d.ts +0 -0
  156. /package/dist/ts4.2/{src/e2ee → e2ee}/worker/naluUtils.d.ts +0 -0
  157. /package/dist/ts4.2/{src/room → room}/DefaultReconnectPolicy.d.ts +0 -0
  158. /package/dist/ts4.2/{src/room → room}/DeviceManager.d.ts +0 -0
  159. /package/dist/ts4.2/{src/room → room}/ReconnectPolicy.d.ts +0 -0
  160. /package/dist/ts4.2/{src/room → room}/RegionUrlProvider.d.ts +0 -0
  161. /package/dist/ts4.2/{src/room → room}/attribute-typings.d.ts +0 -0
  162. /package/dist/ts4.2/{src/room → room}/data-stream/incoming/StreamReader.d.ts +0 -0
  163. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -0
  164. /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/StreamWriter.d.ts +0 -0
  165. /package/dist/ts4.2/{src/room → room}/defaults.d.ts +0 -0
  166. /package/dist/ts4.2/{src/room → room}/events.d.ts +0 -0
  167. /package/dist/ts4.2/{src/room → room}/participant/LocalParticipant.d.ts +0 -0
  168. /package/dist/ts4.2/{src/room → room}/participant/ParticipantTrackPermission.d.ts +0 -0
  169. /package/dist/ts4.2/{src/room → room}/participant/RemoteParticipant.d.ts +0 -0
  170. /package/dist/ts4.2/{src/room → room}/participant/publishUtils.d.ts +0 -0
  171. /package/dist/ts4.2/{src/room → room}/rpc.d.ts +0 -0
  172. /package/dist/ts4.2/{src/room → room}/stats.d.ts +0 -0
  173. /package/dist/ts4.2/{src/room → room}/timers.d.ts +0 -0
  174. /package/dist/ts4.2/{src/room → room}/track/LocalAudioTrack.d.ts +0 -0
  175. /package/dist/ts4.2/{src/room → room}/track/LocalTrackPublication.d.ts +0 -0
  176. /package/dist/ts4.2/{src/room → room}/track/LocalVideoTrack.d.ts +0 -0
  177. /package/dist/ts4.2/{src/room → room}/track/RemoteAudioTrack.d.ts +0 -0
  178. /package/dist/ts4.2/{src/room → room}/track/RemoteTrack.d.ts +0 -0
  179. /package/dist/ts4.2/{src/room → room}/track/RemoteTrackPublication.d.ts +0 -0
  180. /package/dist/ts4.2/{src/room → room}/track/RemoteVideoTrack.d.ts +0 -0
  181. /package/dist/ts4.2/{src/room → room}/track/Track.d.ts +0 -0
  182. /package/dist/ts4.2/{src/room → room}/track/TrackPublication.d.ts +0 -0
  183. /package/dist/ts4.2/{src/room → room}/track/create.d.ts +0 -0
  184. /package/dist/ts4.2/{src/room → room}/track/facingMode.d.ts +0 -0
  185. /package/dist/ts4.2/{src/room → room}/track/processor/types.d.ts +0 -0
  186. /package/dist/ts4.2/{src/room → room}/track/record.d.ts +0 -0
  187. /package/dist/ts4.2/{src/room → room}/track/types.d.ts +0 -0
  188. /package/dist/ts4.2/{src/room → room}/track/utils.d.ts +0 -0
  189. /package/dist/ts4.2/{src/test → test}/MockMediaStreamTrack.d.ts +0 -0
  190. /package/dist/ts4.2/{src/test → test}/mocks.d.ts +0 -0
  191. /package/dist/ts4.2/{src/utils → utils}/AsyncQueue.d.ts +0 -0
  192. /package/dist/ts4.2/{src/utils → utils}/browserParser.d.ts +0 -0
  193. /package/dist/ts4.2/{src/utils → utils}/cloneDeep.d.ts +0 -0
  194. /package/dist/ts4.2/{src/utils → utils}/dataPacketBuffer.d.ts +0 -0
  195. /package/dist/ts4.2/{src/utils → utils}/ttlmap.d.ts +0 -0
  196. /package/dist/ts4.2/{src/version.d.ts → version.d.ts} +0 -0
@@ -0,0 +1,285 @@
1
+ import { Mutex } from '@livekit/mutex';
2
+ import {
3
+ RoomAgentDispatch,
4
+ RoomConfiguration,
5
+ TokenSourceRequest,
6
+ TokenSourceResponse,
7
+ } from '@livekit/protocol';
8
+ import {
9
+ TokenSourceConfigurable,
10
+ type TokenSourceFetchOptions,
11
+ TokenSourceFixed,
12
+ type TokenSourceResponseObject,
13
+ } from './types';
14
+ import { decodeTokenPayload, isResponseTokenValid } from './utils';
15
+
16
+ /** A TokenSourceCached is a TokenSource which caches the last {@link TokenSourceResponseObject} value and returns it
17
+ * until a) it expires or b) the {@link TokenSourceFetchOptions} provided to .fetch(...) change. */
18
+ abstract class TokenSourceCached extends TokenSourceConfigurable {
19
+ private cachedFetchOptions: TokenSourceFetchOptions | null = null;
20
+
21
+ private cachedResponse: TokenSourceResponse | null = null;
22
+
23
+ private fetchMutex = new Mutex();
24
+
25
+ private isSameAsCachedFetchOptions(options: TokenSourceFetchOptions) {
26
+ if (!this.cachedFetchOptions) {
27
+ return false;
28
+ }
29
+
30
+ for (const key of Object.keys(this.cachedFetchOptions) as Array<
31
+ keyof TokenSourceFetchOptions
32
+ >) {
33
+ switch (key) {
34
+ case 'roomName':
35
+ case 'participantName':
36
+ case 'participantIdentity':
37
+ case 'participantMetadata':
38
+ case 'participantAttributes':
39
+ case 'agentName':
40
+ case 'agentMetadata':
41
+ if (this.cachedFetchOptions[key] !== options[key]) {
42
+ return false;
43
+ }
44
+ break;
45
+ default:
46
+ // ref: https://stackoverflow.com/a/58009992
47
+ const exhaustiveCheckedKey: never = key;
48
+ throw new Error(`Options key ${exhaustiveCheckedKey} not being checked for equality!`);
49
+ }
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ private shouldReturnCachedValueFromFetch(fetchOptions: TokenSourceFetchOptions) {
56
+ if (!this.cachedResponse) {
57
+ return false;
58
+ }
59
+ if (!isResponseTokenValid(this.cachedResponse)) {
60
+ return false;
61
+ }
62
+ if (this.isSameAsCachedFetchOptions(fetchOptions)) {
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ getCachedResponseJwtPayload() {
69
+ if (!this.cachedResponse) {
70
+ return null;
71
+ }
72
+ return decodeTokenPayload(this.cachedResponse.participantToken);
73
+ }
74
+
75
+ async fetch(options: TokenSourceFetchOptions): Promise<TokenSourceResponseObject> {
76
+ const unlock = await this.fetchMutex.lock();
77
+ try {
78
+ if (this.shouldReturnCachedValueFromFetch(options)) {
79
+ return this.cachedResponse!.toJson() as TokenSourceResponseObject;
80
+ }
81
+ this.cachedFetchOptions = options;
82
+
83
+ const tokenResponse = await this.update(options);
84
+ this.cachedResponse = tokenResponse;
85
+ return tokenResponse.toJson() as TokenSourceResponseObject;
86
+ } finally {
87
+ unlock();
88
+ }
89
+ }
90
+
91
+ protected abstract update(options: TokenSourceFetchOptions): Promise<TokenSourceResponse>;
92
+ }
93
+
94
+ type LiteralOrFn =
95
+ | TokenSourceResponseObject
96
+ | (() => TokenSourceResponseObject | Promise<TokenSourceResponseObject>);
97
+ export class TokenSourceLiteral extends TokenSourceFixed {
98
+ private literalOrFn: LiteralOrFn;
99
+
100
+ constructor(literalOrFn: LiteralOrFn) {
101
+ super();
102
+ this.literalOrFn = literalOrFn;
103
+ }
104
+
105
+ async fetch(): Promise<TokenSourceResponseObject> {
106
+ if (typeof this.literalOrFn === 'function') {
107
+ return this.literalOrFn();
108
+ } else {
109
+ return this.literalOrFn;
110
+ }
111
+ }
112
+ }
113
+
114
+ type CustomFn = (
115
+ options: TokenSourceFetchOptions,
116
+ ) => TokenSourceResponseObject | Promise<TokenSourceResponseObject>;
117
+ export class TokenSourceCustom extends TokenSourceCached {
118
+ private customFn: CustomFn;
119
+
120
+ constructor(customFn: CustomFn) {
121
+ super();
122
+ this.customFn = customFn;
123
+ }
124
+
125
+ protected async update(options: TokenSourceFetchOptions) {
126
+ const resultMaybePromise = this.customFn(options);
127
+
128
+ let result;
129
+ if (resultMaybePromise instanceof Promise) {
130
+ result = await resultMaybePromise;
131
+ } else {
132
+ result = resultMaybePromise;
133
+ }
134
+
135
+ return TokenSourceResponse.fromJson(result, {
136
+ // NOTE: it could be possible that the response body could contain more fields than just
137
+ // what's in TokenSourceResponse depending on the implementation
138
+ ignoreUnknownFields: true,
139
+ });
140
+ }
141
+ }
142
+
143
+ export type EndpointOptions = Omit<RequestInit, 'body'>;
144
+
145
+ export class TokenSourceEndpoint extends TokenSourceCached {
146
+ private url: string;
147
+
148
+ private endpointOptions: EndpointOptions;
149
+
150
+ constructor(url: string, options: EndpointOptions = {}) {
151
+ super();
152
+ this.url = url;
153
+ this.endpointOptions = options;
154
+ }
155
+
156
+ private createRequestFromOptions(options: TokenSourceFetchOptions) {
157
+ const request = new TokenSourceRequest();
158
+
159
+ for (const key of Object.keys(options) as Array<keyof TokenSourceFetchOptions>) {
160
+ switch (key) {
161
+ case 'roomName':
162
+ case 'participantName':
163
+ case 'participantIdentity':
164
+ case 'participantMetadata':
165
+ request[key] = options[key];
166
+ break;
167
+
168
+ case 'participantAttributes':
169
+ request.participantAttributes = options.participantAttributes ?? {};
170
+ break;
171
+
172
+ case 'agentName':
173
+ request.roomConfig = request.roomConfig ?? new RoomConfiguration();
174
+ if (request.roomConfig.agents.length === 0) {
175
+ request.roomConfig.agents.push(new RoomAgentDispatch());
176
+ }
177
+ request.roomConfig.agents[0].agentName = options.agentName!;
178
+ break;
179
+
180
+ case 'agentMetadata':
181
+ request.roomConfig = request.roomConfig ?? new RoomConfiguration();
182
+ if (request.roomConfig.agents.length === 0) {
183
+ request.roomConfig.agents.push(new RoomAgentDispatch());
184
+ }
185
+ request.roomConfig.agents[0].metadata = options.agentMetadata!;
186
+ break;
187
+
188
+ default:
189
+ // ref: https://stackoverflow.com/a/58009992
190
+ const exhaustiveCheckedKey: never = key;
191
+ throw new Error(
192
+ `Options key ${exhaustiveCheckedKey} not being included in forming request!`,
193
+ );
194
+ }
195
+ }
196
+
197
+ return request;
198
+ }
199
+
200
+ protected async update(options: TokenSourceFetchOptions) {
201
+ const request = this.createRequestFromOptions(options);
202
+
203
+ const response = await fetch(this.url, {
204
+ ...this.endpointOptions,
205
+ method: this.endpointOptions.method ?? 'POST',
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ ...this.endpointOptions.headers,
209
+ },
210
+ body: request.toJsonString({
211
+ useProtoFieldName: true,
212
+ }),
213
+ });
214
+
215
+ if (!response.ok) {
216
+ throw new Error(
217
+ `Error generating token from endpoint ${this.url}: received ${response.status} / ${await response.text()}`,
218
+ );
219
+ }
220
+
221
+ const body = await response.json();
222
+ return TokenSourceResponse.fromJson(body, {
223
+ // NOTE: it could be possible that the response body could contain more fields than just
224
+ // what's in TokenSourceResponse depending on the implementation (ie, SandboxTokenServer)
225
+ ignoreUnknownFields: true,
226
+ });
227
+ }
228
+ }
229
+
230
+ export type SandboxTokenServerOptions = {
231
+ baseUrl?: string;
232
+ };
233
+
234
+ export class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
235
+ constructor(sandboxId: string, options: SandboxTokenServerOptions) {
236
+ const { baseUrl = 'https://cloud-api.livekit.io', ...rest } = options;
237
+
238
+ super(`${baseUrl}/api/v2/sandbox/connection-details`, {
239
+ ...rest,
240
+ headers: {
241
+ 'X-Sandbox-ID': sandboxId,
242
+ },
243
+ });
244
+ }
245
+ }
246
+
247
+ export const TokenSource = {
248
+ /** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
249
+ * credentials, either provided directly or returned from a provided function. */
250
+ literal(literalOrFn: LiteralOrFn) {
251
+ return new TokenSourceLiteral(literalOrFn);
252
+ },
253
+
254
+ /**
255
+ * TokenSource.custom allows a user to define a manual function which generates new
256
+ * {@link TokenSourceResponseObject} values on demand.
257
+ *
258
+ * Use this to get credentials from custom backends / etc.
259
+ */
260
+ custom(customFn: CustomFn) {
261
+ return new TokenSourceCustom(customFn);
262
+ },
263
+
264
+ /**
265
+ * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
266
+ * the standard endpoint format:
267
+ * FIXME: add docs link here in the future!
268
+ */
269
+ endpoint(url: string, options: EndpointOptions = {}) {
270
+ return new TokenSourceEndpoint(url, options);
271
+ },
272
+
273
+ /**
274
+ * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
275
+ * which supports quick prototyping / getting started types of use cases.
276
+ *
277
+ * This token provider is INSECURE and should NOT be used in production.
278
+ *
279
+ * For more info:
280
+ * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
281
+ */
282
+ sandboxTokenServer(sandboxId: string, options: SandboxTokenServerOptions = {}) {
283
+ return new TokenSourceSandboxTokenServer(sandboxId, options);
284
+ },
285
+ };
@@ -0,0 +1,84 @@
1
+ import { RoomConfiguration, TokenSourceRequest, TokenSourceResponse } from '@livekit/protocol';
2
+ import type { JWTPayload } from 'jose';
3
+ import type { ValueToSnakeCase } from '../../utils/camelToSnakeCase';
4
+ // The below imports are being linked in tsdoc comments, so they have to be imported even if they
5
+ // aren't being used.
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ import type { TokenSourceCustom, TokenSourceEndpoint, TokenSourceLiteral } from './TokenSource';
8
+
9
+ export type TokenSourceRequestObject = Required<
10
+ NonNullable<ConstructorParameters<typeof TokenSourceRequest>[0]>
11
+ >;
12
+ export type TokenSourceResponseObject = Required<
13
+ NonNullable<ConstructorParameters<typeof TokenSourceResponse>[0]>
14
+ >;
15
+
16
+ /** The `TokenSource` request object sent to the server as part of fetching a configurable
17
+ * `TokenSource` like {@link TokenSourceEndpoint}.
18
+ *
19
+ * Use this as a type for your request body if implementing a server endpoint in node.js.
20
+ */
21
+ export type TokenSourceRequestPayload = ValueToSnakeCase<TokenSourceRequestObject>;
22
+
23
+ /** The `TokenSource` response object sent from the server as part of fetching a configurable
24
+ * `TokenSource` like {@link TokenSourceEndpoint}.
25
+ *
26
+ * Use this as a type for your response body if implementing a server endpoint in node.js.
27
+ */
28
+ export type TokenSourceResponsePayload = ValueToSnakeCase<TokenSourceResponseObject>;
29
+
30
+ /** The payload of a LiveKit JWT token. */
31
+ export type TokenPayload = JWTPayload & {
32
+ name?: string;
33
+ metadata?: string;
34
+ attributes?: Record<string, string>;
35
+ video?: {
36
+ room?: string;
37
+ roomJoin?: boolean;
38
+ canPublish?: boolean;
39
+ canPublishData?: boolean;
40
+ canSubscribe?: boolean;
41
+ };
42
+ roomConfig?: RoomConfigurationObject;
43
+ };
44
+ export type RoomConfigurationObject = NonNullable<
45
+ ConstructorParameters<typeof RoomConfiguration>[0]
46
+ >;
47
+
48
+ /** A Fixed TokenSource is a token source that takes no parameters and returns a completely
49
+ * independently derived value on each fetch() call.
50
+ *
51
+ * The most common downstream implementer is {@link TokenSourceLiteral}.
52
+ */
53
+ export abstract class TokenSourceFixed {
54
+ abstract fetch(): Promise<TokenSourceResponseObject>;
55
+ }
56
+
57
+ export type TokenSourceFetchOptions = {
58
+ roomName?: string;
59
+ participantName?: string;
60
+ participantIdentity?: string;
61
+ participantMetadata?: string;
62
+ participantAttributes?: { [key: string]: string };
63
+
64
+ agentName?: string;
65
+ agentMetadata?: string;
66
+ };
67
+
68
+ /** A Configurable TokenSource is a token source that takes a
69
+ * {@link TokenSourceFetchOptions} object as input and returns a deterministic
70
+ * {@link TokenSourceResponseObject} output based on the options specified.
71
+ *
72
+ * For example, if options.participantName is set, it should be expected that
73
+ * all tokens that are generated will have participant name field set to the
74
+ * provided value.
75
+ *
76
+ * A few common downstream implementers are {@link TokenSourceEndpoint}
77
+ * and {@link TokenSourceCustom}.
78
+ */
79
+ export abstract class TokenSourceConfigurable {
80
+ abstract fetch(options: TokenSourceFetchOptions): Promise<TokenSourceResponseObject>;
81
+ }
82
+
83
+ /** A TokenSource is a mechanism for fetching credentials required to connect to a LiveKit Room. */
84
+ export type TokenSourceBase = TokenSourceFixed | TokenSourceConfigurable;
@@ -0,0 +1,63 @@
1
+ import { TokenSourceResponse } from '@livekit/protocol';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { decodeTokenPayload, isResponseTokenValid } from './utils';
4
+
5
+ // Test JWTs created for test purposes only.
6
+ // None of these actually auth against anything.
7
+ const TOKENS = {
8
+ // Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
9
+ // Exp date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
10
+ // A dummy roomConfig value is also set, with room_config.name = "test room name", and room_config.agents = [{"agentName": "test agent name","metadata":"test agent metadata"}]
11
+ VALID:
12
+ 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjEwLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwicm9vbUNvbmZpZyI6eyJuYW1lIjoidGVzdCByb29tIG5hbWUiLCJlbXB0eVRpbWVvdXQiOjAsImRlcGFydHVyZVRpbWVvdXQiOjAsIm1heFBhcnRpY2lwYW50cyI6MCwibWluUGxheW91dERlbGF5IjowLCJtYXhQbGF5b3V0RGVsYXkiOjAsInN5bmNTdHJlYW1zIjpmYWxzZSwiYWdlbnRzIjpbeyJhZ2VudE5hbWUiOiJ0ZXN0IGFnZW50IG5hbWUiLCJtZXRhZGF0YSI6InRlc3QgYWdlbnQgbWV0YWRhdGEifV0sIm1ldGFkYXRhIjoiIn19.EDetpHG8cSubaApzgWJaQrpCiSy9KDBlfCfVdIydbQ-_CHiNnXOK_f_mCJbTf9A-duT1jmvPOkLrkkWFT60XPQ',
13
+
14
+ // Nbf date set at 9876543210 seconds (Fri Dec 22 2282 20:13:30 GMT+0000)
15
+ // Exp date set at 9876543211 seconds (Fri Dec 22 2282 20:13:31 GMT+0000)
16
+ NBF_IN_FUTURE:
17
+ 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjo5ODc2NTQzMjExLCJuYmYiOjk4NzY1NDMyMTAsImlhdCI6MTIzNDU2Nzg5MH0.DcMmdKrD76eJg7IUBZqoTRDvBaXtCcwtuE5h7IwVXhG_6nvgxN_ix30_AmLgnYhvhkN-x9dTRPoHg-CME72AbQ',
18
+
19
+ // Nbf date set at 1234567890 seconds (Fri Feb 13 2009 23:31:30 GMT+0000)
20
+ // Exp date set at 1234567891 seconds (Fri Feb 13 2009 23:31:31 GMT+0000)
21
+ EXP_IN_PAST:
22
+ 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxMjM0NTY3ODkxLCJuYmYiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MH0.OYP1NITayotBYt0mioInLJmaIM0bHyyR-yG6iwKyQDzhoGha15qbsc7dOJlzz4za1iW5EzCgjc2_xGxqaSu5XA',
23
+ };
24
+
25
+ describe('isResponseTokenValid', () => {
26
+ it('should find a valid jwt not expired', () => {
27
+ const isValid = isResponseTokenValid(
28
+ TokenSourceResponse.fromJson({
29
+ serverUrl: 'ws://localhost:7800',
30
+ participantToken: TOKENS.VALID,
31
+ }),
32
+ );
33
+ expect(isValid).toBe(true);
34
+ });
35
+ it('should find a long ago expired jwt as expired', () => {
36
+ const isValid = isResponseTokenValid(
37
+ TokenSourceResponse.fromJson({
38
+ serverUrl: 'ws://localhost:7800',
39
+ participantToken: TOKENS.EXP_IN_PAST,
40
+ }),
41
+ );
42
+ expect(isValid).toBe(false);
43
+ });
44
+ it('should find a jwt that has not become active yet as expired', () => {
45
+ const isValid = isResponseTokenValid(
46
+ TokenSourceResponse.fromJson({
47
+ serverUrl: 'ws://localhost:7800',
48
+ participantToken: TOKENS.NBF_IN_FUTURE,
49
+ }),
50
+ );
51
+ expect(isValid).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('decodeTokenPayload', () => {
56
+ it('should extract roomconfig metadata from a token', () => {
57
+ const payload = decodeTokenPayload(TOKENS.VALID);
58
+ expect(payload.roomConfig?.name).toBe('test room name');
59
+ expect(payload.roomConfig?.agents).toHaveLength(1);
60
+ expect(payload.roomConfig?.agents![0].agentName).toBe('test agent name');
61
+ expect(payload.roomConfig?.agents![0].metadata).toBe('test agent metadata');
62
+ });
63
+ });
@@ -0,0 +1,40 @@
1
+ import { RoomConfiguration, type TokenSourceResponse } from '@livekit/protocol';
2
+ import { decodeJwt } from 'jose';
3
+ import type { RoomConfigurationObject, TokenPayload } from './types';
4
+
5
+ const ONE_SECOND_IN_MILLISECONDS = 1000;
6
+ const ONE_MINUTE_IN_MILLISECONDS = 60 * ONE_SECOND_IN_MILLISECONDS;
7
+
8
+ export function isResponseTokenValid(response: TokenSourceResponse) {
9
+ const jwtPayload = decodeTokenPayload(response.participantToken);
10
+ if (!jwtPayload?.nbf || !jwtPayload?.exp) {
11
+ return true;
12
+ }
13
+
14
+ const now = new Date();
15
+
16
+ const nbfInMilliseconds = jwtPayload.nbf * ONE_SECOND_IN_MILLISECONDS;
17
+ const nbfDate = new Date(nbfInMilliseconds);
18
+
19
+ const expInMilliseconds = jwtPayload.exp * ONE_SECOND_IN_MILLISECONDS;
20
+ const expDate = new Date(expInMilliseconds - ONE_MINUTE_IN_MILLISECONDS);
21
+
22
+ return nbfDate <= now && expDate > now;
23
+ }
24
+
25
+ export function decodeTokenPayload(token: string) {
26
+ const payload = decodeJwt<Omit<TokenPayload, 'roomConfig'>>(token);
27
+
28
+ const { roomConfig, ...rest } = payload;
29
+
30
+ const mappedPayload: TokenPayload = {
31
+ ...rest,
32
+ roomConfig: payload.roomConfig
33
+ ? (RoomConfiguration.fromJson(
34
+ payload.roomConfig as Record<string, any>,
35
+ ) as RoomConfigurationObject)
36
+ : undefined,
37
+ };
38
+
39
+ return mappedPayload;
40
+ }
@@ -244,7 +244,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
244
244
  const trackIsSilent = await detectSilence(this);
245
245
  if (trackIsSilent) {
246
246
  if (!this.isMuted) {
247
- this.log.warn('silence detected on local audio track', this.logContext);
247
+ this.log.debug('silence detected on local audio track', this.logContext);
248
248
  }
249
249
  this.emit(TrackEvent.AudioSilenceDetected);
250
250
  }
@@ -670,7 +670,7 @@ export default abstract class LocalTrack<
670
670
  }
671
671
 
672
672
  /** @internal */
673
- getPreConnectBuffer() {
673
+ getPreConnectBuffer(): ReadableStream<Uint8Array> | undefined {
674
674
  return this.localTrackRecorder?.byteStream;
675
675
  }
676
676
 
@@ -390,18 +390,26 @@ export interface AudioPreset {
390
390
  priority?: RTCPriorityType;
391
391
  }
392
392
 
393
- const backupCodecs = ['vp8', 'h264'] as const;
393
+ // `red` is not technically a codec, but treated as one in signalling protocol
394
+ export const audioCodecs = ['opus', 'red'] as const;
395
+
396
+ export type AudioCodec = (typeof audioCodecs)[number];
397
+
398
+ const backupVideoCodecs = ['vp8', 'h264'] as const;
394
399
 
395
400
  export const videoCodecs = ['vp8', 'h264', 'vp9', 'av1', 'h265'] as const;
396
401
 
397
402
  export type VideoCodec = (typeof videoCodecs)[number];
398
403
 
399
- export type BackupVideoCodec = (typeof backupCodecs)[number];
404
+ export type BackupVideoCodec = (typeof backupVideoCodecs)[number];
400
405
 
401
- export function isBackupCodec(codec: string): codec is BackupVideoCodec {
402
- return !!backupCodecs.find((backup) => backup === codec);
406
+ export function isBackupVideoCodec(codec: string): codec is BackupVideoCodec {
407
+ return !!backupVideoCodecs.find((backup) => backup === codec);
403
408
  }
404
409
 
410
+ /** @deprecated Use {@link isBackupVideoCodec} instead */
411
+ export const isBackupCodec = isBackupVideoCodec;
412
+
405
413
  export enum BackupCodecPolicy {
406
414
  // codec regression is preferred, the sfu will try to regress codec if possible but not guaranteed
407
415
  PREFER_REGRESSION = 0,
@@ -147,10 +147,18 @@ export function getNewAudioContext(): AudioContext | void {
147
147
  }
148
148
  } catch (e) {
149
149
  console.warn('Error trying to auto-resume audio context', e);
150
+ } finally {
151
+ window.document.body?.removeEventListener('click', handleResume);
150
152
  }
151
-
152
- window.document.body?.removeEventListener('click', handleResume);
153
153
  };
154
+
155
+ // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/statechange_event
156
+ audioContext.addEventListener('statechange', () => {
157
+ if (audioContext.state === 'closed') {
158
+ window.document.body?.removeEventListener('click', handleResume);
159
+ }
160
+ });
161
+
154
162
  window.document.body.addEventListener('click', handleResume);
155
163
  }
156
164
  return audioContext;
package/src/room/types.ts CHANGED
@@ -136,6 +136,7 @@ export interface BaseStreamInfo {
136
136
  /** total size in bytes for finite streams and undefined for streams of unknown size */
137
137
  size?: number;
138
138
  attributes?: Record<string, string>;
139
+ encryptionType: Encryption_Type;
139
140
  }
140
141
  export interface ByteStreamInfo extends BaseStreamInfo {
141
142
  name: string;
package/src/room/utils.ts CHANGED
@@ -22,7 +22,7 @@ import type RemoteTrackPublication from './track/RemoteTrackPublication';
22
22
  import type RemoteVideoTrack from './track/RemoteVideoTrack';
23
23
  import { Track } from './track/Track';
24
24
  import type { TrackPublication } from './track/TrackPublication';
25
- import { type VideoCodec, videoCodecs } from './track/options';
25
+ import { type AudioCodec, type VideoCodec, audioCodecs, videoCodecs } from './track/options';
26
26
  import { getNewAudioContext } from './track/utils';
27
27
  import type { ChatMessage, LiveKitReactNativeInfo, TranscriptionSegment } from './types';
28
28
 
@@ -73,7 +73,7 @@ export function supportsAV1(): boolean {
73
73
  let hasAV1 = false;
74
74
  if (capabilities) {
75
75
  for (const codec of capabilities.codecs) {
76
- if (codec.mimeType === 'video/AV1') {
76
+ if (codec.mimeType.toLowerCase() === 'video/av1') {
77
77
  hasAV1 = true;
78
78
  break;
79
79
  }
@@ -110,7 +110,7 @@ export function supportsVP9(): boolean {
110
110
  let hasVP9 = false;
111
111
  if (capabilities) {
112
112
  for (const codec of capabilities.codecs) {
113
- if (codec.mimeType === 'video/VP9') {
113
+ if (codec.mimeType.toLowerCase() === 'video/vp9') {
114
114
  hasVP9 = true;
115
115
  break;
116
116
  }
@@ -128,7 +128,7 @@ export function supportsH265(): boolean {
128
128
  let hasH265 = false;
129
129
  if (capabilities) {
130
130
  for (const codec of capabilities.codecs) {
131
- if (codec.mimeType === 'video/H265') {
131
+ if (codec.mimeType.toLowerCase() === 'video/h265') {
132
132
  hasH265 = true;
133
133
  break;
134
134
  }
@@ -151,6 +151,15 @@ export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
151
151
  return 'setSinkId' in elm;
152
152
  }
153
153
 
154
+ /**
155
+ * Checks whether or not setting an audio output via {@link Room#setActiveDevice}
156
+ * is supported for the current browser.
157
+ */
158
+ export function supportsAudioOutputSelection(): boolean {
159
+ // Note: this is method publicly exported under a user friendly name and currently only proxying `supportsSetSinkId`
160
+ return supportsSetSinkId();
161
+ }
162
+
154
163
  export function isBrowserSupported() {
155
164
  if (typeof RTCPeerConnection === 'undefined') {
156
165
  return false;
@@ -418,6 +427,26 @@ export function getEmptyAudioStreamTrack() {
418
427
  return emptyAudioStreamTrack.clone();
419
428
  }
420
429
 
430
+ export function getStereoAudioStreamTrack() {
431
+ const ctx = new AudioContext();
432
+ const oscLeft = ctx.createOscillator();
433
+ const oscRight = ctx.createOscillator();
434
+ oscLeft.frequency.value = 440;
435
+ oscRight.frequency.value = 220;
436
+ const merger = ctx.createChannelMerger(2);
437
+ oscLeft.connect(merger, 0, 0); // left channel
438
+ oscRight.connect(merger, 0, 1); // right channel
439
+ const dst = ctx.createMediaStreamDestination();
440
+ merger.connect(dst);
441
+ oscLeft.start();
442
+ oscRight.start();
443
+ const [stereoTrack] = dst.stream.getAudioTracks();
444
+ if (!stereoTrack) {
445
+ throw Error('Could not get stereo media stream audio track');
446
+ }
447
+ return stereoTrack;
448
+ }
449
+
421
450
  export class Future<T> {
422
451
  promise: Promise<T>;
423
452
 
@@ -533,6 +562,10 @@ export function createAudioAnalyser(
533
562
  return { calculateVolume, analyser, cleanup };
534
563
  }
535
564
 
565
+ export function isAudioCodec(maybeCodec: string): maybeCodec is AudioCodec {
566
+ return audioCodecs.includes(maybeCodec as AudioCodec);
567
+ }
568
+
536
569
  export function isVideoCodec(maybeCodec: string): maybeCodec is VideoCodec {
537
570
  return videoCodecs.includes(maybeCodec as VideoCodec);
538
571
  }
@@ -0,0 +1,16 @@
1
+ export type CamelToSnakeCase<Str extends string> = Str extends `${infer First}${infer Rest}`
2
+ ? `${First extends Capitalize<First> ? '_' : ''}${Lowercase<First>}${CamelToSnakeCase<Rest>}`
3
+ : Str;
4
+
5
+ type ArrayValuesToSnakeCase<Item> = Array<ValueToSnakeCase<Item>>;
6
+
7
+ type ObjectKeysToSnakeCase<Obj> = {
8
+ [Key in keyof Obj as CamelToSnakeCase<string & Key>]: NonNullable<ValueToSnakeCase<Obj[Key]>>;
9
+ };
10
+
11
+ export type ValueToSnakeCase<Value> =
12
+ Value extends Array<infer Item>
13
+ ? ArrayValuesToSnakeCase<Item>
14
+ : Value extends object
15
+ ? ObjectKeysToSnakeCase<Value>
16
+ : Value;