@vapi-ai/web 2.6.3 → 2.7.1

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 (8) hide show
  1. package/README.md +14 -0
  2. package/api.ts +25665 -11101
  3. package/dist/api.d.ts +22973 -13375
  4. package/dist/api.js +2171 -697
  5. package/dist/vapi.d.ts +44 -0
  6. package/dist/vapi.js +171 -58
  7. package/package.json +1 -1
  8. package/vapi.ts +208 -78
package/dist/vapi.d.ts CHANGED
@@ -109,6 +109,16 @@ type WebCall = {
109
109
  provider?: string;
110
110
  };
111
111
  };
112
+ /**
113
+ * The Vapi WebCall transport. When video recording is enabled, the server
114
+ * sets `callToken` to a Daily meeting token that auto-starts the cloud
115
+ * recording on join.
116
+ *
117
+ * call.transport
118
+ */
119
+ transport?: {
120
+ callToken?: string;
121
+ };
112
122
  };
113
123
  declare class VapiEventEmitter extends EventEmitter {
114
124
  on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
@@ -144,6 +154,25 @@ export default class Vapi extends VapiEventEmitter {
144
154
  * consumers register. Audio processing is non-critical, so the call continues.
145
155
  */
146
156
  private emitAudioProcessingError;
157
+ /**
158
+ * Starts Daily's local (microphone) audio level observer.
159
+ *
160
+ * Only runs when something is listening for 'local-volume-level'. The observer
161
+ * costs an AudioContext plus an AudioWorklet for the lifetime of the call and
162
+ * nothing else in the SDK reads the level, so starting it unconditionally
163
+ * charges every consumer for a feature almost none of them use. Listeners
164
+ * attached after the call is under way should call the public
165
+ * `startLocalAudioLevelObserver()`.
166
+ *
167
+ * Must run after the noise-cancellation processor has settled. Krisp replaces
168
+ * the microphone track, Daily reacts to any local track change by closing the
169
+ * AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
170
+ * and Chrome and Firefox reject that load with "AbortError: Unable to load a
171
+ * worklet's module". Daily answers by stopping the observer for the rest of
172
+ * the call, so losing this race costs the feature, not just console noise.
173
+ * Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
174
+ */
175
+ private maybeStartLocalAudioLevelObserver;
147
176
  start(assistant?: CreateAssistantDTO | string, assistantOverrides?: AssistantOverrides, squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides, options?: StartCallOptions): Promise<Call | null>;
148
177
  private onAppMessage;
149
178
  private handleRemoteParticipantsAudioLevel;
@@ -256,6 +285,21 @@ export default class Vapi extends VapiEventEmitter {
256
285
  isLocalAudioLevelObserverRunning(): boolean;
257
286
  startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings): void;
258
287
  stopScreenSharing(): void;
288
+ /**
289
+ * Starts (or retries) a cloud recording of the call.
290
+ *
291
+ * Recording start is asynchronous: success is signaled by the
292
+ * 'recording-started' event and failure by 'recording-error'. Daily does not
293
+ * retry automatically, so if you receive 'recording-error' (e.g. the
294
+ * recording start timed out), you can call this method again a few seconds
295
+ * later to retry.
296
+ */
297
+ startRecording(): void;
298
+ /**
299
+ * Stops the in-progress recording. Completion is signaled by the
300
+ * 'recording-stopped' event.
301
+ */
302
+ stopRecording(): void;
259
303
  /**
260
304
  * Reconnects to an active call.
261
305
  *
package/dist/vapi.js CHANGED
@@ -181,6 +181,35 @@ class Vapi extends VapiEventEmitter {
181
181
  timestamp: new Date().toISOString(),
182
182
  });
183
183
  }
184
+ /**
185
+ * Starts Daily's local (microphone) audio level observer.
186
+ *
187
+ * Only runs when something is listening for 'local-volume-level'. The observer
188
+ * costs an AudioContext plus an AudioWorklet for the lifetime of the call and
189
+ * nothing else in the SDK reads the level, so starting it unconditionally
190
+ * charges every consumer for a feature almost none of them use. Listeners
191
+ * attached after the call is under way should call the public
192
+ * `startLocalAudioLevelObserver()`.
193
+ *
194
+ * Must run after the noise-cancellation processor has settled. Krisp replaces
195
+ * the microphone track, Daily reacts to any local track change by closing the
196
+ * AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
197
+ * and Chrome and Firefox reject that load with "AbortError: Unable to load a
198
+ * worklet's module". Daily answers by stopping the observer for the rest of
199
+ * the call, so losing this race costs the feature, not just console noise.
200
+ * Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
201
+ */
202
+ async maybeStartLocalAudioLevelObserver() {
203
+ if (!this.call || this.listenerCount('local-volume-level') === 0) {
204
+ return;
205
+ }
206
+ try {
207
+ await this.call.startLocalAudioLevelObserver(100);
208
+ }
209
+ catch (error) {
210
+ this.emit('local-audio-level-observer-error', serializeError(error));
211
+ }
212
+ }
184
213
  async start(assistant, assistantOverrides, squad, workflow, workflowOverrides, options) {
185
214
  const startTime = Date.now();
186
215
  // Input validation with detailed error messages
@@ -256,6 +285,11 @@ class Vapi extends VapiEventEmitter {
256
285
  }
257
286
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
258
287
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
288
+ // When video recording is enabled, the server sends a Daily meeting
289
+ // token that auto-starts the cloud recording on join — more reliable
290
+ // than calling startRecording() from a weak client network. Older
291
+ // servers don't send one; then the client starts the recording itself.
292
+ const callToken = webCall?.transport?.callToken;
259
293
  // Stage 2: Create Daily call object
260
294
  this.emit('call-start-progress', {
261
295
  stage: 'daily-call-object-creation',
@@ -313,6 +347,8 @@ class Vapi extends VapiEventEmitter {
313
347
  this.hasEmittedCallEndedStatus = true;
314
348
  }
315
349
  if (isVideoRecordingEnabled) {
350
+ // When using a token, the recording doesn't stop automatically on
351
+ // leave, so still stop it manually to preserve existing behavior.
316
352
  this.call?.stopRecording();
317
353
  }
318
354
  this.cleanup().catch(console.error);
@@ -378,6 +414,26 @@ class Vapi extends VapiEventEmitter {
378
414
  }
379
415
  this.detachAudioPlayer(e.participant.session_id);
380
416
  });
417
+ // Registered before join(): a token-auto-started recording can begin
418
+ // while join() is still in flight, so a listener added after join
419
+ // could miss the event.
420
+ let recordingRequestedTime = 0;
421
+ if (isVideoRecordingEnabled) {
422
+ this.call.once('recording-started', () => {
423
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
424
+ this.emit('call-start-progress', {
425
+ stage: 'video-recording-started',
426
+ status: 'completed',
427
+ timestamp: new Date().toISOString(),
428
+ metadata: { delaySeconds: totalRecordingDelay }
429
+ });
430
+ this.send({
431
+ type: 'control',
432
+ control: 'say-first-message',
433
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
434
+ });
435
+ });
436
+ }
381
437
  // Stage 3: Mobile device handling and permissions
382
438
  const isMobile = this.isMobileDevice();
383
439
  this.emit('call-start-progress', {
@@ -413,10 +469,14 @@ class Vapi extends VapiEventEmitter {
413
469
  timestamp: new Date().toISOString()
414
470
  });
415
471
  const joinStartTime = Date.now();
472
+ recordingRequestedTime = joinStartTime;
416
473
  try {
417
474
  await this.call.join({
418
475
  // @ts-expect-error This exists
419
476
  url: webCall.webCallUrl,
477
+ // daily-js rejects a `token` key that is present but undefined, so
478
+ // only include it when the server actually sent one.
479
+ ...(callToken ? { token: callToken } : {}),
420
480
  subscribeToTracksAutomatically: false,
421
481
  });
422
482
  const joinDuration = Date.now() - joinStartTime;
@@ -447,23 +507,26 @@ class Vapi extends VapiEventEmitter {
447
507
  throw error;
448
508
  }
449
509
  // Stage 5: Video recording setup (if enabled)
450
- if (isVideoRecordingEnabled) {
510
+ if (isVideoRecordingEnabled && callToken) {
511
+ // The meeting token auto-started the recording at join, so there is
512
+ // nothing to request from the client.
513
+ this.emit('call-start-progress', {
514
+ stage: 'video-recording-setup',
515
+ status: 'completed',
516
+ timestamp: new Date().toISOString(),
517
+ metadata: { action: 'auto-started-via-meeting-token' }
518
+ });
519
+ }
520
+ else if (isVideoRecordingEnabled) {
451
521
  this.emit('call-start-progress', {
452
522
  stage: 'video-recording-setup',
453
523
  status: 'started',
454
524
  timestamp: new Date().toISOString()
455
525
  });
456
- const recordingRequestedTime = new Date().getTime();
457
526
  const recordingStartTime = Date.now();
458
527
  try {
459
- this.call.startRecording({
460
- width: 1280,
461
- height: 720,
462
- backgroundColor: '#FF1F2D3D',
463
- layout: {
464
- preset: 'default',
465
- },
466
- });
528
+ recordingRequestedTime = new Date().getTime();
529
+ this.startRecording();
467
530
  const recordingSetupDuration = Date.now() - recordingStartTime;
468
531
  this.emit('call-start-progress', {
469
532
  stage: 'video-recording-setup',
@@ -471,20 +534,6 @@ class Vapi extends VapiEventEmitter {
471
534
  duration: recordingSetupDuration,
472
535
  timestamp: new Date().toISOString()
473
536
  });
474
- this.call.on('recording-started', () => {
475
- const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
476
- this.emit('call-start-progress', {
477
- stage: 'video-recording-started',
478
- status: 'completed',
479
- timestamp: new Date().toISOString(),
480
- metadata: { delaySeconds: totalRecordingDelay }
481
- });
482
- this.send({
483
- type: 'control',
484
- control: 'say-first-message',
485
- videoRecordingStartDelaySeconds: totalRecordingDelay,
486
- });
487
- });
488
537
  }
489
538
  catch (error) {
490
539
  const recordingSetupDuration = Date.now() - recordingStartTime;
@@ -522,7 +571,6 @@ class Vapi extends VapiEventEmitter {
522
571
  const audioObserverStartTime = Date.now();
523
572
  try {
524
573
  this.call.startRemoteParticipantsAudioLevelObserver(100);
525
- this.call.startLocalAudioLevelObserver(100);
526
574
  const audioObserverDuration = Date.now() - audioObserverStartTime;
527
575
  this.emit('call-start-progress', {
528
576
  stage: 'audio-observer-setup',
@@ -588,17 +636,26 @@ class Vapi extends VapiEventEmitter {
588
636
  });
589
637
  const audioProcessingStartTime = Date.now();
590
638
  try {
591
- this.call
592
- .updateInputSettings({
639
+ const audioProcessingUpdate = this.call.updateInputSettings({
593
640
  audio: {
594
641
  processor: {
595
642
  type: 'noise-cancellation',
596
643
  },
597
644
  },
598
- })
599
- .catch((error) => {
645
+ });
646
+ audioProcessingUpdate.catch((error) => {
600
647
  this.emitAudioProcessingError('audio-processing-setup', error);
601
648
  });
649
+ // The observer waits for this update rather than starting alongside the
650
+ // remote one above, so that Krisp has already swapped the microphone
651
+ // track. See maybeStartLocalAudioLevelObserver().
652
+ //
653
+ // A separate chain, not another link on the one above: EventEmitter
654
+ // rethrows out of emit('error') when a consumer registered no 'error'
655
+ // listener, and whether the observer starts must not hinge on that.
656
+ audioProcessingUpdate
657
+ .catch(() => { })
658
+ .then(() => this.maybeStartLocalAudioLevelObserver());
602
659
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
603
660
  this.emit('call-start-progress', {
604
661
  stage: 'audio-processing-setup',
@@ -1001,6 +1058,35 @@ class Vapi extends VapiEventEmitter {
1001
1058
  stopScreenSharing() {
1002
1059
  this.call?.stopScreenShare();
1003
1060
  }
1061
+ /**
1062
+ * Starts (or retries) a cloud recording of the call.
1063
+ *
1064
+ * Recording start is asynchronous: success is signaled by the
1065
+ * 'recording-started' event and failure by 'recording-error'. Daily does not
1066
+ * retry automatically, so if you receive 'recording-error' (e.g. the
1067
+ * recording start timed out), you can call this method again a few seconds
1068
+ * later to retry.
1069
+ */
1070
+ startRecording() {
1071
+ if (!this.call) {
1072
+ throw new Error('Call object is not available.');
1073
+ }
1074
+ this.call.startRecording({
1075
+ width: 1280,
1076
+ height: 720,
1077
+ backgroundColor: '#FF1F2D3D',
1078
+ layout: {
1079
+ preset: 'default',
1080
+ },
1081
+ });
1082
+ }
1083
+ /**
1084
+ * Stops the in-progress recording. Completion is signaled by the
1085
+ * 'recording-stopped' event.
1086
+ */
1087
+ stopRecording() {
1088
+ this.call?.stopRecording();
1089
+ }
1004
1090
  /**
1005
1091
  * Reconnects to an active call.
1006
1092
  *
@@ -1043,6 +1129,10 @@ class Vapi extends VapiEventEmitter {
1043
1129
  }
1044
1130
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
1045
1131
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
1132
+ // Same auto-start token as in start(). Rejoining with it while the
1133
+ // recording is still running is safe; if the recording stopped when the
1134
+ // user left, the token starts a new one.
1135
+ const callToken = webCall?.transport?.callToken;
1046
1136
  // Stage 1: Create Daily call object
1047
1137
  this.emit('call-start-progress', {
1048
1138
  stage: 'daily-call-object-creation',
@@ -1081,6 +1171,8 @@ class Vapi extends VapiEventEmitter {
1081
1171
  this.hasEmittedCallEndedStatus = true;
1082
1172
  }
1083
1173
  if (isVideoRecordingEnabled) {
1174
+ // When using a token, the recording doesn't stop automatically on
1175
+ // leave, so still stop it manually to preserve existing behavior.
1084
1176
  this.call?.stopRecording();
1085
1177
  }
1086
1178
  this.cleanup().catch(console.error);
@@ -1177,6 +1269,26 @@ class Vapi extends VapiEventEmitter {
1177
1269
  });
1178
1270
  }
1179
1271
  });
1272
+ // Registered before join(): a token-auto-started recording can begin
1273
+ // while join() is still in flight, so a listener added after join
1274
+ // could miss the event.
1275
+ let recordingRequestedTime = 0;
1276
+ if (isVideoRecordingEnabled) {
1277
+ this.call.once('recording-started', () => {
1278
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
1279
+ this.emit('call-start-progress', {
1280
+ stage: 'video-recording-started',
1281
+ status: 'completed',
1282
+ timestamp: new Date().toISOString(),
1283
+ metadata: { delaySeconds: totalRecordingDelay }
1284
+ });
1285
+ this.send({
1286
+ type: 'control',
1287
+ control: 'say-first-message',
1288
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
1289
+ });
1290
+ });
1291
+ }
1180
1292
  // Stage 2: Mobile device handling and permissions
1181
1293
  const isMobile = this.isMobileDevice();
1182
1294
  this.emit('call-start-progress', {
@@ -1212,8 +1324,12 @@ class Vapi extends VapiEventEmitter {
1212
1324
  timestamp: new Date().toISOString()
1213
1325
  });
1214
1326
  const joinStartTime = Date.now();
1327
+ recordingRequestedTime = joinStartTime;
1215
1328
  await this.call.join({
1216
1329
  url: webCall.webCallUrl,
1330
+ // daily-js rejects a `token` key that is present but undefined, so
1331
+ // only include it when the server actually sent one.
1332
+ ...(callToken ? { token: callToken } : {}),
1217
1333
  subscribeToTracksAutomatically: false,
1218
1334
  });
1219
1335
  const joinDuration = Date.now() - joinStartTime;
@@ -1224,23 +1340,26 @@ class Vapi extends VapiEventEmitter {
1224
1340
  timestamp: new Date().toISOString()
1225
1341
  });
1226
1342
  // Stage 4: Video recording setup (if enabled)
1227
- if (isVideoRecordingEnabled) {
1343
+ if (isVideoRecordingEnabled && callToken) {
1344
+ // The meeting token auto-started the recording at join, so there is
1345
+ // nothing to request from the client.
1346
+ this.emit('call-start-progress', {
1347
+ stage: 'video-recording-setup',
1348
+ status: 'completed',
1349
+ timestamp: new Date().toISOString(),
1350
+ metadata: { action: 'auto-started-via-meeting-token' }
1351
+ });
1352
+ }
1353
+ else if (isVideoRecordingEnabled) {
1228
1354
  this.emit('call-start-progress', {
1229
1355
  stage: 'video-recording-setup',
1230
1356
  status: 'started',
1231
1357
  timestamp: new Date().toISOString()
1232
1358
  });
1233
1359
  const recordingStartTime = Date.now();
1234
- const recordingRequestedTime = new Date().getTime();
1235
1360
  try {
1236
- this.call.startRecording({
1237
- width: 1280,
1238
- height: 720,
1239
- backgroundColor: '#FF1F2D3D',
1240
- layout: {
1241
- preset: 'default',
1242
- },
1243
- });
1361
+ recordingRequestedTime = new Date().getTime();
1362
+ this.startRecording();
1244
1363
  const recordingSetupDuration = Date.now() - recordingStartTime;
1245
1364
  this.emit('call-start-progress', {
1246
1365
  stage: 'video-recording-setup',
@@ -1248,20 +1367,6 @@ class Vapi extends VapiEventEmitter {
1248
1367
  duration: recordingSetupDuration,
1249
1368
  timestamp: new Date().toISOString()
1250
1369
  });
1251
- this.call.on('recording-started', () => {
1252
- const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
1253
- this.emit('call-start-progress', {
1254
- stage: 'video-recording-started',
1255
- status: 'completed',
1256
- timestamp: new Date().toISOString(),
1257
- metadata: { delaySeconds: totalRecordingDelay }
1258
- });
1259
- this.send({
1260
- type: 'control',
1261
- control: 'say-first-message',
1262
- videoRecordingStartDelaySeconds: totalRecordingDelay,
1263
- });
1264
- });
1265
1370
  }
1266
1371
  catch (error) {
1267
1372
  const recordingSetupDuration = Date.now() - recordingStartTime;
@@ -1293,7 +1398,6 @@ class Vapi extends VapiEventEmitter {
1293
1398
  const audioObserverStartTime = Date.now();
1294
1399
  try {
1295
1400
  this.call.startRemoteParticipantsAudioLevelObserver(100);
1296
- this.call.startLocalAudioLevelObserver(100);
1297
1401
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1298
1402
  this.emit('call-start-progress', {
1299
1403
  stage: 'audio-observer-setup',
@@ -1322,17 +1426,26 @@ class Vapi extends VapiEventEmitter {
1322
1426
  });
1323
1427
  const audioProcessingStartTime = Date.now();
1324
1428
  try {
1325
- this.call
1326
- .updateInputSettings({
1429
+ const audioProcessingUpdate = this.call.updateInputSettings({
1327
1430
  audio: {
1328
1431
  processor: {
1329
1432
  type: 'noise-cancellation',
1330
1433
  },
1331
1434
  },
1332
- })
1333
- .catch((error) => {
1435
+ });
1436
+ audioProcessingUpdate.catch((error) => {
1334
1437
  this.emitAudioProcessingError('audio-processing-setup', error);
1335
1438
  });
1439
+ // The observer waits for this update rather than starting alongside the
1440
+ // remote one above, so that Krisp has already swapped the microphone
1441
+ // track. See maybeStartLocalAudioLevelObserver().
1442
+ //
1443
+ // A separate chain, not another link on the one above: EventEmitter
1444
+ // rethrows out of emit('error') when a consumer registered no 'error'
1445
+ // listener, and whether the observer starts must not hinge on that.
1446
+ audioProcessingUpdate
1447
+ .catch(() => { })
1448
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1336
1449
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1337
1450
  this.emit('call-start-progress', {
1338
1451
  stage: 'audio-processing-setup',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "2.6.3",
3
+ "version": "2.7.1",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",