@vapi-ai/web 2.4.0 → 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 +9 -0
- package/dist/api.d.ts +9 -0
- package/dist/vapi.d.ts +70 -2
- package/dist/vapi.js +389 -1
- package/package.json +3 -2
- package/vapi.ts +477 -1
package/api.ts
CHANGED
|
@@ -18143,6 +18143,15 @@ export interface CreateWebCallDTO {
|
|
|
18143
18143
|
workflow?: CreateWorkflowDTO;
|
|
18144
18144
|
/** These are the overrides for the `workflow` or `workflowId`'s settings and template variables. */
|
|
18145
18145
|
workflowOverrides?: WorkflowOverrides;
|
|
18146
|
+
/**
|
|
18147
|
+
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
|
|
18148
|
+
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
|
|
18149
|
+
* If set to `true`, the room will be deleted and reconnection will not be allowed.
|
|
18150
|
+
*
|
|
18151
|
+
* Defaults to `true`.
|
|
18152
|
+
* @example true
|
|
18153
|
+
*/
|
|
18154
|
+
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
18146
18155
|
}
|
|
18147
18156
|
|
|
18148
18157
|
export interface UpdateCallDTO {
|
package/dist/api.d.ts
CHANGED
|
@@ -11558,6 +11558,15 @@ export interface CreateWebCallDTO {
|
|
|
11558
11558
|
workflow?: CreateWorkflowDTO;
|
|
11559
11559
|
/** These are the overrides for the `workflow` or `workflowId`'s settings and template variables. */
|
|
11560
11560
|
workflowOverrides?: WorkflowOverrides;
|
|
11561
|
+
/**
|
|
11562
|
+
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
|
|
11563
|
+
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
|
|
11564
|
+
* If set to `true`, the room will be deleted and reconnection will not be allowed.
|
|
11565
|
+
*
|
|
11566
|
+
* Defaults to `true`.
|
|
11567
|
+
* @example true
|
|
11568
|
+
*/
|
|
11569
|
+
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
11561
11570
|
}
|
|
11562
11571
|
export interface UpdateCallDTO {
|
|
11563
11572
|
/**
|
package/dist/vapi.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { ChatCompletionMessageParam } from 'openai/resources';
|
|
|
2
2
|
import { DailyCall, DailyAdvancedConfig, DailyFactoryOptions, DailyParticipant, DailyVideoSendSettings } from '@daily-co/daily-js';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWorkflowDTO, WorkflowOverrides } from './api';
|
|
5
|
+
export interface EndCallMessage {
|
|
6
|
+
type: 'end-call';
|
|
7
|
+
}
|
|
5
8
|
export interface AddMessageMessage {
|
|
6
9
|
type: 'add-message';
|
|
7
10
|
message: ChatCompletionMessageParam;
|
|
@@ -19,7 +22,7 @@ export interface SayMessage {
|
|
|
19
22
|
interruptionsEnabled?: boolean;
|
|
20
23
|
interruptAssistantEnabled?: boolean;
|
|
21
24
|
}
|
|
22
|
-
type VapiClientToServerMessage = AddMessageMessage | ControlMessages | SayMessage;
|
|
25
|
+
type VapiClientToServerMessage = AddMessageMessage | ControlMessages | SayMessage | EndCallMessage;
|
|
23
26
|
type VapiEventNames = 'call-end' | 'call-start' | 'volume-level' | 'speech-start' | 'speech-end' | 'message' | 'video' | 'error' | 'camera-error' | 'network-quality-change' | 'network-connection' | 'daily-participant-updated' | 'call-start-progress' | 'call-start-success' | 'call-start-failed';
|
|
24
27
|
interface CallStartProgressEvent {
|
|
25
28
|
stage: string;
|
|
@@ -58,6 +61,47 @@ type VapiEventListeners = {
|
|
|
58
61
|
'call-start-success': (event: CallStartSuccessEvent) => void;
|
|
59
62
|
'call-start-failed': (event: CallStartFailedEvent) => void;
|
|
60
63
|
};
|
|
64
|
+
type StartCallOptions = {
|
|
65
|
+
/**
|
|
66
|
+
* This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
|
|
67
|
+
* If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
|
|
68
|
+
* If set to `true`, the room will be deleted and reconnection will not be allowed.
|
|
69
|
+
*
|
|
70
|
+
* Defaults to `true`.
|
|
71
|
+
* @example true
|
|
72
|
+
*/
|
|
73
|
+
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
74
|
+
};
|
|
75
|
+
type WebCall = {
|
|
76
|
+
/**
|
|
77
|
+
* The Vapi WebCall URL. This is the URL that the call will be joined on.
|
|
78
|
+
*
|
|
79
|
+
* call.webCallUrl or call.transport.callUrl
|
|
80
|
+
*/
|
|
81
|
+
webCallUrl: string;
|
|
82
|
+
/**
|
|
83
|
+
* The Vapi WebCall ID. This is the ID of the call.
|
|
84
|
+
*
|
|
85
|
+
* call.id
|
|
86
|
+
*/
|
|
87
|
+
id?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The Vapi WebCall artifact plan. This is the artifact plan of the call.
|
|
90
|
+
*/
|
|
91
|
+
artifactPlan?: {
|
|
92
|
+
videoRecordingEnabled?: boolean;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The Vapi WebCall assistant. This is the assistant of the call.
|
|
96
|
+
*
|
|
97
|
+
* call.assistant
|
|
98
|
+
*/
|
|
99
|
+
assistant?: {
|
|
100
|
+
voice?: {
|
|
101
|
+
provider?: string;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
61
105
|
declare class VapiEventEmitter extends EventEmitter {
|
|
62
106
|
on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
|
|
63
107
|
once<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
|
|
@@ -76,19 +120,43 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
76
120
|
private cleanup;
|
|
77
121
|
private isMobileDevice;
|
|
78
122
|
private sleep;
|
|
79
|
-
start(assistant?: CreateAssistantDTO | string, assistantOverrides?: AssistantOverrides, squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides): Promise<Call | null>;
|
|
123
|
+
start(assistant?: CreateAssistantDTO | string, assistantOverrides?: AssistantOverrides, squad?: CreateSquadDTO | string, workflow?: CreateWorkflowDTO | string, workflowOverrides?: WorkflowOverrides, options?: StartCallOptions): Promise<Call | null>;
|
|
80
124
|
private onAppMessage;
|
|
81
125
|
private handleRemoteParticipantsAudioLevel;
|
|
126
|
+
/**
|
|
127
|
+
* Stops the call by destroying the Daily call object.
|
|
128
|
+
*
|
|
129
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `false`, the Vapi call will be kept alive, allowing reconnections to the same call using the `reconnect` method.
|
|
130
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections.
|
|
131
|
+
*/
|
|
82
132
|
stop(): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Sends a Live Call Control message to the Vapi server.
|
|
135
|
+
*
|
|
136
|
+
* Docs: https://docs.vapi.ai/calls/call-features
|
|
137
|
+
*/
|
|
83
138
|
send(message: VapiClientToServerMessage): void;
|
|
84
139
|
setMuted(mute: boolean): void;
|
|
85
140
|
isMuted(): boolean;
|
|
86
141
|
say(message: string, endCallAfterSpoken?: boolean, interruptionsEnabled?: boolean, interruptAssistantEnabled?: boolean): void;
|
|
142
|
+
/**
|
|
143
|
+
* Ends the call immediately by sending a `end-call` message using Live Call Control, and destroys the Daily call object.
|
|
144
|
+
*
|
|
145
|
+
* This method always ends the call, regardless of the `roomDeleteOnUserLeaveEnabled` option.
|
|
146
|
+
*/
|
|
147
|
+
end(): void;
|
|
87
148
|
setInputDevicesAsync(options: Parameters<DailyCall['setInputDevicesAsync']>[0]): Promise<void>;
|
|
88
149
|
increaseMicLevel(gain: number): Promise<void>;
|
|
89
150
|
setOutputDeviceAsync(options: Parameters<DailyCall['setOutputDeviceAsync']>[0]): void;
|
|
90
151
|
getDailyCallObject(): DailyCall | null;
|
|
91
152
|
startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings): void;
|
|
92
153
|
stopScreenSharing(): void;
|
|
154
|
+
/**
|
|
155
|
+
* Reconnects to an active call.
|
|
156
|
+
*
|
|
157
|
+
*
|
|
158
|
+
* @param webCall
|
|
159
|
+
*/
|
|
160
|
+
reconnect(webCall: WebCall): Promise<void>;
|
|
93
161
|
}
|
|
94
162
|
export {};
|
package/dist/vapi.js
CHANGED
|
@@ -90,7 +90,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
90
90
|
async sleep(ms) {
|
|
91
91
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
92
|
}
|
|
93
|
-
async start(assistant, assistantOverrides, squad, workflow, workflowOverrides) {
|
|
93
|
+
async start(assistant, assistantOverrides, squad, workflow, workflowOverrides, options) {
|
|
94
94
|
const startTime = Date.now();
|
|
95
95
|
// Input validation with detailed error messages
|
|
96
96
|
if (!assistant && !squad && !workflow) {
|
|
@@ -140,6 +140,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
140
140
|
workflow: typeof workflow === 'string' ? undefined : workflow,
|
|
141
141
|
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
142
142
|
workflowOverrides,
|
|
143
|
+
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
143
144
|
})).data;
|
|
144
145
|
const webCallDuration = Date.now() - webCallStartTime;
|
|
145
146
|
this.emit('call-start-progress', {
|
|
@@ -587,6 +588,12 @@ class Vapi extends VapiEventEmitter {
|
|
|
587
588
|
this.speakingTimeout = null;
|
|
588
589
|
}, 1000);
|
|
589
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Stops the call by destroying the Daily call object.
|
|
593
|
+
*
|
|
594
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `false`, the Vapi call will be kept alive, allowing reconnections to the same call using the `reconnect` method.
|
|
595
|
+
* If `roomDeleteOnUserLeaveEnabled` is set to `true`, the Vapi call will also be destroyed, preventing any reconnections.
|
|
596
|
+
*/
|
|
590
597
|
async stop() {
|
|
591
598
|
this.started = false;
|
|
592
599
|
if (this.call) {
|
|
@@ -595,6 +602,11 @@ class Vapi extends VapiEventEmitter {
|
|
|
595
602
|
}
|
|
596
603
|
this.speakingTimeout = null;
|
|
597
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Sends a Live Call Control message to the Vapi server.
|
|
607
|
+
*
|
|
608
|
+
* Docs: https://docs.vapi.ai/calls/call-features
|
|
609
|
+
*/
|
|
598
610
|
send(message) {
|
|
599
611
|
this.call?.sendAppMessage(JSON.stringify(message));
|
|
600
612
|
}
|
|
@@ -616,6 +628,17 @@ class Vapi extends VapiEventEmitter {
|
|
|
616
628
|
interruptAssistantEnabled: interruptAssistantEnabled ?? false,
|
|
617
629
|
});
|
|
618
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Ends the call immediately by sending a `end-call` message using Live Call Control, and destroys the Daily call object.
|
|
633
|
+
*
|
|
634
|
+
* This method always ends the call, regardless of the `roomDeleteOnUserLeaveEnabled` option.
|
|
635
|
+
*/
|
|
636
|
+
end() {
|
|
637
|
+
this.send({
|
|
638
|
+
type: 'end-call',
|
|
639
|
+
});
|
|
640
|
+
this.stop();
|
|
641
|
+
}
|
|
619
642
|
setInputDevicesAsync(options) {
|
|
620
643
|
return (0, daily_guards_1.safeSetInputDevicesAsync)(this.call, options);
|
|
621
644
|
}
|
|
@@ -654,5 +677,370 @@ class Vapi extends VapiEventEmitter {
|
|
|
654
677
|
stopScreenSharing() {
|
|
655
678
|
this.call?.stopScreenShare();
|
|
656
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Reconnects to an active call.
|
|
682
|
+
*
|
|
683
|
+
*
|
|
684
|
+
* @param webCall
|
|
685
|
+
*/
|
|
686
|
+
async reconnect(webCall) {
|
|
687
|
+
const startTime = Date.now();
|
|
688
|
+
if (this.started) {
|
|
689
|
+
throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.');
|
|
690
|
+
}
|
|
691
|
+
if (!webCall.webCallUrl) {
|
|
692
|
+
throw new Error('webCallUrl is required for reconnection.');
|
|
693
|
+
}
|
|
694
|
+
this.emit('call-start-progress', {
|
|
695
|
+
stage: 'reconnect-initialization',
|
|
696
|
+
status: 'started',
|
|
697
|
+
timestamp: new Date().toISOString(),
|
|
698
|
+
metadata: {
|
|
699
|
+
callId: webCall.id || 'unknown',
|
|
700
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
701
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown'
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
this.started = true;
|
|
705
|
+
try {
|
|
706
|
+
// Clean up any existing call object
|
|
707
|
+
if (this.call) {
|
|
708
|
+
this.emit('call-start-progress', {
|
|
709
|
+
stage: 'cleanup-existing-call',
|
|
710
|
+
status: 'started',
|
|
711
|
+
timestamp: new Date().toISOString()
|
|
712
|
+
});
|
|
713
|
+
await this.cleanup();
|
|
714
|
+
this.emit('call-start-progress', {
|
|
715
|
+
stage: 'cleanup-existing-call',
|
|
716
|
+
status: 'completed',
|
|
717
|
+
timestamp: new Date().toISOString()
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
721
|
+
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
722
|
+
// Stage 1: Create Daily call object
|
|
723
|
+
this.emit('call-start-progress', {
|
|
724
|
+
stage: 'daily-call-object-creation',
|
|
725
|
+
status: 'started',
|
|
726
|
+
timestamp: new Date().toISOString(),
|
|
727
|
+
metadata: {
|
|
728
|
+
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
729
|
+
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
730
|
+
isVideoRecordingEnabled,
|
|
731
|
+
isVideoEnabled
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
const dailyCallStartTime = Date.now();
|
|
735
|
+
this.call = daily_js_1.default.createCallObject({
|
|
736
|
+
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
737
|
+
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
738
|
+
dailyConfig: this.dailyCallConfig,
|
|
739
|
+
});
|
|
740
|
+
const dailyCallDuration = Date.now() - dailyCallStartTime;
|
|
741
|
+
this.emit('call-start-progress', {
|
|
742
|
+
stage: 'daily-call-object-creation',
|
|
743
|
+
status: 'completed',
|
|
744
|
+
duration: dailyCallDuration,
|
|
745
|
+
timestamp: new Date().toISOString()
|
|
746
|
+
});
|
|
747
|
+
this.call.iframe()?.style.setProperty('display', 'none');
|
|
748
|
+
// Set up event listeners
|
|
749
|
+
this.call.on('left-meeting', () => {
|
|
750
|
+
this.emit('call-end');
|
|
751
|
+
if (!this.hasEmittedCallEndedStatus) {
|
|
752
|
+
this.emit('message', {
|
|
753
|
+
type: 'status-update',
|
|
754
|
+
status: 'ended',
|
|
755
|
+
'endedReason': 'customer-ended-call',
|
|
756
|
+
});
|
|
757
|
+
this.hasEmittedCallEndedStatus = true;
|
|
758
|
+
}
|
|
759
|
+
if (isVideoRecordingEnabled) {
|
|
760
|
+
this.call?.stopRecording();
|
|
761
|
+
}
|
|
762
|
+
this.cleanup().catch(console.error);
|
|
763
|
+
});
|
|
764
|
+
this.call.on('error', (error) => {
|
|
765
|
+
this.emit('error', error);
|
|
766
|
+
if (isVideoRecordingEnabled) {
|
|
767
|
+
this.call?.stopRecording();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
this.call.on('camera-error', (error) => {
|
|
771
|
+
this.emit('camera-error', error);
|
|
772
|
+
});
|
|
773
|
+
this.call.on('network-quality-change', (event) => {
|
|
774
|
+
this.emit('network-quality-change', event);
|
|
775
|
+
});
|
|
776
|
+
this.call.on('network-connection', (event) => {
|
|
777
|
+
this.emit('network-connection', event);
|
|
778
|
+
});
|
|
779
|
+
this.call.on('track-started', async (e) => {
|
|
780
|
+
if (!e || !e.participant) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (e.participant?.local) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (e.participant?.user_name !== 'Vapi Speaker') {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (e.track.kind === 'video') {
|
|
790
|
+
this.emit('video', e.track);
|
|
791
|
+
}
|
|
792
|
+
if (e.track.kind === 'audio') {
|
|
793
|
+
await buildAudioPlayer(e.track, e.participant.session_id);
|
|
794
|
+
}
|
|
795
|
+
this.call?.sendAppMessage('playable');
|
|
796
|
+
});
|
|
797
|
+
this.call.on('participant-joined', (e) => {
|
|
798
|
+
if (!e || !this.call)
|
|
799
|
+
return;
|
|
800
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled, isVideoEnabled);
|
|
801
|
+
});
|
|
802
|
+
this.call.on('participant-updated', (e) => {
|
|
803
|
+
if (!e) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
this.emit('daily-participant-updated', e.participant);
|
|
807
|
+
});
|
|
808
|
+
this.call.on('participant-left', (e) => {
|
|
809
|
+
if (!e) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
destroyAudioPlayer(e.participant.session_id);
|
|
813
|
+
});
|
|
814
|
+
this.call.on('remote-participants-audio-level', (e) => {
|
|
815
|
+
if (e)
|
|
816
|
+
this.handleRemoteParticipantsAudioLevel(e);
|
|
817
|
+
});
|
|
818
|
+
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
819
|
+
this.call.on('nonfatal-error', (e) => {
|
|
820
|
+
// https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
|
|
821
|
+
if (e?.type === 'audio-processor-error') {
|
|
822
|
+
this.call
|
|
823
|
+
?.updateInputSettings({
|
|
824
|
+
audio: {
|
|
825
|
+
processor: {
|
|
826
|
+
type: 'none',
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
})
|
|
830
|
+
.then(() => {
|
|
831
|
+
(0, daily_guards_1.safeSetLocalAudio)(this.call, true);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
// Stage 2: Mobile device handling and permissions
|
|
836
|
+
const isMobile = this.isMobileDevice();
|
|
837
|
+
this.emit('call-start-progress', {
|
|
838
|
+
stage: 'mobile-permissions',
|
|
839
|
+
status: 'started',
|
|
840
|
+
timestamp: new Date().toISOString(),
|
|
841
|
+
metadata: { isMobile }
|
|
842
|
+
});
|
|
843
|
+
if (isMobile) {
|
|
844
|
+
const mobileWaitStartTime = Date.now();
|
|
845
|
+
await this.sleep(1000);
|
|
846
|
+
const mobileWaitDuration = Date.now() - mobileWaitStartTime;
|
|
847
|
+
this.emit('call-start-progress', {
|
|
848
|
+
stage: 'mobile-permissions',
|
|
849
|
+
status: 'completed',
|
|
850
|
+
duration: mobileWaitDuration,
|
|
851
|
+
timestamp: new Date().toISOString(),
|
|
852
|
+
metadata: { action: 'permissions-wait' }
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
this.emit('call-start-progress', {
|
|
857
|
+
stage: 'mobile-permissions',
|
|
858
|
+
status: 'completed',
|
|
859
|
+
timestamp: new Date().toISOString(),
|
|
860
|
+
metadata: { action: 'skipped-not-mobile' }
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// Stage 3: Join the call
|
|
864
|
+
this.emit('call-start-progress', {
|
|
865
|
+
stage: 'daily-call-join',
|
|
866
|
+
status: 'started',
|
|
867
|
+
timestamp: new Date().toISOString()
|
|
868
|
+
});
|
|
869
|
+
const joinStartTime = Date.now();
|
|
870
|
+
await this.call.join({
|
|
871
|
+
url: webCall.webCallUrl,
|
|
872
|
+
subscribeToTracksAutomatically: false,
|
|
873
|
+
});
|
|
874
|
+
const joinDuration = Date.now() - joinStartTime;
|
|
875
|
+
this.emit('call-start-progress', {
|
|
876
|
+
stage: 'daily-call-join',
|
|
877
|
+
status: 'completed',
|
|
878
|
+
duration: joinDuration,
|
|
879
|
+
timestamp: new Date().toISOString()
|
|
880
|
+
});
|
|
881
|
+
// Stage 4: Video recording setup (if enabled)
|
|
882
|
+
if (isVideoRecordingEnabled) {
|
|
883
|
+
this.emit('call-start-progress', {
|
|
884
|
+
stage: 'video-recording-setup',
|
|
885
|
+
status: 'started',
|
|
886
|
+
timestamp: new Date().toISOString()
|
|
887
|
+
});
|
|
888
|
+
const recordingStartTime = Date.now();
|
|
889
|
+
const recordingRequestedTime = new Date().getTime();
|
|
890
|
+
try {
|
|
891
|
+
this.call.startRecording({
|
|
892
|
+
width: 1280,
|
|
893
|
+
height: 720,
|
|
894
|
+
backgroundColor: '#FF1F2D3D',
|
|
895
|
+
layout: {
|
|
896
|
+
preset: 'default',
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
900
|
+
this.emit('call-start-progress', {
|
|
901
|
+
stage: 'video-recording-setup',
|
|
902
|
+
status: 'completed',
|
|
903
|
+
duration: recordingSetupDuration,
|
|
904
|
+
timestamp: new Date().toISOString()
|
|
905
|
+
});
|
|
906
|
+
this.call.on('recording-started', () => {
|
|
907
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
908
|
+
this.emit('call-start-progress', {
|
|
909
|
+
stage: 'video-recording-started',
|
|
910
|
+
status: 'completed',
|
|
911
|
+
timestamp: new Date().toISOString(),
|
|
912
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
913
|
+
});
|
|
914
|
+
this.send({
|
|
915
|
+
type: 'control',
|
|
916
|
+
control: 'say-first-message',
|
|
917
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
923
|
+
this.emit('call-start-progress', {
|
|
924
|
+
stage: 'video-recording-setup',
|
|
925
|
+
status: 'failed',
|
|
926
|
+
duration: recordingSetupDuration,
|
|
927
|
+
timestamp: new Date().toISOString(),
|
|
928
|
+
metadata: { error: error?.toString() }
|
|
929
|
+
});
|
|
930
|
+
// Don't throw here, video recording is optional
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
this.emit('call-start-progress', {
|
|
935
|
+
stage: 'video-recording-setup',
|
|
936
|
+
status: 'completed',
|
|
937
|
+
timestamp: new Date().toISOString(),
|
|
938
|
+
metadata: { action: 'skipped-not-enabled' }
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
// Stage 5: Audio level observer setup
|
|
942
|
+
this.emit('call-start-progress', {
|
|
943
|
+
stage: 'audio-observer-setup',
|
|
944
|
+
status: 'started',
|
|
945
|
+
timestamp: new Date().toISOString()
|
|
946
|
+
});
|
|
947
|
+
const audioObserverStartTime = Date.now();
|
|
948
|
+
try {
|
|
949
|
+
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
950
|
+
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
951
|
+
this.emit('call-start-progress', {
|
|
952
|
+
stage: 'audio-observer-setup',
|
|
953
|
+
status: 'completed',
|
|
954
|
+
duration: audioObserverDuration,
|
|
955
|
+
timestamp: new Date().toISOString()
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
960
|
+
this.emit('call-start-progress', {
|
|
961
|
+
stage: 'audio-observer-setup',
|
|
962
|
+
status: 'failed',
|
|
963
|
+
duration: audioObserverDuration,
|
|
964
|
+
timestamp: new Date().toISOString(),
|
|
965
|
+
metadata: { error: error?.toString() }
|
|
966
|
+
});
|
|
967
|
+
// Don't throw here, this is non-critical
|
|
968
|
+
}
|
|
969
|
+
// Stage 6: Audio processing setup
|
|
970
|
+
this.emit('call-start-progress', {
|
|
971
|
+
stage: 'audio-processing-setup',
|
|
972
|
+
status: 'started',
|
|
973
|
+
timestamp: new Date().toISOString()
|
|
974
|
+
});
|
|
975
|
+
const audioProcessingStartTime = Date.now();
|
|
976
|
+
try {
|
|
977
|
+
this.call.updateInputSettings({
|
|
978
|
+
audio: {
|
|
979
|
+
processor: {
|
|
980
|
+
type: 'noise-cancellation',
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
985
|
+
this.emit('call-start-progress', {
|
|
986
|
+
stage: 'audio-processing-setup',
|
|
987
|
+
status: 'completed',
|
|
988
|
+
duration: audioProcessingDuration,
|
|
989
|
+
timestamp: new Date().toISOString()
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
994
|
+
this.emit('call-start-progress', {
|
|
995
|
+
stage: 'audio-processing-setup',
|
|
996
|
+
status: 'failed',
|
|
997
|
+
duration: audioProcessingDuration,
|
|
998
|
+
timestamp: new Date().toISOString(),
|
|
999
|
+
metadata: { error: error?.toString() }
|
|
1000
|
+
});
|
|
1001
|
+
// Don't throw here, this is non-critical
|
|
1002
|
+
}
|
|
1003
|
+
const totalDuration = Date.now() - startTime;
|
|
1004
|
+
this.emit('call-start-success', {
|
|
1005
|
+
totalDuration,
|
|
1006
|
+
callId: webCall?.id || 'unknown',
|
|
1007
|
+
timestamp: new Date().toISOString()
|
|
1008
|
+
});
|
|
1009
|
+
this.emit('call-start');
|
|
1010
|
+
}
|
|
1011
|
+
catch (e) {
|
|
1012
|
+
const totalDuration = Date.now() - startTime;
|
|
1013
|
+
this.emit('call-start-failed', {
|
|
1014
|
+
stage: 'reconnect',
|
|
1015
|
+
totalDuration,
|
|
1016
|
+
error: e?.toString() || 'Unknown error occurred',
|
|
1017
|
+
errorStack: e instanceof Error ? e.stack : 'No stack trace available',
|
|
1018
|
+
timestamp: new Date().toISOString(),
|
|
1019
|
+
context: {
|
|
1020
|
+
isReconnect: true,
|
|
1021
|
+
callId: webCall?.id || 'unknown',
|
|
1022
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
1023
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown',
|
|
1024
|
+
isMobile: this.isMobileDevice()
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
// Also emit the generic error event for backward compatibility
|
|
1028
|
+
this.emit('error', {
|
|
1029
|
+
type: 'reconnect-error',
|
|
1030
|
+
error: e,
|
|
1031
|
+
totalDuration,
|
|
1032
|
+
timestamp: new Date().toISOString(),
|
|
1033
|
+
context: {
|
|
1034
|
+
isReconnect: true,
|
|
1035
|
+
callId: webCall?.id || 'unknown',
|
|
1036
|
+
hasVideoRecording: !!webCall?.artifactPlan?.videoRecordingEnabled,
|
|
1037
|
+
voiceProvider: webCall?.assistant?.voice?.provider || 'unknown',
|
|
1038
|
+
isMobile: this.isMobileDevice()
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
await this.cleanup();
|
|
1042
|
+
throw e;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
657
1045
|
}
|
|
658
1046
|
exports.default = Vapi;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vapi-ai/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/vapi.js",
|
|
6
6
|
"types": "dist/vapi.d.ts",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"test:example": "npm run pack:local && cd example && npm install && npm run build",
|
|
15
15
|
"pack:local": "./scripts/build-latest.sh",
|
|
16
16
|
"clean-builds": "rm -f vapi-ai-web-*.tgz && echo '🧹 Cleaned up all build tarballs'",
|
|
17
|
-
"dev:example": "npm run pack:local && cd example && npm install && npm run dev"
|
|
17
|
+
"dev:example": "npm run pack:local && cd example && npm install && npm run dev",
|
|
18
|
+
"re-build-local-example": "./scripts/re-build-local-example.sh"
|
|
18
19
|
},
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
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,
|
|
@@ -246,6 +288,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
246
288
|
squad?: CreateSquadDTO | string,
|
|
247
289
|
workflow?: CreateWorkflowDTO | string,
|
|
248
290
|
workflowOverrides?: WorkflowOverrides,
|
|
291
|
+
options?: StartCallOptions
|
|
249
292
|
): Promise<Call | null> {
|
|
250
293
|
const startTime = Date.now();
|
|
251
294
|
|
|
@@ -304,6 +347,7 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
304
347
|
workflow: typeof workflow === 'string' ? undefined : workflow,
|
|
305
348
|
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
306
349
|
workflowOverrides,
|
|
350
|
+
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
307
351
|
})
|
|
308
352
|
).data;
|
|
309
353
|
|
|
@@ -802,6 +846,12 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
802
846
|
}, 1000);
|
|
803
847
|
}
|
|
804
848
|
|
|
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
|
+
*/
|
|
805
855
|
async stop(): Promise<void> {
|
|
806
856
|
this.started = false;
|
|
807
857
|
if (this.call) {
|
|
@@ -811,6 +861,11 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
811
861
|
this.speakingTimeout = null;
|
|
812
862
|
}
|
|
813
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Sends a Live Call Control message to the Vapi server.
|
|
866
|
+
*
|
|
867
|
+
* Docs: https://docs.vapi.ai/calls/call-features
|
|
868
|
+
*/
|
|
814
869
|
send(message: VapiClientToServerMessage): void {
|
|
815
870
|
this.call?.sendAppMessage(JSON.stringify(message));
|
|
816
871
|
}
|
|
@@ -837,6 +892,18 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
837
892
|
});
|
|
838
893
|
}
|
|
839
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
|
+
|
|
840
907
|
public setInputDevicesAsync(
|
|
841
908
|
options: Parameters<DailyCall['setInputDevicesAsync']>[0],
|
|
842
909
|
) {
|
|
@@ -889,4 +956,413 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
889
956
|
public stopScreenSharing() {
|
|
890
957
|
this.call?.stopScreenShare();
|
|
891
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
|
+
}
|
|
892
1368
|
}
|