@vapi-ai/web 3.0.1 → 3.1.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.
package/dist/vapi.js CHANGED
@@ -58,6 +58,8 @@ function serializeError(error) {
58
58
  }
59
59
  return { message: String(error) };
60
60
  }
61
+ // The app message Vapi sends when it couldn't start a server-mode recording.
62
+ const VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE = 'video-recording.start-failed';
61
63
  async function startAudioPlayer(player, track) {
62
64
  player.muted = false;
63
65
  player.autoplay = true;
@@ -88,13 +90,13 @@ function destroyAudioPlayer(participantId) {
88
90
  const player = document.querySelector(`audio[data-participant-id="${participantId}"]`);
89
91
  player?.remove();
90
92
  }
91
- function subscribeToTracks(e, call, isVideoRecordingEnabled, isVideoEnabled) {
93
+ function subscribeToTracks(e, call, isVideoRecordingEnabled) {
92
94
  if (e.participant.local)
93
95
  return;
94
96
  call.updateParticipant(e.participant.session_id, {
95
97
  setSubscribedTracks: {
96
98
  audio: true,
97
- video: isVideoRecordingEnabled || isVideoEnabled,
99
+ video: isVideoRecordingEnabled,
98
100
  },
99
101
  });
100
102
  }
@@ -136,6 +138,7 @@ class Vapi extends VapiEventEmitter {
136
138
  dailyCallObject = {};
137
139
  microphoneTrack = null;
138
140
  cancelMicrophonePreflight = null;
141
+ clientSideNoiseCancellationEnabled = true;
139
142
  async microphoneAcquire() {
140
143
  const source = this.dailyCallObject.audioSource;
141
144
  if (source && typeof source === 'object') {
@@ -278,6 +281,7 @@ class Vapi extends VapiEventEmitter {
278
281
  }
279
282
  });
280
283
  this.started = true;
284
+ this.clientSideNoiseCancellationEnabled = options?.clientSideNoiseCancellationEnabled ?? true;
281
285
  let failureStage = 'microphone-preflight';
282
286
  try {
283
287
  if (this.call) {
@@ -295,7 +299,9 @@ class Vapi extends VapiEventEmitter {
295
299
  timestamp: new Date().toISOString()
296
300
  });
297
301
  const webCallStartTime = Date.now();
298
- const webCall = (await client_1.client.call.callControllerCreateWebCall({
302
+ // An unset option stays undefined, so JSON.stringify drops the key and
303
+ // older servers never see it.
304
+ const createWebCallBody = {
299
305
  assistant: typeof assistant === 'string' ? undefined : assistant,
300
306
  assistantId: typeof assistant === 'string' ? assistant : undefined,
301
307
  assistantOverrides,
@@ -305,7 +311,9 @@ class Vapi extends VapiEventEmitter {
305
311
  workflowId: typeof workflow === 'string' ? workflow : undefined,
306
312
  workflowOverrides,
307
313
  roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
308
- })).data;
314
+ videoRecordingStartMode: options?.videoRecordingStartMode,
315
+ };
316
+ const webCall = (await client_1.client.call.callControllerCreateWebCall(createWebCallBody)).data;
309
317
  const webCallDuration = Date.now() - webCallStartTime;
310
318
  this.emit('call-start-progress', {
311
319
  stage: 'web-call-creation',
@@ -319,12 +327,16 @@ class Vapi extends VapiEventEmitter {
319
327
  }
320
328
  });
321
329
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
322
- const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
323
330
  // When video recording is enabled, the server sends a Daily meeting
324
- // token that auto-starts the cloud recording on join — more reliable
325
- // than calling startRecording() from a weak client network. Older
326
- // servers don't send one; then the client starts the recording itself.
327
- const callToken = webCall?.transport?.callToken;
331
+ // token. In client mode it auto-starts the cloud recording on join. In
332
+ // server mode it starts nothing: Vapi starts the recording from its
333
+ // servers once the user has joined, which a weak client network can't
334
+ // stop. Without a token the SDK emits recording-error and starts
335
+ // nothing.
336
+ const transport = webCall?.transport;
337
+ const callToken = transport?.callToken;
338
+ const isRecordingStartedByServer = transport?.videoRecordingStartMode === 'server';
339
+ const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
328
340
  // Stage 2: Create Daily call object
329
341
  this.emit('call-start-progress', {
330
342
  stage: 'daily-call-object-creation',
@@ -334,7 +346,6 @@ class Vapi extends VapiEventEmitter {
334
346
  audioSource: this.dailyCallObject.audioSource ?? true,
335
347
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
336
348
  isVideoRecordingEnabled,
337
- isVideoEnabled
338
349
  }
339
350
  });
340
351
  const dailyCallStartTime = Date.now();
@@ -435,7 +446,7 @@ class Vapi extends VapiEventEmitter {
435
446
  this.call.on('participant-joined', (e) => {
436
447
  if (!e || !this.call)
437
448
  return;
438
- subscribeToTracks(e, this.call, isVideoRecordingEnabled, isVideoEnabled);
449
+ subscribeToTracks(e, this.call, isVideoRecordingEnabled);
439
450
  });
440
451
  this.call.on('participant-updated', (e) => {
441
452
  if (!e) {
@@ -449,6 +460,10 @@ class Vapi extends VapiEventEmitter {
449
460
  }
450
461
  this.detachAudioPlayer(e.participant.session_id);
451
462
  });
463
+ // Registered before join(), as in reconnect(): Vapi starts a server-mode
464
+ // recording when it sees the user join, so its start-failed message can
465
+ // arrive while join() is still in flight.
466
+ this.call.on('app-message', (e) => this.onAppMessage(e));
452
467
  // Registered before join(): a token-auto-started recording can begin
453
468
  // while join() is still in flight, so a listener added after join
454
469
  // could miss the event.
@@ -543,51 +558,29 @@ class Vapi extends VapiEventEmitter {
543
558
  }
544
559
  // Stage 5: Video recording setup (if enabled)
545
560
  if (isVideoRecordingEnabled && callToken) {
546
- // The meeting token auto-started the recording at join, so there is
547
- // nothing to request from the client.
561
+ // The meeting token auto-started the recording at join, or Vapi
562
+ // starts it from its servers, so there is nothing to request from the
563
+ // client.
548
564
  this.emit('call-start-progress', {
549
565
  stage: 'video-recording-setup',
550
566
  status: 'completed',
551
567
  timestamp: new Date().toISOString(),
552
- metadata: { action: 'auto-started-via-meeting-token' }
568
+ metadata: {
569
+ action: isRecordingStartedByServer
570
+ ? 'started-by-server'
571
+ : 'auto-started-via-meeting-token'
572
+ }
553
573
  });
554
574
  }
555
575
  else if (isVideoRecordingEnabled) {
576
+ // No token: nothing starts the recording. The recording-error is
577
+ // reported once start() has finished.
556
578
  this.emit('call-start-progress', {
557
579
  stage: 'video-recording-setup',
558
- status: 'started',
559
- timestamp: new Date().toISOString()
580
+ status: 'failed',
581
+ timestamp: new Date().toISOString(),
582
+ metadata: { action: 'no-meeting-token' }
560
583
  });
561
- const recordingStartTime = Date.now();
562
- try {
563
- recordingRequestedTime = new Date().getTime();
564
- this.startRecording();
565
- const recordingSetupDuration = Date.now() - recordingStartTime;
566
- this.emit('call-start-progress', {
567
- stage: 'video-recording-setup',
568
- status: 'completed',
569
- duration: recordingSetupDuration,
570
- timestamp: new Date().toISOString()
571
- });
572
- }
573
- catch (error) {
574
- const recordingSetupDuration = Date.now() - recordingStartTime;
575
- const serializedError = serializeError(error);
576
- this.emit('call-start-progress', {
577
- stage: 'video-recording-setup',
578
- status: 'failed',
579
- duration: recordingSetupDuration,
580
- timestamp: new Date().toISOString(),
581
- metadata: { error: serializedError.message }
582
- });
583
- this.emit('error', {
584
- type: 'video-recording-setup-error',
585
- stage: 'video-recording-setup',
586
- error: serializedError,
587
- timestamp: new Date().toISOString()
588
- });
589
- // Don't throw here, video recording is optional
590
- }
591
584
  }
592
585
  else {
593
586
  this.emit('call-start-progress', {
@@ -640,7 +633,6 @@ class Vapi extends VapiEventEmitter {
640
633
  if (e)
641
634
  this.handleLocalAudioLevel(e);
642
635
  });
643
- this.call.on('app-message', (e) => this.onAppMessage(e));
644
636
  this.call.on('nonfatal-error', (e) => {
645
637
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
646
638
  if (e?.type === 'local-audio-level-observer-error') {
@@ -663,59 +655,66 @@ class Vapi extends VapiEventEmitter {
663
655
  });
664
656
  }
665
657
  });
666
- // Stage 7: Audio processing setup
667
- this.emit('call-start-progress', {
668
- stage: 'audio-processing-setup',
669
- status: 'started',
670
- timestamp: new Date().toISOString()
671
- });
672
- const audioProcessingStartTime = Date.now();
673
- try {
674
- const audioProcessingUpdate = this.call.updateInputSettings({
675
- audio: {
676
- processor: {
677
- type: 'noise-cancellation',
678
- },
679
- },
680
- });
681
- audioProcessingUpdate.catch((error) => {
682
- this.emitAudioProcessingError('audio-processing-setup', error);
683
- });
684
- // The observer waits for this update rather than starting alongside the
685
- // remote one above, so that Krisp has already swapped the microphone
686
- // track. See maybeStartLocalAudioLevelObserver().
687
- //
688
- // A separate chain, not another link on the one above: EventEmitter
689
- // rethrows out of emit('error') when a consumer registered no 'error'
690
- // listener, and whether the observer starts must not hinge on that.
691
- audioProcessingUpdate
692
- .catch(() => { })
693
- .then(() => this.maybeStartLocalAudioLevelObserver());
694
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
658
+ if (this.clientSideNoiseCancellationEnabled) {
659
+ // Stage 7: Audio processing setup
695
660
  this.emit('call-start-progress', {
696
661
  stage: 'audio-processing-setup',
697
- status: 'completed',
698
- duration: audioProcessingDuration,
662
+ status: 'started',
699
663
  timestamp: new Date().toISOString()
700
664
  });
665
+ const audioProcessingStartTime = Date.now();
666
+ try {
667
+ const audioProcessingUpdate = this.call.updateInputSettings({
668
+ audio: {
669
+ processor: {
670
+ type: 'noise-cancellation',
671
+ },
672
+ },
673
+ });
674
+ audioProcessingUpdate.catch((error) => {
675
+ this.emitAudioProcessingError('audio-processing-setup', error);
676
+ });
677
+ // The observer waits for this update rather than starting alongside the
678
+ // remote one above, so that Krisp has already swapped the microphone
679
+ // track. See maybeStartLocalAudioLevelObserver().
680
+ //
681
+ // A separate chain, not another link on the one above: EventEmitter
682
+ // rethrows out of emit('error') when a consumer registered no 'error'
683
+ // listener, and whether the observer starts must not hinge on that.
684
+ audioProcessingUpdate
685
+ .catch(() => { })
686
+ .then(() => this.maybeStartLocalAudioLevelObserver());
687
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
688
+ this.emit('call-start-progress', {
689
+ stage: 'audio-processing-setup',
690
+ status: 'completed',
691
+ duration: audioProcessingDuration,
692
+ timestamp: new Date().toISOString()
693
+ });
694
+ }
695
+ catch (error) {
696
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
697
+ const serializedError = serializeError(error);
698
+ this.emit('call-start-progress', {
699
+ stage: 'audio-processing-setup',
700
+ status: 'failed',
701
+ duration: audioProcessingDuration,
702
+ timestamp: new Date().toISOString(),
703
+ metadata: { error: serializedError.message }
704
+ });
705
+ this.emit('error', {
706
+ type: 'audio-processing-setup-error',
707
+ stage: 'audio-processing-setup',
708
+ error: serializedError,
709
+ timestamp: new Date().toISOString()
710
+ });
711
+ // Don't throw here, this is non-critical
712
+ }
701
713
  }
702
- catch (error) {
703
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
704
- const serializedError = serializeError(error);
705
- this.emit('call-start-progress', {
706
- stage: 'audio-processing-setup',
707
- status: 'failed',
708
- duration: audioProcessingDuration,
709
- timestamp: new Date().toISOString(),
710
- metadata: { error: serializedError.message }
711
- });
712
- this.emit('error', {
713
- type: 'audio-processing-setup-error',
714
- stage: 'audio-processing-setup',
715
- error: serializedError,
716
- timestamp: new Date().toISOString()
717
- });
718
- // Don't throw here, this is non-critical
714
+ else {
715
+ // Daily's default processor is 'none', so the microphone track is never
716
+ // swapped and the observer has nothing to wait for.
717
+ void this.maybeStartLocalAudioLevelObserver();
719
718
  }
720
719
  const totalDuration = Date.now() - startTime;
721
720
  this.emit('call-start-success', {
@@ -723,6 +722,9 @@ class Vapi extends VapiEventEmitter {
723
722
  callId: webCall?.id || 'unknown',
724
723
  timestamp: new Date().toISOString()
725
724
  });
725
+ if (recordingTokenMissing) {
726
+ this.recordingTokenMissingReportSchedule();
727
+ }
726
728
  return webCall;
727
729
  }
728
730
  catch (e) {
@@ -772,6 +774,12 @@ class Vapi extends VapiEventEmitter {
772
774
  else {
773
775
  try {
774
776
  const parsedMessage = JSON.parse(e.data);
777
+ // Not a Vapi client message: re-emitted as recording-error, and kept
778
+ // out of 'message' because its consumers switch on `type`.
779
+ if (parsedMessage?.type === VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE) {
780
+ this.videoRecordingStartFailedReport(parsedMessage);
781
+ return;
782
+ }
775
783
  this.emit('message', parsedMessage);
776
784
  if (parsedMessage && 'type' in parsedMessage && 'status' in parsedMessage && parsedMessage.type === 'status-update' && parsedMessage.status === 'ended') {
777
785
  this.hasEmittedCallEndedStatus = true;
@@ -939,6 +947,37 @@ class Vapi extends VapiEventEmitter {
939
947
  console.error(`[vapi] ${event} event listener threw`, error);
940
948
  }
941
949
  }
950
+ /**
951
+ * Reported in a later task, like Daily's own recording-error: start() or
952
+ * reconnect() has resolved and the caller's await continuation has run, so a
953
+ * listener registered right after it still hears this, and one that calls
954
+ * stop() can't interrupt call setup. Skipped if that call was stopped or
955
+ * replaced.
956
+ */
957
+ recordingTokenMissingReportSchedule() {
958
+ const call = this.call;
959
+ if (!call)
960
+ return;
961
+ setTimeout(() => {
962
+ if (!this.started || this.call !== call)
963
+ return;
964
+ this.emitToConsumer('recording-error', {
965
+ action: 'recording-error',
966
+ callClientId: call.callClientId,
967
+ errorMsg: 'Vapi returned no recording token, so the recording was not started. Call startRecording() to try starting it from the client.',
968
+ });
969
+ }, 0);
970
+ }
971
+ /** The fields are read defensively: the app message is untrusted JSON. */
972
+ videoRecordingStartFailedReport(message) {
973
+ const outcome = typeof message.outcome === 'string' ? message.outcome : 'unknown';
974
+ const error = typeof message.error === 'string' ? message.error : 'unknown';
975
+ this.emitToConsumer('recording-error', {
976
+ action: 'recording-error',
977
+ callClientId: this.call?.callClientId ?? '',
978
+ errorMsg: `Vapi couldn't start the recording (${outcome}): ${error}`,
979
+ });
980
+ }
942
981
  /** Drops the reference before removing, so nothing hands out a detached element. */
943
982
  detachAudioPlayer(participantId) {
944
983
  if (this.audioPlayer?.dataset.participantId === participantId) {
@@ -1133,10 +1172,10 @@ class Vapi extends VapiEventEmitter {
1133
1172
  /**
1134
1173
  * Reconnects to an active call.
1135
1174
  *
1136
- *
1137
1175
  * @param webCall
1176
+ * @param options
1138
1177
  */
1139
- async reconnect(webCall) {
1178
+ async reconnect(webCall, options) {
1140
1179
  const startTime = Date.now();
1141
1180
  if (this.started) {
1142
1181
  throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.');
@@ -1144,6 +1183,9 @@ class Vapi extends VapiEventEmitter {
1144
1183
  if (!webCall.webCallUrl) {
1145
1184
  throw new Error('webCallUrl is required for reconnection.');
1146
1185
  }
1186
+ if (options?.clientSideNoiseCancellationEnabled !== undefined) {
1187
+ this.clientSideNoiseCancellationEnabled = options.clientSideNoiseCancellationEnabled;
1188
+ }
1147
1189
  this.emit('call-start-progress', {
1148
1190
  stage: 'reconnect-initialization',
1149
1191
  status: 'started',
@@ -1171,11 +1213,14 @@ class Vapi extends VapiEventEmitter {
1171
1213
  });
1172
1214
  }
1173
1215
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
1174
- const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
1175
- // Same auto-start token as in start(). Rejoining with it while the
1216
+ // Same token as in start(). In client mode, rejoining with it while the
1176
1217
  // recording is still running is safe; if the recording stopped when the
1177
- // user left, the token starts a new one.
1218
+ // user left, the token starts a new one. In server mode it starts
1219
+ // nothing. Without a token the SDK emits recording-error and starts
1220
+ // nothing.
1178
1221
  const callToken = webCall?.transport?.callToken;
1222
+ const isRecordingStartedByServer = webCall?.transport?.videoRecordingStartMode === 'server';
1223
+ const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
1179
1224
  // Stage 1: Create Daily call object
1180
1225
  this.emit('call-start-progress', {
1181
1226
  stage: 'daily-call-object-creation',
@@ -1185,7 +1230,6 @@ class Vapi extends VapiEventEmitter {
1185
1230
  audioSource: this.dailyCallObject.audioSource ?? true,
1186
1231
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
1187
1232
  isVideoRecordingEnabled,
1188
- isVideoEnabled
1189
1233
  }
1190
1234
  });
1191
1235
  const dailyCallStartTime = Date.now();
@@ -1267,7 +1311,7 @@ class Vapi extends VapiEventEmitter {
1267
1311
  this.call.on('participant-joined', (e) => {
1268
1312
  if (!e || !this.call)
1269
1313
  return;
1270
- subscribeToTracks(e, this.call, isVideoRecordingEnabled, isVideoEnabled);
1314
+ subscribeToTracks(e, this.call, isVideoRecordingEnabled);
1271
1315
  });
1272
1316
  this.call.on('participant-updated', (e) => {
1273
1317
  if (!e) {
@@ -1384,45 +1428,29 @@ class Vapi extends VapiEventEmitter {
1384
1428
  });
1385
1429
  // Stage 4: Video recording setup (if enabled)
1386
1430
  if (isVideoRecordingEnabled && callToken) {
1387
- // The meeting token auto-started the recording at join, so there is
1388
- // nothing to request from the client.
1431
+ // The meeting token auto-started the recording at join, or Vapi
1432
+ // starts it from its servers, so there is nothing to request from the
1433
+ // client.
1389
1434
  this.emit('call-start-progress', {
1390
1435
  stage: 'video-recording-setup',
1391
1436
  status: 'completed',
1392
1437
  timestamp: new Date().toISOString(),
1393
- metadata: { action: 'auto-started-via-meeting-token' }
1438
+ metadata: {
1439
+ action: isRecordingStartedByServer
1440
+ ? 'started-by-server'
1441
+ : 'auto-started-via-meeting-token'
1442
+ }
1394
1443
  });
1395
1444
  }
1396
1445
  else if (isVideoRecordingEnabled) {
1446
+ // No token: nothing starts the recording. The recording-error is
1447
+ // reported once reconnect() has finished.
1397
1448
  this.emit('call-start-progress', {
1398
1449
  stage: 'video-recording-setup',
1399
- status: 'started',
1400
- timestamp: new Date().toISOString()
1450
+ status: 'failed',
1451
+ timestamp: new Date().toISOString(),
1452
+ metadata: { action: 'no-meeting-token' }
1401
1453
  });
1402
- const recordingStartTime = Date.now();
1403
- try {
1404
- recordingRequestedTime = new Date().getTime();
1405
- this.startRecording();
1406
- const recordingSetupDuration = Date.now() - recordingStartTime;
1407
- this.emit('call-start-progress', {
1408
- stage: 'video-recording-setup',
1409
- status: 'completed',
1410
- duration: recordingSetupDuration,
1411
- timestamp: new Date().toISOString()
1412
- });
1413
- }
1414
- catch (error) {
1415
- const recordingSetupDuration = Date.now() - recordingStartTime;
1416
- const serializedError = serializeError(error);
1417
- this.emit('call-start-progress', {
1418
- stage: 'video-recording-setup',
1419
- status: 'failed',
1420
- duration: recordingSetupDuration,
1421
- timestamp: new Date().toISOString(),
1422
- metadata: { error: serializedError.message }
1423
- });
1424
- // Don't throw here, video recording is optional
1425
- }
1426
1454
  }
1427
1455
  else {
1428
1456
  this.emit('call-start-progress', {
@@ -1461,53 +1489,60 @@ class Vapi extends VapiEventEmitter {
1461
1489
  });
1462
1490
  // Don't throw here, this is non-critical
1463
1491
  }
1464
- // Stage 6: Audio processing setup
1465
- this.emit('call-start-progress', {
1466
- stage: 'audio-processing-setup',
1467
- status: 'started',
1468
- timestamp: new Date().toISOString()
1469
- });
1470
- const audioProcessingStartTime = Date.now();
1471
- try {
1472
- const audioProcessingUpdate = this.call.updateInputSettings({
1473
- audio: {
1474
- processor: {
1475
- type: 'noise-cancellation',
1476
- },
1477
- },
1478
- });
1479
- audioProcessingUpdate.catch((error) => {
1480
- this.emitAudioProcessingError('audio-processing-setup', error);
1481
- });
1482
- // The observer waits for this update rather than starting alongside the
1483
- // remote one above, so that Krisp has already swapped the microphone
1484
- // track. See maybeStartLocalAudioLevelObserver().
1485
- //
1486
- // A separate chain, not another link on the one above: EventEmitter
1487
- // rethrows out of emit('error') when a consumer registered no 'error'
1488
- // listener, and whether the observer starts must not hinge on that.
1489
- audioProcessingUpdate
1490
- .catch(() => { })
1491
- .then(() => this.maybeStartLocalAudioLevelObserver());
1492
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1492
+ if (this.clientSideNoiseCancellationEnabled) {
1493
+ // Stage 6: Audio processing setup
1493
1494
  this.emit('call-start-progress', {
1494
1495
  stage: 'audio-processing-setup',
1495
- status: 'completed',
1496
- duration: audioProcessingDuration,
1496
+ status: 'started',
1497
1497
  timestamp: new Date().toISOString()
1498
1498
  });
1499
+ const audioProcessingStartTime = Date.now();
1500
+ try {
1501
+ const audioProcessingUpdate = this.call.updateInputSettings({
1502
+ audio: {
1503
+ processor: {
1504
+ type: 'noise-cancellation',
1505
+ },
1506
+ },
1507
+ });
1508
+ audioProcessingUpdate.catch((error) => {
1509
+ this.emitAudioProcessingError('audio-processing-setup', error);
1510
+ });
1511
+ // The observer waits for this update rather than starting alongside the
1512
+ // remote one above, so that Krisp has already swapped the microphone
1513
+ // track. See maybeStartLocalAudioLevelObserver().
1514
+ //
1515
+ // A separate chain, not another link on the one above: EventEmitter
1516
+ // rethrows out of emit('error') when a consumer registered no 'error'
1517
+ // listener, and whether the observer starts must not hinge on that.
1518
+ audioProcessingUpdate
1519
+ .catch(() => { })
1520
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1521
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1522
+ this.emit('call-start-progress', {
1523
+ stage: 'audio-processing-setup',
1524
+ status: 'completed',
1525
+ duration: audioProcessingDuration,
1526
+ timestamp: new Date().toISOString()
1527
+ });
1528
+ }
1529
+ catch (error) {
1530
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1531
+ const serializedError = serializeError(error);
1532
+ this.emit('call-start-progress', {
1533
+ stage: 'audio-processing-setup',
1534
+ status: 'failed',
1535
+ duration: audioProcessingDuration,
1536
+ timestamp: new Date().toISOString(),
1537
+ metadata: { error: serializedError.message }
1538
+ });
1539
+ // Don't throw here, this is non-critical
1540
+ }
1499
1541
  }
1500
- catch (error) {
1501
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1502
- const serializedError = serializeError(error);
1503
- this.emit('call-start-progress', {
1504
- stage: 'audio-processing-setup',
1505
- status: 'failed',
1506
- duration: audioProcessingDuration,
1507
- timestamp: new Date().toISOString(),
1508
- metadata: { error: serializedError.message }
1509
- });
1510
- // Don't throw here, this is non-critical
1542
+ else {
1543
+ // Daily's default processor is 'none', so the microphone track is never
1544
+ // swapped and the observer has nothing to wait for.
1545
+ void this.maybeStartLocalAudioLevelObserver();
1511
1546
  }
1512
1547
  const totalDuration = Date.now() - startTime;
1513
1548
  this.emit('call-start-success', {
@@ -1516,6 +1551,9 @@ class Vapi extends VapiEventEmitter {
1516
1551
  timestamp: new Date().toISOString()
1517
1552
  });
1518
1553
  this.emit('call-start');
1554
+ if (recordingTokenMissing) {
1555
+ this.recordingTokenMissingReportSchedule();
1556
+ }
1519
1557
  }
1520
1558
  catch (e) {
1521
1559
  const totalDuration = Date.now() - startTime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",