@vapi-ai/web 2.3.10 → 2.5.0
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/api.ts +2293 -504
- package/dist/api.d.ts +1515 -212
- package/dist/api.js +156 -2
- package/dist/vapi.d.ts +71 -3
- package/dist/vapi.js +403 -10
- package/package.json +4 -3
- package/vapi.ts +491 -10
package/vapi.ts
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
safeSetInputDevicesAsync,
|
|
29
29
|
} from './daily-guards';
|
|
30
30
|
|
|
31
|
+
export interface EndCallMessage {
|
|
32
|
+
type: 'end-call';
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
export interface AddMessageMessage {
|
|
32
36
|
type: 'add-message';
|
|
33
37
|
message: ChatCompletionMessageParam;
|
|
@@ -51,7 +55,8 @@ export interface SayMessage {
|
|
|
51
55
|
type VapiClientToServerMessage =
|
|
52
56
|
| AddMessageMessage
|
|
53
57
|
| ControlMessages
|
|
54
|
-
| SayMessage
|
|
58
|
+
| SayMessage
|
|
59
|
+
| EndCallMessage;
|
|
55
60
|
|
|
56
61
|
type VapiEventNames =
|
|
57
62
|
| 'call-end'
|
|
@@ -111,6 +116,43 @@ type VapiEventListeners = {
|
|
|
111
116
|
'call-start-failed': (event: CallStartFailedEvent) => void;
|
|
112
117
|
};
|
|
113
118
|
|
|
119
|
+
type StartCallOptions = {
|
|
120
|
+
/**
|
|
121
|
+
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
|
|
122
|
+
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
|
|
123
|
+
* If set to `true`, the room will be deleted and reconnection will not be allowed.
|
|
124
|
+
*
|
|
125
|
+
* Defaults to `true`.
|
|
126
|
+
* @example true
|
|
127
|
+
*/
|
|
128
|
+
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type WebCall = {
|
|
132
|
+
/**
|
|
133
|
+
* The Vapi WebCall URL. This is the URL that the call will be joined on.
|
|
134
|
+
*
|
|
135
|
+
* call.webCallUrl or call.transport.callUrl
|
|
136
|
+
*/
|
|
137
|
+
webCallUrl: string;
|
|
138
|
+
/**
|
|
139
|
+
* The Vapi WebCall ID. This is the ID of the call.
|
|
140
|
+
*
|
|
141
|
+
* call.id
|
|
142
|
+
*/
|
|
143
|
+
id?: string;
|
|
144
|
+
/**
|
|
145
|
+
* The Vapi WebCall artifact plan. This is the artifact plan of the call.
|
|
146
|
+
*/
|
|
147
|
+
artifactPlan?: { videoRecordingEnabled?: boolean };
|
|
148
|
+
/**
|
|
149
|
+
* The Vapi WebCall assistant. This is the assistant of the call.
|
|
150
|
+
*
|
|
151
|
+
* call.assistant
|
|
152
|
+
*/
|
|
153
|
+
assistant?: { voice?: { provider?: string } };
|
|
154
|
+
}
|
|
155
|
+
|
|
114
156
|
async function startAudioPlayer(
|
|
115
157
|
player: HTMLAudioElement,
|
|
116
158
|
track: MediaStreamTrack,
|
|
@@ -216,11 +258,13 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
216
258
|
this.dailyCallObject = createSafeDailyFactoryOptions(dailyCallObject);
|
|
217
259
|
}
|
|
218
260
|
|
|
219
|
-
private cleanup() {
|
|
261
|
+
private async cleanup() {
|
|
220
262
|
this.started = false;
|
|
221
263
|
this.hasEmittedCallEndedStatus = false;
|
|
222
|
-
this.call
|
|
223
|
-
|
|
264
|
+
if (this.call) {
|
|
265
|
+
await this.call.destroy();
|
|
266
|
+
this.call = null;
|
|
267
|
+
}
|
|
224
268
|
this.speakingTimeout = null;
|
|
225
269
|
}
|
|
226
270
|
|
|
@@ -244,6 +288,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
244
288
|
squad?: CreateSquadDTO | string,
|
|
245
289
|
workflow?: CreateWorkflowDTO | string,
|
|
246
290
|
workflowOverrides?: WorkflowOverrides,
|
|
291
|
+
options?: StartCallOptions
|
|
247
292
|
): Promise<Call | null> {
|
|
248
293
|
const startTime = Date.now();
|
|
249
294
|
|
|
@@ -302,6 +347,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
302
347
|
workflow: typeof workflow === 'string' ? undefined : workflow,
|
|
303
348
|
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
304
349
|
workflowOverrides,
|
|
350
|
+
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
305
351
|
})
|
|
306
352
|
).data;
|
|
307
353
|
|
|
@@ -325,7 +371,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
325
371
|
timestamp: new Date().toISOString(),
|
|
326
372
|
metadata: { action: 'cleanup-existing' }
|
|
327
373
|
});
|
|
328
|
-
this.cleanup();
|
|
374
|
+
await this.cleanup();
|
|
329
375
|
}
|
|
330
376
|
|
|
331
377
|
const isVideoRecordingEnabled =
|
|
@@ -395,7 +441,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
395
441
|
if (isVideoRecordingEnabled) {
|
|
396
442
|
this.call?.stopRecording();
|
|
397
443
|
}
|
|
398
|
-
this.cleanup();
|
|
444
|
+
this.cleanup().catch(console.error);
|
|
399
445
|
});
|
|
400
446
|
|
|
401
447
|
this.call.on('error', (error: any) => {
|
|
@@ -743,7 +789,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
743
789
|
}
|
|
744
790
|
});
|
|
745
791
|
|
|
746
|
-
this.cleanup();
|
|
792
|
+
await this.cleanup();
|
|
747
793
|
return null;
|
|
748
794
|
}
|
|
749
795
|
}
|
|
@@ -800,12 +846,26 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
800
846
|
}, 1000);
|
|
801
847
|
}
|
|
802
848
|
|
|
803
|
-
|
|
849
|
+
/**
|
|
850
|
+
* Stops the call by destroying the Daily call object.
|
|
851
|
+
*
|
|
852
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `false`, the Vapi call will be kept alive, allowing reconnections to the same call using the `reconnect` method.
|
|
853
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections.
|
|
854
|
+
*/
|
|
855
|
+
async stop(): Promise<void> {
|
|
804
856
|
this.started = false;
|
|
805
|
-
this.call
|
|
806
|
-
|
|
857
|
+
if (this.call) {
|
|
858
|
+
await this.call.destroy();
|
|
859
|
+
this.call = null;
|
|
860
|
+
}
|
|
861
|
+
this.speakingTimeout = null;
|
|
807
862
|
}
|
|
808
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Sends a Live Call Control message to the Vapi server.
|
|
866
|
+
*
|
|
867
|
+
* Docs: https://docs.vapi.ai/calls/call-features
|
|
868
|
+
*/
|
|
809
869
|
send(message: VapiClientToServerMessage): void {
|
|
810
870
|
this.call?.sendAppMessage(JSON.stringify(message));
|
|
811
871
|
}
|
|
@@ -832,6 +892,18 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
832
892
|
});
|
|
833
893
|
}
|
|
834
894
|
|
|
895
|
+
/**
|
|
896
|
+
* Ends the call immediately by sending a `end-call` message using Live Call Control, and destroys the Daily call object.
|
|
897
|
+
*
|
|
898
|
+
* This method always ends the call, regardless of the `roomDeleteOnUserLeaveEnabled` option.
|
|
899
|
+
*/
|
|
900
|
+
public end() {
|
|
901
|
+
this.send({
|
|
902
|
+
type: 'end-call',
|
|
903
|
+
});
|
|
904
|
+
this.stop();
|
|
905
|
+
}
|
|
906
|
+
|
|
835
907
|
public setInputDevicesAsync(
|
|
836
908
|
options: Parameters<DailyCall['setInputDevicesAsync']>[0],
|
|
837
909
|
) {
|
|
@@ -884,4 +956,413 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
884
956
|
public stopScreenSharing() {
|
|
885
957
|
this.call?.stopScreenShare();
|
|
886
958
|
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Reconnects to an active call.
|
|
962
|
+
*
|
|
963
|
+
*
|
|
964
|
+
* @param webCall
|
|
965
|
+
*/
|
|
966
|
+
async reconnect(webCall: WebCall): Promise<void> {
|
|
967
|
+
const startTime = Date.now();
|
|
968
|
+
|
|
969
|
+
if (this.started) {
|
|
970
|
+
throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (!webCall.webCallUrl) {
|
|
974
|
+
throw new Error('webCallUrl is required for reconnection.');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
this.emit('call-start-progress', {
|
|
978
|
+
stage: 'reconnect-initialization',
|
|
979
|
+
status: 'started',
|
|
980
|
+
timestamp: new Date().toISOString(),
|
|
981
|
+
metadata: {
|
|
982
|
+
callId: webCall.id || 'unknown',
|
|
983
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
984
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown'
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
this.started = true;
|
|
989
|
+
|
|
990
|
+
try {
|
|
991
|
+
// Clean up any existing call object
|
|
992
|
+
if (this.call) {
|
|
993
|
+
this.emit('call-start-progress', {
|
|
994
|
+
stage: 'cleanup-existing-call',
|
|
995
|
+
status: 'started',
|
|
996
|
+
timestamp: new Date().toISOString()
|
|
997
|
+
});
|
|
998
|
+
await this.cleanup();
|
|
999
|
+
this.emit('call-start-progress', {
|
|
1000
|
+
stage: 'cleanup-existing-call',
|
|
1001
|
+
status: 'completed',
|
|
1002
|
+
timestamp: new Date().toISOString()
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1007
|
+
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1008
|
+
|
|
1009
|
+
// Stage 1: Create Daily call object
|
|
1010
|
+
this.emit('call-start-progress', {
|
|
1011
|
+
stage: 'daily-call-object-creation',
|
|
1012
|
+
status: 'started',
|
|
1013
|
+
timestamp: new Date().toISOString(),
|
|
1014
|
+
metadata: {
|
|
1015
|
+
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
1016
|
+
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
1017
|
+
isVideoRecordingEnabled,
|
|
1018
|
+
isVideoEnabled
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const dailyCallStartTime = Date.now();
|
|
1023
|
+
this.call = DailyIframe.createCallObject({
|
|
1024
|
+
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
1025
|
+
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
1026
|
+
dailyConfig: this.dailyCallConfig,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const dailyCallDuration = Date.now() - dailyCallStartTime;
|
|
1030
|
+
this.emit('call-start-progress', {
|
|
1031
|
+
stage: 'daily-call-object-creation',
|
|
1032
|
+
status: 'completed',
|
|
1033
|
+
duration: dailyCallDuration,
|
|
1034
|
+
timestamp: new Date().toISOString()
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
this.call.iframe()?.style.setProperty('display', 'none');
|
|
1038
|
+
|
|
1039
|
+
// Set up event listeners
|
|
1040
|
+
this.call.on('left-meeting', () => {
|
|
1041
|
+
this.emit('call-end');
|
|
1042
|
+
if (!this.hasEmittedCallEndedStatus) {
|
|
1043
|
+
this.emit('message', {
|
|
1044
|
+
type: 'status-update',
|
|
1045
|
+
status: 'ended',
|
|
1046
|
+
'endedReason': 'customer-ended-call',
|
|
1047
|
+
});
|
|
1048
|
+
this.hasEmittedCallEndedStatus = true;
|
|
1049
|
+
}
|
|
1050
|
+
if (isVideoRecordingEnabled) {
|
|
1051
|
+
this.call?.stopRecording();
|
|
1052
|
+
}
|
|
1053
|
+
this.cleanup().catch(console.error);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
this.call.on('error', (error: any) => {
|
|
1057
|
+
this.emit('error', error);
|
|
1058
|
+
if (isVideoRecordingEnabled) {
|
|
1059
|
+
this.call?.stopRecording();
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
this.call.on('camera-error', (error: any) => {
|
|
1064
|
+
this.emit('camera-error', error);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
this.call.on('network-quality-change', (event: any) => {
|
|
1068
|
+
this.emit('network-quality-change', event);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
this.call.on('network-connection', (event: any) => {
|
|
1072
|
+
this.emit('network-connection', event);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
this.call.on('track-started', async (e) => {
|
|
1076
|
+
if (!e || !e.participant) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (e.participant?.local) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (e.participant?.user_name !== 'Vapi Speaker') {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (e.track.kind === 'video') {
|
|
1086
|
+
this.emit('video', e.track);
|
|
1087
|
+
}
|
|
1088
|
+
if (e.track.kind === 'audio') {
|
|
1089
|
+
await buildAudioPlayer(e.track, e.participant.session_id);
|
|
1090
|
+
}
|
|
1091
|
+
this.call?.sendAppMessage('playable');
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
this.call.on('participant-joined', (e) => {
|
|
1095
|
+
if (!e || !this.call) return;
|
|
1096
|
+
subscribeToTracks(
|
|
1097
|
+
e,
|
|
1098
|
+
this.call,
|
|
1099
|
+
isVideoRecordingEnabled,
|
|
1100
|
+
isVideoEnabled,
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
this.call.on('participant-updated', (e) => {
|
|
1105
|
+
if (!e) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.emit('daily-participant-updated', e.participant);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
this.call.on('participant-left', (e) => {
|
|
1112
|
+
if (!e) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
destroyAudioPlayer(e.participant.session_id);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
this.call.on('remote-participants-audio-level', (e) => {
|
|
1119
|
+
if (e) this.handleRemoteParticipantsAudioLevel(e);
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
1123
|
+
|
|
1124
|
+
this.call.on('nonfatal-error', (e) => {
|
|
1125
|
+
// https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
|
|
1126
|
+
if (e?.type === 'audio-processor-error') {
|
|
1127
|
+
this.call
|
|
1128
|
+
?.updateInputSettings({
|
|
1129
|
+
audio: {
|
|
1130
|
+
processor: {
|
|
1131
|
+
type: 'none',
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
})
|
|
1135
|
+
.then(() => {
|
|
1136
|
+
safeSetLocalAudio(this.call, true);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
// Stage 2: Mobile device handling and permissions
|
|
1142
|
+
const isMobile = this.isMobileDevice();
|
|
1143
|
+
this.emit('call-start-progress', {
|
|
1144
|
+
stage: 'mobile-permissions',
|
|
1145
|
+
status: 'started',
|
|
1146
|
+
timestamp: new Date().toISOString(),
|
|
1147
|
+
metadata: { isMobile }
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
if (isMobile) {
|
|
1151
|
+
const mobileWaitStartTime = Date.now();
|
|
1152
|
+
await this.sleep(1000);
|
|
1153
|
+
const mobileWaitDuration = Date.now() - mobileWaitStartTime;
|
|
1154
|
+
this.emit('call-start-progress', {
|
|
1155
|
+
stage: 'mobile-permissions',
|
|
1156
|
+
status: 'completed',
|
|
1157
|
+
duration: mobileWaitDuration,
|
|
1158
|
+
timestamp: new Date().toISOString(),
|
|
1159
|
+
metadata: { action: 'permissions-wait' }
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
this.emit('call-start-progress', {
|
|
1163
|
+
stage: 'mobile-permissions',
|
|
1164
|
+
status: 'completed',
|
|
1165
|
+
timestamp: new Date().toISOString(),
|
|
1166
|
+
metadata: { action: 'skipped-not-mobile' }
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Stage 3: Join the call
|
|
1171
|
+
this.emit('call-start-progress', {
|
|
1172
|
+
stage: 'daily-call-join',
|
|
1173
|
+
status: 'started',
|
|
1174
|
+
timestamp: new Date().toISOString()
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
const joinStartTime = Date.now();
|
|
1178
|
+
await this.call.join({
|
|
1179
|
+
url: webCall.webCallUrl,
|
|
1180
|
+
subscribeToTracksAutomatically: false,
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
const joinDuration = Date.now() - joinStartTime;
|
|
1184
|
+
this.emit('call-start-progress', {
|
|
1185
|
+
stage: 'daily-call-join',
|
|
1186
|
+
status: 'completed',
|
|
1187
|
+
duration: joinDuration,
|
|
1188
|
+
timestamp: new Date().toISOString()
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// Stage 4: Video recording setup (if enabled)
|
|
1192
|
+
if (isVideoRecordingEnabled) {
|
|
1193
|
+
this.emit('call-start-progress', {
|
|
1194
|
+
stage: 'video-recording-setup',
|
|
1195
|
+
status: 'started',
|
|
1196
|
+
timestamp: new Date().toISOString()
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
const recordingStartTime = Date.now();
|
|
1200
|
+
const recordingRequestedTime = new Date().getTime();
|
|
1201
|
+
|
|
1202
|
+
try {
|
|
1203
|
+
this.call.startRecording({
|
|
1204
|
+
width: 1280,
|
|
1205
|
+
height: 720,
|
|
1206
|
+
backgroundColor: '#FF1F2D3D',
|
|
1207
|
+
layout: {
|
|
1208
|
+
preset: 'default',
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1213
|
+
this.emit('call-start-progress', {
|
|
1214
|
+
stage: 'video-recording-setup',
|
|
1215
|
+
status: 'completed',
|
|
1216
|
+
duration: recordingSetupDuration,
|
|
1217
|
+
timestamp: new Date().toISOString()
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
this.call.on('recording-started', () => {
|
|
1221
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1222
|
+
this.emit('call-start-progress', {
|
|
1223
|
+
stage: 'video-recording-started',
|
|
1224
|
+
status: 'completed',
|
|
1225
|
+
timestamp: new Date().toISOString(),
|
|
1226
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
this.send({
|
|
1230
|
+
type: 'control',
|
|
1231
|
+
control: 'say-first-message',
|
|
1232
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1237
|
+
this.emit('call-start-progress', {
|
|
1238
|
+
stage: 'video-recording-setup',
|
|
1239
|
+
status: 'failed',
|
|
1240
|
+
duration: recordingSetupDuration,
|
|
1241
|
+
timestamp: new Date().toISOString(),
|
|
1242
|
+
metadata: { error: error?.toString() }
|
|
1243
|
+
});
|
|
1244
|
+
// Don't throw here, video recording is optional
|
|
1245
|
+
}
|
|
1246
|
+
} else {
|
|
1247
|
+
this.emit('call-start-progress', {
|
|
1248
|
+
stage: 'video-recording-setup',
|
|
1249
|
+
status: 'completed',
|
|
1250
|
+
timestamp: new Date().toISOString(),
|
|
1251
|
+
metadata: { action: 'skipped-not-enabled' }
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Stage 5: Audio level observer setup
|
|
1256
|
+
this.emit('call-start-progress', {
|
|
1257
|
+
stage: 'audio-observer-setup',
|
|
1258
|
+
status: 'started',
|
|
1259
|
+
timestamp: new Date().toISOString()
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const audioObserverStartTime = Date.now();
|
|
1263
|
+
|
|
1264
|
+
try {
|
|
1265
|
+
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
1266
|
+
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
1267
|
+
this.emit('call-start-progress', {
|
|
1268
|
+
stage: 'audio-observer-setup',
|
|
1269
|
+
status: 'completed',
|
|
1270
|
+
duration: audioObserverDuration,
|
|
1271
|
+
timestamp: new Date().toISOString()
|
|
1272
|
+
});
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
1275
|
+
this.emit('call-start-progress', {
|
|
1276
|
+
stage: 'audio-observer-setup',
|
|
1277
|
+
status: 'failed',
|
|
1278
|
+
duration: audioObserverDuration,
|
|
1279
|
+
timestamp: new Date().toISOString(),
|
|
1280
|
+
metadata: { error: error?.toString() }
|
|
1281
|
+
});
|
|
1282
|
+
// Don't throw here, this is non-critical
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Stage 6: Audio processing setup
|
|
1286
|
+
this.emit('call-start-progress', {
|
|
1287
|
+
stage: 'audio-processing-setup',
|
|
1288
|
+
status: 'started',
|
|
1289
|
+
timestamp: new Date().toISOString()
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const audioProcessingStartTime = Date.now();
|
|
1293
|
+
|
|
1294
|
+
try {
|
|
1295
|
+
this.call.updateInputSettings({
|
|
1296
|
+
audio: {
|
|
1297
|
+
processor: {
|
|
1298
|
+
type: 'noise-cancellation',
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1304
|
+
this.emit('call-start-progress', {
|
|
1305
|
+
stage: 'audio-processing-setup',
|
|
1306
|
+
status: 'completed',
|
|
1307
|
+
duration: audioProcessingDuration,
|
|
1308
|
+
timestamp: new Date().toISOString()
|
|
1309
|
+
});
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1312
|
+
this.emit('call-start-progress', {
|
|
1313
|
+
stage: 'audio-processing-setup',
|
|
1314
|
+
status: 'failed',
|
|
1315
|
+
duration: audioProcessingDuration,
|
|
1316
|
+
timestamp: new Date().toISOString(),
|
|
1317
|
+
metadata: { error: error?.toString() }
|
|
1318
|
+
});
|
|
1319
|
+
// Don't throw here, this is non-critical
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const totalDuration = Date.now() - startTime;
|
|
1323
|
+
this.emit('call-start-success', {
|
|
1324
|
+
totalDuration,
|
|
1325
|
+
callId: webCall?.id || 'unknown',
|
|
1326
|
+
timestamp: new Date().toISOString()
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
this.emit('call-start');
|
|
1330
|
+
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
const totalDuration = Date.now() - startTime;
|
|
1333
|
+
|
|
1334
|
+
this.emit('call-start-failed', {
|
|
1335
|
+
stage: 'reconnect',
|
|
1336
|
+
totalDuration,
|
|
1337
|
+
error: e?.toString() || 'Unknown error occurred',
|
|
1338
|
+
errorStack: e instanceof Error ? e.stack : 'No stack trace available',
|
|
1339
|
+
timestamp: new Date().toISOString(),
|
|
1340
|
+
context: {
|
|
1341
|
+
isReconnect: true,
|
|
1342
|
+
callId: webCall?.id || 'unknown',
|
|
1343
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
1344
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown',
|
|
1345
|
+
isMobile: this.isMobileDevice()
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Also emit the generic error event for backward compatibility
|
|
1350
|
+
this.emit('error', {
|
|
1351
|
+
type: 'reconnect-error',
|
|
1352
|
+
error: e,
|
|
1353
|
+
totalDuration,
|
|
1354
|
+
timestamp: new Date().toISOString(),
|
|
1355
|
+
context: {
|
|
1356
|
+
isReconnect: true,
|
|
1357
|
+
callId: webCall?.id || 'unknown',
|
|
1358
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
1359
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown',
|
|
1360
|
+
isMobile: this.isMobileDevice()
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
await this.cleanup();
|
|
1365
|
+
throw e;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
887
1368
|
}
|