@vapi-ai/web 3.0.0 → 3.0.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/api.js CHANGED
@@ -218,6 +218,24 @@ class Api extends HttpClient {
218
218
  format: "json",
219
219
  ...params,
220
220
  }),
221
+ /**
222
+ * No description
223
+ *
224
+ * @tags Assistants
225
+ * @name AssistantControllerValidateBackgroundSoundUrl
226
+ * @summary Validate Background Sound URL
227
+ * @request POST:/assistant/background-sound/validate
228
+ * @secure
229
+ */
230
+ assistantControllerValidateBackgroundSoundUrl: (data, params = {}) => this.request({
231
+ path: `/assistant/background-sound/validate`,
232
+ method: "POST",
233
+ body: data,
234
+ secure: true,
235
+ type: ContentType.Json,
236
+ format: "json",
237
+ ...params,
238
+ }),
221
239
  /**
222
240
  * No description
223
241
  *
@@ -441,6 +459,24 @@ class Api extends HttpClient {
441
459
  format: "json",
442
460
  ...params,
443
461
  }),
462
+ /**
463
+ * No description
464
+ *
465
+ * @tags Assistants
466
+ * @name AssistantControllerUpsertDraft
467
+ * @summary Upsert Assistant Draft
468
+ * @request PUT:/assistant/{id}/draft
469
+ * @secure
470
+ */
471
+ assistantControllerUpsertDraft: (id, data, params = {}) => this.request({
472
+ path: `/assistant/${id}/draft`,
473
+ method: "PUT",
474
+ body: data,
475
+ secure: true,
476
+ type: ContentType.Json,
477
+ format: "json",
478
+ ...params,
479
+ }),
444
480
  /**
445
481
  * No description
446
482
  *
@@ -680,6 +716,23 @@ class Api extends HttpClient {
680
716
  format: "json",
681
717
  ...params,
682
718
  }),
719
+ /**
720
+ * No description
721
+ *
722
+ * @tags Files
723
+ * @name FileV2ControllerFindAllPaginated
724
+ * @summary List Files with pagination
725
+ * @request GET:/v2/file
726
+ * @secure
727
+ */
728
+ fileV2ControllerFindAllPaginated: (query, params = {}) => this.request({
729
+ path: `/v2/file`,
730
+ method: "GET",
731
+ query: query,
732
+ secure: true,
733
+ format: "json",
734
+ ...params,
735
+ }),
683
736
  /**
684
737
  * No description
685
738
  *
@@ -918,6 +971,56 @@ class Api extends HttpClient {
918
971
  format: "json",
919
972
  ...params,
920
973
  }),
974
+ /**
975
+ * @description Returns the version history for the squad.
976
+ *
977
+ * @tags Squads
978
+ * @name SquadControllerFindAllVersions
979
+ * @summary List Squad Versions
980
+ * @request GET:/squad/{id}/versions
981
+ * @secure
982
+ */
983
+ squadControllerFindAllVersions: (id, query, params = {}) => this.request({
984
+ path: `/squad/${id}/versions`,
985
+ method: "GET",
986
+ query: query,
987
+ secure: true,
988
+ format: "json",
989
+ ...params,
990
+ }),
991
+ /**
992
+ * No description
993
+ *
994
+ * @tags Squads
995
+ * @name SquadControllerFindOneVersion
996
+ * @summary Get Squad Version
997
+ * @request GET:/squad/{id}/versions/{version}
998
+ * @secure
999
+ */
1000
+ squadControllerFindOneVersion: (id, version, query, params = {}) => this.request({
1001
+ path: `/squad/${id}/versions/${version}`,
1002
+ method: "GET",
1003
+ query: query,
1004
+ secure: true,
1005
+ format: "json",
1006
+ ...params,
1007
+ }),
1008
+ /**
1009
+ * @description Deletes the selected version and rolls the squad back to the highest remaining live version. Deleting a version that is already deleted returns 200 with that version.
1010
+ *
1011
+ * @tags Squads
1012
+ * @name SquadControllerSoftDeleteVersion
1013
+ * @summary Delete Squad Version
1014
+ * @request DELETE:/squad/{id}/versions/{version}
1015
+ * @secure
1016
+ */
1017
+ squadControllerSoftDeleteVersion: (id, version, params = {}) => this.request({
1018
+ path: `/squad/${id}/versions/${version}`,
1019
+ method: "DELETE",
1020
+ secure: true,
1021
+ format: "json",
1022
+ ...params,
1023
+ }),
921
1024
  };
