@vapi-ai/web 2.5.1 → 2.6.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/vapi.ts CHANGED
@@ -5,7 +5,12 @@ import DailyIframe, {
5
5
  DailyAdvancedConfig,
6
6
  DailyFactoryOptions,
7
7
  DailyEventObjectAppMessage,
8
+ DailyEventObjectLocalAudioLevel,
9
+ DailyEventObjectNoPayload,
8
10
  DailyEventObjectParticipant,
11
+ DailyEventObjectRecordingError,
12
+ DailyEventObjectRecordingStarted,
13
+ DailyEventObjectRecordingStopped,
9
14
  DailyEventObjectRemoteParticipantsAudioLevel,
10
15
  DailyParticipant,
11
16
  DailyVideoSendSettings,
@@ -62,6 +67,8 @@ type VapiEventNames =
62
67
  | 'call-end'
63
68
  | 'call-start'
64
69
  | 'volume-level'
70
+ | 'local-volume-level'
71
+ | 'local-audio-level-observer-error'
65
72
  | 'speech-start'
66
73
  | 'speech-end'
67
74
  | 'message'
@@ -73,7 +80,12 @@ type VapiEventNames =
73
80
  | 'daily-participant-updated'
74
81
  | 'call-start-progress'
75
82
  | 'call-start-success'
76
- | 'call-start-failed';
83
+ | 'call-start-failed'
84
+ | 'recording-started'
85
+ | 'recording-stopped'
86
+ | 'recording-stats'
87
+ | 'recording-error'
88
+ | 'recording-upload-completed';
77
89
 
78
90
  interface CallStartProgressEvent {
79
91
  stage: string;
@@ -98,10 +110,79 @@ interface CallStartFailedEvent {
98
110
  context: Record<string, any>;
99
111
  }
100
112
 
113
+ interface SerializedError {
114
+ message: string;
115
+ name?: string;
116
+ stack?: string;
117
+ code?: string | number;
118
+ cause?: string;
119
+ [key: string]: any;
120
+ }
121
+
122
+ /**
123
+ * Extracts error details into a plain object that serializes properly to JSON.
124
+ * Error objects don't serialize well because their properties (message, stack, name)
125
+ * are not enumerable and get lost when using JSON.stringify().
126
+ */
127
+ function serializeError(error: unknown): SerializedError {
128
+ if (error === null || error === undefined) {
129
+ return { message: 'Unknown error (null or undefined)' };
130
+ }
131
+
132
+ if (error instanceof Error) {
133
+ const serialized: SerializedError = {
134
+ message: error.message,
135
+ name: error.name,
136
+ stack: error.stack,
137
+ };
138
+
139
+ // Include any additional properties that might be on the error
140
+ const errorAsAny = error as any;
141
+ if (errorAsAny.code !== undefined) {
142
+ serialized.code = errorAsAny.code;
143
+ }
144
+ if (errorAsAny.cause !== undefined) {
145
+ serialized.cause = String(errorAsAny.cause);
146
+ }
147
+ if (errorAsAny.reason !== undefined) {
148
+ serialized.reason = errorAsAny.reason;
149
+ }
150
+ if (errorAsAny.details !== undefined) {
151
+ serialized.details = errorAsAny.details;
152
+ }
153
+ // Daily.co specific error properties
154
+ if (errorAsAny.errorMsg !== undefined) {
155
+ serialized.errorMsg = errorAsAny.errorMsg;
156
+ }
157
+ if (errorAsAny.error !== undefined && typeof errorAsAny.error === 'string') {
158
+ serialized.errorDetail = errorAsAny.error;
159
+ }
160
+
161
+ return serialized;
162
+ }
163
+
164
+ if (typeof error === 'string') {
165
+ return { message: error };
166
+ }
167
+
168
+ if (typeof error === 'object') {
169
+ // It's already a plain object, but let's ensure message exists
170
+ const errorObj = error as Record<string, any>;
171
+ return {
172
+ message: errorObj.message || errorObj.error || JSON.stringify(error),
173
+ ...errorObj
174
+ };
175
+ }
176
+
177
+ return { message: String(error) };
178
+ }
179
+
101
180
  type VapiEventListeners = {
102
181
  'call-end': () => void;
103
182
  'call-start': () => void;
104
183
  'volume-level': (volume: number) => void;
184
+ 'local-volume-level': (volume: number) => void;
185
+ 'local-audio-level-observer-error': (error: any) => void;
105
186
  'speech-start': () => void;
106
187
  'speech-end': () => void;
107
188
  video: (track: MediaStreamTrack) => void;
@@ -114,6 +195,11 @@ type VapiEventListeners = {
114
195
  'call-start-progress': (event: CallStartProgressEvent) => void;
115
196
  'call-start-success': (event: CallStartSuccessEvent) => void;
116
197
  'call-start-failed': (event: CallStartFailedEvent) => void;
198
+ 'recording-started': (event: DailyEventObjectRecordingStarted) => void;
199
+ 'recording-stopped': (event: DailyEventObjectRecordingStopped) => void;
200
+ 'recording-stats': (event: DailyEventObjectNoPayload) => void;
201
+ 'recording-error': (event: DailyEventObjectRecordingError) => void;
202
+ 'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
117
203
  };
118
204
 
119
205
  type StartCallOptions = {
@@ -298,7 +384,7 @@ export default class Vapi extends VapiEventEmitter {
298
384
  this.emit('error', {
299
385
  type: 'validation-error',
300
386
  stage: 'input-validation',
301
- message: error.message,
387
+ error: serializeError(error),
302
388
  timestamp: new Date().toISOString()
303
389
  });
304
390
  throw error;
@@ -410,17 +496,18 @@ export default class Vapi extends VapiEventEmitter {
410
496
  });
411
497
  } catch (error) {
412
498
  const dailyCallDuration = Date.now() - dailyCallStartTime;
499
+ const serializedError = serializeError(error);
413
500
  this.emit('call-start-progress', {
414
501
  stage: 'daily-call-object-creation',
415
502
  status: 'failed',
416
503
  duration: dailyCallDuration,
417
504
  timestamp: new Date().toISOString(),
418
- metadata: { error: error?.toString() }
505
+ metadata: { error: serializedError.message }
419
506
  });
420
507
  this.emit('error', {
421
508
  type: 'daily-call-object-creation-error',
422
509
  stage: 'daily-call-object-creation',
423
- error,
510
+ error: serializedError,
424
511
  timestamp: new Date().toISOString()
425
512
  });
426
513
  throw error;
@@ -445,14 +532,22 @@ export default class Vapi extends VapiEventEmitter {
445
532
  });
446
533
 
447
534
  this.call.on('error', (error: any) => {
448
- this.emit('error', error);
535
+ this.emit('error', {
536
+ type: 'daily-error',
537
+ error: serializeError(error),
538
+ timestamp: new Date().toISOString()
539
+ });
449
540
  if (isVideoRecordingEnabled) {
450
541
  this.call?.stopRecording();
451
542
  }
452
543
  });
453
544
 
454
545
  this.call.on('camera-error', (error: any) => {
455
- this.emit('camera-error', error);
546
+ this.emit('camera-error', {
547
+ type: 'camera-error',
548
+ error: serializeError(error),
549
+ timestamp: new Date().toISOString()
550
+ });
456
551
  });
457
552
 
458
553
  this.call.on('network-quality-change', (event: any) => {
@@ -463,6 +558,26 @@ export default class Vapi extends VapiEventEmitter {
463
558
  this.emit('network-connection', event);
464
559
  });
465
560
 
561
+ this.call.on('recording-started', (event) => {
562
+ if (event) this.emit('recording-started', event);
563
+ });
564
+
565
+ this.call.on('recording-stopped', (event) => {
566
+ if (event) this.emit('recording-stopped', event);
567
+ });
568
+
569
+ this.call.on('recording-stats', (event) => {
570
+ if (event) this.emit('recording-stats', event);
571
+ });
572
+
573
+ this.call.on('recording-error', (event) => {
574
+ if (event) this.emit('recording-error', event);
575
+ });
576
+
577
+ this.call.on('recording-upload-completed', (event) => {
578
+ if (event) this.emit('recording-upload-completed', event);
579
+ });
580
+
466
581
  this.call.on('track-started', async (e) => {
467
582
  if (!e || !e.participant) {
468
583
  return;
@@ -560,17 +675,18 @@ export default class Vapi extends VapiEventEmitter {
560
675
  });
561
676
  } catch (error) {
562
677
  const joinDuration = Date.now() - joinStartTime;
678
+ const serializedError = serializeError(error);
563
679
  this.emit('call-start-progress', {
564
680
  stage: 'daily-call-join',
565
681
  status: 'failed',
566
682
  duration: joinDuration,
567
683
  timestamp: new Date().toISOString(),
568
- metadata: { error: error?.toString() }
684
+ metadata: { error: serializedError.message }
569
685
  });
570
686
  this.emit('error', {
571
687
  type: 'daily-call-join-error',
572
688
  stage: 'daily-call-join',
573
- error,
689
+ error: serializedError,
574
690
  duration: joinDuration,
575
691
  timestamp: new Date().toISOString()
576
692
  });
@@ -623,17 +739,18 @@ export default class Vapi extends VapiEventEmitter {
623
739
  });
624
740
  } catch (error) {
625
741
  const recordingSetupDuration = Date.now() - recordingStartTime;
742
+ const serializedError = serializeError(error);
626
743
  this.emit('call-start-progress', {
627
744
  stage: 'video-recording-setup',
628
745
  status: 'failed',
629
746
  duration: recordingSetupDuration,
630
747
  timestamp: new Date().toISOString(),
631
- metadata: { error: error?.toString() }
748
+ metadata: { error: serializedError.message }
632
749
  });
633
750
  this.emit('error', {
634
751
  type: 'video-recording-setup-error',
635
752
  stage: 'video-recording-setup',
636
- error,
753
+ error: serializedError,
637
754
  timestamp: new Date().toISOString()
638
755
  });
639
756
  // Don't throw here, video recording is optional
@@ -658,6 +775,7 @@ export default class Vapi extends VapiEventEmitter {
658
775
 
659
776
  try {
660
777
  this.call.startRemoteParticipantsAudioLevelObserver(100);
778
+ this.call.startLocalAudioLevelObserver(100);
661
779
  const audioObserverDuration = Date.now() - audioObserverStartTime;
662
780
  this.emit('call-start-progress', {
663
781
  stage: 'audio-observer-setup',
@@ -667,17 +785,18 @@ export default class Vapi extends VapiEventEmitter {
667
785
  });
668
786
  } catch (error) {
669
787
  const audioObserverDuration = Date.now() - audioObserverStartTime;
788
+ const serializedError = serializeError(error);
670
789
  this.emit('call-start-progress', {
671
790
  stage: 'audio-observer-setup',
672
791
  status: 'failed',
673
792
  duration: audioObserverDuration,
674
793
  timestamp: new Date().toISOString(),
675
- metadata: { error: error?.toString() }
794
+ metadata: { error: serializedError.message }
676
795
  });
677
796
  this.emit('error', {
678
797
  type: 'audio-observer-setup-error',
679
798
  stage: 'audio-observer-setup',
680
- error,
799
+ error: serializedError,
681
800
  timestamp: new Date().toISOString()
682
801
  });
683
802
  // Don't throw here, this is non-critical
@@ -687,10 +806,17 @@ export default class Vapi extends VapiEventEmitter {
687
806
  if (e) this.handleRemoteParticipantsAudioLevel(e);
688
807
  });
689
808
 
809
+ this.call.on('local-audio-level', (e) => {
810
+ if (e) this.handleLocalAudioLevel(e);
811
+ });
812
+
690
813
  this.call.on('app-message', (e) => this.onAppMessage(e));
691
814
 
692
815
  this.call.on('nonfatal-error', (e) => {
693
816
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
817
+ if (e?.type === 'local-audio-level-observer-error') {
818
+ this.emit('local-audio-level-observer-error', serializeError(e));
819
+ }
694
820
  if (e?.type === 'audio-processor-error') {
695
821
  this.call
696
822
  ?.updateInputSettings({
@@ -733,17 +859,18 @@ export default class Vapi extends VapiEventEmitter {
733
859
  });
734
860
  } catch (error) {
735
861
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
862
+ const serializedError = serializeError(error);
736
863
  this.emit('call-start-progress', {
737
864
  stage: 'audio-processing-setup',
738
865
  status: 'failed',
739
866
  duration: audioProcessingDuration,
740
867
  timestamp: new Date().toISOString(),
741
- metadata: { error: error?.toString() }
868
+ metadata: { error: serializedError.message }
742
869
  });
743
870
  this.emit('error', {
744
871
  type: 'audio-processing-setup-error',
745
872
  stage: 'audio-processing-setup',
746
- error,
873
+ error: serializedError,
747
874
  timestamp: new Date().toISOString()
748
875
  });
749
876
  // Don't throw here, this is non-critical
@@ -759,12 +886,13 @@ export default class Vapi extends VapiEventEmitter {
759
886
  return webCall;
760
887
  } catch (e) {
761
888
  const totalDuration = Date.now() - startTime;
889
+ const serializedError = serializeError(e);
762
890
 
763
891
  this.emit('call-start-failed', {
764
892
  stage: 'unknown',
765
893
  totalDuration,
766
- error: e?.toString() || 'Unknown error occurred',
767
- errorStack: e instanceof Error ? e.stack : 'No stack trace available',
894
+ error: serializedError.message,
895
+ errorStack: serializedError.stack || 'No stack trace available',
768
896
  timestamp: new Date().toISOString(),
769
897
  context: {
770
898
  hasAssistant: !!assistant,
@@ -778,7 +906,7 @@ export default class Vapi extends VapiEventEmitter {
778
906
  this.emit('error', {
779
907
  type: 'start-method-error',
780
908
  stage: 'unknown',
781
- error: e,
909
+ error: serializedError,
782
910
  totalDuration,
783
911
  timestamp: new Date().toISOString(),
784
912
  context: {
@@ -846,6 +974,10 @@ export default class Vapi extends VapiEventEmitter {
846
974
  }, 1000);
847
975
  }
848
976
 
977
+ private handleLocalAudioLevel(e: DailyEventObjectLocalAudioLevel) {
978
+ this.emit('local-volume-level', e.audioLevel);
979
+ }
980
+
849
981
  /**
850
982
  * Stops the call by destroying the Daily call object.
851
983
  *
@@ -946,6 +1078,57 @@ export default class Vapi extends VapiEventEmitter {
946
1078
  return this.call;
947
1079
  }
948
1080
 
1081
+ /**
1082
+ * Returns the most recent local (microphone) audio level, a number between 0 and 1.
1083
+ *
1084
+ * Useful for detecting when the local participant's microphone is not picking up any
1085
+ * audio (e.g. wrong input device selected, OS-level input volume at 0, or muted
1086
+ * hardware) so you can surface a "we can't hear you" style warning in your UI.
1087
+ *
1088
+ * Returns 0 when there is no active call.
1089
+ */
1090
+ public getLocalAudioLevel(): number {
1091
+ if (!this.call) {
1092
+ return 0;
1093
+ }
1094
+ return this.call.getLocalAudioLevel();
1095
+ }
1096
+
1097
+ /**
1098
+ * Starts (or restarts) the local audio level observer. The observer is started
1099
+ * automatically when a call begins, but this can be used to change the sampling
1100
+ * interval or restart it if it was stopped.
1101
+ *
1102
+ * While running, the Vapi instance emits 'local-volume-level' events with the local
1103
+ * microphone audio level (0 to 1).
1104
+ *
1105
+ * @param interval How often, in milliseconds, to emit audio level events. Defaults to Daily's default.
1106
+ */
1107
+ public async startLocalAudioLevelObserver(interval?: number): Promise<void> {
1108
+ if (!this.call) {
1109
+ throw new Error('Call object is not available.');
1110
+ }
1111
+ await this.call.startLocalAudioLevelObserver(interval);
1112
+ }
1113
+
1114
+ /**
1115
+ * Stops the local audio level observer. After calling this, 'local-volume-level'
1116
+ * events will no longer be emitted until the observer is started again.
1117
+ */
1118
+ public stopLocalAudioLevelObserver(): void {
1119
+ this.call?.stopLocalAudioLevelObserver();
1120
+ }
1121
+
1122
+ /**
1123
+ * Returns whether the local audio level observer is currently running.
1124
+ */
1125
+ public isLocalAudioLevelObserverRunning(): boolean {
1126
+ if (!this.call) {
1127
+ return false;
1128
+ }
1129
+ return this.call.isLocalAudioLevelObserverRunning();
1130
+ }
1131
+
949
1132
  public startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings) {
950
1133
  this.call?.startScreenShare({
951
1134
  displayMediaOptions,
@@ -1054,14 +1237,22 @@ export default class Vapi extends VapiEventEmitter {
1054
1237
  });
1055
1238
 
1056
1239
  this.call.on('error', (error: any) => {
1057
- this.emit('error', error);
1240
+ this.emit('error', {
1241
+ type: 'daily-error',
1242
+ error: serializeError(error),
1243
+ timestamp: new Date().toISOString()
1244
+ });
1058
1245
  if (isVideoRecordingEnabled) {
1059
1246
  this.call?.stopRecording();
1060
1247
  }
1061
1248
  });
1062
1249
 
1063
1250
  this.call.on('camera-error', (error: any) => {
1064
- this.emit('camera-error', error);
1251
+ this.emit('camera-error', {
1252
+ type: 'camera-error',
1253
+ error: serializeError(error),
1254
+ timestamp: new Date().toISOString()
1255
+ });
1065
1256
  });
1066
1257
 
1067
1258
  this.call.on('network-quality-change', (event: any) => {
@@ -1072,6 +1263,26 @@ export default class Vapi extends VapiEventEmitter {
1072
1263
  this.emit('network-connection', event);
1073
1264
  });
1074
1265
 
1266
+ this.call.on('recording-started', (event) => {
1267
+ if (event) this.emit('recording-started', event);
1268
+ });
1269
+
1270
+ this.call.on('recording-stopped', (event) => {
1271
+ if (event) this.emit('recording-stopped', event);
1272
+ });
1273
+
1274
+ this.call.on('recording-stats', (event) => {
1275
+ if (event) this.emit('recording-stats', event);
1276
+ });
1277
+
1278
+ this.call.on('recording-error', (event) => {
1279
+ if (event) this.emit('recording-error', event);
1280
+ });
1281
+
1282
+ this.call.on('recording-upload-completed', (event) => {
1283
+ if (event) this.emit('recording-upload-completed', event);
1284
+ });
1285
+
1075
1286
  this.call.on('track-started', async (e) => {
1076
1287
  if (!e || !e.participant) {
1077
1288
  return;
@@ -1119,10 +1330,17 @@ export default class Vapi extends VapiEventEmitter {
1119
1330
  if (e) this.handleRemoteParticipantsAudioLevel(e);
1120
1331
  });
1121
1332
 
1333
+ this.call.on('local-audio-level', (e) => {
1334
+ if (e) this.handleLocalAudioLevel(e);
1335
+ });
1336
+
1122
1337
  this.call.on('app-message', (e) => this.onAppMessage(e));
1123
1338
 
1124
1339
  this.call.on('nonfatal-error', (e) => {
1125
1340
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
1341
+ if (e?.type === 'local-audio-level-observer-error') {
1342
+ this.emit('local-audio-level-observer-error', serializeError(e));
1343
+ }
1126
1344
  if (e?.type === 'audio-processor-error') {
1127
1345
  this.call
1128
1346
  ?.updateInputSettings({
@@ -1234,12 +1452,13 @@ export default class Vapi extends VapiEventEmitter {
1234
1452
  });
1235
1453
  } catch (error) {
1236
1454
  const recordingSetupDuration = Date.now() - recordingStartTime;
1455
+ const serializedError = serializeError(error);
1237
1456
  this.emit('call-start-progress', {
1238
1457
  stage: 'video-recording-setup',
1239
1458
  status: 'failed',
1240
1459
  duration: recordingSetupDuration,
1241
1460
  timestamp: new Date().toISOString(),
1242
- metadata: { error: error?.toString() }
1461
+ metadata: { error: serializedError.message }
1243
1462
  });
1244
1463
  // Don't throw here, video recording is optional
1245
1464
  }
@@ -1263,6 +1482,7 @@ export default class Vapi extends VapiEventEmitter {
1263
1482
 
1264
1483
  try {
1265
1484
  this.call.startRemoteParticipantsAudioLevelObserver(100);
1485
+ this.call.startLocalAudioLevelObserver(100);
1266
1486
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1267
1487
  this.emit('call-start-progress', {
1268
1488
  stage: 'audio-observer-setup',
@@ -1272,12 +1492,13 @@ export default class Vapi extends VapiEventEmitter {
1272
1492
  });
1273
1493
  } catch (error) {
1274
1494
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1495
+ const serializedError = serializeError(error);
1275
1496
  this.emit('call-start-progress', {
1276
1497
  stage: 'audio-observer-setup',
1277
1498
  status: 'failed',
1278
1499
  duration: audioObserverDuration,
1279
1500
  timestamp: new Date().toISOString(),
1280
- metadata: { error: error?.toString() }
1501
+ metadata: { error: serializedError.message }
1281
1502
  });
1282
1503
  // Don't throw here, this is non-critical
1283
1504
  }
@@ -1309,12 +1530,13 @@ export default class Vapi extends VapiEventEmitter {
1309
1530
  });
1310
1531
  } catch (error) {
1311
1532
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1533
+ const serializedError = serializeError(error);
1312
1534
  this.emit('call-start-progress', {
1313
1535
  stage: 'audio-processing-setup',
1314
1536
  status: 'failed',
1315
1537
  duration: audioProcessingDuration,
1316
1538
  timestamp: new Date().toISOString(),
1317
- metadata: { error: error?.toString() }
1539
+ metadata: { error: serializedError.message }
1318
1540
  });
1319
1541
  // Don't throw here, this is non-critical
1320
1542
  }
@@ -1330,12 +1552,13 @@ export default class Vapi extends VapiEventEmitter {
1330
1552
 
1331
1553
  } catch (e) {
1332
1554
  const totalDuration = Date.now() - startTime;
1555
+ const serializedError = serializeError(e);
1333
1556
 
1334
1557
  this.emit('call-start-failed', {
1335
1558
  stage: 'reconnect',
1336
1559
  totalDuration,
1337
- error: e?.toString() || 'Unknown error occurred',
1338
- errorStack: e instanceof Error ? e.stack : 'No stack trace available',
1560
+ error: serializedError.message,
1561
+ errorStack: serializedError.stack || 'No stack trace available',
1339
1562
  timestamp: new Date().toISOString(),
1340
1563
  context: {
1341
1564
  isReconnect: true,
@@ -1349,7 +1572,7 @@ export default class Vapi extends VapiEventEmitter {
1349
1572
  // Also emit the generic error event for backward compatibility
1350
1573
  this.emit('error', {
1351
1574
  type: 'reconnect-error',
1352
- error: e,
1575
+ error: serializedError,
1353
1576
  totalDuration,
1354
1577
  timestamp: new Date().toISOString(),
1355
1578
  context: {