@vapi-ai/web 2.5.2 → 2.6.2

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ChatCompletionMessageParam } from 'openai/resources';
2
- import { DailyCall, DailyAdvancedConfig, DailyFactoryOptions, DailyParticipant, DailyVideoSendSettings } from '@daily-co/daily-js';
2
+ import { DailyCall, DailyAdvancedConfig, DailyFactoryOptions, DailyEventObjectNoPayload, DailyEventObjectRecordingError, DailyEventObjectRecordingStarted, DailyEventObjectRecordingStopped, DailyParticipant, DailyVideoSendSettings } from '@daily-co/daily-js';
3
3
  import EventEmitter from 'events';
4
4
  import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWorkflowDTO, WorkflowOverrides } from './api';
5
5
  export interface EndCallMessage {
@@ -23,7 +23,7 @@ export interface SayMessage {
23
23
  interruptAssistantEnabled?: boolean;
24
24
  }
25
25
  type VapiClientToServerMessage = AddMessageMessage | ControlMessages | SayMessage | EndCallMessage;
26
- type VapiEventNames = 'call-end' | 'call-start' | 'volume-level' | 'speech-start' | 'speech-end' | 'message' | 'video' | 'error' | 'camera-error' | 'network-quality-change' | 'network-connection' | 'daily-participant-updated' | 'call-start-progress' | 'call-start-success' | 'call-start-failed';
26
+ type VapiEventNames = 'call-end' | 'call-start' | 'volume-level' | 'local-volume-level' | 'local-audio-level-observer-error' | 'speech-start' | 'speech-end' | 'message' | 'video' | 'error' | 'camera-error' | 'network-quality-change' | 'network-connection' | 'daily-participant-updated' | 'call-start-progress' | 'call-start-success' | 'call-start-failed' | 'recording-started' | 'recording-stopped' | 'recording-stats' | 'recording-error' | 'recording-upload-completed';
27
27
  interface CallStartProgressEvent {
28
28
  stage: string;
29
29
  status: 'started' | 'completed' | 'failed';
@@ -48,6 +48,8 @@ type VapiEventListeners = {
48
48
  'call-end': () => void;
49
49
  'call-start': () => void;
50
50
  'volume-level': (volume: number) => void;
51
+ 'local-volume-level': (volume: number) => void;
52
+ 'local-audio-level-observer-error': (error: any) => void;
51
53
  'speech-start': () => void;
52
54
  'speech-end': () => void;
53
55
  video: (track: MediaStreamTrack) => void;
@@ -60,6 +62,11 @@ type VapiEventListeners = {
60
62
  'call-start-progress': (event: CallStartProgressEvent) => void;
61
63
  'call-start-success': (event: CallStartSuccessEvent) => void;
62
64
  'call-start-failed': (event: CallStartFailedEvent) => void;
65
+ 'recording-started': (event: DailyEventObjectRecordingStarted) => void;
66
+ 'recording-stopped': (event: DailyEventObjectRecordingStopped) => void;
67
+ 'recording-stats': (event: DailyEventObjectNoPayload) => void;
68
+ 'recording-error': (event: DailyEventObjectRecordingError) => void;
69
+ 'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
63
70
  };
64
71
  type StartCallOptions = {
65
72
  /**
@@ -120,9 +127,21 @@ export default class Vapi extends VapiEventEmitter {
120
127
  private cleanup;
121
128
  private isMobileDevice;
122
129
  private sleep;
130
+ /**
131
+ * Reports a failed audio processing update on the SDK's `error` event.
132
+ *
133
+ * Daily applies input settings inside its remotely loaded call machine, so a
134
+ * failure - Krisp's `KrispInitError: Canceled`, for example - arrives as a
135
+ * rejected promise, which a synchronous try/catch around
136
+ * `updateInputSettings()` cannot see. Without this the rejection escapes to
137
+ * the page as an unhandled rejection instead of reaching the listeners
138
+ * consumers register. Audio processing is non-critical, so the call continues.
139
+ */
140
+ private emitAudioProcessingError;
123
141
  start(assistant?: CreateAssistantDTO | string, assistantOverrides?: AssistantOverrides, squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides, options?: StartCallOptions): Promise<Call | null>;
124
142
  private onAppMessage;
125
143
  private handleRemoteParticipantsAudioLevel;
144
+ private handleLocalAudioLevel;
126
145
  /**
127
146
  * Stops the call by destroying the Daily call object.
128
147
  *
@@ -149,6 +168,36 @@ export default class Vapi extends VapiEventEmitter {
149
168
  increaseMicLevel(gain: number): Promise<void>;
150
169
  setOutputDeviceAsync(options: Parameters<DailyCall['setOutputDeviceAsync']>[0]): void;
151
170
  getDailyCallObject(): DailyCall | null;
171
+ /**
172
+ * Returns the most recent local (microphone) audio level, a number between 0 and 1.
173
+ *
174
+ * Useful for detecting when the local participant's microphone is not picking up any
175
+ * audio (e.g. wrong input device selected, OS-level input volume at 0, or muted
176
+ * hardware) so you can surface a "we can't hear you" style warning in your UI.
177
+ *
178
+ * Returns 0 when there is no active call.
179
+ */
180
+ getLocalAudioLevel(): number;
181
+ /**
182
+ * Starts (or restarts) the local audio level observer. The observer is started
183
+ * automatically when a call begins, but this can be used to change the sampling
184
+ * interval or restart it if it was stopped.
185
+ *
186
+ * While running, the Vapi instance emits 'local-volume-level' events with the local
187
+ * microphone audio level (0 to 1).
188
+ *
189
+ * @param interval How often, in milliseconds, to emit audio level events. Defaults to Daily's default.
190
+ */
191
+ startLocalAudioLevelObserver(interval?: number): Promise<void>;
192
+ /**
193
+ * Stops the local audio level observer. After calling this, 'local-volume-level'
194
+ * events will no longer be emitted until the observer is started again.
195
+ */
196
+ stopLocalAudioLevelObserver(): void;
197
+ /**
198
+ * Returns whether the local audio level observer is currently running.
199
+ */
200
+ isLocalAudioLevelObserverRunning(): boolean;
152
201
  startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings): void;
153
202
  stopScreenSharing(): void;
154
203
  /**
package/dist/vapi.js CHANGED
@@ -141,6 +141,24 @@ class Vapi extends VapiEventEmitter {
141
141
  async sleep(ms) {
142
142
  return new Promise((resolve) => setTimeout(resolve, ms));
143
143
  }
144
+ /**
145
+ * Reports a failed audio processing update on the SDK's `error` event.
146
+ *
147
+ * Daily applies input settings inside its remotely loaded call machine, so a
148
+ * failure - Krisp's `KrispInitError: Canceled`, for example - arrives as a
149
+ * rejected promise, which a synchronous try/catch around
150
+ * `updateInputSettings()` cannot see. Without this the rejection escapes to
151
+ * the page as an unhandled rejection instead of reaching the listeners
152
+ * consumers register. Audio processing is non-critical, so the call continues.
153
+ */
154
+ emitAudioProcessingError(stage, error) {
155
+ this.emit('error', {
156
+ type: `${stage}-error`,
157
+ stage,
158
+ error: serializeError(error),
159
+ timestamp: new Date().toISOString(),
160
+ });
161
+ }
144
162
  async start(assistant, assistantOverrides, squad, workflow, workflowOverrides, options) {
145
163
  const startTime = Date.now();
146
164
  // Input validation with detailed error messages
@@ -300,6 +318,26 @@ class Vapi extends VapiEventEmitter {
300
318
  this.call.on('network-connection', (event) => {
301
319
  this.emit('network-connection', event);
302
320
  });
321
+ this.call.on('recording-started', (event) => {
322
+ if (event)
323
+ this.emit('recording-started', event);
324
+ });
325
+ this.call.on('recording-stopped', (event) => {
326
+ if (event)
327
+ this.emit('recording-stopped', event);
328
+ });
329
+ this.call.on('recording-stats', (event) => {
330
+ if (event)
331
+ this.emit('recording-stats', event);
332
+ });
333
+ this.call.on('recording-error', (event) => {
334
+ if (event)
335
+ this.emit('recording-error', event);
336
+ });
337
+ this.call.on('recording-upload-completed', (event) => {
338
+ if (event)
339
+ this.emit('recording-upload-completed', event);
340
+ });
303
341
  this.call.on('track-started', async (e) => {
304
342
  if (!e || !e.participant) {
305
343
  return;
@@ -479,6 +517,7 @@ class Vapi extends VapiEventEmitter {
479
517
  const audioObserverStartTime = Date.now();
480
518
  try {
481
519
  this.call.startRemoteParticipantsAudioLevelObserver(100);
520
+ this.call.startLocalAudioLevelObserver(100);
482
521
  const audioObserverDuration = Date.now() - audioObserverStartTime;
483
522
  this.emit('call-start-progress', {
484
523
  stage: 'audio-observer-setup',
@@ -509,9 +548,16 @@ class Vapi extends VapiEventEmitter {
509
548
  if (e)
510
549
  this.handleRemoteParticipantsAudioLevel(e);
511
550
  });
551
+ this.call.on('local-audio-level', (e) => {
552
+ if (e)
553
+ this.handleLocalAudioLevel(e);
554
+ });
512
555
  this.call.on('app-message', (e) => this.onAppMessage(e));
513
556
  this.call.on('nonfatal-error', (e) => {
514
557
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
558
+ if (e?.type === 'local-audio-level-observer-error') {
559
+ this.emit('local-audio-level-observer-error', serializeError(e));
560
+ }
515
561
  if (e?.type === 'audio-processor-error') {
516
562
  this.call
517
563
  ?.updateInputSettings({
@@ -523,6 +569,9 @@ class Vapi extends VapiEventEmitter {
523
569
  })
524
570
  .then(() => {
525
571
  (0, daily_guards_1.safeSetLocalAudio)(this.call, true);
572
+ })
573
+ .catch((error) => {
574
+ this.emitAudioProcessingError('audio-processor-recovery', error);
526
575
  });
527
576
  }
528
577
  });
@@ -534,12 +583,16 @@ class Vapi extends VapiEventEmitter {
534
583
  });
535
584
  const audioProcessingStartTime = Date.now();
536
585
  try {
537
- this.call.updateInputSettings({
586
+ this.call
587
+ .updateInputSettings({
538
588
  audio: {
539
589
  processor: {
540
590
  type: 'noise-cancellation',
541
591
  },
542
592
  },
593
+ })
594
+ .catch((error) => {
595
+ this.emitAudioProcessingError('audio-processing-setup', error);
543
596
  });
544
597
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
545
598
  this.emit('call-start-progress', {
@@ -653,6 +706,9 @@ class Vapi extends VapiEventEmitter {
653
706
  this.speakingTimeout = null;
654
707
  }, 1000);
655
708
  }
709
+ handleLocalAudioLevel(e) {
710
+ this.emit('local-volume-level', e.audioLevel);
711
+ }
656
712
  /**
657
713
  * Stops the call by destroying the Daily call object.
658
714
  *
@@ -733,6 +789,53 @@ class Vapi extends VapiEventEmitter {
733
789
  getDailyCallObject() {
734
790
  return this.call;
735
791
  }
792
+ /**
793
+ * Returns the most recent local (microphone) audio level, a number between 0 and 1.
794
+ *
795
+ * Useful for detecting when the local participant's microphone is not picking up any
796
+ * audio (e.g. wrong input device selected, OS-level input volume at 0, or muted
797
+ * hardware) so you can surface a "we can't hear you" style warning in your UI.
798
+ *
799
+ * Returns 0 when there is no active call.
800
+ */
801
+ getLocalAudioLevel() {
802
+ if (!this.call) {
803
+ return 0;
804
+ }
805
+ return this.call.getLocalAudioLevel();
806
+ }
807
+ /**
808
+ * Starts (or restarts) the local audio level observer. The observer is started
809
+ * automatically when a call begins, but this can be used to change the sampling
810
+ * interval or restart it if it was stopped.
811
+ *
812
+ * While running, the Vapi instance emits 'local-volume-level' events with the local
813
+ * microphone audio level (0 to 1).
814
+ *
815
+ * @param interval How often, in milliseconds, to emit audio level events. Defaults to Daily's default.
816
+ */
817
+ async startLocalAudioLevelObserver(interval) {
818
+ if (!this.call) {
819
+ throw new Error('Call object is not available.');
820
+ }
821
+ await this.call.startLocalAudioLevelObserver(interval);
822
+ }
823
+ /**
824
+ * Stops the local audio level observer. After calling this, 'local-volume-level'
825
+ * events will no longer be emitted until the observer is started again.
826
+ */
827
+ stopLocalAudioLevelObserver() {
828
+ this.call?.stopLocalAudioLevelObserver();
829
+ }
830
+ /**
831
+ * Returns whether the local audio level observer is currently running.
832
+ */
833
+ isLocalAudioLevelObserverRunning() {
834
+ if (!this.call) {
835
+ return false;
836
+ }
837
+ return this.call.isLocalAudioLevelObserverRunning();
838
+ }
736
839
  startScreenSharing(displayMediaOptions, screenVideoSendSettings) {
737
840
  this.call?.startScreenShare({
738
841
  displayMediaOptions,
@@ -849,6 +952,26 @@ class Vapi extends VapiEventEmitter {
849
952
  this.call.on('network-connection', (event) => {
850
953
  this.emit('network-connection', event);
851
954
  });
955
+ this.call.on('recording-started', (event) => {
956
+ if (event)
957
+ this.emit('recording-started', event);
958
+ });
959
+ this.call.on('recording-stopped', (event) => {
960
+ if (event)
961
+ this.emit('recording-stopped', event);
962
+ });
963
+ this.call.on('recording-stats', (event) => {
964
+ if (event)
965
+ this.emit('recording-stats', event);
966
+ });
967
+ this.call.on('recording-error', (event) => {
968
+ if (event)
969
+ this.emit('recording-error', event);
970
+ });
971
+ this.call.on('recording-upload-completed', (event) => {
972
+ if (event)
973
+ this.emit('recording-upload-completed', event);
974
+ });
852
975
  this.call.on('track-started', async (e) => {
853
976
  if (!e || !e.participant) {
854
977
  return;
@@ -888,9 +1011,16 @@ class Vapi extends VapiEventEmitter {
888
1011
  if (e)
889
1012
  this.handleRemoteParticipantsAudioLevel(e);
890
1013
  });
1014
+ this.call.on('local-audio-level', (e) => {
1015
+ if (e)
1016
+ this.handleLocalAudioLevel(e);
1017
+ });
891
1018
  this.call.on('app-message', (e) => this.onAppMessage(e));
892
1019
  this.call.on('nonfatal-error', (e) => {
893
1020
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
1021
+ if (e?.type === 'local-audio-level-observer-error') {
1022
+ this.emit('local-audio-level-observer-error', serializeError(e));
1023
+ }
894
1024
  if (e?.type === 'audio-processor-error') {
895
1025
  this.call
896
1026
  ?.updateInputSettings({
@@ -902,6 +1032,9 @@ class Vapi extends VapiEventEmitter {
902
1032
  })
903
1033
  .then(() => {
904
1034
  (0, daily_guards_1.safeSetLocalAudio)(this.call, true);
1035
+ })
1036
+ .catch((error) => {
1037
+ this.emitAudioProcessingError('audio-processor-recovery', error);
905
1038
  });
906
1039
  }
907
1040
  });
@@ -1021,6 +1154,7 @@ class Vapi extends VapiEventEmitter {
1021
1154
  const audioObserverStartTime = Date.now();
1022
1155
  try {
1023
1156
  this.call.startRemoteParticipantsAudioLevelObserver(100);
1157
+ this.call.startLocalAudioLevelObserver(100);
1024
1158
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1025
1159
  this.emit('call-start-progress', {
1026
1160
  stage: 'audio-observer-setup',
@@ -1049,12 +1183,16 @@ class Vapi extends VapiEventEmitter {
1049
1183
  });
1050
1184
  const audioProcessingStartTime = Date.now();
1051
1185
  try {
1052
- this.call.updateInputSettings({
1186
+ this.call
1187
+ .updateInputSettings({
1053
1188
  audio: {
1054
1189
  processor: {
1055
1190
  type: 'noise-cancellation',
1056
1191
  },
1057
1192
  },
1193
+ })
1194
+ .catch((error) => {
1195
+ this.emitAudioProcessingError('audio-processing-setup', error);
1058
1196
  });
1059
1197
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1060
1198
  this.emit('call-start-progress', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "2.5.2",
3
+ "version": "2.6.2",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",
@@ -19,19 +19,19 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/VapiAI/web.git"
22
+ "url": "git+https://github.com/VapiAI/client-sdk-web.git"
23
23
  },
24
24
  "author": "vapi",
25
25
  "license": "MIT",
26
26
  "bugs": {
27
- "url": "https://github.com/VapiAI/web/issues"
27
+ "url": "https://github.com/VapiAI/client-sdk-web/issues"
28
28
  },
29
- "homepage": "https://github.com/VapiAI/web#readme",
29
+ "homepage": "https://github.com/VapiAI/client-sdk-web#readme",
30
30
  "engines": {
31
31
  "node": ">=18.0.0"
32
32
  },
33
33
  "dependencies": {
34
- "@daily-co/daily-js": "^0.85.0",
34
+ "@daily-co/daily-js": "^0.87.0",
35
35
  "events": "^3.3.0"
36
36
  },
37
37
  "devDependencies": {
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;
@@ -169,6 +181,8 @@ type VapiEventListeners = {
169
181
  'call-end': () => void;
170
182
  'call-start': () => void;
171
183
  'volume-level': (volume: number) => void;
184
+ 'local-volume-level': (volume: number) => void;
185
+ 'local-audio-level-observer-error': (error: any) => void;
172
186
  'speech-start': () => void;
173
187
  'speech-end': () => void;
174
188
  video: (track: MediaStreamTrack) => void;
@@ -181,6 +195,11 @@ type VapiEventListeners = {
181
195
  'call-start-progress': (event: CallStartProgressEvent) => void;
182
196
  'call-start-success': (event: CallStartSuccessEvent) => void;
183
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;
184
203
  };
185
204
 
186
205
  type StartCallOptions = {
@@ -349,6 +368,28 @@ export default class Vapi extends VapiEventEmitter {
349
368
  return new Promise((resolve) => setTimeout(resolve, ms));
350
369
  }
351
370
 
371
+ /**
372
+ * Reports a failed audio processing update on the SDK's `error` event.
373
+ *
374
+ * Daily applies input settings inside its remotely loaded call machine, so a
375
+ * failure - Krisp's `KrispInitError: Canceled`, for example - arrives as a
376
+ * rejected promise, which a synchronous try/catch around
377
+ * `updateInputSettings()` cannot see. Without this the rejection escapes to
378
+ * the page as an unhandled rejection instead of reaching the listeners
379
+ * consumers register. Audio processing is non-critical, so the call continues.
380
+ */
381
+ private emitAudioProcessingError(
382
+ stage: 'audio-processing-setup' | 'audio-processor-recovery',
383
+ error: unknown,
384
+ ) {
385
+ this.emit('error', {
386
+ type: `${stage}-error`,
387
+ stage,
388
+ error: serializeError(error),
389
+ timestamp: new Date().toISOString(),
390
+ });
391
+ }
392
+
352
393
  async start(
353
394
  assistant?: CreateAssistantDTO | string,
354
395
  assistantOverrides?: AssistantOverrides,
@@ -539,6 +580,26 @@ export default class Vapi extends VapiEventEmitter {
539
580
  this.emit('network-connection', event);
540
581
  });
541
582
 
583
+ this.call.on('recording-started', (event) => {
584
+ if (event) this.emit('recording-started', event);
585
+ });
586
+
587
+ this.call.on('recording-stopped', (event) => {
588
+ if (event) this.emit('recording-stopped', event);
589
+ });
590
+
591
+ this.call.on('recording-stats', (event) => {
592
+ if (event) this.emit('recording-stats', event);
593
+ });
594
+
595
+ this.call.on('recording-error', (event) => {
596
+ if (event) this.emit('recording-error', event);
597
+ });
598
+
599
+ this.call.on('recording-upload-completed', (event) => {
600
+ if (event) this.emit('recording-upload-completed', event);
601
+ });
602
+
542
603
  this.call.on('track-started', async (e) => {
543
604
  if (!e || !e.participant) {
544
605
  return;
@@ -736,6 +797,7 @@ export default class Vapi extends VapiEventEmitter {
736
797
 
737
798
  try {
738
799
  this.call.startRemoteParticipantsAudioLevelObserver(100);
800
+ this.call.startLocalAudioLevelObserver(100);
739
801
  const audioObserverDuration = Date.now() - audioObserverStartTime;
740
802
  this.emit('call-start-progress', {
741
803
  stage: 'audio-observer-setup',
@@ -766,10 +828,17 @@ export default class Vapi extends VapiEventEmitter {
766
828
  if (e) this.handleRemoteParticipantsAudioLevel(e);
767
829
  });
768
830
 
831
+ this.call.on('local-audio-level', (e) => {
832
+ if (e) this.handleLocalAudioLevel(e);
833
+ });
834
+
769
835
  this.call.on('app-message', (e) => this.onAppMessage(e));
770
836
 
771
837
  this.call.on('nonfatal-error', (e) => {
772
838
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
839
+ if (e?.type === 'local-audio-level-observer-error') {
840
+ this.emit('local-audio-level-observer-error', serializeError(e));
841
+ }
773
842
  if (e?.type === 'audio-processor-error') {
774
843
  this.call
775
844
  ?.updateInputSettings({
@@ -781,6 +850,9 @@ export default class Vapi extends VapiEventEmitter {
781
850
  })
782
851
  .then(() => {
783
852
  safeSetLocalAudio(this.call, true);
853
+ })
854
+ .catch((error) => {
855
+ this.emitAudioProcessingError('audio-processor-recovery', error);
784
856
  });
785
857
  }
786
858
  });
@@ -795,14 +867,18 @@ export default class Vapi extends VapiEventEmitter {
795
867
  const audioProcessingStartTime = Date.now();
796
868
 
797
869
  try {
798
- this.call.updateInputSettings({
799
- audio: {
800
- processor: {
801
- type: 'noise-cancellation',
870
+ this.call
871
+ .updateInputSettings({
872
+ audio: {
873
+ processor: {
874
+ type: 'noise-cancellation',
875
+ },
802
876
  },
803
- },
804
- });
805
-
877
+ })
878
+ .catch((error) => {
879
+ this.emitAudioProcessingError('audio-processing-setup', error);
880
+ });
881
+
806
882
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
807
883
  this.emit('call-start-progress', {
808
884
  stage: 'audio-processing-setup',
@@ -927,6 +1003,10 @@ export default class Vapi extends VapiEventEmitter {
927
1003
  }, 1000);
928
1004
  }
929
1005
 
1006
+ private handleLocalAudioLevel(e: DailyEventObjectLocalAudioLevel) {
1007
+ this.emit('local-volume-level', e.audioLevel);
1008
+ }
1009
+
930
1010
  /**
931
1011
  * Stops the call by destroying the Daily call object.
932
1012
  *
@@ -1027,6 +1107,57 @@ export default class Vapi extends VapiEventEmitter {
1027
1107
  return this.call;
1028
1108
  }
1029
1109
 
1110
+ /**
1111
+ * Returns the most recent local (microphone) audio level, a number between 0 and 1.
1112
+ *
1113
+ * Useful for detecting when the local participant's microphone is not picking up any
1114
+ * audio (e.g. wrong input device selected, OS-level input volume at 0, or muted
1115
+ * hardware) so you can surface a "we can't hear you" style warning in your UI.
1116
+ *
1117
+ * Returns 0 when there is no active call.
1118
+ */
1119
+ public getLocalAudioLevel(): number {
1120
+ if (!this.call) {
1121
+ return 0;
1122
+ }
1123
+ return this.call.getLocalAudioLevel();
1124
+ }
1125
+
1126
+ /**
1127
+ * Starts (or restarts) the local audio level observer. The observer is started
1128
+ * automatically when a call begins, but this can be used to change the sampling
1129
+ * interval or restart it if it was stopped.
1130
+ *
1131
+ * While running, the Vapi instance emits 'local-volume-level' events with the local
1132
+ * microphone audio level (0 to 1).
1133
+ *
1134
+ * @param interval How often, in milliseconds, to emit audio level events. Defaults to Daily's default.
1135
+ */
1136
+ public async startLocalAudioLevelObserver(interval?: number): Promise<void> {
1137
+ if (!this.call) {
1138
+ throw new Error('Call object is not available.');
1139
+ }
1140
+ await this.call.startLocalAudioLevelObserver(interval);
1141
+ }
1142
+
1143
+ /**
1144
+ * Stops the local audio level observer. After calling this, 'local-volume-level'
1145
+ * events will no longer be emitted until the observer is started again.
1146
+ */
1147
+ public stopLocalAudioLevelObserver(): void {
1148
+ this.call?.stopLocalAudioLevelObserver();
1149
+ }
1150
+
1151
+ /**
1152
+ * Returns whether the local audio level observer is currently running.
1153
+ */
1154
+ public isLocalAudioLevelObserverRunning(): boolean {
1155
+ if (!this.call) {
1156
+ return false;
1157
+ }
1158
+ return this.call.isLocalAudioLevelObserverRunning();
1159
+ }
1160
+
1030
1161
  public startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings) {
1031
1162
  this.call?.startScreenShare({
1032
1163
  displayMediaOptions,
@@ -1161,6 +1292,26 @@ export default class Vapi extends VapiEventEmitter {
1161
1292
  this.emit('network-connection', event);
1162
1293
  });
1163
1294
 
1295
+ this.call.on('recording-started', (event) => {
1296
+ if (event) this.emit('recording-started', event);
1297
+ });
1298
+
1299
+ this.call.on('recording-stopped', (event) => {
1300
+ if (event) this.emit('recording-stopped', event);
1301
+ });
1302
+
1303
+ this.call.on('recording-stats', (event) => {
1304
+ if (event) this.emit('recording-stats', event);
1305
+ });
1306
+
1307
+ this.call.on('recording-error', (event) => {
1308
+ if (event) this.emit('recording-error', event);
1309
+ });
1310
+
1311
+ this.call.on('recording-upload-completed', (event) => {
1312
+ if (event) this.emit('recording-upload-completed', event);
1313
+ });
1314
+
1164
1315
  this.call.on('track-started', async (e) => {
1165
1316
  if (!e || !e.participant) {
1166
1317
  return;
@@ -1208,10 +1359,17 @@ export default class Vapi extends VapiEventEmitter {
1208
1359
  if (e) this.handleRemoteParticipantsAudioLevel(e);
1209
1360
  });
1210
1361
 
1362
+ this.call.on('local-audio-level', (e) => {
1363
+ if (e) this.handleLocalAudioLevel(e);
1364
+ });
1365
+
1211
1366
  this.call.on('app-message', (e) => this.onAppMessage(e));
1212
1367
 
1213
1368
  this.call.on('nonfatal-error', (e) => {
1214
1369
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
1370
+ if (e?.type === 'local-audio-level-observer-error') {
1371
+ this.emit('local-audio-level-observer-error', serializeError(e));
1372
+ }
1215
1373
  if (e?.type === 'audio-processor-error') {
1216
1374
  this.call
1217
1375
  ?.updateInputSettings({
@@ -1223,6 +1381,9 @@ export default class Vapi extends VapiEventEmitter {
1223
1381
  })
1224
1382
  .then(() => {
1225
1383
  safeSetLocalAudio(this.call, true);
1384
+ })
1385
+ .catch((error) => {
1386
+ this.emitAudioProcessingError('audio-processor-recovery', error);
1226
1387
  });
1227
1388
  }
1228
1389
  });
@@ -1353,6 +1514,7 @@ export default class Vapi extends VapiEventEmitter {
1353
1514
 
1354
1515
  try {
1355
1516
  this.call.startRemoteParticipantsAudioLevelObserver(100);
1517
+ this.call.startLocalAudioLevelObserver(100);
1356
1518
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1357
1519
  this.emit('call-start-progress', {
1358
1520
  stage: 'audio-observer-setup',
@@ -1383,14 +1545,18 @@ export default class Vapi extends VapiEventEmitter {
1383
1545
  const audioProcessingStartTime = Date.now();
1384
1546
 
1385
1547
  try {
1386
- this.call.updateInputSettings({
1387
- audio: {
1388
- processor: {
1389
- type: 'noise-cancellation',
1548
+ this.call
1549
+ .updateInputSettings({
1550
+ audio: {
1551
+ processor: {
1552
+ type: 'noise-cancellation',
1553
+ },
1390
1554
  },
1391
- },
1392
- });
1393
-
1555
+ })
1556
+ .catch((error) => {
1557
+ this.emitAudioProcessingError('audio-processing-setup', error);
1558
+ });
1559
+
1394
1560
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1395
1561
  this.emit('call-start-progress', {
1396
1562
  stage: 'audio-processing-setup',