922
1025
  workflow = {
923
1026
  /**
@@ -1565,6 +1668,93 @@ class Api extends HttpClient {
1565
1668
  ...params,
1566
1669
  }),
1567
1670
  };
1671
+ trafficAllocations = {
1672
+ /**
1673
+ * @description Creates a new traffic allocation for an assistant, replacing the one currently in effect. To start or adjust a split, send targets naming published versions (such as "v7") with percentages totaling 100; allocationIntent is inferred as 'explicit'. To stop splitting and send every call to the newest published version, send allocationIntent 'follow-latest' with no targets field; stopping always names its intent, so a dropped targets field can never end a split by accident. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1674
+ *
1675
+ * @tags Traffic Allocations
1676
+ * @name TrafficAllocationControllerCreate
1677
+ * @summary Create traffic allocation
1678
+ * @request POST:/traffic-allocations
1679
+ * @secure
1680
+ */
1681
+ trafficAllocationControllerCreate: (data, params = {}) => this.request({
1682
+ path: `/traffic-allocations`,
1683
+ method: "POST",
1684
+ body: data,
1685
+ secure: true,
1686
+ type: ContentType.Json,
1687
+ format: "json",
1688
+ ...params,
1689
+ }),
1690
+ /**
1691
+ * @description The append-only history of an assistant's allocations, newest first. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1692
+ *
1693
+ * @tags Traffic Allocations
1694
+ * @name TrafficAllocationControllerFindAllPaginated
1695
+ * @summary List traffic allocations
1696
+ * @request GET:/traffic-allocations
1697
+ * @secure
1698
+ */
1699
+ trafficAllocationControllerFindAllPaginated: (query, params = {}) => this.request({
1700
+ path: `/traffic-allocations`,
1701
+ method: "GET",
1702
+ query: query,
1703
+ secure: true,
1704
+ format: "json",
1705
+ ...params,
1706
+ }),
1707
+ /**
1708
+ * @description The allocation currently in effect for the assistant, with its targets. The response carries no allocation field when traffic splitting has never been configured. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1709
+ *
1710
+ * @tags Traffic Allocations
1711
+ * @name TrafficAllocationControllerLatestGet
1712
+ * @summary Get latest traffic allocation
1713
+ * @request GET:/traffic-allocations/latest
1714
+ * @secure
1715
+ */
1716
+ trafficAllocationControllerLatestGet: (query, params = {}) => this.request({
1717
+ path: `/traffic-allocations/latest`,
1718
+ method: "GET",
1719
+ query: query,
1720
+ secure: true,
1721
+ format: "json",
1722
+ ...params,
1723
+ }),
1724
+ /**
1725
+ * @description Per-assistant-version call metrics over a time window: call counts, configured vs actual traffic share, duration, cost per minute, latency, ended reasons, a bucketed timeseries, and optional structured output aggregations. Calls with no assistant version are reported as an 'unversioned' row. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1726
+ *
1727
+ * @tags Traffic Allocations
1728
+ * @name TrafficAllocationControllerMetricsGet
1729
+ * @summary Get traffic split metrics
1730
+ * @request GET:/traffic-allocations/metrics
1731
+ * @secure
1732
+ */
1733
+ trafficAllocationControllerMetricsGet: (query, params = {}) => this.request({
1734
+ path: `/traffic-allocations/metrics`,
1735
+ method: "GET",
1736
+ query: query,
1737
+ secure: true,
1738
+ format: "json",
1739
+ ...params,
1740
+ }),
1741
+ /**
1742
+ * @description Returns a single allocation by id, including its targets and actor attribution. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1743
+ *
1744
+ * @tags Traffic Allocations
1745
+ * @name TrafficAllocationControllerFindOne
1746
+ * @summary Get traffic allocation
1747
+ * @request GET:/traffic-allocations/{id}
1748
+ * @secure
1749
+ */
1750
+ trafficAllocationControllerFindOne: (id, params = {}) => this.request({
1751
+ path: `/traffic-allocations/${id}`,
1752
+ method: "GET",
1753
+ secure: true,
1754
+ format: "json",
1755
+ ...params,
1756
+ }),
1757
+ };
1568
1758
  phoneNumber = {
1569
1759
  /**
1570
1760
  * @description Use POST /phone-number instead.
@@ -1862,6 +2052,22 @@ class Api extends HttpClient {
1862
2052
  format: "json",
1863
2053
  ...params,
1864
2054
  }),
2055
+ /**
2056
+ * @description Restores the selected version as the current configuration and publishes it as a new version. Deletes any drafts for this tool.
2057
+ *
2058
+ * @tags Tools
2059
+ * @name ToolControllerToolVersionRestore
2060
+ * @summary Restore Tool Version
2061
+ * @request POST:/tool/{id}/restore/{version}
2062
+ * @secure
2063
+ */
2064
+ toolControllerToolVersionRestore: (id, version, params = {}) => this.request({
2065
+ path: `/tool/${id}/restore/${version}`,
2066
+ method: "POST",
2067
+ secure: true,
2068
+ format: "json",
2069
+ ...params,
2070
+ }),
1865
2071
  /**
1866
2072
  * No description
1867
2073
  *
@@ -2826,7 +3032,7 @@ class Api extends HttpClient {
2826
3032
  ...params,
2827
3033
  }),
2828
3034
  /**
2829
- * No description
3035
+ * @description Returns a pagination envelope when page, limit, name, assistantId, or squadId is supplied; otherwise returns the legacy array.
2830
3036
  *
2831
3037
  * @tags Simulation Suites
2832
3038
  * @name SimulationSuiteControllerFindAll
@@ -2842,6 +3048,22 @@ class Api extends HttpClient {
2842
3048
  format: "json",
2843
3049
  ...params,
2844
3050
  }),
3051
+ /**
3052
+ * No description
3053
+ *
3054
+ * @tags Simulation Suites
3055
+ * @name SimulationSuiteControllerDuplicate
3056
+ * @summary Duplicate simulation suite
3057
+ * @request POST:/eval/simulation/suite/{id}/duplicate
3058
+ * @secure
3059
+ */
3060
+ simulationSuiteControllerDuplicate: (id, params = {}) => this.request({
3061
+ path: `/eval/simulation/suite/${id}/duplicate`,
3062
+ method: "POST",
3063
+ secure: true,
3064
+ format: "json",
3065
+ ...params,
3066
+ }),
2845
3067
  /**
2846
3068
  * No description
2847
3069
  *
@@ -2945,6 +3167,22 @@ class Api extends HttpClient {
2945
3167
  format: "json",
2946
3168
  ...params,
2947
3169
  }),
3170
+ /**
3171
+ * No description
3172
+ *
3173
+ * @tags Simulations
3174
+ * @name SimulationControllerGetConcurrency
3175
+ * @summary Get Simulation Concurrency
3176
+ * @request GET:/eval/simulation/concurrency
3177
+ * @secure
3178
+ */
3179
+ simulationControllerGetConcurrency: (params = {}) => this.request({
3180
+ path: `/eval/simulation/concurrency`,
3181
+ method: "GET",
3182
+ secure: true,
3183
+ format: "json",
3184
+ ...params,
3185
+ }),
2948
3186
  /**
2949
3187
  * No description
2950
3188
  *
@@ -2995,22 +3233,6 @@ class Api extends HttpClient {
2995
3233
  format: "json",
2996
3234
  ...params,
2997
3235
  }),
2998
- /**
2999
- * No description
3000
- *
3001
- * @tags Simulations
3002
- * @name SimulationControllerGetConcurrency
3003
- * @summary Get Simulation Concurrency
3004
- * @request GET:/eval/simulation/concurrency
3005
- * @secure
3006
- */
3007
- simulationControllerGetConcurrency: (params = {}) => this.request({
3008
- path: `/eval/simulation/concurrency`,
3009
- method: "GET",
3010
- secure: true,
3011
- format: "json",
3012
- ...params,
3013
- }),
3014
3236
  /**
3015
3237
  * No description
3016
3238
  *
@@ -3143,6 +3365,7 @@ class Api extends HttpClient {
3143
3365
  body: data,
3144
3366
  secure: true,
3145
3367
  type: ContentType.Json,
3368
+ format: "json",
3146
3369
  ...params,
3147
3370
  }),
3148
3371
  /**
@@ -3526,6 +3749,22 @@ class Api extends HttpClient {
3526
3749
  format: "json",
3527
3750
  ...params,
3528
3751
  }),
3752
+ /**
3753
+ * @description Returns the call concurrency limits and API rate limit usage for the authenticated org. Read-only and cheap enough to poll every few seconds. The X-RateLimit-* response headers describe this endpoint's own polling ceiling, not the API budget reported in apiLimits.
3754
+ *
3755
+ * @tags Orgs
3756
+ * @name OrgControllerLimitsGet
3757
+ * @summary Get org limits
3758
+ * @request GET:/org/limits
3759
+ * @secure
3760
+ */
3761
+ orgControllerLimitsGet: (params = {}) => this.request({
3762
+ path: `/org/limits`,
3763
+ method: "GET",
3764
+ secure: true,
3765
+ format: "json",
3766
+ ...params,
3767
+ }),
3529
3768
  /**
3530
3769
  * No description
3531
3770
  *
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config = {
4
4
  preset: 'ts-jest',
5
+ transform: {
6
+ '^.+\\.tsx?$': ['ts-jest', { tsconfig: { types: ['jest', 'node'] } }],
7
+ },
5
8
  testEnvironment: 'node',
6
9
  // Your Jest configuration options here
7
10
  // For example, to match test files:
package/dist/vapi.d.ts CHANGED
@@ -70,6 +70,8 @@ type VapiEventListeners = {
70
70
  'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
71
71
  };
72
72
  type StartCallOptions = {
73
+ /** Acquire the microphone before creating a backend call. Defaults to true. */
74
+ microphonePreflightEnabled?: boolean;
73
75
  /**
74
76
  * This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
75
77
  * If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
@@ -138,6 +140,9 @@ export default class Vapi extends VapiEventEmitter {
138
140
  private speakingTimeout;
139
141
  private dailyCallConfig;
140
142
  private dailyCallObject;
143
+ private microphoneTrack;
144
+ private cancelMicrophonePreflight;
145
+ private microphoneAcquire;
141
146
  private hasEmittedCallEndedStatus;
142
147
  constructor(apiToken: string, apiBaseUrl?: string, dailyCallConfig?: Pick<DailyAdvancedConfig, 'avoidEval' | 'alwaysIncludeMicInPermissionPrompt'>, dailyCallObject?: Pick<DailyFactoryOptions, 'audioSource' | 'startAudioOff'>);
143
148
  private cleanup;
package/dist/vapi.js CHANGED
@@ -134,6 +134,39 @@ class Vapi extends VapiEventEmitter {
134
134
  speakingTimeout = null;
135
135
  dailyCallConfig = {};
136
136
  dailyCallObject = {};
137
+ microphoneTrack = null;
138
+ cancelMicrophonePreflight = null;
139
+ async microphoneAcquire() {
140
+ const source = this.dailyCallObject.audioSource;
141
+ if (source && typeof source === 'object') {
142
+ if (source.readyState === 'ended') {
143
+ throw new DOMException('The supplied microphone track has ended.', 'NotReadableError');
144
+ }
145
+ return source;
146
+ }
147
+ return new Promise((resolve, reject) => {
148
+ let cancelled = false;
149
+ this.cancelMicrophonePreflight = () => {
150
+ cancelled = true;
151
+ reject(new DOMException('Microphone preflight cancelled.', 'AbortError'));
152
+ };
153
+ navigator.mediaDevices.getUserMedia({
154
+ audio: typeof source === 'string' ? { deviceId: { exact: source } } : true,
155
+ }).then((stream) => {
156
+ const track = stream.getAudioTracks()[0];
157
+ if (cancelled || !track || track.readyState === 'ended') {
158
+ stream.getTracks().forEach((item) => item.stop());
159
+ reject(new DOMException('No live microphone track available.', 'NotFoundError'));
160
+ return;
161
+ }
162
+ stream.getTracks().filter((item) => item !== track).forEach((item) => item.stop());
163
+ this.microphoneTrack = track;
164
+ resolve(track);
165
+ }, reject);
166
+ }).finally(() => {
167
+ this.cancelMicrophonePreflight = null;
168
+ });
169
+ }
137
170
  hasEmittedCallEndedStatus = false;
138
171
  constructor(apiToken, apiBaseUrl, dailyCallConfig, dailyCallObject) {
139
172
  super();
@@ -143,6 +176,8 @@ class Vapi extends VapiEventEmitter {
143
176
  this.dailyCallObject = (0, daily_guards_1.createSafeDailyFactoryOptions)(dailyCallObject);
144
177
  }
145
178
  async cleanup() {
179
+ this.microphoneTrack?.stop();
180
+ this.microphoneTrack = null;
146
181
  this.started = false;
147
182
  this.hasEmittedCallEndedStatus = false;
148
183
  if (this.call) {
@@ -243,7 +278,16 @@ class Vapi extends VapiEventEmitter {
243
278
  }
244
279
  });
245
280
  this.started = true;
281
+ let failureStage = 'microphone-preflight';
246
282
  try {
283
+ if (this.call) {
284
+ await this.cleanup();
285
+ this.started = true;
286
+ }
287
+ const audioSource = options?.microphonePreflightEnabled === false
288
+ ? this.dailyCallObject.audioSource ?? true
289
+ : await this.microphoneAcquire();
290
+ failureStage = 'unknown';
247
291
  // Stage 1: Create web call
248
292
  this.emit('call-start-progress', {
249
293
  stage: 'web-call-creation',
@@ -274,15 +318,6 @@ class Vapi extends VapiEventEmitter {
274
318
  voiceProvider: webCall?.assistant?.voice?.provider || 'unknown'
275
319
  }
276
320
  });
277
- if (this.call) {
278
- this.emit('call-start-progress', {
279
- stage: 'daily-call-object-creation',
280
- status: 'started',
281
- timestamp: new Date().toISOString(),
282
- metadata: { action: 'cleanup-existing' }
283
- });
284
- await this.cleanup();
285
- }
286
321
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
287
322
  const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
288
323
  // When video recording is enabled, the server sends a Daily meeting
@@ -305,7 +340,7 @@ class Vapi extends VapiEventEmitter {
305
340
  const dailyCallStartTime = Date.now();
306
341
  try {
307
342
  this.call = daily_js_1.default.createCallObject({
308
- audioSource: this.dailyCallObject.audioSource ?? true,
343
+ audioSource,
309
344
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
310
345
  dailyConfig: this.dailyCallConfig,
311
346
  });
@@ -693,8 +728,9 @@ class Vapi extends VapiEventEmitter {
693
728
  catch (e) {
694
729
  const totalDuration = Date.now() - startTime;
695
730
  const serializedError = serializeError(e);
731
+ await this.cleanup();
696
732
  this.emit('call-start-failed', {
697
- stage: 'unknown',
733
+ stage: failureStage,
698
734
  totalDuration,
699
735
  error: serializedError.message,
700
736
  errorStack: serializedError.stack || 'No stack trace available',
@@ -708,8 +744,10 @@ class Vapi extends VapiEventEmitter {
708
744
  });
709
745
  // Also emit the generic error event for backward compatibility
710
746
  this.emit('error', {
711
- type: 'start-method-error',
712
- stage: 'unknown',
747
+ type: failureStage === 'microphone-preflight'
748
+ ? `microphone-preflight-${e && typeof e === 'object' && 'name' in e ? e.name : 'error'}`
749
+ : 'start-method-error',
750
+ stage: failureStage,
713
751
  error: serializedError,
714
752
  totalDuration,
715
753
  timestamp: new Date().toISOString(),
@@ -720,7 +758,6 @@ class Vapi extends VapiEventEmitter {
720
758
  isMobile: this.isMobileDevice()
721
759
  }
722
760
  });
723
- await this.cleanup();
724
761
  return null;
725
762
  }
726
763
  }
@@ -778,6 +815,12 @@ class Vapi extends VapiEventEmitter {
778
815
  * If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections.
779
816
  */
780
817
  async stop() {
818
+ if (this.cancelMicrophonePreflight) {
819
+ this.cancelMicrophonePreflight();
820
+ return;
821
+ }
822
+ this.microphoneTrack?.stop();
823
+ this.microphoneTrack = null;
781
824
  this.started = false;
782
825
  if (this.call) {
783
826
  await this.call.destroy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",
@@ -31,7 +31,7 @@
31
31
  "node": ">=22.14.0"
32
32
  },
33
33
  "dependencies": {
34
- "@daily-co/daily-js": "^0.93.0",
34
+ "@daily-co/daily-js": "^0.94.0",
35
35
  "events": "^3.3.0"
36
36
  },
37
37
  "devDependencies": {
package/vapi.ts CHANGED
@@ -206,6 +206,8 @@ type VapiEventListeners = {
206
206
  };
207
207
 
208
208
  type StartCallOptions = {
209
+ /** Acquire the microphone before creating a backend call. Defaults to true. */
210
+ microphonePreflightEnabled?: boolean;
209
211
  /**
210
212
  * This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
211
213
  * If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
@@ -356,6 +358,40 @@ export default class Vapi extends VapiEventEmitter {
356
358
  private speakingTimeout: NodeJS.Timeout | null = null;
357
359
  private dailyCallConfig: DailyAdvancedConfig = {};
358
360
  private dailyCallObject: DailyFactoryOptions = {};
361
+ private microphoneTrack: MediaStreamTrack | null = null;
362
+ private cancelMicrophonePreflight: (() => void) | null = null;
363
+
364
+ private async microphoneAcquire(): Promise<MediaStreamTrack> {
365
+ const source = this.dailyCallObject.audioSource;
366
+ if (source && typeof source === 'object') {
367
+ if (source.readyState === 'ended') {
368
+ throw new DOMException('The supplied microphone track has ended.', 'NotReadableError');
369
+ }
370
+ return source;
371
+ }
372
+ return new Promise<MediaStreamTrack>((resolve, reject) => {
373
+ let cancelled = false;
374
+ this.cancelMicrophonePreflight = () => {
375
+ cancelled = true;
376
+ reject(new DOMException('Microphone preflight cancelled.', 'AbortError'));
377
+ };
378
+ navigator.mediaDevices.getUserMedia({
379
+ audio: typeof source === 'string' ? { deviceId: { exact: source } } : true,
380
+ }).then((stream) => {
381
+ const track = stream.getAudioTracks()[0];
382
+ if (cancelled || !track || track.readyState === 'ended') {
383
+ stream.getTracks().forEach((item) => item.stop());
384
+ reject(new DOMException('No live microphone track available.', 'NotFoundError'));
385
+ return;
386
+ }
387
+ stream.getTracks().filter((item) => item !== track).forEach((item) => item.stop());
388
+ this.microphoneTrack = track;
389
+ resolve(track);
390
+ }, reject);
391
+ }).finally(() => {
392
+ this.cancelMicrophonePreflight = null;
393
+ });
394
+ }
359
395
 
360
396
  private hasEmittedCallEndedStatus: boolean = false;
361
397
 
@@ -376,6 +412,8 @@ export default class Vapi extends VapiEventEmitter {
376
412
  }
377
413
 
378
414
  private async cleanup() {
415
+ this.microphoneTrack?.stop();
416
+ this.microphoneTrack = null;
379
417
  this.started = false;
380
418
  this.hasEmittedCallEndedStatus = false;
381
419
  if (this.call) {
@@ -498,7 +536,16 @@ export default class Vapi extends VapiEventEmitter {
498
536
 
499
537
  this.started = true;
500
538
 
539
+ let failureStage = 'microphone-preflight';
501
540
  try {
541
+ if (this.call) {
542
+ await this.cleanup();
543
+ this.started = true;
544
+ }
545
+ const audioSource = options?.microphonePreflightEnabled === false
546
+ ? this.dailyCallObject.audioSource ?? true
547
+ : await this.microphoneAcquire();
548
+ failureStage = 'unknown';
502
549
  // Stage 1: Create web call
503
550
  this.emit('call-start-progress', {
504
551
  stage: 'web-call-creation',
@@ -535,16 +582,6 @@ export default class Vapi extends VapiEventEmitter {
535
582
  }
536
583
  });
537
584
 
538
- if (this.call) {
539
- this.emit('call-start-progress', {
540
- stage: 'daily-call-object-creation',
541
- status: 'started',
542
- timestamp: new Date().toISOString(),
543
- metadata: { action: 'cleanup-existing' }
544
- });
545
- await this.cleanup();
546
- }
547
-
548
585
  const isVideoRecordingEnabled =
549
586
  webCall?.artifactPlan?.videoRecordingEnabled ?? false;
550
587
 
@@ -575,7 +612,7 @@ export default class Vapi extends VapiEventEmitter {
575
612
 
576
613
  try {
577
614
  this.call = DailyIframe.createCallObject({
578
- audioSource: this.dailyCallObject.audioSource ?? true,
615
+ audioSource,
579
616
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
580
617
  dailyConfig: this.dailyCallConfig,
581
618
  });
@@ -994,9 +1031,10 @@ export default class Vapi extends VapiEventEmitter {
994
1031
  } catch (e) {
995
1032
  const totalDuration = Date.now() - startTime;
996
1033
  const serializedError = serializeError(e);
1034
+ await this.cleanup();
997
1035
 
998
1036
  this.emit('call-start-failed', {
999
- stage: 'unknown',
1037
+ stage: failureStage,
1000
1038
  totalDuration,
1001
1039
  error: serializedError.message,
1002
1040
  errorStack: serializedError.stack || 'No stack trace available',
@@ -1011,8 +1049,10 @@ export default class Vapi extends VapiEventEmitter {
1011
1049
 
1012
1050
  // Also emit the generic error event for backward compatibility
1013
1051
  this.emit('error', {
1014
- type: 'start-method-error',
1015
- stage: 'unknown',
1052
+ type: failureStage === 'microphone-preflight'
1053
+ ? `microphone-preflight-${e && typeof e === 'object' && 'name' in e ? e.name : 'error'}`
1054
+ : 'start-method-error',
1055
+ stage: failureStage,
1016
1056
  error: serializedError,
1017
1057
  totalDuration,
1018
1058
  timestamp: new Date().toISOString(),
@@ -1024,7 +1064,6 @@ export default class Vapi extends VapiEventEmitter {
1024
1064
  }
1025
1065
  });
1026
1066
 
1027
- await this.cleanup();
1028
1067
  return null;
1029
1068
  }
1030
1069
  }
@@ -1092,6 +1131,12 @@ export default class Vapi extends VapiEventEmitter {
1092
1131
  * If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections.
1093
1132
  */
1094
1133
  async stop(): Promise<void> {
1134
+ if (this.cancelMicrophonePreflight) {
1135
+ this.cancelMicrophonePreflight();
1136
+ return;
1137
+ }
1138
+ this.microphoneTrack?.stop();
1139
+ this.microphoneTrack = null;
1095
1140
  this.started = false;
1096
1141
  if (this.call) {
1097
1142
  await this.call.destroy();