@vapi-ai/web 3.0.1 → 3.1.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 +11 -1
- package/api.ts +6928 -5521
- package/dist/api.d.ts +3686 -2653
- package/dist/api.js +130 -5
- package/dist/vapi.d.ts +64 -5
- package/dist/vapi.js +211 -173
- package/package.json +1 -1
- package/vapi.ts +305 -213
package/dist/vapi.js
CHANGED
|
@@ -58,6 +58,8 @@ function serializeError(error) {
|
|
|
58
58
|
}
|
|
59
59
|
return { message: String(error) };
|
|
60
60
|
}
|
|
61
|
+
// The app message Vapi sends when it couldn't start a server-mode recording.
|
|
62
|
+
const VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE = 'video-recording.start-failed';
|
|
61
63
|
async function startAudioPlayer(player, track) {
|
|
62
64
|
player.muted = false;
|
|
63
65
|
player.autoplay = true;
|
|
@@ -88,13 +90,13 @@ function destroyAudioPlayer(participantId) {
|
|
|
88
90
|
const player = document.querySelector(`audio[data-participant-id="${participantId}"]`);
|
|
89
91
|
player?.remove();
|
|
90
92
|
}
|
|
91
|
-
function subscribeToTracks(e, call, isVideoRecordingEnabled
|
|
93
|
+
function subscribeToTracks(e, call, isVideoRecordingEnabled) {
|
|
92
94
|
if (e.participant.local)
|
|
93
95
|
return;
|
|
94
96
|
call.updateParticipant(e.participant.session_id, {
|
|
95
97
|
setSubscribedTracks: {
|
|
96
98
|
audio: true,
|
|
97
|
-
video: isVideoRecordingEnabled
|
|
99
|
+
video: isVideoRecordingEnabled,
|
|
98
100
|
},
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -136,6 +138,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
136
138
|
dailyCallObject = {};
|
|
137
139
|
microphoneTrack = null;
|
|
138
140
|
cancelMicrophonePreflight = null;
|
|
141
|
+
clientSideNoiseCancellationEnabled = true;
|
|
139
142
|
async microphoneAcquire() {
|
|
140
143
|
const source = this.dailyCallObject.audioSource;
|
|
141
144
|
if (source && typeof source === 'object') {
|
|
@@ -278,6 +281,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
278
281
|
}
|
|
279
282
|
});
|
|
280
283
|
this.started = true;
|
|
284
|
+
this.clientSideNoiseCancellationEnabled = options?.clientSideNoiseCancellationEnabled ?? true;
|
|
281
285
|
let failureStage = 'microphone-preflight';
|
|
282
286
|
try {
|
|
283
287
|
if (this.call) {
|
|
@@ -295,7 +299,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
295
299
|
timestamp: new Date().toISOString()
|
|
296
300
|
});
|
|
297
301
|
const webCallStartTime = Date.now();
|
|
298
|
-
|
|
302
|
+
// An unset option stays undefined, so JSON.stringify drops the key and
|
|
303
|
+
// older servers never see it.
|
|
304
|
+
const createWebCallBody = {
|
|
299
305
|
assistant: typeof assistant === 'string' ? undefined : assistant,
|
|
300
306
|
assistantId: typeof assistant === 'string' ? assistant : undefined,
|
|
301
307
|
assistantOverrides,
|
|
@@ -305,7 +311,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
305
311
|
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
306
312
|
workflowOverrides,
|
|
307
313
|
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
308
|
-
|
|
314
|
+
videoRecordingStartMode: options?.videoRecordingStartMode,
|
|
315
|
+
};
|
|
316
|
+
const webCall = (await client_1.client.call.callControllerCreateWebCall(createWebCallBody)).data;
|
|
309
317
|
const webCallDuration = Date.now() - webCallStartTime;
|
|
310
318
|
this.emit('call-start-progress', {
|
|
311
319
|
stage: 'web-call-creation',
|
|
@@ -319,12 +327,16 @@ class Vapi extends VapiEventEmitter {
|
|
|
319
327
|
}
|
|
320
328
|
});
|
|
321
329
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
322
|
-
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
323
330
|
// When video recording is enabled, the server sends a Daily meeting
|
|
324
|
-
// token
|
|
325
|
-
//
|
|
326
|
-
// servers
|
|
327
|
-
|
|
331
|
+
// token. In client mode it auto-starts the cloud recording on join. In
|
|
332
|
+
// server mode it starts nothing: Vapi starts the recording from its
|
|
333
|
+
// servers once the user has joined, which a weak client network can't
|
|
334
|
+
// stop. Without a token the SDK emits recording-error and starts
|
|
335
|
+
// nothing.
|
|
336
|
+
const transport = webCall?.transport;
|
|
337
|
+
const callToken = transport?.callToken;
|
|
338
|
+
const isRecordingStartedByServer = transport?.videoRecordingStartMode === 'server';
|
|
339
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
328
340
|
// Stage 2: Create Daily call object
|
|
329
341
|
this.emit('call-start-progress', {
|
|
330
342
|
stage: 'daily-call-object-creation',
|
|
@@ -334,7 +346,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
334
346
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
335
347
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
336
348
|
isVideoRecordingEnabled,
|
|
337
|
-
isVideoEnabled
|
|
338
349
|
}
|
|
339
350
|
});
|
|
340
351
|
const dailyCallStartTime = Date.now();
|
|
@@ -435,7 +446,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
435
446
|
this.call.on('participant-joined', (e) => {
|
|
436
447
|
if (!e || !this.call)
|
|
437
448
|
return;
|
|
438
|
-
subscribeToTracks(e, this.call, isVideoRecordingEnabled
|
|
449
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
439
450
|
});
|
|
440
451
|
this.call.on('participant-updated', (e) => {
|
|
441
452
|
if (!e) {
|
|
@@ -449,6 +460,10 @@ class Vapi extends VapiEventEmitter {
|
|
|
449
460
|
}
|
|
450
461
|
this.detachAudioPlayer(e.participant.session_id);
|
|
451
462
|
});
|
|
463
|
+
// Registered before join(), as in reconnect(): Vapi starts a server-mode
|
|
464
|
+
// recording when it sees the user join, so its start-failed message can
|
|
465
|
+
// arrive while join() is still in flight.
|
|
466
|
+
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
452
467
|
// Registered before join(): a token-auto-started recording can begin
|
|
453
468
|
// while join() is still in flight, so a listener added after join
|
|
454
469
|
// could miss the event.
|
|
@@ -543,51 +558,29 @@ class Vapi extends VapiEventEmitter {
|
|
|
543
558
|
}
|
|
544
559
|
// Stage 5: Video recording setup (if enabled)
|
|
545
560
|
if (isVideoRecordingEnabled && callToken) {
|
|
546
|
-
// The meeting token auto-started the recording at join,
|
|
547
|
-
// nothing to request from the
|
|
561
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
562
|
+
// starts it from its servers, so there is nothing to request from the
|
|
563
|
+
// client.
|
|
548
564
|
this.emit('call-start-progress', {
|
|
549
565
|
stage: 'video-recording-setup',
|
|
550
566
|
status: 'completed',
|
|
551
567
|
timestamp: new Date().toISOString(),
|
|
552
|
-
metadata: {
|
|
568
|
+
metadata: {
|
|
569
|
+
action: isRecordingStartedByServer
|
|
570
|
+
? 'started-by-server'
|
|
571
|
+
: 'auto-started-via-meeting-token'
|
|
572
|
+
}
|
|
553
573
|
});
|
|
554
574
|
}
|
|
555
575
|
else if (isVideoRecordingEnabled) {
|
|
576
|
+
// No token: nothing starts the recording. The recording-error is
|
|
577
|
+
// reported once start() has finished.
|
|
556
578
|
this.emit('call-start-progress', {
|
|
557
579
|
stage: 'video-recording-setup',
|
|
558
|
-
status: '
|
|
559
|
-
timestamp: new Date().toISOString()
|
|
580
|
+
status: 'failed',
|
|
581
|
+
timestamp: new Date().toISOString(),
|
|
582
|
+
metadata: { action: 'no-meeting-token' }
|
|
560
583
|
});
|
|
561
|
-
const recordingStartTime = Date.now();
|
|
562
|
-
try {
|
|
563
|
-
recordingRequestedTime = new Date().getTime();
|
|
564
|
-
this.startRecording();
|
|
565
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
566
|
-
this.emit('call-start-progress', {
|
|
567
|
-
stage: 'video-recording-setup',
|
|
568
|
-
status: 'completed',
|
|
569
|
-
duration: recordingSetupDuration,
|
|
570
|
-
timestamp: new Date().toISOString()
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
575
|
-
const serializedError = serializeError(error);
|
|
576
|
-
this.emit('call-start-progress', {
|
|
577
|
-
stage: 'video-recording-setup',
|
|
578
|
-
status: 'failed',
|
|
579
|
-
duration: recordingSetupDuration,
|
|
580
|
-
timestamp: new Date().toISOString(),
|
|
581
|
-
metadata: { error: serializedError.message }
|
|
582
|
-
});
|
|
583
|
-
this.emit('error', {
|
|
584
|
-
type: 'video-recording-setup-error',
|
|
585
|
-
stage: 'video-recording-setup',
|
|
586
|
-
error: serializedError,
|
|
587
|
-
timestamp: new Date().toISOString()
|
|
588
|
-
});
|
|
589
|
-
// Don't throw here, video recording is optional
|
|
590
|
-
}
|
|
591
584
|
}
|
|
592
585
|
else {
|
|
593
586
|
this.emit('call-start-progress', {
|
|
@@ -640,7 +633,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
640
633
|
if (e)
|
|
641
634
|
this.handleLocalAudioLevel(e);
|
|
642
635
|
});
|
|
643
|
-
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
644
636
|
this.call.on('nonfatal-error', (e) => {
|
|
645
637
|
// https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
|
|
646
638
|
if (e?.type === 'local-audio-level-observer-error') {
|
|
@@ -663,59 +655,66 @@ class Vapi extends VapiEventEmitter {
|
|
|
663
655
|
});
|
|
664
656
|
}
|
|
665
657
|
});
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
stage: 'audio-processing-setup',
|
|
669
|
-
status: 'started',
|
|
670
|
-
timestamp: new Date().toISOString()
|
|
671
|
-
});
|
|
672
|
-
const audioProcessingStartTime = Date.now();
|
|
673
|
-
try {
|
|
674
|
-
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
675
|
-
audio: {
|
|
676
|
-
processor: {
|
|
677
|
-
type: 'noise-cancellation',
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
});
|
|
681
|
-
audioProcessingUpdate.catch((error) => {
|
|
682
|
-
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
683
|
-
});
|
|
684
|
-
// The observer waits for this update rather than starting alongside the
|
|
685
|
-
// remote one above, so that Krisp has already swapped the microphone
|
|
686
|
-
// track. See maybeStartLocalAudioLevelObserver().
|
|
687
|
-
//
|
|
688
|
-
// A separate chain, not another link on the one above: EventEmitter
|
|
689
|
-
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
690
|
-
// listener, and whether the observer starts must not hinge on that.
|
|
691
|
-
audioProcessingUpdate
|
|
692
|
-
.catch(() => { })
|
|
693
|
-
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
694
|
-
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
658
|
+
if (this.clientSideNoiseCancellationEnabled) {
|
|
659
|
+
// Stage 7: Audio processing setup
|
|
695
660
|
this.emit('call-start-progress', {
|
|
696
661
|
stage: 'audio-processing-setup',
|
|
697
|
-
status: '
|
|
698
|
-
duration: audioProcessingDuration,
|
|
662
|
+
status: 'started',
|
|
699
663
|
timestamp: new Date().toISOString()
|
|
700
664
|
});
|
|
665
|
+
const audioProcessingStartTime = Date.now();
|
|
666
|
+
try {
|
|
667
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
668
|
+
audio: {
|
|
669
|
+
processor: {
|
|
670
|
+
type: 'noise-cancellation',
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
audioProcessingUpdate.catch((error) => {
|
|
675
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
676
|
+
});
|
|
677
|
+
// The observer waits for this update rather than starting alongside the
|
|
678
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
679
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
680
|
+
//
|
|
681
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
682
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
683
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
684
|
+
audioProcessingUpdate
|
|
685
|
+
.catch(() => { })
|
|
686
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
687
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
688
|
+
this.emit('call-start-progress', {
|
|
689
|
+
stage: 'audio-processing-setup',
|
|
690
|
+
status: 'completed',
|
|
691
|
+
duration: audioProcessingDuration,
|
|
692
|
+
timestamp: new Date().toISOString()
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
697
|
+
const serializedError = serializeError(error);
|
|
698
|
+
this.emit('call-start-progress', {
|
|
699
|
+
stage: 'audio-processing-setup',
|
|
700
|
+
status: 'failed',
|
|
701
|
+
duration: audioProcessingDuration,
|
|
702
|
+
timestamp: new Date().toISOString(),
|
|
703
|
+
metadata: { error: serializedError.message }
|
|
704
|
+
});
|
|
705
|
+
this.emit('error', {
|
|
706
|
+
type: 'audio-processing-setup-error',
|
|
707
|
+
stage: 'audio-processing-setup',
|
|
708
|
+
error: serializedError,
|
|
709
|
+
timestamp: new Date().toISOString()
|
|
710
|
+
});
|
|
711
|
+
// Don't throw here, this is non-critical
|
|
712
|
+
}
|
|
701
713
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
this.
|
|
706
|
-
stage: 'audio-processing-setup',
|
|
707
|
-
status: 'failed',
|
|
708
|
-
duration: audioProcessingDuration,
|
|
709
|
-
timestamp: new Date().toISOString(),
|
|
710
|
-
metadata: { error: serializedError.message }
|
|
711
|
-
});
|
|
712
|
-
this.emit('error', {
|
|
713
|
-
type: 'audio-processing-setup-error',
|
|
714
|
-
stage: 'audio-processing-setup',
|
|
715
|
-
error: serializedError,
|
|
716
|
-
timestamp: new Date().toISOString()
|
|
717
|
-
});
|
|
718
|
-
// Don't throw here, this is non-critical
|
|
714
|
+
else {
|
|
715
|
+
// Daily's default processor is 'none', so the microphone track is never
|
|
716
|
+
// swapped and the observer has nothing to wait for.
|
|
717
|
+
void this.maybeStartLocalAudioLevelObserver();
|
|
719
718
|
}
|
|
720
719
|
const totalDuration = Date.now() - startTime;
|
|
721
720
|
this.emit('call-start-success', {
|
|
@@ -723,6 +722,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
723
722
|
callId: webCall?.id || 'unknown',
|
|
724
723
|
timestamp: new Date().toISOString()
|
|
725
724
|
});
|
|
725
|
+
if (recordingTokenMissing) {
|
|
726
|
+
this.recordingTokenMissingReportSchedule();
|
|
727
|
+
}
|
|
726
728
|
return webCall;
|
|
727
729
|
}
|
|
728
730
|
catch (e) {
|
|
@@ -772,6 +774,12 @@ class Vapi extends VapiEventEmitter {
|
|
|
772
774
|
else {
|
|
773
775
|
try {
|
|
774
776
|
const parsedMessage = JSON.parse(e.data);
|
|
777
|
+
// Not a Vapi client message: re-emitted as recording-error, and kept
|
|
778
|
+
// out of 'message' because its consumers switch on `type`.
|
|
779
|
+
if (parsedMessage?.type === VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE) {
|
|
780
|
+
this.videoRecordingStartFailedReport(parsedMessage);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
775
783
|
this.emit('message', parsedMessage);
|
|
776
784
|
if (parsedMessage && 'type' in parsedMessage && 'status' in parsedMessage && parsedMessage.type === 'status-update' && parsedMessage.status === 'ended') {
|
|
777
785
|
this.hasEmittedCallEndedStatus = true;
|
|
@@ -939,6 +947,37 @@ class Vapi extends VapiEventEmitter {
|
|
|
939
947
|
console.error(`[vapi] ${event} event listener threw`, error);
|
|
940
948
|
}
|
|
941
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Reported in a later task, like Daily's own recording-error: start() or
|
|
952
|
+
* reconnect() has resolved and the caller's await continuation has run, so a
|
|
953
|
+
* listener registered right after it still hears this, and one that calls
|
|
954
|
+
* stop() can't interrupt call setup. Skipped if that call was stopped or
|
|
955
|
+
* replaced.
|
|
956
|
+
*/
|
|
957
|
+
recordingTokenMissingReportSchedule() {
|
|
958
|
+
const call = this.call;
|
|
959
|
+
if (!call)
|
|
960
|
+
return;
|
|
961
|
+
setTimeout(() => {
|
|
962
|
+
if (!this.started || this.call !== call)
|
|
963
|
+
return;
|
|
964
|
+
this.emitToConsumer('recording-error', {
|
|
965
|
+
action: 'recording-error',
|
|
966
|
+
callClientId: call.callClientId,
|
|
967
|
+
errorMsg: 'Vapi returned no recording token, so the recording was not started. Call startRecording() to try starting it from the client.',
|
|
968
|
+
});
|
|
969
|
+
}, 0);
|
|
970
|
+
}
|
|
971
|
+
/** The fields are read defensively: the app message is untrusted JSON. */
|
|
972
|
+
videoRecordingStartFailedReport(message) {
|
|
973
|
+
const outcome = typeof message.outcome === 'string' ? message.outcome : 'unknown';
|
|
974
|
+
const error = typeof message.error === 'string' ? message.error : 'unknown';
|
|
975
|
+
this.emitToConsumer('recording-error', {
|
|
976
|
+
action: 'recording-error',
|
|
977
|
+
callClientId: this.call?.callClientId ?? '',
|
|
978
|
+
errorMsg: `Vapi couldn't start the recording (${outcome}): ${error}`,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
942
981
|
/** Drops the reference before removing, so nothing hands out a detached element. */
|
|
943
982
|
detachAudioPlayer(participantId) {
|
|
944
983
|
if (this.audioPlayer?.dataset.participantId === participantId) {
|
|
@@ -1133,10 +1172,10 @@ class Vapi extends VapiEventEmitter {
|
|
|
1133
1172
|
/**
|
|
1134
1173
|
* Reconnects to an active call.
|
|
1135
1174
|
*
|
|
1136
|
-
*
|
|
1137
1175
|
* @param webCall
|
|
1176
|
+
* @param options
|
|
1138
1177
|
*/
|
|
1139
|
-
async reconnect(webCall) {
|
|
1178
|
+
async reconnect(webCall, options) {
|
|
1140
1179
|
const startTime = Date.now();
|
|
1141
1180
|
if (this.started) {
|
|
1142
1181
|
throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.');
|
|
@@ -1144,6 +1183,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
1144
1183
|
if (!webCall.webCallUrl) {
|
|
1145
1184
|
throw new Error('webCallUrl is required for reconnection.');
|
|
1146
1185
|
}
|
|
1186
|
+
if (options?.clientSideNoiseCancellationEnabled !== undefined) {
|
|
1187
|
+
this.clientSideNoiseCancellationEnabled = options.clientSideNoiseCancellationEnabled;
|
|
1188
|
+
}
|
|
1147
1189
|
this.emit('call-start-progress', {
|
|
1148
1190
|
stage: 'reconnect-initialization',
|
|
1149
1191
|
status: 'started',
|
|
@@ -1171,11 +1213,14 @@ class Vapi extends VapiEventEmitter {
|
|
|
1171
1213
|
});
|
|
1172
1214
|
}
|
|
1173
1215
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1174
|
-
|
|
1175
|
-
// Same auto-start token as in start(). Rejoining with it while the
|
|
1216
|
+
// Same token as in start(). In client mode, rejoining with it while the
|
|
1176
1217
|
// recording is still running is safe; if the recording stopped when the
|
|
1177
|
-
// user left, the token starts a new one.
|
|
1218
|
+
// user left, the token starts a new one. In server mode it starts
|
|
1219
|
+
// nothing. Without a token the SDK emits recording-error and starts
|
|
1220
|
+
// nothing.
|
|
1178
1221
|
const callToken = webCall?.transport?.callToken;
|
|
1222
|
+
const isRecordingStartedByServer = webCall?.transport?.videoRecordingStartMode === 'server';
|
|
1223
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
1179
1224
|
// Stage 1: Create Daily call object
|
|
1180
1225
|
this.emit('call-start-progress', {
|
|
1181
1226
|
stage: 'daily-call-object-creation',
|
|
@@ -1185,7 +1230,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
1185
1230
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
1186
1231
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
1187
1232
|
isVideoRecordingEnabled,
|
|
1188
|
-
isVideoEnabled
|
|
1189
1233
|
}
|
|
1190
1234
|
});
|
|
1191
1235
|
const dailyCallStartTime = Date.now();
|
|
@@ -1267,7 +1311,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
1267
1311
|
this.call.on('participant-joined', (e) => {
|
|
1268
1312
|
if (!e || !this.call)
|
|
1269
1313
|
return;
|
|
1270
|
-
subscribeToTracks(e, this.call, isVideoRecordingEnabled
|
|
1314
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
1271
1315
|
});
|
|
1272
1316
|
this.call.on('participant-updated', (e) => {
|
|
1273
1317
|
if (!e) {
|
|
@@ -1384,45 +1428,29 @@ class Vapi extends VapiEventEmitter {
|
|
|
1384
1428
|
});
|
|
1385
1429
|
// Stage 4: Video recording setup (if enabled)
|
|
1386
1430
|
if (isVideoRecordingEnabled && callToken) {
|
|
1387
|
-
// The meeting token auto-started the recording at join,
|
|
1388
|
-
// nothing to request from the
|
|
1431
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
1432
|
+
// starts it from its servers, so there is nothing to request from the
|
|
1433
|
+
// client.
|
|
1389
1434
|
this.emit('call-start-progress', {
|
|
1390
1435
|
stage: 'video-recording-setup',
|
|
1391
1436
|
status: 'completed',
|
|
1392
1437
|
timestamp: new Date().toISOString(),
|
|
1393
|
-
metadata: {
|
|
1438
|
+
metadata: {
|
|
1439
|
+
action: isRecordingStartedByServer
|
|
1440
|
+
? 'started-by-server'
|
|
1441
|
+
: 'auto-started-via-meeting-token'
|
|
1442
|
+
}
|
|
1394
1443
|
});
|
|
1395
1444
|
}
|
|
1396
1445
|
else if (isVideoRecordingEnabled) {
|
|
1446
|
+
// No token: nothing starts the recording. The recording-error is
|
|
1447
|
+
// reported once reconnect() has finished.
|
|
1397
1448
|
this.emit('call-start-progress', {
|
|
1398
1449
|
stage: 'video-recording-setup',
|
|
1399
|
-
status: '
|
|
1400
|
-
timestamp: new Date().toISOString()
|
|
1450
|
+
status: 'failed',
|
|
1451
|
+
timestamp: new Date().toISOString(),
|
|
1452
|
+
metadata: { action: 'no-meeting-token' }
|
|
1401
1453
|
});
|
|
1402
|
-
const recordingStartTime = Date.now();
|
|
1403
|
-
try {
|
|
1404
|
-
recordingRequestedTime = new Date().getTime();
|
|
1405
|
-
this.startRecording();
|
|
1406
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1407
|
-
this.emit('call-start-progress', {
|
|
1408
|
-
stage: 'video-recording-setup',
|
|
1409
|
-
status: 'completed',
|
|
1410
|
-
duration: recordingSetupDuration,
|
|
1411
|
-
timestamp: new Date().toISOString()
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
catch (error) {
|
|
1415
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1416
|
-
const serializedError = serializeError(error);
|
|
1417
|
-
this.emit('call-start-progress', {
|
|
1418
|
-
stage: 'video-recording-setup',
|
|
1419
|
-
status: 'failed',
|
|
1420
|
-
duration: recordingSetupDuration,
|
|
1421
|
-
timestamp: new Date().toISOString(),
|
|
1422
|
-
metadata: { error: serializedError.message }
|
|
1423
|
-
});
|
|
1424
|
-
// Don't throw here, video recording is optional
|
|
1425
|
-
}
|
|
1426
1454
|
}
|
|
1427
1455
|
else {
|
|
1428
1456
|
this.emit('call-start-progress', {
|
|
@@ -1461,53 +1489,60 @@ class Vapi extends VapiEventEmitter {
|
|
|
1461
1489
|
});
|
|
1462
1490
|
// Don't throw here, this is non-critical
|
|
1463
1491
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
stage: 'audio-processing-setup',
|
|
1467
|
-
status: 'started',
|
|
1468
|
-
timestamp: new Date().toISOString()
|
|
1469
|
-
});
|
|
1470
|
-
const audioProcessingStartTime = Date.now();
|
|
1471
|
-
try {
|
|
1472
|
-
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1473
|
-
audio: {
|
|
1474
|
-
processor: {
|
|
1475
|
-
type: 'noise-cancellation',
|
|
1476
|
-
},
|
|
1477
|
-
},
|
|
1478
|
-
});
|
|
1479
|
-
audioProcessingUpdate.catch((error) => {
|
|
1480
|
-
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1481
|
-
});
|
|
1482
|
-
// The observer waits for this update rather than starting alongside the
|
|
1483
|
-
// remote one above, so that Krisp has already swapped the microphone
|
|
1484
|
-
// track. See maybeStartLocalAudioLevelObserver().
|
|
1485
|
-
//
|
|
1486
|
-
// A separate chain, not another link on the one above: EventEmitter
|
|
1487
|
-
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1488
|
-
// listener, and whether the observer starts must not hinge on that.
|
|
1489
|
-
audioProcessingUpdate
|
|
1490
|
-
.catch(() => { })
|
|
1491
|
-
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1492
|
-
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1492
|
+
if (this.clientSideNoiseCancellationEnabled) {
|
|
1493
|
+
// Stage 6: Audio processing setup
|
|
1493
1494
|
this.emit('call-start-progress', {
|
|
1494
1495
|
stage: 'audio-processing-setup',
|
|
1495
|
-
status: '
|
|
1496
|
-
duration: audioProcessingDuration,
|
|
1496
|
+
status: 'started',
|
|
1497
1497
|
timestamp: new Date().toISOString()
|
|
1498
1498
|
});
|
|
1499
|
+
const audioProcessingStartTime = Date.now();
|
|
1500
|
+
try {
|
|
1501
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1502
|
+
audio: {
|
|
1503
|
+
processor: {
|
|
1504
|
+
type: 'noise-cancellation',
|
|
1505
|
+
},
|
|
1506
|
+
},
|
|
1507
|
+
});
|
|
1508
|
+
audioProcessingUpdate.catch((error) => {
|
|
1509
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1510
|
+
});
|
|
1511
|
+
// The observer waits for this update rather than starting alongside the
|
|
1512
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
1513
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
1514
|
+
//
|
|
1515
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
1516
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1517
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
1518
|
+
audioProcessingUpdate
|
|
1519
|
+
.catch(() => { })
|
|
1520
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1521
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1522
|
+
this.emit('call-start-progress', {
|
|
1523
|
+
stage: 'audio-processing-setup',
|
|
1524
|
+
status: 'completed',
|
|
1525
|
+
duration: audioProcessingDuration,
|
|
1526
|
+
timestamp: new Date().toISOString()
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
catch (error) {
|
|
1530
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1531
|
+
const serializedError = serializeError(error);
|
|
1532
|
+
this.emit('call-start-progress', {
|
|
1533
|
+
stage: 'audio-processing-setup',
|
|
1534
|
+
status: 'failed',
|
|
1535
|
+
duration: audioProcessingDuration,
|
|
1536
|
+
timestamp: new Date().toISOString(),
|
|
1537
|
+
metadata: { error: serializedError.message }
|
|
1538
|
+
});
|
|
1539
|
+
// Don't throw here, this is non-critical
|
|
1540
|
+
}
|
|
1499
1541
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
this.
|
|
1504
|
-
stage: 'audio-processing-setup',
|
|
1505
|
-
status: 'failed',
|
|
1506
|
-
duration: audioProcessingDuration,
|
|
1507
|
-
timestamp: new Date().toISOString(),
|
|
1508
|
-
metadata: { error: serializedError.message }
|
|
1509
|
-
});
|
|
1510
|
-
// Don't throw here, this is non-critical
|
|
1542
|
+
else {
|
|
1543
|
+
// Daily's default processor is 'none', so the microphone track is never
|
|
1544
|
+
// swapped and the observer has nothing to wait for.
|
|
1545
|
+
void this.maybeStartLocalAudioLevelObserver();
|
|
1511
1546
|
}
|
|
1512
1547
|
const totalDuration = Date.now() - startTime;
|
|
1513
1548
|
this.emit('call-start-success', {
|
|
@@ -1516,6 +1551,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
1516
1551
|
timestamp: new Date().toISOString()
|
|
1517
1552
|
});
|
|
1518
1553
|
this.emit('call-start');
|
|
1554
|
+
if (recordingTokenMissing) {
|
|
1555
|
+
this.recordingTokenMissingReportSchedule();
|
|
1556
|
+
}
|
|
1519
1557
|
}
|
|
1520
1558
|
catch (e) {
|
|
1521
1559
|
const totalDuration = Date.now() - startTime;
|