@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/vapi.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
CreateSquadDTO,
|
|
24
24
|
CreateAssistantDTO,
|
|
25
25
|
AssistantOverrides,
|
|
26
|
+
CreateWebCallDTO,
|
|
26
27
|
CreateWorkflowDTO,
|
|
27
28
|
WorkflowOverrides,
|
|
28
29
|
} from './api';
|
|
@@ -205,9 +206,33 @@ type VapiEventListeners = {
|
|
|
205
206
|
'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
|
|
206
207
|
};
|
|
207
208
|
|
|
209
|
+
type VideoRecordingStartMode = NonNullable<
|
|
210
|
+
CreateWebCallDTO['videoRecordingStartMode']
|
|
211
|
+
>;
|
|
212
|
+
|
|
213
|
+
// The app message Vapi sends when it couldn't start a server-mode recording.
|
|
214
|
+
const VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE = 'video-recording.start-failed';
|
|
215
|
+
|
|
216
|
+
type VideoRecordingStartFailedMessage = {
|
|
217
|
+
type: typeof VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE;
|
|
218
|
+
/** `not-hosting` when Daily saw nobody in the room; `failed` for any other failure. */
|
|
219
|
+
outcome: 'not-hosting' | 'failed';
|
|
220
|
+
/** A short description of the failure. */
|
|
221
|
+
error: string;
|
|
222
|
+
};
|
|
223
|
+
|
|
208
224
|
type StartCallOptions = {
|
|
209
225
|
/** Acquire the microphone before creating a backend call. Defaults to true. */
|
|
210
226
|
microphonePreflightEnabled?: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Run Daily's Krisp noise cancellation on the microphone
|
|
229
|
+
* in the browser, before the audio leaves the device. Set to `false` to send
|
|
230
|
+
* the raw microphone audio. Defaults to true.
|
|
231
|
+
*
|
|
232
|
+
* Separate from the assistant's server-side `backgroundSpeechDenoisingPlan`,
|
|
233
|
+
* which this option does not change.
|
|
234
|
+
*/
|
|
235
|
+
clientSideNoiseCancellationEnabled?: boolean;
|
|
211
236
|
/**
|
|
212
237
|
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
|
|
213
238
|
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
|
|
@@ -217,6 +242,37 @@ type StartCallOptions = {
|
|
|
217
242
|
* @example true
|
|
218
243
|
*/
|
|
219
244
|
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* This determines who starts the video recording. Only applies when video
|
|
247
|
+
* recording is enabled (`artifactPlan.videoRecordingEnabled`).
|
|
248
|
+
*
|
|
249
|
+
* - `client`: the web client starts the recording when it joins.
|
|
250
|
+
* - `server`: Vapi starts the recording once the user has joined, so a weak
|
|
251
|
+
* client connection can't stop it from starting. If Vapi can't set up the
|
|
252
|
+
* server-side start, or the start itself fails, the SDK emits
|
|
253
|
+
* `recording-error`.
|
|
254
|
+
*
|
|
255
|
+
* In `server` mode the SDK never calls `startRecording()` itself. If the
|
|
256
|
+
* server-side start couldn't be set up, calling it yourself may still start
|
|
257
|
+
* the recording.
|
|
258
|
+
*
|
|
259
|
+
* Requires a Vapi API that supports this field; older servers reject the
|
|
260
|
+
* request. The server defaults to `client` when it is not set.
|
|
261
|
+
* @example "server"
|
|
262
|
+
*/
|
|
263
|
+
videoRecordingStartMode?: VideoRecordingStartMode;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
type ReconnectOptions = {
|
|
267
|
+
/**
|
|
268
|
+
* Run Daily's Krisp noise cancellation on the microphone
|
|
269
|
+
* in the browser. Defaults to the value used by the last `start()` or
|
|
270
|
+
* `reconnect()`, or true if neither set it.
|
|
271
|
+
*
|
|
272
|
+
* Separate from the assistant's server-side `backgroundSpeechDenoisingPlan`,
|
|
273
|
+
* which this option does not change.
|
|
274
|
+
*/
|
|
275
|
+
clientSideNoiseCancellationEnabled?: boolean;
|
|
220
276
|
}
|
|
221
277
|
|
|
222
278
|
type WebCall = {
|
|
@@ -244,12 +300,22 @@ type WebCall = {
|
|
|
244
300
|
assistant?: { voice?: { provider?: string } };
|
|
245
301
|
/**
|
|
246
302
|
* The Vapi WebCall transport. When video recording is enabled, the server
|
|
247
|
-
* sets `callToken` to
|
|
248
|
-
* recording on join
|
|
303
|
+
* sets `callToken` to the Daily meeting token to join with. In `client` mode
|
|
304
|
+
* the token auto-starts the cloud recording on join; in `server` mode it
|
|
305
|
+
* starts nothing and Vapi starts the recording. If there is no token, the
|
|
306
|
+
* SDK emits `recording-error` and starts nothing, though `startRecording()`
|
|
307
|
+
* may still start the recording from the client.
|
|
308
|
+
*
|
|
309
|
+
* `videoRecordingStartMode` is `server` when Vapi starts the recording, and
|
|
310
|
+
* is absent when video recording is off or the server-side start couldn't
|
|
311
|
+
* be set up.
|
|
249
312
|
*
|
|
250
313
|
* call.transport
|
|
251
314
|
*/
|
|
252
|
-
transport?: {
|
|
315
|
+
transport?: {
|
|
316
|
+
callToken?: string;
|
|
317
|
+
videoRecordingStartMode?: VideoRecordingStartMode;
|
|
318
|
+
};
|
|
253
319
|
}
|
|
254
320
|
|
|
255
321
|
async function startAudioPlayer(
|
|
@@ -297,14 +363,13 @@ function subscribeToTracks(
|
|
|
297
363
|
e: DailyEventObjectParticipant,
|
|
298
364
|
call: DailyCall,
|
|
299
365
|
isVideoRecordingEnabled?: boolean,
|
|
300
|
-
isVideoEnabled?: boolean,
|
|
301
366
|
) {
|
|
302
367
|
if (e.participant.local) return;
|
|
303
368
|
|
|
304
369
|
call.updateParticipant(e.participant.session_id, {
|
|
305
370
|
setSubscribedTracks: {
|
|
306
371
|
audio: true,
|
|
307
|
-
video: isVideoRecordingEnabled
|
|
372
|
+
video: isVideoRecordingEnabled,
|
|
308
373
|
},
|
|
309
374
|
});
|
|
310
375
|
}
|
|
@@ -360,6 +425,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
360
425
|
private dailyCallObject: DailyFactoryOptions = {};
|
|
361
426
|
private microphoneTrack: MediaStreamTrack | null = null;
|
|
362
427
|
private cancelMicrophonePreflight: (() => void) | null = null;
|
|
428
|
+
private clientSideNoiseCancellationEnabled: boolean = true;
|
|
363
429
|
|
|
364
430
|
private async microphoneAcquire(): Promise<MediaStreamTrack> {
|
|
365
431
|
const source = this.dailyCallObject.audioSource;
|
|
@@ -535,6 +601,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
535
601
|
});
|
|
536
602
|
|
|
537
603
|
this.started = true;
|
|
604
|
+
this.clientSideNoiseCancellationEnabled = options?.clientSideNoiseCancellationEnabled ?? true;
|
|
538
605
|
|
|
539
606
|
let failureStage = 'microphone-preflight';
|
|
540
607
|
try {
|
|
@@ -555,18 +622,23 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
555
622
|
|
|
556
623
|
const webCallStartTime = Date.now();
|
|
557
624
|
|
|
625
|
+
// An unset option stays undefined, so JSON.stringify drops the key and
|
|
626
|
+
// older servers never see it.
|
|
627
|
+
const createWebCallBody: CreateWebCallDTO = {
|
|
628
|
+
assistant: typeof assistant === 'string' ? undefined : assistant,
|
|
629
|
+
assistantId: typeof assistant === 'string' ? assistant : undefined,
|
|
630
|
+
assistantOverrides,
|
|
631
|
+
squad: typeof squad === 'string' ? undefined : squad,
|
|
632
|
+
squadId: typeof squad === 'string' ? squad : undefined,
|
|
633
|
+
workflow: typeof workflow === 'string' ? undefined : workflow,
|
|
634
|
+
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
635
|
+
workflowOverrides,
|
|
636
|
+
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
637
|
+
videoRecordingStartMode: options?.videoRecordingStartMode,
|
|
638
|
+
};
|
|
639
|
+
|
|
558
640
|
const webCall = (
|
|
559
|
-
await client.call.callControllerCreateWebCall(
|
|
560
|
-
assistant: typeof assistant === 'string' ? undefined : assistant,
|
|
561
|
-
assistantId: typeof assistant === 'string' ? assistant : undefined,
|
|
562
|
-
assistantOverrides,
|
|
563
|
-
squad: typeof squad === 'string' ? undefined : squad,
|
|
564
|
-
squadId: typeof squad === 'string' ? squad : undefined,
|
|
565
|
-
workflow: typeof workflow === 'string' ? undefined : workflow,
|
|
566
|
-
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
567
|
-
workflowOverrides,
|
|
568
|
-
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
569
|
-
})
|
|
641
|
+
await client.call.callControllerCreateWebCall(createWebCallBody)
|
|
570
642
|
).data;
|
|
571
643
|
|
|
572
644
|
const webCallDuration = Date.now() - webCallStartTime;
|
|
@@ -585,15 +657,17 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
585
657
|
const isVideoRecordingEnabled =
|
|
586
658
|
webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
587
659
|
|
|
588
|
-
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
589
|
-
|
|
590
660
|
// When video recording is enabled, the server sends a Daily meeting
|
|
591
|
-
// token
|
|
592
|
-
//
|
|
593
|
-
// servers
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
661
|
+
// token. In client mode it auto-starts the cloud recording on join. In
|
|
662
|
+
// server mode it starts nothing: Vapi starts the recording from its
|
|
663
|
+
// servers once the user has joined, which a weak client network can't
|
|
664
|
+
// stop. Without a token the SDK emits recording-error and starts
|
|
665
|
+
// nothing.
|
|
666
|
+
const transport = webCall?.transport as WebCall['transport'];
|
|
667
|
+
const callToken = transport?.callToken;
|
|
668
|
+
const isRecordingStartedByServer =
|
|
669
|
+
transport?.videoRecordingStartMode === 'server';
|
|
670
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
597
671
|
|
|
598
672
|
// Stage 2: Create Daily call object
|
|
599
673
|
this.emit('call-start-progress', {
|
|
@@ -604,7 +678,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
604
678
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
605
679
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
606
680
|
isVideoRecordingEnabled,
|
|
607
|
-
isVideoEnabled
|
|
608
681
|
}
|
|
609
682
|
});
|
|
610
683
|
|
|
@@ -714,12 +787,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
714
787
|
|
|
715
788
|
this.call.on('participant-joined', (e) => {
|
|
716
789
|
if (!e || !this.call) return;
|
|
717
|
-
subscribeToTracks(
|
|
718
|
-
e,
|
|
719
|
-
this.call,
|
|
720
|
-
isVideoRecordingEnabled,
|
|
721
|
-
isVideoEnabled,
|
|
722
|
-
);
|
|
790
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
723
791
|
});
|
|
724
792
|
|
|
725
793
|
this.call.on('participant-updated', (e) => {
|
|
@@ -736,6 +804,11 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
736
804
|
this.detachAudioPlayer(e.participant.session_id);
|
|
737
805
|
});
|
|
738
806
|
|
|
807
|
+
// Registered before join(), as in reconnect(): Vapi starts a server-mode
|
|
808
|
+
// recording when it sees the user join, so its start-failed message can
|
|
809
|
+
// arrive while join() is still in flight.
|
|
810
|
+
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
811
|
+
|
|
739
812
|
// Registered before join(): a token-auto-started recording can begin
|
|
740
813
|
// while join() is still in flight, so a listener added after join
|
|
741
814
|
// could miss the event.
|
|
@@ -836,52 +909,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
836
909
|
|
|
837
910
|
// Stage 5: Video recording setup (if enabled)
|
|
838
911
|
if (isVideoRecordingEnabled && callToken) {
|
|
839
|
-
// The meeting token auto-started the recording at join,
|
|
840
|
-
// nothing to request from the
|
|
912
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
913
|
+
// starts it from its servers, so there is nothing to request from the
|
|
914
|
+
// client.
|
|
841
915
|
this.emit('call-start-progress', {
|
|
842
916
|
stage: 'video-recording-setup',
|
|
843
917
|
status: 'completed',
|
|
844
918
|
timestamp: new Date().toISOString(),
|
|
845
|
-
metadata: {
|
|
919
|
+
metadata: {
|
|
920
|
+
action: isRecordingStartedByServer
|
|
921
|
+
? 'started-by-server'
|
|
922
|
+
: 'auto-started-via-meeting-token'
|
|
923
|
+
}
|
|
846
924
|
});
|
|
847
925
|
} else if (isVideoRecordingEnabled) {
|
|
926
|
+
// No token: nothing starts the recording. The recording-error is
|
|
927
|
+
// reported once start() has finished.
|
|
848
928
|
this.emit('call-start-progress', {
|
|
849
929
|
stage: 'video-recording-setup',
|
|
850
|
-
status: '
|
|
851
|
-
timestamp: new Date().toISOString()
|
|
930
|
+
status: 'failed',
|
|
931
|
+
timestamp: new Date().toISOString(),
|
|
932
|
+
metadata: { action: 'no-meeting-token' }
|
|
852
933
|
});
|
|
853
|
-
|
|
854
|
-
const recordingStartTime = Date.now();
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
recordingRequestedTime = new Date().getTime();
|
|
858
|
-
this.startRecording();
|
|
859
|
-
|
|
860
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
861
|
-
this.emit('call-start-progress', {
|
|
862
|
-
stage: 'video-recording-setup',
|
|
863
|
-
status: 'completed',
|
|
864
|
-
duration: recordingSetupDuration,
|
|
865
|
-
timestamp: new Date().toISOString()
|
|
866
|
-
});
|
|
867
|
-
} catch (error) {
|
|
868
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
869
|
-
const serializedError = serializeError(error);
|
|
870
|
-
this.emit('call-start-progress', {
|
|
871
|
-
stage: 'video-recording-setup',
|
|
872
|
-
status: 'failed',
|
|
873
|
-
duration: recordingSetupDuration,
|
|
874
|
-
timestamp: new Date().toISOString(),
|
|
875
|
-
metadata: { error: serializedError.message }
|
|
876
|
-
});
|
|
877
|
-
this.emit('error', {
|
|
878
|
-
type: 'video-recording-setup-error',
|
|
879
|
-
stage: 'video-recording-setup',
|
|
880
|
-
error: serializedError,
|
|
881
|
-
timestamp: new Date().toISOString()
|
|
882
|
-
});
|
|
883
|
-
// Don't throw here, video recording is optional
|
|
884
|
-
}
|
|
885
934
|
} else {
|
|
886
935
|
this.emit('call-start-progress', {
|
|
887
936
|
stage: 'video-recording-setup',
|
|
@@ -936,8 +985,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
936
985
|
if (e) this.handleLocalAudioLevel(e);
|
|
937
986
|
});
|
|
938
987
|
|
|
939
|
-
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
940
|
-
|
|
941
988
|
this.call.on('nonfatal-error', (e) => {
|
|
942
989
|
// https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
|
|
943
990
|
if (e?.type === 'local-audio-level-observer-error') {
|
|
@@ -961,63 +1008,69 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
961
1008
|
}
|
|
962
1009
|
});
|
|
963
1010
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1011
|
+
if (this.clientSideNoiseCancellationEnabled) {
|
|
1012
|
+
// Stage 7: Audio processing setup
|
|
1013
|
+
this.emit('call-start-progress', {
|
|
1014
|
+
stage: 'audio-processing-setup',
|
|
1015
|
+
status: 'started',
|
|
1016
|
+
timestamp: new Date().toISOString()
|
|
1017
|
+
});
|
|
970
1018
|
|
|
971
|
-
|
|
1019
|
+
const audioProcessingStartTime = Date.now();
|
|
972
1020
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1023
|
+
audio: {
|
|
1024
|
+
processor: {
|
|
1025
|
+
type: 'noise-cancellation',
|
|
1026
|
+
},
|
|
978
1027
|
},
|
|
979
|
-
}
|
|
980
|
-
});
|
|
1028
|
+
});
|
|
981
1029
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1030
|
+
audioProcessingUpdate.catch((error) => {
|
|
1031
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1032
|
+
});
|
|
985
1033
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1034
|
+
// The observer waits for this update rather than starting alongside the
|
|
1035
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
1036
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
1037
|
+
//
|
|
1038
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
1039
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1040
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
1041
|
+
audioProcessingUpdate
|
|
1042
|
+
.catch(() => {})
|
|
1043
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1044
|
+
|
|
1045
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1046
|
+
this.emit('call-start-progress', {
|
|
1047
|
+
stage: 'audio-processing-setup',
|
|
1048
|
+
status: 'completed',
|
|
1049
|
+
duration: audioProcessingDuration,
|
|
1050
|
+
timestamp: new Date().toISOString()
|
|
1051
|
+
});
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1054
|
+
const serializedError = serializeError(error);
|
|
1055
|
+
this.emit('call-start-progress', {
|
|
1056
|
+
stage: 'audio-processing-setup',
|
|
1057
|
+
status: 'failed',
|
|
1058
|
+
duration: audioProcessingDuration,
|
|
1059
|
+
timestamp: new Date().toISOString(),
|
|
1060
|
+
metadata: { error: serializedError.message }
|
|
1061
|
+
});
|
|
1062
|
+
this.emit('error', {
|
|
1063
|
+
type: 'audio-processing-setup-error',
|
|
1064
|
+
stage: 'audio-processing-setup',
|
|
1065
|
+
error: serializedError,
|
|
1066
|
+
timestamp: new Date().toISOString()
|
|
1067
|
+
});
|
|
1068
|
+
// Don't throw here, this is non-critical
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
// Daily's default processor is 'none', so the microphone track is never
|
|
1072
|
+
// swapped and the observer has nothing to wait for.
|
|
1073
|
+
void this.maybeStartLocalAudioLevelObserver();
|
|
1021
1074
|
}
|
|
1022
1075
|
|
|
1023
1076
|
const totalDuration = Date.now() - startTime;
|
|
@@ -1027,6 +1080,10 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1027
1080
|
timestamp: new Date().toISOString()
|
|
1028
1081
|
});
|
|
1029
1082
|
|
|
1083
|
+
if (recordingTokenMissing) {
|
|
1084
|
+
this.recordingTokenMissingReportSchedule();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1030
1087
|
return webCall;
|
|
1031
1088
|
} catch (e) {
|
|
1032
1089
|
const totalDuration = Date.now() - startTime;
|
|
@@ -1078,6 +1135,12 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1078
1135
|
} else {
|
|
1079
1136
|
try {
|
|
1080
1137
|
const parsedMessage = JSON.parse(e.data);
|
|
1138
|
+
// Not a Vapi client message: re-emitted as recording-error, and kept
|
|
1139
|
+
// out of 'message' because its consumers switch on `type`.
|
|
1140
|
+
if (parsedMessage?.type === VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE) {
|
|
1141
|
+
this.videoRecordingStartFailedReport(parsedMessage);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1081
1144
|
this.emit('message', parsedMessage);
|
|
1082
1145
|
if (parsedMessage && 'type' in parsedMessage && 'status' in parsedMessage && parsedMessage.type === 'status-update' && parsedMessage.status === 'ended') {
|
|
1083
1146
|
this.hasEmittedCallEndedStatus = true;
|
|
@@ -1276,6 +1339,41 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1276
1339
|
}
|
|
1277
1340
|
}
|
|
1278
1341
|
|
|
1342
|
+
/**
|
|
1343
|
+
* Reported in a later task, like Daily's own recording-error: start() or
|
|
1344
|
+
* reconnect() has resolved and the caller's await continuation has run, so a
|
|
1345
|
+
* listener registered right after it still hears this, and one that calls
|
|
1346
|
+
* stop() can't interrupt call setup. Skipped if that call was stopped or
|
|
1347
|
+
* replaced.
|
|
1348
|
+
*/
|
|
1349
|
+
private recordingTokenMissingReportSchedule() {
|
|
1350
|
+
const call = this.call;
|
|
1351
|
+
if (!call) return;
|
|
1352
|
+
setTimeout(() => {
|
|
1353
|
+
if (!this.started || this.call !== call) return;
|
|
1354
|
+
this.emitToConsumer('recording-error', {
|
|
1355
|
+
action: 'recording-error',
|
|
1356
|
+
callClientId: call.callClientId,
|
|
1357
|
+
errorMsg:
|
|
1358
|
+
'Vapi returned no recording token, so the recording was not started. Call startRecording() to try starting it from the client.',
|
|
1359
|
+
});
|
|
1360
|
+
}, 0);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** The fields are read defensively: the app message is untrusted JSON. */
|
|
1364
|
+
private videoRecordingStartFailedReport(
|
|
1365
|
+
message: Partial<VideoRecordingStartFailedMessage>,
|
|
1366
|
+
) {
|
|
1367
|
+
const outcome =
|
|
1368
|
+
typeof message.outcome === 'string' ? message.outcome : 'unknown';
|
|
1369
|
+
const error = typeof message.error === 'string' ? message.error : 'unknown';
|
|
1370
|
+
this.emitToConsumer('recording-error', {
|
|
1371
|
+
action: 'recording-error',
|
|
1372
|
+
callClientId: this.call?.callClientId ?? '',
|
|
1373
|
+
errorMsg: `Vapi couldn't start the recording (${outcome}): ${error}`,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1279
1377
|
/** Drops the reference before removing, so nothing hands out a detached element. */
|
|
1280
1378
|
private detachAudioPlayer(participantId: string) {
|
|
1281
1379
|
if (this.audioPlayer?.dataset.participantId === participantId) {
|
|
@@ -1498,11 +1596,11 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1498
1596
|
|
|
1499
1597
|
/**
|
|
1500
1598
|
* Reconnects to an active call.
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
* @param
|
|
1599
|
+
*
|
|
1600
|
+
* @param webCall
|
|
1601
|
+
* @param options
|
|
1504
1602
|
*/
|
|
1505
|
-
async reconnect(webCall: WebCall): Promise<void> {
|
|
1603
|
+
async reconnect(webCall: WebCall, options?: ReconnectOptions): Promise<void> {
|
|
1506
1604
|
const startTime = Date.now();
|
|
1507
1605
|
|
|
1508
1606
|
if (this.started) {
|
|
@@ -1513,6 +1611,10 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1513
1611
|
throw new Error('webCallUrl is required for reconnection.');
|
|
1514
1612
|
}
|
|
1515
1613
|
|
|
1614
|
+
if (options?.clientSideNoiseCancellationEnabled !== undefined) {
|
|
1615
|
+
this.clientSideNoiseCancellationEnabled = options.clientSideNoiseCancellationEnabled;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1516
1618
|
this.emit('call-start-progress', {
|
|
1517
1619
|
stage: 'reconnect-initialization',
|
|
1518
1620
|
status: 'started',
|
|
@@ -1543,12 +1645,16 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1543
1645
|
}
|
|
1544
1646
|
|
|
1545
1647
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1546
|
-
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1547
1648
|
|
|
1548
|
-
// Same
|
|
1649
|
+
// Same token as in start(). In client mode, rejoining with it while the
|
|
1549
1650
|
// recording is still running is safe; if the recording stopped when the
|
|
1550
|
-
// user left, the token starts a new one.
|
|
1651
|
+
// user left, the token starts a new one. In server mode it starts
|
|
1652
|
+
// nothing. Without a token the SDK emits recording-error and starts
|
|
1653
|
+
// nothing.
|
|
1551
1654
|
const callToken = webCall?.transport?.callToken;
|
|
1655
|
+
const isRecordingStartedByServer =
|
|
1656
|
+
webCall?.transport?.videoRecordingStartMode === 'server';
|
|
1657
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
1552
1658
|
|
|
1553
1659
|
// Stage 1: Create Daily call object
|
|
1554
1660
|
this.emit('call-start-progress', {
|
|
@@ -1559,7 +1665,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1559
1665
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
1560
1666
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
1561
1667
|
isVideoRecordingEnabled,
|
|
1562
|
-
isVideoEnabled
|
|
1563
1668
|
}
|
|
1564
1669
|
});
|
|
1565
1670
|
|
|
@@ -1650,12 +1755,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1650
1755
|
|
|
1651
1756
|
this.call.on('participant-joined', (e) => {
|
|
1652
1757
|
if (!e || !this.call) return;
|
|
1653
|
-
subscribeToTracks(
|
|
1654
|
-
e,
|
|
1655
|
-
this.call,
|
|
1656
|
-
isVideoRecordingEnabled,
|
|
1657
|
-
isVideoEnabled,
|
|
1658
|
-
);
|
|
1758
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
1659
1759
|
});
|
|
1660
1760
|
|
|
1661
1761
|
this.call.on('participant-updated', (e) => {
|
|
@@ -1783,46 +1883,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1783
1883
|
|
|
1784
1884
|
// Stage 4: Video recording setup (if enabled)
|
|
1785
1885
|
if (isVideoRecordingEnabled && callToken) {
|
|
1786
|
-
// The meeting token auto-started the recording at join,
|
|
1787
|
-
// nothing to request from the
|
|
1886
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
1887
|
+
// starts it from its servers, so there is nothing to request from the
|
|
1888
|
+
// client.
|
|
1788
1889
|
this.emit('call-start-progress', {
|
|
1789
1890
|
stage: 'video-recording-setup',
|
|
1790
1891
|
status: 'completed',
|
|
1791
1892
|
timestamp: new Date().toISOString(),
|
|
1792
|
-
metadata: {
|
|
1893
|
+
metadata: {
|
|
1894
|
+
action: isRecordingStartedByServer
|
|
1895
|
+
? 'started-by-server'
|
|
1896
|
+
: 'auto-started-via-meeting-token'
|
|
1897
|
+
}
|
|
1793
1898
|
});
|
|
1794
1899
|
} else if (isVideoRecordingEnabled) {
|
|
1900
|
+
// No token: nothing starts the recording. The recording-error is
|
|
1901
|
+
// reported once reconnect() has finished.
|
|
1795
1902
|
this.emit('call-start-progress', {
|
|
1796
1903
|
stage: 'video-recording-setup',
|
|
1797
|
-
status: '
|
|
1798
|
-
timestamp: new Date().toISOString()
|
|
1904
|
+
status: 'failed',
|
|
1905
|
+
timestamp: new Date().toISOString(),
|
|
1906
|
+
metadata: { action: 'no-meeting-token' }
|
|
1799
1907
|
});
|
|
1800
|
-
|
|
1801
|
-
const recordingStartTime = Date.now();
|
|
1802
|
-
|
|
1803
|
-
try {
|
|
1804
|
-
recordingRequestedTime = new Date().getTime();
|
|
1805
|
-
this.startRecording();
|
|
1806
|
-
|
|
1807
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1808
|
-
this.emit('call-start-progress', {
|
|
1809
|
-
stage: 'video-recording-setup',
|
|
1810
|
-
status: 'completed',
|
|
1811
|
-
duration: recordingSetupDuration,
|
|
1812
|
-
timestamp: new Date().toISOString()
|
|
1813
|
-
});
|
|
1814
|
-
} catch (error) {
|
|
1815
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1816
|
-
const serializedError = serializeError(error);
|
|
1817
|
-
this.emit('call-start-progress', {
|
|
1818
|
-
stage: 'video-recording-setup',
|
|
1819
|
-
status: 'failed',
|
|
1820
|
-
duration: recordingSetupDuration,
|
|
1821
|
-
timestamp: new Date().toISOString(),
|
|
1822
|
-
metadata: { error: serializedError.message }
|
|
1823
|
-
});
|
|
1824
|
-
// Don't throw here, video recording is optional
|
|
1825
|
-
}
|
|
1826
1908
|
} else {
|
|
1827
1909
|
this.emit('call-start-progress', {
|
|
1828
1910
|
stage: 'video-recording-setup',
|
|
@@ -1863,57 +1945,63 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1863
1945
|
// Don't throw here, this is non-critical
|
|
1864
1946
|
}
|
|
1865
1947
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1948
|
+
if (this.clientSideNoiseCancellationEnabled) {
|
|
1949
|
+
// Stage 6: Audio processing setup
|
|
1950
|
+
this.emit('call-start-progress', {
|
|
1951
|
+
stage: 'audio-processing-setup',
|
|
1952
|
+
status: 'started',
|
|
1953
|
+
timestamp: new Date().toISOString()
|
|
1954
|
+
});
|
|
1872
1955
|
|
|
1873
|
-
|
|
1956
|
+
const audioProcessingStartTime = Date.now();
|
|
1874
1957
|
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1958
|
+
try {
|
|
1959
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1960
|
+
audio: {
|
|
1961
|
+
processor: {
|
|
1962
|
+
type: 'noise-cancellation',
|
|
1963
|
+
},
|
|
1880
1964
|
},
|
|
1881
|
-
}
|
|
1882
|
-
});
|
|
1965
|
+
});
|
|
1883
1966
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1967
|
+
audioProcessingUpdate.catch((error) => {
|
|
1968
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1969
|
+
});
|
|
1887
1970
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1971
|
+
// The observer waits for this update rather than starting alongside the
|
|
1972
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
1973
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
1974
|
+
//
|
|
1975
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
1976
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1977
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
1978
|
+
audioProcessingUpdate
|
|
1979
|
+
.catch(() => {})
|
|
1980
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1981
|
+
|
|
1982
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1983
|
+
this.emit('call-start-progress', {
|
|
1984
|
+
stage: 'audio-processing-setup',
|
|
1985
|
+
status: 'completed',
|
|
1986
|
+
duration: audioProcessingDuration,
|
|
1987
|
+
timestamp: new Date().toISOString()
|
|
1988
|
+
});
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1991
|
+
const serializedError = serializeError(error);
|
|
1992
|
+
this.emit('call-start-progress', {
|
|
1993
|
+
stage: 'audio-processing-setup',
|
|
1994
|
+
status: 'failed',
|
|
1995
|
+
duration: audioProcessingDuration,
|
|
1996
|
+
timestamp: new Date().toISOString(),
|
|
1997
|
+
metadata: { error: serializedError.message }
|
|
1998
|
+
});
|
|
1999
|
+
// Don't throw here, this is non-critical
|
|
2000
|
+
}
|
|
2001
|
+
} else {
|
|
2002
|
+
// Daily's default processor is 'none', so the microphone track is never
|
|
2003
|
+
// swapped and the observer has nothing to wait for.
|
|
2004
|
+
void this.maybeStartLocalAudioLevelObserver();
|
|
1917
2005
|
}
|
|
1918
2006
|
|
|
1919
2007
|
const totalDuration = Date.now() - startTime;
|
|
@@ -1925,6 +2013,10 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1925
2013
|
|
|
1926
2014
|
this.emit('call-start');
|
|
1927
2015
|
|
|
2016
|
+
if (recordingTokenMissing) {
|
|
2017
|
+
this.recordingTokenMissingReportSchedule();
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1928
2020
|
} catch (e) {
|
|
1929
2021
|
const totalDuration = Date.now() - startTime;
|
|
1930
2022
|
const serializedError = serializeError(e);
|