@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.
- package/README.md +14 -0
- package/api.ts +25665 -11101
- package/dist/api.d.ts +22973 -13375
- package/dist/api.js +2171 -697
- package/dist/vapi.d.ts +44 -0
- package/dist/vapi.js +171 -58
- package/package.json +1 -1
- package/vapi.ts +208 -78
package/dist/vapi.d.ts
CHANGED
|
@@ -109,6 +109,16 @@ type WebCall = {
|
|
|
109
109
|
provider?: string;
|
|
110
110
|
};
|
|
111
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* The Vapi WebCall transport. When video recording is enabled, the server
|
|
114
|
+
* sets `callToken` to a Daily meeting token that auto-starts the cloud
|
|
115
|
+
* recording on join.
|
|
116
|
+
*
|
|
117
|
+
* call.transport
|
|
118
|
+
*/
|
|
119
|
+
transport?: {
|
|
120
|
+
callToken?: string;
|
|
121
|
+
};
|
|
112
122
|
};
|
|
113
123
|
declare class VapiEventEmitter extends EventEmitter {
|
|
114
124
|
on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
|
|
@@ -144,6 +154,25 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
144
154
|
* consumers register. Audio processing is non-critical, so the call continues.
|
|
145
155
|
*/
|
|
146
156
|
private emitAudioProcessingError;
|
|
157
|
+
/**
|
|
158
|
+
* Starts Daily's local (microphone) audio level observer.
|
|
159
|
+
*
|
|
160
|
+
* Only runs when something is listening for 'local-volume-level'. The observer
|
|
161
|
+
* costs an AudioContext plus an AudioWorklet for the lifetime of the call and
|
|
162
|
+
* nothing else in the SDK reads the level, so starting it unconditionally
|
|
163
|
+
* charges every consumer for a feature almost none of them use. Listeners
|
|
164
|
+
* attached after the call is under way should call the public
|
|
165
|
+
* `startLocalAudioLevelObserver()`.
|
|
166
|
+
*
|
|
167
|
+
* Must run after the noise-cancellation processor has settled. Krisp replaces
|
|
168
|
+
* the microphone track, Daily reacts to any local track change by closing the
|
|
169
|
+
* AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
|
|
170
|
+
* and Chrome and Firefox reject that load with "AbortError: Unable to load a
|
|
171
|
+
* worklet's module". Daily answers by stopping the observer for the rest of
|
|
172
|
+
* the call, so losing this race costs the feature, not just console noise.
|
|
173
|
+
* Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
|
|
174
|
+
*/
|
|
175
|
+
private maybeStartLocalAudioLevelObserver;
|
|
147
176
|
start(assistant?: CreateAssistantDTO | string, assistantOverrides?: AssistantOverrides, squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides, options?: StartCallOptions): Promise<Call | null>;
|
|
148
177
|
private onAppMessage;
|
|
149
178
|
private handleRemoteParticipantsAudioLevel;
|
|
@@ -256,6 +285,21 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
256
285
|
isLocalAudioLevelObserverRunning(): boolean;
|
|
257
286
|
startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings): void;
|
|
258
287
|
stopScreenSharing(): void;
|
|
288
|
+
/**
|
|
289
|
+
* Starts (or retries) a cloud recording of the call.
|
|
290
|
+
*
|
|
291
|
+
* Recording start is asynchronous: success is signaled by the
|
|
292
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
293
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
294
|
+
* recording start timed out), you can call this method again a few seconds
|
|
295
|
+
* later to retry.
|
|
296
|
+
*/
|
|
297
|
+
startRecording(): void;
|
|
298
|
+
/**
|
|
299
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
300
|
+
* 'recording-stopped' event.
|
|
301
|
+
*/
|
|
302
|
+
stopRecording(): void;
|
|
259
303
|
/**
|
|
260
304
|
* Reconnects to an active call.
|
|
261
305
|
*
|
package/dist/vapi.js
CHANGED
|
@@ -181,6 +181,35 @@ class Vapi extends VapiEventEmitter {
|
|
|
181
181
|
timestamp: new Date().toISOString(),
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Starts Daily's local (microphone) audio level observer.
|
|
186
|
+
*
|
|
187
|
+
* Only runs when something is listening for 'local-volume-level'. The observer
|
|
188
|
+
* costs an AudioContext plus an AudioWorklet for the lifetime of the call and
|
|
189
|
+
* nothing else in the SDK reads the level, so starting it unconditionally
|
|
190
|
+
* charges every consumer for a feature almost none of them use. Listeners
|
|
191
|
+
* attached after the call is under way should call the public
|
|
192
|
+
* `startLocalAudioLevelObserver()`.
|
|
193
|
+
*
|
|
194
|
+
* Must run after the noise-cancellation processor has settled. Krisp replaces
|
|
195
|
+
* the microphone track, Daily reacts to any local track change by closing the
|
|
196
|
+
* AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
|
|
197
|
+
* and Chrome and Firefox reject that load with "AbortError: Unable to load a
|
|
198
|
+
* worklet's module". Daily answers by stopping the observer for the rest of
|
|
199
|
+
* the call, so losing this race costs the feature, not just console noise.
|
|
200
|
+
* Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
|
|
201
|
+
*/
|
|
202
|
+
async maybeStartLocalAudioLevelObserver() {
|
|
203
|
+
if (!this.call || this.listenerCount('local-volume-level') === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await this.call.startLocalAudioLevelObserver(100);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
this.emit('local-audio-level-observer-error', serializeError(error));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
184
213
|
async start(assistant, assistantOverrides, squad, workflow, workflowOverrides, options) {
|
|
185
214
|
const startTime = Date.now();
|
|
186
215
|
// Input validation with detailed error messages
|
|
@@ -256,6 +285,11 @@ class Vapi extends VapiEventEmitter {
|
|
|
256
285
|
}
|
|
257
286
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
258
287
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
288
|
+
// When video recording is enabled, the server sends a Daily meeting
|
|
289
|
+
// token that auto-starts the cloud recording on join — more reliable
|
|
290
|
+
// than calling startRecording() from a weak client network. Older
|
|
291
|
+
// servers don't send one; then the client starts the recording itself.
|
|
292
|
+
const callToken = webCall?.transport?.callToken;
|
|
259
293
|
// Stage 2: Create Daily call object
|
|
260
294
|
this.emit('call-start-progress', {
|
|
261
295
|
stage: 'daily-call-object-creation',
|
|
@@ -313,6 +347,8 @@ class Vapi extends VapiEventEmitter {
|
|
|
313
347
|
this.hasEmittedCallEndedStatus = true;
|
|
314
348
|
}
|
|
315
349
|
if (isVideoRecordingEnabled) {
|
|
350
|
+
// When using a token, the recording doesn't stop automatically on
|
|
351
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
316
352
|
this.call?.stopRecording();
|
|
317
353
|
}
|
|
318
354
|
this.cleanup().catch(console.error);
|
|
@@ -378,6 +414,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
378
414
|
}
|
|
379
415
|
this.detachAudioPlayer(e.participant.session_id);
|
|
380
416
|
});
|
|
417
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
418
|
+
// while join() is still in flight, so a listener added after join
|
|
419
|
+
// could miss the event.
|
|
420
|
+
let recordingRequestedTime = 0;
|
|
421
|
+
if (isVideoRecordingEnabled) {
|
|
422
|
+
this.call.once('recording-started', () => {
|
|
423
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
424
|
+
this.emit('call-start-progress', {
|
|
425
|
+
stage: 'video-recording-started',
|
|
426
|
+
status: 'completed',
|
|
427
|
+
timestamp: new Date().toISOString(),
|
|
428
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
429
|
+
});
|
|
430
|
+
this.send({
|
|
431
|
+
type: 'control',
|
|
432
|
+
control: 'say-first-message',
|
|
433
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
381
437
|
// Stage 3: Mobile device handling and permissions
|
|
382
438
|
const isMobile = this.isMobileDevice();
|
|
383
439
|
this.emit('call-start-progress', {
|
|
@@ -413,10 +469,14 @@ class Vapi extends VapiEventEmitter {
|
|
|
413
469
|
timestamp: new Date().toISOString()
|
|
414
470
|
});
|
|
415
471
|
const joinStartTime = Date.now();
|
|
472
|
+
recordingRequestedTime = joinStartTime;
|
|
416
473
|
try {
|
|
417
474
|
await this.call.join({
|
|
418
475
|
// @ts-expect-error This exists
|
|
419
476
|
url: webCall.webCallUrl,
|
|
477
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
478
|
+
// only include it when the server actually sent one.
|
|
479
|
+
...(callToken ? { token: callToken } : {}),
|
|
420
480
|
subscribeToTracksAutomatically: false,
|
|
421
481
|
});
|
|
422
482
|
const joinDuration = Date.now() - joinStartTime;
|
|
@@ -447,23 +507,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
447
507
|
throw error;
|
|
448
508
|
}
|
|
449
509
|
// Stage 5: Video recording setup (if enabled)
|
|
450
|
-
if (isVideoRecordingEnabled) {
|
|
510
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
511
|
+
// The meeting token auto-started the recording at join, so there is
|
|
512
|
+
// nothing to request from the client.
|
|
513
|
+
this.emit('call-start-progress', {
|
|
514
|
+
stage: 'video-recording-setup',
|
|
515
|
+
status: 'completed',
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else if (isVideoRecordingEnabled) {
|
|
451
521
|
this.emit('call-start-progress', {
|
|
452
522
|
stage: 'video-recording-setup',
|
|
453
523
|
status: 'started',
|
|
454
524
|
timestamp: new Date().toISOString()
|
|
455
525
|
});
|
|
456
|
-
const recordingRequestedTime = new Date().getTime();
|
|
457
526
|
const recordingStartTime = Date.now();
|
|
458
527
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
height: 720,
|
|
462
|
-
backgroundColor: '#FF1F2D3D',
|
|
463
|
-
layout: {
|
|
464
|
-
preset: 'default',
|
|
465
|
-
},
|
|
466
|
-
});
|
|
528
|
+
recordingRequestedTime = new Date().getTime();
|
|
529
|
+
this.startRecording();
|
|
467
530
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
468
531
|
this.emit('call-start-progress', {
|
|
469
532
|
stage: 'video-recording-setup',
|
|
@@ -471,20 +534,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
471
534
|
duration: recordingSetupDuration,
|
|
472
535
|
timestamp: new Date().toISOString()
|
|
473
536
|
});
|
|
474
|
-
this.call.on('recording-started', () => {
|
|
475
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
476
|
-
this.emit('call-start-progress', {
|
|
477
|
-
stage: 'video-recording-started',
|
|
478
|
-
status: 'completed',
|
|
479
|
-
timestamp: new Date().toISOString(),
|
|
480
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
481
|
-
});
|
|
482
|
-
this.send({
|
|
483
|
-
type: 'control',
|
|
484
|
-
control: 'say-first-message',
|
|
485
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
537
|
}
|
|
489
538
|
catch (error) {
|
|
490
539
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
@@ -522,7 +571,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
522
571
|
const audioObserverStartTime = Date.now();
|
|
523
572
|
try {
|
|
524
573
|
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
525
|
-
this.call.startLocalAudioLevelObserver(100);
|
|
526
574
|
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
527
575
|
this.emit('call-start-progress', {
|
|
528
576
|
stage: 'audio-observer-setup',
|
|
@@ -588,17 +636,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
588
636
|
});
|
|
589
637
|
const audioProcessingStartTime = Date.now();
|
|
590
638
|
try {
|
|
591
|
-
this.call
|
|
592
|
-
.updateInputSettings({
|
|
639
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
593
640
|
audio: {
|
|
594
641
|
processor: {
|
|
595
642
|
type: 'noise-cancellation',
|
|
596
643
|
},
|
|
597
644
|
},
|
|
598
|
-
})
|
|
599
|
-
|
|
645
|
+
});
|
|
646
|
+
audioProcessingUpdate.catch((error) => {
|
|
600
647
|
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
601
648
|
});
|
|
649
|
+
// The observer waits for this update rather than starting alongside the
|
|
650
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
651
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
652
|
+
//
|
|
653
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
654
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
655
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
656
|
+
audioProcessingUpdate
|
|
657
|
+
.catch(() => { })
|
|
658
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
602
659
|
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
603
660
|
this.emit('call-start-progress', {
|
|
604
661
|
stage: 'audio-processing-setup',
|
|
@@ -1001,6 +1058,35 @@ class Vapi extends VapiEventEmitter {
|
|
|
1001
1058
|
stopScreenSharing() {
|
|
1002
1059
|
this.call?.stopScreenShare();
|
|
1003
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Starts (or retries) a cloud recording of the call.
|
|
1063
|
+
*
|
|
1064
|
+
* Recording start is asynchronous: success is signaled by the
|
|
1065
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
1066
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
1067
|
+
* recording start timed out), you can call this method again a few seconds
|
|
1068
|
+
* later to retry.
|
|
1069
|
+
*/
|
|
1070
|
+
startRecording() {
|
|
1071
|
+
if (!this.call) {
|
|
1072
|
+
throw new Error('Call object is not available.');
|
|
1073
|
+
}
|
|
1074
|
+
this.call.startRecording({
|
|
1075
|
+
width: 1280,
|
|
1076
|
+
height: 720,
|
|
1077
|
+
backgroundColor: '#FF1F2D3D',
|
|
1078
|
+
layout: {
|
|
1079
|
+
preset: 'default',
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
1085
|
+
* 'recording-stopped' event.
|
|
1086
|
+
*/
|
|
1087
|
+
stopRecording() {
|
|
1088
|
+
this.call?.stopRecording();
|
|
1089
|
+
}
|
|
1004
1090
|
/**
|
|
1005
1091
|
* Reconnects to an active call.
|
|
1006
1092
|
*
|
|
@@ -1043,6 +1129,10 @@ class Vapi extends VapiEventEmitter {
|
|
|
1043
1129
|
}
|
|
1044
1130
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1045
1131
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1132
|
+
// Same auto-start token as in start(). Rejoining with it while the
|
|
1133
|
+
// recording is still running is safe; if the recording stopped when the
|
|
1134
|
+
// user left, the token starts a new one.
|
|
1135
|
+
const callToken = webCall?.transport?.callToken;
|
|
1046
1136
|
// Stage 1: Create Daily call object
|
|
1047
1137
|
this.emit('call-start-progress', {
|
|
1048
1138
|
stage: 'daily-call-object-creation',
|
|
@@ -1081,6 +1171,8 @@ class Vapi extends VapiEventEmitter {
|
|
|
1081
1171
|
this.hasEmittedCallEndedStatus = true;
|
|
1082
1172
|
}
|
|
1083
1173
|
if (isVideoRecordingEnabled) {
|
|
1174
|
+
// When using a token, the recording doesn't stop automatically on
|
|
1175
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
1084
1176
|
this.call?.stopRecording();
|
|
1085
1177
|
}
|
|
1086
1178
|
this.cleanup().catch(console.error);
|
|
@@ -1177,6 +1269,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
1177
1269
|
});
|
|
1178
1270
|
}
|
|
1179
1271
|
});
|
|
1272
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
1273
|
+
// while join() is still in flight, so a listener added after join
|
|
1274
|
+
// could miss the event.
|
|
1275
|
+
let recordingRequestedTime = 0;
|
|
1276
|
+
if (isVideoRecordingEnabled) {
|
|
1277
|
+
this.call.once('recording-started', () => {
|
|
1278
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1279
|
+
this.emit('call-start-progress', {
|
|
1280
|
+
stage: 'video-recording-started',
|
|
1281
|
+
status: 'completed',
|
|
1282
|
+
timestamp: new Date().toISOString(),
|
|
1283
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
1284
|
+
});
|
|
1285
|
+
this.send({
|
|
1286
|
+
type: 'control',
|
|
1287
|
+
control: 'say-first-message',
|
|
1288
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1180
1292
|
// Stage 2: Mobile device handling and permissions
|
|
1181
1293
|
const isMobile = this.isMobileDevice();
|
|
1182
1294
|
this.emit('call-start-progress', {
|
|
@@ -1212,8 +1324,12 @@ class Vapi extends VapiEventEmitter {
|
|
|
1212
1324
|
timestamp: new Date().toISOString()
|
|
1213
1325
|
});
|
|
1214
1326
|
const joinStartTime = Date.now();
|
|
1327
|
+
recordingRequestedTime = joinStartTime;
|
|
1215
1328
|
await this.call.join({
|
|
1216
1329
|
url: webCall.webCallUrl,
|
|
1330
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
1331
|
+
// only include it when the server actually sent one.
|
|
1332
|
+
...(callToken ? { token: callToken } : {}),
|
|
1217
1333
|
subscribeToTracksAutomatically: false,
|
|
1218
1334
|
});
|
|
1219
1335
|
const joinDuration = Date.now() - joinStartTime;
|
|
@@ -1224,23 +1340,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
1224
1340
|
timestamp: new Date().toISOString()
|
|
1225
1341
|
});
|
|
1226
1342
|
// Stage 4: Video recording setup (if enabled)
|
|
1227
|
-
if (isVideoRecordingEnabled) {
|
|
1343
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
1344
|
+
// The meeting token auto-started the recording at join, so there is
|
|
1345
|
+
// nothing to request from the client.
|
|
1346
|
+
this.emit('call-start-progress', {
|
|
1347
|
+
stage: 'video-recording-setup',
|
|
1348
|
+
status: 'completed',
|
|
1349
|
+
timestamp: new Date().toISOString(),
|
|
1350
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
else if (isVideoRecordingEnabled) {
|
|
1228
1354
|
this.emit('call-start-progress', {
|
|
1229
1355
|
stage: 'video-recording-setup',
|
|
1230
1356
|
status: 'started',
|
|
1231
1357
|
timestamp: new Date().toISOString()
|
|
1232
1358
|
});
|
|
1233
1359
|
const recordingStartTime = Date.now();
|
|
1234
|
-
const recordingRequestedTime = new Date().getTime();
|
|
1235
1360
|
try {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
height: 720,
|
|
1239
|
-
backgroundColor: '#FF1F2D3D',
|
|
1240
|
-
layout: {
|
|
1241
|
-
preset: 'default',
|
|
1242
|
-
},
|
|
1243
|
-
});
|
|
1361
|
+
recordingRequestedTime = new Date().getTime();
|
|
1362
|
+
this.startRecording();
|
|
1244
1363
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1245
1364
|
this.emit('call-start-progress', {
|
|
1246
1365
|
stage: 'video-recording-setup',
|
|
@@ -1248,20 +1367,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
1248
1367
|
duration: recordingSetupDuration,
|
|
1249
1368
|
timestamp: new Date().toISOString()
|
|
1250
1369
|
});
|
|
1251
|
-
this.call.on('recording-started', () => {
|
|
1252
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1253
|
-
this.emit('call-start-progress', {
|
|
1254
|
-
stage: 'video-recording-started',
|
|
1255
|
-
status: 'completed',
|
|
1256
|
-
timestamp: new Date().toISOString(),
|
|
1257
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
1258
|
-
});
|
|
1259
|
-
this.send({
|
|
1260
|
-
type: 'control',
|
|
1261
|
-
control: 'say-first-message',
|
|
1262
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1263
|
-
});
|
|
1264
|
-
});
|
|
1265
1370
|
}
|
|
1266
1371
|
catch (error) {
|
|
1267
1372
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
@@ -1293,7 +1398,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
1293
1398
|
const audioObserverStartTime = Date.now();
|
|
1294
1399
|
try {
|
|
1295
1400
|
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
1296
|
-
this.call.startLocalAudioLevelObserver(100);
|
|
1297
1401
|
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
1298
1402
|
this.emit('call-start-progress', {
|
|
1299
1403
|
stage: 'audio-observer-setup',
|
|
@@ -1322,17 +1426,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
1322
1426
|
});
|
|
1323
1427
|
const audioProcessingStartTime = Date.now();
|
|
1324
1428
|
try {
|
|
1325
|
-
this.call
|
|
1326
|
-
.updateInputSettings({
|
|
1429
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1327
1430
|
audio: {
|
|
1328
1431
|
processor: {
|
|
1329
1432
|
type: 'noise-cancellation',
|
|
1330
1433
|
},
|
|
1331
1434
|
},
|
|
1332
|
-
})
|
|
1333
|
-
|
|
1435
|
+
});
|
|
1436
|
+
audioProcessingUpdate.catch((error) => {
|
|
1334
1437
|
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1335
1438
|
});
|
|
1439
|
+
// The observer waits for this update rather than starting alongside the
|
|
1440
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
1441
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
1442
|
+
//
|
|
1443
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
1444
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1445
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
1446
|
+
audioProcessingUpdate
|
|
1447
|
+
.catch(() => { })
|
|
1448
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1336
1449
|
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1337
1450
|
this.emit('call-start-progress', {
|
|
1338
1451
|
stage: 'audio-processing-setup',
|