@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/vapi.ts CHANGED
@@ -236,10 +236,18 @@ type WebCall = {
236
236
  artifactPlan?: { videoRecordingEnabled?: boolean };
237
237
  /**
238
238
  * The Vapi WebCall assistant. This is the assistant of the call.
239
- *
239
+ *
240
240
  * call.assistant
241
241
  */
242
242
  assistant?: { voice?: { provider?: string } };
243
+ /**
244
+ * The Vapi WebCall transport. When video recording is enabled, the server
245
+ * sets `callToken` to a Daily meeting token that auto-starts the cloud
246
+ * recording on join.
247
+ *
248
+ * call.transport
249
+ */
250
+ transport?: { callToken?: string };
243
251
  }
244
252
 
245
253
  async function startAudioPlayer(
@@ -415,6 +423,36 @@ export default class Vapi extends VapiEventEmitter {
415
423
  });
416
424
  }
417
425
 
426
+ /**
427
+ * Starts Daily's local (microphone) audio level observer.
428
+ *
429
+ * Only runs when something is listening for 'local-volume-level'. The observer
430
+ * costs an AudioContext plus an AudioWorklet for the lifetime of the call and
431
+ * nothing else in the SDK reads the level, so starting it unconditionally
432
+ * charges every consumer for a feature almost none of them use. Listeners
433
+ * attached after the call is under way should call the public
434
+ * `startLocalAudioLevelObserver()`.
435
+ *
436
+ * Must run after the noise-cancellation processor has settled. Krisp replaces
437
+ * the microphone track, Daily reacts to any local track change by closing the
438
+ * AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
439
+ * and Chrome and Firefox reject that load with "AbortError: Unable to load a
440
+ * worklet's module". Daily answers by stopping the observer for the rest of
441
+ * the call, so losing this race costs the feature, not just console noise.
442
+ * Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
443
+ */
444
+ private async maybeStartLocalAudioLevelObserver(): Promise<void> {
445
+ if (!this.call || this.listenerCount('local-volume-level') === 0) {
446
+ return;
447
+ }
448
+
449
+ try {
450
+ await this.call.startLocalAudioLevelObserver(100);
451
+ } catch (error) {
452
+ this.emit('local-audio-level-observer-error', serializeError(error));
453
+ }
454
+ }
455
+
418
456
  async start(
419
457
  assistant?: CreateAssistantDTO | string,
420
458
  assistantOverrides?: AssistantOverrides,
@@ -512,6 +550,14 @@ export default class Vapi extends VapiEventEmitter {
512
550
 
513
551
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
514
552
 
553
+ // When video recording is enabled, the server sends a Daily meeting
554
+ // token that auto-starts the cloud recording on join — more reliable
555
+ // than calling startRecording() from a weak client network. Older
556
+ // servers don't send one; then the client starts the recording itself.
557
+ const callToken = (
558
+ webCall?.transport as { callToken?: string } | undefined
559
+ )?.callToken;
560
+
515
561
  // Stage 2: Create Daily call object
516
562
  this.emit('call-start-progress', {
517
563
  stage: 'daily-call-object-creation',
@@ -573,6 +619,8 @@ export default class Vapi extends VapiEventEmitter {
573
619
  this.hasEmittedCallEndedStatus = true;
574
620
  }
575
621
  if (isVideoRecordingEnabled) {
622
+ // When using a token, the recording doesn't stop automatically on
623
+ // leave, so still stop it manually to preserve existing behavior.
576
624
  this.call?.stopRecording();
577
625
  }
578
626
  this.cleanup().catch(console.error);
@@ -651,6 +699,28 @@ export default class Vapi extends VapiEventEmitter {
651
699
  this.detachAudioPlayer(e.participant.session_id);
652
700
  });
653
701
 
702
+ // Registered before join(): a token-auto-started recording can begin
703
+ // while join() is still in flight, so a listener added after join
704
+ // could miss the event.
705
+ let recordingRequestedTime = 0;
706
+ if (isVideoRecordingEnabled) {
707
+ this.call.once('recording-started', () => {
708
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
709
+ this.emit('call-start-progress', {
710
+ stage: 'video-recording-started',
711
+ status: 'completed',
712
+ timestamp: new Date().toISOString(),
713
+ metadata: { delaySeconds: totalRecordingDelay }
714
+ });
715
+
716
+ this.send({
717
+ type: 'control',
718
+ control: 'say-first-message',
719
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
720
+ });
721
+ });
722
+ }
723
+
654
724
  // Stage 3: Mobile device handling and permissions
655
725
  const isMobile = this.isMobileDevice();
656
726
  this.emit('call-start-progress', {
@@ -688,11 +758,15 @@ export default class Vapi extends VapiEventEmitter {
688
758
  });
689
759
 
690
760
  const joinStartTime = Date.now();
691
-
761
+ recordingRequestedTime = joinStartTime;
762
+
692
763
  try {
693
764
  await this.call.join({
694
765
  // @ts-expect-error This exists
695
766
  url: webCall.webCallUrl,
767
+ // daily-js rejects a `token` key that is present but undefined, so
768
+ // only include it when the server actually sent one.
769
+ ...(callToken ? { token: callToken } : {}),
696
770
  subscribeToTracksAutomatically: false,
697
771
  });
698
772
 
@@ -724,25 +798,27 @@ export default class Vapi extends VapiEventEmitter {
724
798
  }
725
799
 
726
800
  // Stage 5: Video recording setup (if enabled)
727
- if (isVideoRecordingEnabled) {
801
+ if (isVideoRecordingEnabled && callToken) {
802
+ // The meeting token auto-started the recording at join, so there is
803
+ // nothing to request from the client.
804
+ this.emit('call-start-progress', {
805
+ stage: 'video-recording-setup',
806
+ status: 'completed',
807
+ timestamp: new Date().toISOString(),
808
+ metadata: { action: 'auto-started-via-meeting-token' }
809
+ });
810
+ } else if (isVideoRecordingEnabled) {
728
811
  this.emit('call-start-progress', {
729
812
  stage: 'video-recording-setup',
730
813
  status: 'started',
731
814
  timestamp: new Date().toISOString()
732
815
  });
733
-
734
- const recordingRequestedTime = new Date().getTime();
816
+
735
817
  const recordingStartTime = Date.now();
736
818
 
737
819
  try {
738
- this.call.startRecording({
739
- width: 1280,
740
- height: 720,
741
- backgroundColor: '#FF1F2D3D',
742
- layout: {
743
- preset: 'default',
744
- },
745
- });
820
+ recordingRequestedTime = new Date().getTime();
821
+ this.startRecording();
746
822
 
747
823
  const recordingSetupDuration = Date.now() - recordingStartTime;
748
824
  this.emit('call-start-progress', {
@@ -751,22 +827,6 @@ export default class Vapi extends VapiEventEmitter {
751
827
  duration: recordingSetupDuration,
752
828
  timestamp: new Date().toISOString()
753
829
  });
754
-
755
- this.call.on('recording-started', () => {
756
- const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
757
- this.emit('call-start-progress', {
758
- stage: 'video-recording-started',
759
- status: 'completed',
760
- timestamp: new Date().toISOString(),
761
- metadata: { delaySeconds: totalRecordingDelay }
762
- });
763
-
764
- this.send({
765
- type: 'control',
766
- control: 'say-first-message',
767
- videoRecordingStartDelaySeconds: totalRecordingDelay,
768
- });
769
- });
770
830
  } catch (error) {
771
831
  const recordingSetupDuration = Date.now() - recordingStartTime;
772
832
  const serializedError = serializeError(error);
@@ -805,7 +865,6 @@ export default class Vapi extends VapiEventEmitter {
805
865
 
806
866
  try {
807
867
  this.call.startRemoteParticipantsAudioLevelObserver(100);
808
- this.call.startLocalAudioLevelObserver(100);
809
868
  const audioObserverDuration = Date.now() - audioObserverStartTime;
810
869
  this.emit('call-start-progress', {
811
870
  stage: 'audio-observer-setup',
@@ -875,17 +934,28 @@ export default class Vapi extends VapiEventEmitter {
875
934
  const audioProcessingStartTime = Date.now();
876
935
 
877
936
  try {
878
- this.call
879
- .updateInputSettings({
880
- audio: {
881
- processor: {
882
- type: 'noise-cancellation',
883
- },
937
+ const audioProcessingUpdate = this.call.updateInputSettings({
938
+ audio: {
939
+ processor: {
940
+ type: 'noise-cancellation',
884
941
  },
885
- })
886
- .catch((error) => {
887
- this.emitAudioProcessingError('audio-processing-setup', error);
888
- });
942
+ },
943
+ });
944
+
945
+ audioProcessingUpdate.catch((error) => {
946
+ this.emitAudioProcessingError('audio-processing-setup', error);
947
+ });
948
+
949
+ // The observer waits for this update rather than starting alongside the
950
+ // remote one above, so that Krisp has already swapped the microphone
951
+ // track. See maybeStartLocalAudioLevelObserver().
952
+ //
953
+ // A separate chain, not another link on the one above: EventEmitter
954
+ // rethrows out of emit('error') when a consumer registered no 'error'
955
+ // listener, and whether the observer starts must not hinge on that.
956
+ audioProcessingUpdate
957
+ .catch(() => {})
958
+ .then(() => this.maybeStartLocalAudioLevelObserver());
889
959
 
890
960
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
891
961
  this.emit('call-start-progress', {
@@ -1350,6 +1420,37 @@ export default class Vapi extends VapiEventEmitter {
1350
1420
  this.call?.stopScreenShare();
1351
1421
  }
1352
1422
 
1423
+ /**
1424
+ * Starts (or retries) a cloud recording of the call.
1425
+ *
1426
+ * Recording start is asynchronous: success is signaled by the
1427
+ * 'recording-started' event and failure by 'recording-error'. Daily does not
1428
+ * retry automatically, so if you receive 'recording-error' (e.g. the
1429
+ * recording start timed out), you can call this method again a few seconds
1430
+ * later to retry.
1431
+ */
1432
+ public startRecording(): void {
1433
+ if (!this.call) {
1434
+ throw new Error('Call object is not available.');
1435
+ }
1436
+ this.call.startRecording({
1437
+ width: 1280,
1438
+ height: 720,
1439
+ backgroundColor: '#FF1F2D3D',
1440
+ layout: {
1441
+ preset: 'default',
1442
+ },
1443
+ });
1444
+ }
1445
+
1446
+ /**
1447
+ * Stops the in-progress recording. Completion is signaled by the
1448
+ * 'recording-stopped' event.
1449
+ */
1450
+ public stopRecording(): void {
1451
+ this.call?.stopRecording();
1452
+ }
1453
+
1353
1454
  /**
1354
1455
  * Reconnects to an active call.
1355
1456
  *
@@ -1399,6 +1500,11 @@ export default class Vapi extends VapiEventEmitter {
1399
1500
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
1400
1501
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
1401
1502
 
1503
+ // Same auto-start token as in start(). Rejoining with it while the
1504
+ // recording is still running is safe; if the recording stopped when the
1505
+ // user left, the token starts a new one.
1506
+ const callToken = webCall?.transport?.callToken;
1507
+
1402
1508
  // Stage 1: Create Daily call object
1403
1509
  this.emit('call-start-progress', {
1404
1510
  stage: 'daily-call-object-creation',
@@ -1441,6 +1547,8 @@ export default class Vapi extends VapiEventEmitter {
1441
1547
  this.hasEmittedCallEndedStatus = true;
1442
1548
  }
1443
1549
  if (isVideoRecordingEnabled) {
1550
+ // When using a token, the recording doesn't stop automatically on
1551
+ // leave, so still stop it manually to preserve existing behavior.
1444
1552
  this.call?.stopRecording();
1445
1553
  }
1446
1554
  this.cleanup().catch(console.error);
@@ -1552,6 +1660,28 @@ export default class Vapi extends VapiEventEmitter {
1552
1660
  }
1553
1661
  });
1554
1662
 
1663
+ // Registered before join(): a token-auto-started recording can begin
1664
+ // while join() is still in flight, so a listener added after join
1665
+ // could miss the event.
1666
+ let recordingRequestedTime = 0;
1667
+ if (isVideoRecordingEnabled) {
1668
+ this.call.once('recording-started', () => {
1669
+ const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
1670
+ this.emit('call-start-progress', {
1671
+ stage: 'video-recording-started',
1672
+ status: 'completed',
1673
+ timestamp: new Date().toISOString(),
1674
+ metadata: { delaySeconds: totalRecordingDelay }
1675
+ });
1676
+
1677
+ this.send({
1678
+ type: 'control',
1679
+ control: 'say-first-message',
1680
+ videoRecordingStartDelaySeconds: totalRecordingDelay,
1681
+ });
1682
+ });
1683
+ }
1684
+
1555
1685
  // Stage 2: Mobile device handling and permissions
1556
1686
  const isMobile = this.isMobileDevice();
1557
1687
  this.emit('call-start-progress', {
@@ -1589,8 +1719,12 @@ export default class Vapi extends VapiEventEmitter {
1589
1719
  });
1590
1720
 
1591
1721
  const joinStartTime = Date.now();
1722
+ recordingRequestedTime = joinStartTime;
1592
1723
  await this.call.join({
1593
1724
  url: webCall.webCallUrl,
1725
+ // daily-js rejects a `token` key that is present but undefined, so
1726
+ // only include it when the server actually sent one.
1727
+ ...(callToken ? { token: callToken } : {}),
1594
1728
  subscribeToTracksAutomatically: false,
1595
1729
  });
1596
1730
 
@@ -1603,25 +1737,27 @@ export default class Vapi extends VapiEventEmitter {
1603
1737
  });
1604
1738
 
1605
1739
  // Stage 4: Video recording setup (if enabled)
1606
- if (isVideoRecordingEnabled) {
1740
+ if (isVideoRecordingEnabled && callToken) {
1741
+ // The meeting token auto-started the recording at join, so there is
1742
+ // nothing to request from the client.
1743
+ this.emit('call-start-progress', {
1744
+ stage: 'video-recording-setup',
1745
+ status: 'completed',
1746
+ timestamp: new Date().toISOString(),
1747
+ metadata: { action: 'auto-started-via-meeting-token' }
1748
+ });
1749
+ } else if (isVideoRecordingEnabled) {
1607
1750
  this.emit('call-start-progress', {
1608
1751
  stage: 'video-recording-setup',
1609
1752
  status: 'started',
1610
1753
  timestamp: new Date().toISOString()
1611
1754
  });
1612
-
1755
+
1613
1756
  const recordingStartTime = Date.now();
1614
- const recordingRequestedTime = new Date().getTime();
1615
1757
 
1616
1758
  try {
1617
- this.call.startRecording({
1618
- width: 1280,
1619
- height: 720,
1620
- backgroundColor: '#FF1F2D3D',
1621
- layout: {
1622
- preset: 'default',
1623
- },
1624
- });
1759
+ recordingRequestedTime = new Date().getTime();
1760
+ this.startRecording();
1625
1761
 
1626
1762
  const recordingSetupDuration = Date.now() - recordingStartTime;
1627
1763
  this.emit('call-start-progress', {
@@ -1630,22 +1766,6 @@ export default class Vapi extends VapiEventEmitter {
1630
1766
  duration: recordingSetupDuration,
1631
1767
  timestamp: new Date().toISOString()
1632
1768
  });
1633
-
1634
- this.call.on('recording-started', () => {
1635
- const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
1636
- this.emit('call-start-progress', {
1637
- stage: 'video-recording-started',
1638
- status: 'completed',
1639
- timestamp: new Date().toISOString(),
1640
- metadata: { delaySeconds: totalRecordingDelay }
1641
- });
1642
-
1643
- this.send({
1644
- type: 'control',
1645
- control: 'say-first-message',
1646
- videoRecordingStartDelaySeconds: totalRecordingDelay,
1647
- });
1648
- });
1649
1769
  } catch (error) {
1650
1770
  const recordingSetupDuration = Date.now() - recordingStartTime;
1651
1771
  const serializedError = serializeError(error);
@@ -1678,7 +1798,6 @@ export default class Vapi extends VapiEventEmitter {
1678
1798
 
1679
1799
  try {
1680
1800
  this.call.startRemoteParticipantsAudioLevelObserver(100);
1681
- this.call.startLocalAudioLevelObserver(100);
1682
1801
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1683
1802
  this.emit('call-start-progress', {
1684
1803
  stage: 'audio-observer-setup',
@@ -1709,17 +1828,28 @@ export default class Vapi extends VapiEventEmitter {
1709
1828
  const audioProcessingStartTime = Date.now();
1710
1829
 
1711
1830
  try {
1712
- this.call
1713
- .updateInputSettings({
1714
- audio: {
1715
- processor: {
1716
- type: 'noise-cancellation',
1717
- },
1831
+ const audioProcessingUpdate = this.call.updateInputSettings({
1832
+ audio: {
1833
+ processor: {
1834
+ type: 'noise-cancellation',
1718
1835
  },
1719
- })
1720
- .catch((error) => {
1721
- this.emitAudioProcessingError('audio-processing-setup', error);
1722
- });
1836
+ },
1837
+ });
1838
+
1839
+ audioProcessingUpdate.catch((error) => {
1840
+ this.emitAudioProcessingError('audio-processing-setup', error);
1841
+ });
1842
+
1843
+ // The observer waits for this update rather than starting alongside the
1844
+ // remote one above, so that Krisp has already swapped the microphone
1845
+ // track. See maybeStartLocalAudioLevelObserver().
1846
+ //
1847
+ // A separate chain, not another link on the one above: EventEmitter
1848
+ // rethrows out of emit('error') when a consumer registered no 'error'
1849
+ // listener, and whether the observer starts must not hinge on that.
1850
+ audioProcessingUpdate
1851
+ .catch(() => {})
1852
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1723
1853
 
1724
1854
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1725
1855
  this.emit('call-start-progress', {