@vapi-ai/web 3.0.1 → 3.1.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/README.md +11 -1
- package/api.ts +6195 -5230
- package/dist/api.d.ts +3430 -2788
- package/dist/api.js +94 -5
- package/dist/vapi.d.ts +41 -3
- package/dist/vapi.js +100 -81
- package/package.json +1 -1
- package/vapi.ts +164 -111
package/dist/api.js
CHANGED
|
@@ -1338,6 +1338,41 @@ class Api extends HttpClient {
|
|
|
1338
1338
|
secure: true,
|
|
1339
1339
|
...params,
|
|
1340
1340
|
}),
|
|
1341
|
+
/**
|
|
1342
|
+
* No description
|
|
1343
|
+
*
|
|
1344
|
+
* @tags Calls
|
|
1345
|
+
* @name GoalEvaluationCurrentControllerCurrentGet
|
|
1346
|
+
* @summary Get the current goal evaluation for a call
|
|
1347
|
+
* @request GET:/call/{id}/goal-evaluation
|
|
1348
|
+
* @secure
|
|
1349
|
+
*/
|
|
1350
|
+
goalEvaluationCurrentControllerCurrentGet: (id, query, params = {}) => this.request({
|
|
1351
|
+
path: `/call/${id}/goal-evaluation`,
|
|
1352
|
+
method: "GET",
|
|
1353
|
+
query: query,
|
|
1354
|
+
secure: true,
|
|
1355
|
+
format: "json",
|
|
1356
|
+
...params,
|
|
1357
|
+
}),
|
|
1358
|
+
/**
|
|
1359
|
+
* No description
|
|
1360
|
+
*
|
|
1361
|
+
* @tags Calls
|
|
1362
|
+
* @name GoalEvaluationCurrentControllerCurrentBatchGet
|
|
1363
|
+
* @summary Get current goal evaluations for up to 100 call/goal pairs
|
|
1364
|
+
* @request POST:/call/goal-evaluation/batch
|
|
1365
|
+
* @secure
|
|
1366
|
+
*/
|
|
1367
|
+
goalEvaluationCurrentControllerCurrentBatchGet: (data, params = {}) => this.request({
|
|
1368
|
+
path: `/call/goal-evaluation/batch`,
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
body: data,
|
|
1371
|
+
secure: true,
|
|
1372
|
+
type: ContentType.Json,
|
|
1373
|
+
format: "json",
|
|
1374
|
+
...params,
|
|
1375
|
+
}),
|
|
1341
1376
|
};
|
|
1342
1377
|
chat = {
|
|
1343
1378
|
/**
|
|
@@ -1670,7 +1705,7 @@ class Api extends HttpClient {
|
|
|
1670
1705
|
};
|
|
1671
1706
|
trafficAllocations = {
|
|
1672
1707
|
/**
|
|
1673
|
-
* @description Creates a new traffic allocation for an assistant, replacing the one currently in effect. To start or adjust a split, send targets naming published versions (such as "v7") with percentages totaling 100; allocationIntent is inferred as 'explicit'. To stop splitting and send every call to the newest published version, send allocationIntent 'follow-latest' with no targets field; stopping always names its intent, so a dropped targets field can never end a split by accident.
|
|
1708
|
+
* @description Creates a new traffic allocation for an assistant, replacing the one currently in effect. To start or adjust a split, send targets naming published versions (such as "v7") with percentages totaling 100; allocationIntent is inferred as 'explicit'. To stop splitting and send every call to the newest published version, send allocationIntent 'follow-latest' with no targets field; stopping always names its intent, so a dropped targets field can never end a split by accident.
|
|
1674
1709
|
*
|
|
1675
1710
|
* @tags Traffic Allocations
|
|
1676
1711
|
* @name TrafficAllocationControllerCreate
|
|
@@ -1688,7 +1723,7 @@ class Api extends HttpClient {
|
|
|
1688
1723
|
...params,
|
|
1689
1724
|
}),
|
|
1690
1725
|
/**
|
|
1691
|
-
* @description The append-only history of an assistant's allocations, newest first.
|
|
1726
|
+
* @description The append-only history of an assistant's allocations, newest first.
|
|
1692
1727
|
*
|
|
1693
1728
|
* @tags Traffic Allocations
|
|
1694
1729
|
* @name TrafficAllocationControllerFindAllPaginated
|
|
@@ -1705,7 +1740,7 @@ class Api extends HttpClient {
|
|
|
1705
1740
|
...params,
|
|
1706
1741
|
}),
|
|
1707
1742
|
/**
|
|
1708
|
-
* @description The allocation currently in effect for the assistant, with its targets. The response carries no allocation field when traffic splitting has never been configured.
|
|
1743
|
+
* @description The allocation currently in effect for the assistant, with its targets. The response carries no allocation field when traffic splitting has never been configured.
|
|
1709
1744
|
*
|
|
1710
1745
|
* @tags Traffic Allocations
|
|
1711
1746
|
* @name TrafficAllocationControllerLatestGet
|
|
@@ -1722,7 +1757,7 @@ class Api extends HttpClient {
|
|
|
1722
1757
|
...params,
|
|
1723
1758
|
}),
|
|
1724
1759
|
/**
|
|
1725
|
-
* @description Per-assistant-version call metrics over a time window: call counts, configured vs actual traffic share, duration, cost per minute, latency, ended reasons, a bucketed timeseries, and optional structured output aggregations. Calls with no assistant version are reported as an 'unversioned' row.
|
|
1760
|
+
* @description Per-assistant-version call metrics over a time window: call counts, configured vs actual traffic share, duration, cost per minute, latency, ended reasons, a bucketed timeseries, and optional structured output aggregations. Calls with no assistant version are reported as an 'unversioned' row.
|
|
1726
1761
|
*
|
|
1727
1762
|
* @tags Traffic Allocations
|
|
1728
1763
|
* @name TrafficAllocationControllerMetricsGet
|
|
@@ -1739,7 +1774,7 @@ class Api extends HttpClient {
|
|
|
1739
1774
|
...params,
|
|
1740
1775
|
}),
|
|
1741
1776
|
/**
|
|
1742
|
-
* @description Returns a single allocation by id, including its targets and actor attribution.
|
|
1777
|
+
* @description Returns a single allocation by id, including its targets and actor attribution.
|
|
1743
1778
|
*
|
|
1744
1779
|
* @tags Traffic Allocations
|
|
1745
1780
|
* @name TrafficAllocationControllerFindOne
|
|
@@ -3132,6 +3167,60 @@ class Api extends HttpClient {
|
|
|
3132
3167
|
format: "json",
|
|
3133
3168
|
...params,
|
|
3134
3169
|
}),
|
|
3170
|
+
/**
|
|
3171
|
+
* @description Generate draft test requirements from an assistant's prompt, tools, and handoff configuration. The response includes numbered requirements describing expected behavior and open questions about behavior that needs clarification. Use this endpoint before generating a simulation suite. Review the requirements and resolve any open questions. To revise a draft, send it as `previousDraft` together with natural-language `feedback` describing the changes or answers. Once the requirements are ready, pass them to `POST /eval/simulation/onboarding/suite`. Requests can take up to 2 minutes. Each organization can draft requirements 60 times per UTC day, shared with the dashboard. A 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED` response means the daily limit has been reached. Wait until the limit resets at 00:00 UTC before retrying.
|
|
3172
|
+
*
|
|
3173
|
+
* @tags Simulation Suites
|
|
3174
|
+
* @name SimulationOnboardingControllerRequirementsDraft
|
|
3175
|
+
* @summary Draft simulation requirements
|
|
3176
|
+
* @request POST:/eval/simulation/onboarding/requirements
|
|
3177
|
+
* @secure
|
|
3178
|
+
*/
|
|
3179
|
+
simulationOnboardingControllerRequirementsDraft: (data, params = {}) => this.request({
|
|
3180
|
+
path: `/eval/simulation/onboarding/requirements`,
|
|
3181
|
+
method: "POST",
|
|
3182
|
+
body: data,
|
|
3183
|
+
secure: true,
|
|
3184
|
+
type: ContentType.Json,
|
|
3185
|
+
format: "json",
|
|
3186
|
+
...params,
|
|
3187
|
+
}),
|
|
3188
|
+
/**
|
|
3189
|
+
* @description Generate and save a simulation suite from reviewed test requirements, and assign it to an assistant. First draft requirements with `POST /eval/simulation/onboarding/requirements`. The response includes the saved suite, its simulations, and requirement coverage, including any gaps. Requests can take up to 3 minutes. Set a client timeout of at least 200 seconds. Use a unique `idempotencyKey` for each suite generation request. Retrying with the same key does not create a duplicate suite: - On a timeout or a 5xx (502, 504, 524), retry with the same key. You get the saved suite, a 409 while it is still generating, or a fresh generation if the first one never saved. - On a 409 `SIMULATION_ONBOARDING_GENERATION_IN_PROGRESS`, the first request is still generating. Wait `Retry-After` seconds (15) and retry with the same key. - On a 409 `SIMULATION_ONBOARDING_IDEMPOTENCY_KEY_REUSED`, the key was used for a different assistant or requirements. Send a new key. Review the generated simulations and coverage gaps. Revise the suite with `PATCH /eval/simulation/onboarding/suite/{suiteId}`, then run it with `POST /eval/simulation/run`. Each organization can generate 20 suites per UTC day, shared with the dashboard, and 2 at a time. A retry that returns a saved suite does not count toward these limits. On a 429 `SIMULATION_ONBOARDING_CONCURRENCY_LIMITED`, wait `Retry-After` seconds (30) before retrying. On a 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED`, wait until the daily limit resets at 00:00 UTC before retrying.
|
|
3190
|
+
*
|
|
3191
|
+
* @tags Simulation Suites
|
|
3192
|
+
* @name SimulationOnboardingControllerSuiteCreate
|
|
3193
|
+
* @summary Generate a simulation suite
|
|
3194
|
+
* @request POST:/eval/simulation/onboarding/suite
|
|
3195
|
+
* @secure
|
|
3196
|
+
*/
|
|
3197
|
+
simulationOnboardingControllerSuiteCreate: (data, params = {}) => this.request({
|
|
3198
|
+
path: `/eval/simulation/onboarding/suite`,
|
|
3199
|
+
method: "POST",
|
|
3200
|
+
body: data,
|
|
3201
|
+
secure: true,
|
|
3202
|
+
type: ContentType.Json,
|
|
3203
|
+
format: "json",
|
|
3204
|
+
...params,
|
|
3205
|
+
}),
|
|
3206
|
+
/**
|
|
3207
|
+
* @description Revise a suite created by `POST /eval/simulation/onboarding/suite`. Remove simulations, rewrite scenarios using natural-language instructions, or set `retryFailed` to regenerate scenarios that failed or exceeded generation limits. The response includes the updated suite and requirement coverage. Requests can take up to 2 minutes. Send a new `patchKey` for each edit. Replaying an applied key changes nothing and returns the current suite, with `failedEditSimulationIds` listing the simulations that edit could not rewrite, so retry a timeout or a 5xx with the same key. A suite remembers its last 50 keys, so never reuse one. Each organization can make 60 edits per UTC day, shared with the dashboard. Requests that only remove simulations and replays of applied edits do not count toward this limit. On a 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED`, wait until the daily limit resets at 00:00 UTC before retrying.
|
|
3208
|
+
*
|
|
3209
|
+
* @tags Simulation Suites
|
|
3210
|
+
* @name SimulationOnboardingControllerSuiteUpdate
|
|
3211
|
+
* @summary Edit a generated simulation suite
|
|
3212
|
+
* @request PATCH:/eval/simulation/onboarding/suite/{suiteId}
|
|
3213
|
+
* @secure
|
|
3214
|
+
*/
|
|
3215
|
+
simulationOnboardingControllerSuiteUpdate: (suiteId, data, params = {}) => this.request({
|
|
3216
|
+
path: `/eval/simulation/onboarding/suite/${suiteId}`,
|
|
3217
|
+
method: "PATCH",
|
|
3218
|
+
body: data,
|
|
3219
|
+
secure: true,
|
|
3220
|
+
type: ContentType.Json,
|
|
3221
|
+
format: "json",
|
|
3222
|
+
...params,
|
|
3223
|
+
}),
|
|
3135
3224
|
/**
|
|
3136
3225
|
* No description
|
|
3137
3226
|
*
|
package/dist/vapi.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatCompletionMessageParam } from 'openai/resources';
|
|
2
2
|
import { DailyCall, DailyAdvancedConfig, DailyFactoryOptions, DailyEventObjectNoPayload, DailyEventObjectRecordingError, DailyEventObjectRecordingStarted, DailyEventObjectRecordingStopped, DailyParticipant, DailyVideoSendSettings } from '@daily-co/daily-js';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
|
-
import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWorkflowDTO, WorkflowOverrides } from './api';
|
|
4
|
+
import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWebCallDTO, CreateWorkflowDTO, WorkflowOverrides } from './api';
|
|
5
5
|
export interface EndCallMessage {
|
|
6
6
|
type: 'end-call';
|
|
7
7
|
}
|
|
@@ -69,6 +69,7 @@ type VapiEventListeners = {
|
|
|
69
69
|
'recording-error': (event: DailyEventObjectRecordingError) => void;
|
|
70
70
|
'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
|
|
71
71
|
};
|
|
72
|
+
type VideoRecordingStartMode = NonNullable<CreateWebCallDTO['videoRecordingStartMode']>;
|
|
72
73
|
type StartCallOptions = {
|
|
73
74
|
/** Acquire the microphone before creating a backend call. Defaults to true. */
|
|
74
75
|
microphonePreflightEnabled?: boolean;
|
|
@@ -81,6 +82,25 @@ type StartCallOptions = {
|
|
|
81
82
|
* @example true
|
|
82
83
|
*/
|
|
83
84
|
roomDeleteOnUserLeaveEnabled?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* This determines who starts the video recording. Only applies when video
|
|
87
|
+
* recording is enabled (`artifactPlan.videoRecordingEnabled`).
|
|
88
|
+
*
|
|
89
|
+
* - `client`: the web client starts the recording when it joins.
|
|
90
|
+
* - `server`: Vapi starts the recording once the user has joined, so a weak
|
|
91
|
+
* client connection can't stop it from starting. If Vapi can't set up the
|
|
92
|
+
* server-side start, or the start itself fails, the SDK emits
|
|
93
|
+
* `recording-error`.
|
|
94
|
+
*
|
|
95
|
+
* In `server` mode the SDK never calls `startRecording()` itself. If the
|
|
96
|
+
* server-side start couldn't be set up, calling it yourself may still start
|
|
97
|
+
* the recording.
|
|
98
|
+
*
|
|
99
|
+
* Requires a Vapi API that supports this field; older servers reject the
|
|
100
|
+
* request. The server defaults to `client` when it is not set.
|
|
101
|
+
* @example "server"
|
|
102
|
+
*/
|
|
103
|
+
videoRecordingStartMode?: VideoRecordingStartMode;
|
|
84
104
|
};
|
|
85
105
|
type WebCall = {
|
|
86
106
|
/**
|
|
@@ -113,13 +133,21 @@ type WebCall = {
|
|
|
113
133
|
};
|
|
114
134
|
/**
|
|
115
135
|
* The Vapi WebCall transport. When video recording is enabled, the server
|
|
116
|
-
* sets `callToken` to
|
|
117
|
-
* recording on join
|
|
136
|
+
* sets `callToken` to the Daily meeting token to join with. In `client` mode
|
|
137
|
+
* the token auto-starts the cloud recording on join; in `server` mode it
|
|
138
|
+
* starts nothing and Vapi starts the recording. If there is no token, the
|
|
139
|
+
* SDK emits `recording-error` and starts nothing, though `startRecording()`
|
|
140
|
+
* may still start the recording from the client.
|
|
141
|
+
*
|
|
142
|
+
* `videoRecordingStartMode` is `server` when Vapi starts the recording, and
|
|
143
|
+
* is absent when video recording is off or the server-side start couldn't
|
|
144
|
+
* be set up.
|
|
118
145
|
*
|
|
119
146
|
* call.transport
|
|
120
147
|
*/
|
|
121
148
|
transport?: {
|
|
122
149
|
callToken?: string;
|
|
150
|
+
videoRecordingStartMode?: VideoRecordingStartMode;
|
|
123
151
|
};
|
|
124
152
|
};
|
|
125
153
|
declare class VapiEventEmitter extends EventEmitter {
|
|
@@ -220,6 +248,16 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
220
248
|
* which EventEmitter throws on when nothing is listening.
|
|
221
249
|
*/
|
|
222
250
|
private emitToConsumer;
|
|
251
|
+
/**
|
|
252
|
+
* Reported in a later task, like Daily's own recording-error: start() or
|
|
253
|
+
* reconnect() has resolved and the caller's await continuation has run, so a
|
|
254
|
+
* listener registered right after it still hears this, and one that calls
|
|
255
|
+
* stop() can't interrupt call setup. Skipped if that call was stopped or
|
|
256
|
+
* replaced.
|
|
257
|
+
*/
|
|
258
|
+
private recordingTokenMissingReportSchedule;
|
|
259
|
+
/** The fields are read defensively: the app message is untrusted JSON. */
|
|
260
|
+
private videoRecordingStartFailedReport;
|
|
223
261
|
/** Drops the reference before removing, so nothing hands out a detached element. */
|
|
224
262
|
private detachAudioPlayer;
|
|
225
263
|
/**
|
package/dist/vapi.js
CHANGED
|
@@ -58,6 +58,8 @@ function serializeError(error) {
|
|
|
58
58
|
}
|
|
59
59
|
return { message: String(error) };
|
|
60
60
|
}
|
|
61
|
+
// The app message Vapi sends when it couldn't start a server-mode recording.
|
|
62
|
+
const VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE = 'video-recording.start-failed';
|
|
61
63
|
async function startAudioPlayer(player, track) {
|
|
62
64
|
player.muted = false;
|
|
63
65
|
player.autoplay = true;
|
|
@@ -88,13 +90,13 @@ function destroyAudioPlayer(participantId) {
|
|
|
88
90
|
const player = document.querySelector(`audio[data-participant-id="${participantId}"]`);
|
|
89
91
|
player?.remove();
|
|
90
92
|
}
|
|
91
|
-
function subscribeToTracks(e, call, isVideoRecordingEnabled
|
|
93
|
+
function subscribeToTracks(e, call, isVideoRecordingEnabled) {
|
|
92
94
|
if (e.participant.local)
|
|
93
95
|
return;
|
|
94
96
|
call.updateParticipant(e.participant.session_id, {
|
|
95
97
|
setSubscribedTracks: {
|
|
96
98
|
audio: true,
|
|
97
|
-
video: isVideoRecordingEnabled
|
|
99
|
+
video: isVideoRecordingEnabled,
|
|
98
100
|
},
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -295,7 +297,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
295
297
|
timestamp: new Date().toISOString()
|
|
296
298
|
});
|
|
297
299
|
const webCallStartTime = Date.now();
|
|
298
|
-
|
|
300
|
+
// An unset option stays undefined, so JSON.stringify drops the key and
|
|
301
|
+
// older servers never see it.
|
|
302
|
+
const createWebCallBody = {
|
|
299
303
|
assistant: typeof assistant === 'string' ? undefined : assistant,
|
|
300
304
|
assistantId: typeof assistant === 'string' ? assistant : undefined,
|
|
301
305
|
assistantOverrides,
|
|
@@ -305,7 +309,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
305
309
|
workflowId: typeof workflow === 'string' ? workflow : undefined,
|
|
306
310
|
workflowOverrides,
|
|
307
311
|
roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
|
|
308
|
-
|
|
312
|
+
videoRecordingStartMode: options?.videoRecordingStartMode,
|
|
313
|
+
};
|
|
314
|
+
const webCall = (await client_1.client.call.callControllerCreateWebCall(createWebCallBody)).data;
|
|
309
315
|
const webCallDuration = Date.now() - webCallStartTime;
|
|
310
316
|
this.emit('call-start-progress', {
|
|
311
317
|
stage: 'web-call-creation',
|
|
@@ -319,12 +325,16 @@ class Vapi extends VapiEventEmitter {
|
|
|
319
325
|
}
|
|
320
326
|
});
|
|
321
327
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
322
|
-
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
323
328
|
// When video recording is enabled, the server sends a Daily meeting
|
|
324
|
-
// token
|
|
325
|
-
//
|
|
326
|
-
// servers
|
|
327
|
-
|
|
329
|
+
// token. In client mode it auto-starts the cloud recording on join. In
|
|
330
|
+
// server mode it starts nothing: Vapi starts the recording from its
|
|
331
|
+
// servers once the user has joined, which a weak client network can't
|
|
332
|
+
// stop. Without a token the SDK emits recording-error and starts
|
|
333
|
+
// nothing.
|
|
334
|
+
const transport = webCall?.transport;
|
|
335
|
+
const callToken = transport?.callToken;
|
|
336
|
+
const isRecordingStartedByServer = transport?.videoRecordingStartMode === 'server';
|
|
337
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
328
338
|
// Stage 2: Create Daily call object
|
|
329
339
|
this.emit('call-start-progress', {
|
|
330
340
|
stage: 'daily-call-object-creation',
|
|
@@ -334,7 +344,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
334
344
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
335
345
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
336
346
|
isVideoRecordingEnabled,
|
|
337
|
-
isVideoEnabled
|
|
338
347
|
}
|
|
339
348
|
});
|
|
340
349
|
const dailyCallStartTime = Date.now();
|
|
@@ -435,7 +444,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
435
444
|
this.call.on('participant-joined', (e) => {
|
|
436
445
|
if (!e || !this.call)
|
|
437
446
|
return;
|
|
438
|
-
subscribeToTracks(e, this.call, isVideoRecordingEnabled
|
|
447
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
439
448
|
});
|
|
440
449
|
this.call.on('participant-updated', (e) => {
|
|
441
450
|
if (!e) {
|
|
@@ -449,6 +458,10 @@ class Vapi extends VapiEventEmitter {
|
|
|
449
458
|
}
|
|
450
459
|
this.detachAudioPlayer(e.participant.session_id);
|
|
451
460
|
});
|
|
461
|
+
// Registered before join(), as in reconnect(): Vapi starts a server-mode
|
|
462
|
+
// recording when it sees the user join, so its start-failed message can
|
|
463
|
+
// arrive while join() is still in flight.
|
|
464
|
+
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
452
465
|
// Registered before join(): a token-auto-started recording can begin
|
|
453
466
|
// while join() is still in flight, so a listener added after join
|
|
454
467
|
// could miss the event.
|
|
@@ -543,51 +556,29 @@ class Vapi extends VapiEventEmitter {
|
|
|
543
556
|
}
|
|
544
557
|
// Stage 5: Video recording setup (if enabled)
|
|
545
558
|
if (isVideoRecordingEnabled && callToken) {
|
|
546
|
-
// The meeting token auto-started the recording at join,
|
|
547
|
-
// nothing to request from the
|
|
559
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
560
|
+
// starts it from its servers, so there is nothing to request from the
|
|
561
|
+
// client.
|
|
548
562
|
this.emit('call-start-progress', {
|
|
549
563
|
stage: 'video-recording-setup',
|
|
550
564
|
status: 'completed',
|
|
551
565
|
timestamp: new Date().toISOString(),
|
|
552
|
-
metadata: {
|
|
566
|
+
metadata: {
|
|
567
|
+
action: isRecordingStartedByServer
|
|
568
|
+
? 'started-by-server'
|
|
569
|
+
: 'auto-started-via-meeting-token'
|
|
570
|
+
}
|
|
553
571
|
});
|
|
554
572
|
}
|
|
555
573
|
else if (isVideoRecordingEnabled) {
|
|
574
|
+
// No token: nothing starts the recording. The recording-error is
|
|
575
|
+
// reported once start() has finished.
|
|
556
576
|
this.emit('call-start-progress', {
|
|
557
577
|
stage: 'video-recording-setup',
|
|
558
|
-
status: '
|
|
559
|
-
timestamp: new Date().toISOString()
|
|
578
|
+
status: 'failed',
|
|
579
|
+
timestamp: new Date().toISOString(),
|
|
580
|
+
metadata: { action: 'no-meeting-token' }
|
|
560
581
|
});
|
|
561
|
-
const recordingStartTime = Date.now();
|
|
562
|
-
try {
|
|
563
|
-
recordingRequestedTime = new Date().getTime();
|
|
564
|
-
this.startRecording();
|
|
565
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
566
|
-
this.emit('call-start-progress', {
|
|
567
|
-
stage: 'video-recording-setup',
|
|
568
|
-
status: 'completed',
|
|
569
|
-
duration: recordingSetupDuration,
|
|
570
|
-
timestamp: new Date().toISOString()
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
575
|
-
const serializedError = serializeError(error);
|
|
576
|
-
this.emit('call-start-progress', {
|
|
577
|
-
stage: 'video-recording-setup',
|
|
578
|
-
status: 'failed',
|
|
579
|
-
duration: recordingSetupDuration,
|
|
580
|
-
timestamp: new Date().toISOString(),
|
|
581
|
-
metadata: { error: serializedError.message }
|
|
582
|
-
});
|
|
583
|
-
this.emit('error', {
|
|
584
|
-
type: 'video-recording-setup-error',
|
|
585
|
-
stage: 'video-recording-setup',
|
|
586
|
-
error: serializedError,
|
|
587
|
-
timestamp: new Date().toISOString()
|
|
588
|
-
});
|
|
589
|
-
// Don't throw here, video recording is optional
|
|
590
|
-
}
|
|
591
582
|
}
|
|
592
583
|
else {
|
|
593
584
|
this.emit('call-start-progress', {
|
|
@@ -640,7 +631,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
640
631
|
if (e)
|
|
641
632
|
this.handleLocalAudioLevel(e);
|
|
642
633
|
});
|
|
643
|
-
this.call.on('app-message', (e) => this.onAppMessage(e));
|
|
644
634
|
this.call.on('nonfatal-error', (e) => {
|
|
645
635
|
// https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
|
|
646
636
|
if (e?.type === 'local-audio-level-observer-error') {
|
|
@@ -723,6 +713,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
723
713
|
callId: webCall?.id || 'unknown',
|
|
724
714
|
timestamp: new Date().toISOString()
|
|
725
715
|
});
|
|
716
|
+
if (recordingTokenMissing) {
|
|
717
|
+
this.recordingTokenMissingReportSchedule();
|
|
718
|
+
}
|
|
726
719
|
return webCall;
|
|
727
720
|
}
|
|
728
721
|
catch (e) {
|
|
@@ -772,6 +765,12 @@ class Vapi extends VapiEventEmitter {
|
|
|
772
765
|
else {
|
|
773
766
|
try {
|
|
774
767
|
const parsedMessage = JSON.parse(e.data);
|
|
768
|
+
// Not a Vapi client message: re-emitted as recording-error, and kept
|
|
769
|
+
// out of 'message' because its consumers switch on `type`.
|
|
770
|
+
if (parsedMessage?.type === VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE) {
|
|
771
|
+
this.videoRecordingStartFailedReport(parsedMessage);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
775
774
|
this.emit('message', parsedMessage);
|
|
776
775
|
if (parsedMessage && 'type' in parsedMessage && 'status' in parsedMessage && parsedMessage.type === 'status-update' && parsedMessage.status === 'ended') {
|
|
777
776
|
this.hasEmittedCallEndedStatus = true;
|
|
@@ -939,6 +938,37 @@ class Vapi extends VapiEventEmitter {
|
|
|
939
938
|
console.error(`[vapi] ${event} event listener threw`, error);
|
|
940
939
|
}
|
|
941
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Reported in a later task, like Daily's own recording-error: start() or
|
|
943
|
+
* reconnect() has resolved and the caller's await continuation has run, so a
|
|
944
|
+
* listener registered right after it still hears this, and one that calls
|
|
945
|
+
* stop() can't interrupt call setup. Skipped if that call was stopped or
|
|
946
|
+
* replaced.
|
|
947
|
+
*/
|
|
948
|
+
recordingTokenMissingReportSchedule() {
|
|
949
|
+
const call = this.call;
|
|
950
|
+
if (!call)
|
|
951
|
+
return;
|
|
952
|
+
setTimeout(() => {
|
|
953
|
+
if (!this.started || this.call !== call)
|
|
954
|
+
return;
|
|
955
|
+
this.emitToConsumer('recording-error', {
|
|
956
|
+
action: 'recording-error',
|
|
957
|
+
callClientId: call.callClientId,
|
|
958
|
+
errorMsg: 'Vapi returned no recording token, so the recording was not started. Call startRecording() to try starting it from the client.',
|
|
959
|
+
});
|
|
960
|
+
}, 0);
|
|
961
|
+
}
|
|
962
|
+
/** The fields are read defensively: the app message is untrusted JSON. */
|
|
963
|
+
videoRecordingStartFailedReport(message) {
|
|
964
|
+
const outcome = typeof message.outcome === 'string' ? message.outcome : 'unknown';
|
|
965
|
+
const error = typeof message.error === 'string' ? message.error : 'unknown';
|
|
966
|
+
this.emitToConsumer('recording-error', {
|
|
967
|
+
action: 'recording-error',
|
|
968
|
+
callClientId: this.call?.callClientId ?? '',
|
|
969
|
+
errorMsg: `Vapi couldn't start the recording (${outcome}): ${error}`,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
942
972
|
/** Drops the reference before removing, so nothing hands out a detached element. */
|
|
943
973
|
detachAudioPlayer(participantId) {
|
|
944
974
|
if (this.audioPlayer?.dataset.participantId === participantId) {
|
|
@@ -1171,11 +1201,14 @@ class Vapi extends VapiEventEmitter {
|
|
|
1171
1201
|
});
|
|
1172
1202
|
}
|
|
1173
1203
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1174
|
-
|
|
1175
|
-
// Same auto-start token as in start(). Rejoining with it while the
|
|
1204
|
+
// Same token as in start(). In client mode, rejoining with it while the
|
|
1176
1205
|
// recording is still running is safe; if the recording stopped when the
|
|
1177
|
-
// user left, the token starts a new one.
|
|
1206
|
+
// user left, the token starts a new one. In server mode it starts
|
|
1207
|
+
// nothing. Without a token the SDK emits recording-error and starts
|
|
1208
|
+
// nothing.
|
|
1178
1209
|
const callToken = webCall?.transport?.callToken;
|
|
1210
|
+
const isRecordingStartedByServer = webCall?.transport?.videoRecordingStartMode === 'server';
|
|
1211
|
+
const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
|
|
1179
1212
|
// Stage 1: Create Daily call object
|
|
1180
1213
|
this.emit('call-start-progress', {
|
|
1181
1214
|
stage: 'daily-call-object-creation',
|
|
@@ -1185,7 +1218,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
1185
1218
|
audioSource: this.dailyCallObject.audioSource ?? true,
|
|
1186
1219
|
videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
|
|
1187
1220
|
isVideoRecordingEnabled,
|
|
1188
|
-
isVideoEnabled
|
|
1189
1221
|
}
|
|
1190
1222
|
});
|
|
1191
1223
|
const dailyCallStartTime = Date.now();
|
|
@@ -1267,7 +1299,7 @@ class Vapi extends VapiEventEmitter {
|
|
|
1267
1299
|
this.call.on('participant-joined', (e) => {
|
|
1268
1300
|
if (!e || !this.call)
|
|
1269
1301
|
return;
|
|
1270
|
-
subscribeToTracks(e, this.call, isVideoRecordingEnabled
|
|
1302
|
+
subscribeToTracks(e, this.call, isVideoRecordingEnabled);
|
|
1271
1303
|
});
|
|
1272
1304
|
this.call.on('participant-updated', (e) => {
|
|
1273
1305
|
if (!e) {
|
|
@@ -1384,45 +1416,29 @@ class Vapi extends VapiEventEmitter {
|
|
|
1384
1416
|
});
|
|
1385
1417
|
// Stage 4: Video recording setup (if enabled)
|
|
1386
1418
|
if (isVideoRecordingEnabled && callToken) {
|
|
1387
|
-
// The meeting token auto-started the recording at join,
|
|
1388
|
-
// nothing to request from the
|
|
1419
|
+
// The meeting token auto-started the recording at join, or Vapi
|
|
1420
|
+
// starts it from its servers, so there is nothing to request from the
|
|
1421
|
+
// client.
|
|
1389
1422
|
this.emit('call-start-progress', {
|
|
1390
1423
|
stage: 'video-recording-setup',
|
|
1391
1424
|
status: 'completed',
|
|
1392
1425
|
timestamp: new Date().toISOString(),
|
|
1393
|
-
metadata: {
|
|
1426
|
+
metadata: {
|
|
1427
|
+
action: isRecordingStartedByServer
|
|
1428
|
+
? 'started-by-server'
|
|
1429
|
+
: 'auto-started-via-meeting-token'
|
|
1430
|
+
}
|
|
1394
1431
|
});
|
|
1395
1432
|
}
|
|
1396
1433
|
else if (isVideoRecordingEnabled) {
|
|
1434
|
+
// No token: nothing starts the recording. The recording-error is
|
|
1435
|
+
// reported once reconnect() has finished.
|
|
1397
1436
|
this.emit('call-start-progress', {
|
|
1398
1437
|
stage: 'video-recording-setup',
|
|
1399
|
-
status: '
|
|
1400
|
-
timestamp: new Date().toISOString()
|
|
1438
|
+
status: 'failed',
|
|
1439
|
+
timestamp: new Date().toISOString(),
|
|
1440
|
+
metadata: { action: 'no-meeting-token' }
|
|
1401
1441
|
});
|
|
1402
|
-
const recordingStartTime = Date.now();
|
|
1403
|
-
try {
|
|
1404
|
-
recordingRequestedTime = new Date().getTime();
|
|
1405
|
-
this.startRecording();
|
|
1406
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1407
|
-
this.emit('call-start-progress', {
|
|
1408
|
-
stage: 'video-recording-setup',
|
|
1409
|
-
status: 'completed',
|
|
1410
|
-
duration: recordingSetupDuration,
|
|
1411
|
-
timestamp: new Date().toISOString()
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
catch (error) {
|
|
1415
|
-
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1416
|
-
const serializedError = serializeError(error);
|
|
1417
|
-
this.emit('call-start-progress', {
|
|
1418
|
-
stage: 'video-recording-setup',
|
|
1419
|
-
status: 'failed',
|
|
1420
|
-
duration: recordingSetupDuration,
|
|
1421
|
-
timestamp: new Date().toISOString(),
|
|
1422
|
-
metadata: { error: serializedError.message }
|
|
1423
|
-
});
|
|
1424
|
-
// Don't throw here, video recording is optional
|
|
1425
|
-
}
|
|
1426
1442
|
}
|
|
1427
1443
|
else {
|
|
1428
1444
|
this.emit('call-start-progress', {
|
|
@@ -1516,6 +1532,9 @@ class Vapi extends VapiEventEmitter {
|
|
|
1516
1532
|
timestamp: new Date().toISOString()
|
|
1517
1533
|
});
|
|
1518
1534
|
this.emit('call-start');
|
|
1535
|
+
if (recordingTokenMissing) {
|
|
1536
|
+
this.recordingTokenMissingReportSchedule();
|
|
1537
|
+
}
|
|
1519
1538
|
}
|
|
1520
1539
|
catch (e) {
|
|
1521
1540
|
const totalDuration = Date.now() - startTime;
|