@vapi-ai/web 2.6.3 → 2.7.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 +14 -0
- package/api.ts +25665 -11101
- package/dist/api.d.ts +22973 -13375
- package/dist/api.js +2171 -697
- package/dist/vapi.d.ts +25 -0
- package/dist/vapi.js +116 -48
- package/package.json +1 -1
- package/vapi.ts +136 -56
package/dist/vapi.d.ts
CHANGED
|
@@ -109,6 +109,16 @@ type WebCall = {
|
|
|
109
109
|
provider?: string;
|
|
110
110
|
};
|
|
111
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* The Vapi WebCall transport. When video recording is enabled, the server
|
|
114
|
+
* sets `callToken` to a Daily meeting token that auto-starts the cloud
|
|
115
|
+
* recording on join.
|
|
116
|
+
*
|
|
117
|
+
* call.transport
|
|
118
|
+
*/
|
|
119
|
+
transport?: {
|
|
120
|
+
callToken?: string;
|
|
121
|
+
};
|
|
112
122
|
};
|
|
113
123
|
declare class VapiEventEmitter extends EventEmitter {
|
|
114
124
|
on<E extends VapiEventNames>(event: E, listener: VapiEventListeners[E]): this;
|
|
@@ -256,6 +266,21 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
256
266
|
isLocalAudioLevelObserverRunning(): boolean;
|
|
257
267
|
startScreenSharing(displayMediaOptions?: DisplayMediaStreamOptions, screenVideoSendSettings?: DailyVideoSendSettings): void;
|
|
258
268
|
stopScreenSharing(): void;
|
|
269
|
+
/**
|
|
270
|
+
* Starts (or retries) a cloud recording of the call.
|
|
271
|
+
*
|
|
272
|
+
* Recording start is asynchronous: success is signaled by the
|
|
273
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
274
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
275
|
+
* recording start timed out), you can call this method again a few seconds
|
|
276
|
+
* later to retry.
|
|
277
|
+
*/
|
|
278
|
+
startRecording(): void;
|
|
279
|
+
/**
|
|
280
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
281
|
+
* 'recording-stopped' event.
|
|
282
|
+
*/
|
|
283
|
+
stopRecording(): void;
|
|
259
284
|
/**
|
|
260
285
|
* Reconnects to an active call.
|
|
261
286
|
*
|
package/dist/vapi.js
CHANGED
|
@@ -256,6 +256,11 @@ class Vapi extends VapiEventEmitter {
|
|
|
256
256
|
}
|
|
257
257
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
258
258
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
259
|
+
// When video recording is enabled, the server sends a Daily meeting
|
|
260
|
+
// token that auto-starts the cloud recording on join — more reliable
|
|
261
|
+
// than calling startRecording() from a weak client network. Older
|
|
262
|
+
// servers don't send one; then the client starts the recording itself.
|
|
263
|
+
const callToken = webCall?.transport?.callToken;
|
|
259
264
|
// Stage 2: Create Daily call object
|
|
260
265
|
this.emit('call-start-progress', {
|
|
261
266
|
stage: 'daily-call-object-creation',
|
|
@@ -313,6 +318,8 @@ class Vapi extends VapiEventEmitter {
|
|
|
313
318
|
this.hasEmittedCallEndedStatus = true;
|
|
314
319
|
}
|
|
315
320
|
if (isVideoRecordingEnabled) {
|
|
321
|
+
// When using a token, the recording doesn't stop automatically on
|
|
322
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
316
323
|
this.call?.stopRecording();
|
|
317
324
|
}
|
|
318
325
|
this.cleanup().catch(console.error);
|
|
@@ -378,6 +385,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
378
385
|
}
|
|
379
386
|
this.detachAudioPlayer(e.participant.session_id);
|
|
380
387
|
});
|
|
388
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
389
|
+
// while join() is still in flight, so a listener added after join
|
|
390
|
+
// could miss the event.
|
|
391
|
+
let recordingRequestedTime = 0;
|
|
392
|
+
if (isVideoRecordingEnabled) {
|
|
393
|
+
this.call.once('recording-started', () => {
|
|
394
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
395
|
+
this.emit('call-start-progress', {
|
|
396
|
+
stage: 'video-recording-started',
|
|
397
|
+
status: 'completed',
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
400
|
+
});
|
|
401
|
+
this.send({
|
|
402
|
+
type: 'control',
|
|
403
|
+
control: 'say-first-message',
|
|
404
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
381
408
|
// Stage 3: Mobile device handling and permissions
|
|
382
409
|
const isMobile = this.isMobileDevice();
|
|
383
410
|
this.emit('call-start-progress', {
|
|
@@ -413,10 +440,14 @@ class Vapi extends VapiEventEmitter {
|
|
|
413
440
|
timestamp: new Date().toISOString()
|
|
414
441
|
});
|
|
415
442
|
const joinStartTime = Date.now();
|
|
443
|
+
recordingRequestedTime = joinStartTime;
|
|
416
444
|
try {
|
|
417
445
|
await this.call.join({
|
|
418
446
|
// @ts-expect-error This exists
|
|
419
447
|
url: webCall.webCallUrl,
|
|
448
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
449
|
+
// only include it when the server actually sent one.
|
|
450
|
+
...(callToken ? { token: callToken } : {}),
|
|
420
451
|
subscribeToTracksAutomatically: false,
|
|
421
452
|
});
|
|
422
453
|
const joinDuration = Date.now() - joinStartTime;
|
|
@@ -447,23 +478,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
447
478
|
throw error;
|
|
448
479
|
}
|
|
449
480
|
// Stage 5: Video recording setup (if enabled)
|
|
450
|
-
if (isVideoRecordingEnabled) {
|
|
481
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
482
|
+
// The meeting token auto-started the recording at join, so there is
|
|
483
|
+
// nothing to request from the client.
|
|
484
|
+
this.emit('call-start-progress', {
|
|
485
|
+
stage: 'video-recording-setup',
|
|
486
|
+
status: 'completed',
|
|
487
|
+
timestamp: new Date().toISOString(),
|
|
488
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (isVideoRecordingEnabled) {
|
|
451
492
|
this.emit('call-start-progress', {
|
|
452
493
|
stage: 'video-recording-setup',
|
|
453
494
|
status: 'started',
|
|
454
495
|
timestamp: new Date().toISOString()
|
|
455
496
|
});
|
|
456
|
-
const recordingRequestedTime = new Date().getTime();
|
|
457
497
|
const recordingStartTime = Date.now();
|
|
458
498
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
height: 720,
|
|
462
|
-
backgroundColor: '#FF1F2D3D',
|
|
463
|
-
layout: {
|
|
464
|
-
preset: 'default',
|
|
465
|
-
},
|
|
466
|
-
});
|
|
499
|
+
recordingRequestedTime = new Date().getTime();
|
|
500
|
+
this.startRecording();
|
|
467
501
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
468
502
|
this.emit('call-start-progress', {
|
|
469
503
|
stage: 'video-recording-setup',
|
|
@@ -471,20 +505,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
471
505
|
duration: recordingSetupDuration,
|
|
472
506
|
timestamp: new Date().toISOString()
|
|
473
507
|
});
|
|
474
|
-
this.call.on('recording-started', () => {
|
|
475
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
476
|
-
this.emit('call-start-progress', {
|
|
477
|
-
stage: 'video-recording-started',
|
|
478
|
-
status: 'completed',
|
|
479
|
-
timestamp: new Date().toISOString(),
|
|
480
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
481
|
-
});
|
|
482
|
-
this.send({
|
|
483
|
-
type: 'control',
|
|
484
|
-
control: 'say-first-message',
|
|
485
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
508
|
}
|
|
489
509
|
catch (error) {
|
|
490
510
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
@@ -1001,6 +1021,35 @@ class Vapi extends VapiEventEmitter {
|
|
|
1001
1021
|
stopScreenSharing() {
|
|
1002
1022
|
this.call?.stopScreenShare();
|
|
1003
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Starts (or retries) a cloud recording of the call.
|
|
1026
|
+
*
|
|
1027
|
+
* Recording start is asynchronous: success is signaled by the
|
|
1028
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
1029
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
1030
|
+
* recording start timed out), you can call this method again a few seconds
|
|
1031
|
+
* later to retry.
|
|
1032
|
+
*/
|
|
1033
|
+
startRecording() {
|
|
1034
|
+
if (!this.call) {
|
|
1035
|
+
throw new Error('Call object is not available.');
|
|
1036
|
+
}
|
|
1037
|
+
this.call.startRecording({
|
|
1038
|
+
width: 1280,
|
|
1039
|
+
height: 720,
|
|
1040
|
+
backgroundColor: '#FF1F2D3D',
|
|
1041
|
+
layout: {
|
|
1042
|
+
preset: 'default',
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
1048
|
+
* 'recording-stopped' event.
|
|
1049
|
+
*/
|
|
1050
|
+
stopRecording() {
|
|
1051
|
+
this.call?.stopRecording();
|
|
1052
|
+
}
|
|
1004
1053
|
/**
|
|
1005
1054
|
* Reconnects to an active call.
|
|
1006
1055
|
*
|
|
@@ -1043,6 +1092,10 @@ class Vapi extends VapiEventEmitter {
|
|
|
1043
1092
|
}
|
|
1044
1093
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1045
1094
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1095
|
+
// Same auto-start token as in start(). Rejoining with it while the
|
|
1096
|
+
// recording is still running is safe; if the recording stopped when the
|
|
1097
|
+
// user left, the token starts a new one.
|
|
1098
|
+
const callToken = webCall?.transport?.callToken;
|
|
1046
1099
|
// Stage 1: Create Daily call object
|
|
1047
1100
|
this.emit('call-start-progress', {
|
|
1048
1101
|
stage: 'daily-call-object-creation',
|
|
@@ -1081,6 +1134,8 @@ class Vapi extends VapiEventEmitter {
|
|
|
1081
1134
|
this.hasEmittedCallEndedStatus = true;
|
|
1082
1135
|
}
|
|
1083
1136
|
if (isVideoRecordingEnabled) {
|
|
1137
|
+
// When using a token, the recording doesn't stop automatically on
|
|
1138
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
1084
1139
|
this.call?.stopRecording();
|
|
1085
1140
|
}
|
|
1086
1141
|
this.cleanup().catch(console.error);
|
|
@@ -1177,6 +1232,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
1177
1232
|
});
|
|
1178
1233
|
}
|
|
1179
1234
|
});
|
|
1235
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
1236
|
+
// while join() is still in flight, so a listener added after join
|
|
1237
|
+
// could miss the event.
|
|
1238
|
+
let recordingRequestedTime = 0;
|
|
1239
|
+
if (isVideoRecordingEnabled) {
|
|
1240
|
+
this.call.once('recording-started', () => {
|
|
1241
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1242
|
+
this.emit('call-start-progress', {
|
|
1243
|
+
stage: 'video-recording-started',
|
|
1244
|
+
status: 'completed',
|
|
1245
|
+
timestamp: new Date().toISOString(),
|
|
1246
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
1247
|
+
});
|
|
1248
|
+
this.send({
|
|
1249
|
+
type: 'control',
|
|
1250
|
+
control: 'say-first-message',
|
|
1251
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1180
1255
|
// Stage 2: Mobile device handling and permissions
|
|
1181
1256
|
const isMobile = this.isMobileDevice();
|
|
1182
1257
|
this.emit('call-start-progress', {
|
|
@@ -1212,8 +1287,12 @@ class Vapi extends VapiEventEmitter {
|
|
|
1212
1287
|
timestamp: new Date().toISOString()
|
|
1213
1288
|
});
|
|
1214
1289
|
const joinStartTime = Date.now();
|
|
1290
|
+
recordingRequestedTime = joinStartTime;
|
|
1215
1291
|
await this.call.join({
|
|
1216
1292
|
url: webCall.webCallUrl,
|
|
1293
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
1294
|
+
// only include it when the server actually sent one.
|
|
1295
|
+
...(callToken ? { token: callToken } : {}),
|
|
1217
1296
|
subscribeToTracksAutomatically: false,
|
|
1218
1297
|
});
|
|
1219
1298
|
const joinDuration = Date.now() - joinStartTime;
|
|
@@ -1224,23 +1303,26 @@ class Vapi extends VapiEventEmitter {
|
|
|
1224
1303
|
timestamp: new Date().toISOString()
|
|
1225
1304
|
});
|
|
1226
1305
|
// Stage 4: Video recording setup (if enabled)
|
|
1227
|
-
if (isVideoRecordingEnabled) {
|
|
1306
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
1307
|
+
// The meeting token auto-started the recording at join, so there is
|
|
1308
|
+
// nothing to request from the client.
|
|
1309
|
+
this.emit('call-start-progress', {
|
|
1310
|
+
stage: 'video-recording-setup',
|
|
1311
|
+
status: 'completed',
|
|
1312
|
+
timestamp: new Date().toISOString(),
|
|
1313
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
else if (isVideoRecordingEnabled) {
|
|
1228
1317
|
this.emit('call-start-progress', {
|
|
1229
1318
|
stage: 'video-recording-setup',
|
|
1230
1319
|
status: 'started',
|
|
1231
1320
|
timestamp: new Date().toISOString()
|
|
1232
1321
|
});
|
|
1233
1322
|
const recordingStartTime = Date.now();
|
|
1234
|
-
const recordingRequestedTime = new Date().getTime();
|
|
1235
1323
|
try {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
height: 720,
|
|
1239
|
-
backgroundColor: '#FF1F2D3D',
|
|
1240
|
-
layout: {
|
|
1241
|
-
preset: 'default',
|
|
1242
|
-
},
|
|
1243
|
-
});
|
|
1324
|
+
recordingRequestedTime = new Date().getTime();
|
|
1325
|
+
this.startRecording();
|
|
1244
1326
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1245
1327
|
this.emit('call-start-progress', {
|
|
1246
1328
|
stage: 'video-recording-setup',
|
|
@@ -1248,20 +1330,6 @@ class Vapi extends VapiEventEmitter {
|
|
|
1248
1330
|
duration: recordingSetupDuration,
|
|
1249
1331
|
timestamp: new Date().toISOString()
|
|
1250
1332
|
});
|
|
1251
|
-
this.call.on('recording-started', () => {
|
|
1252
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1253
|
-
this.emit('call-start-progress', {
|
|
1254
|
-
stage: 'video-recording-started',
|
|
1255
|
-
status: 'completed',
|
|
1256
|
-
timestamp: new Date().toISOString(),
|
|
1257
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
1258
|
-
});
|
|
1259
|
-
this.send({
|
|
1260
|
-
type: 'control',
|
|
1261
|
-
control: 'say-first-message',
|
|
1262
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1263
|
-
});
|
|
1264
|
-
});
|
|
1265
1333
|
}
|
|
1266
1334
|
catch (error) {
|
|
1267
1335
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
package/package.json
CHANGED
package/vapi.ts
CHANGED
|
@@ -236,10 +236,18 @@ type WebCall = {
|
|
|
236
236
|
artifactPlan?: { videoRecordingEnabled?: boolean };
|
|
237
237
|
/**
|
|
238
238
|
* The Vapi WebCall assistant. This is the assistant of the call.
|
|
239
|
-
*
|
|
239
|
+
*
|
|
240
240
|
* call.assistant
|
|
241
241
|
*/
|
|
242
242
|
assistant?: { voice?: { provider?: string } };
|
|
243
|
+
/**
|
|
244
|
+
* The Vapi WebCall transport. When video recording is enabled, the server
|
|
245
|
+
* sets `callToken` to a Daily meeting token that auto-starts the cloud
|
|
246
|
+
* recording on join.
|
|
247
|
+
*
|
|
248
|
+
* call.transport
|
|
249
|
+
*/
|
|
250
|
+
transport?: { callToken?: string };
|
|
243
251
|
}
|
|
244
252
|
|
|
245
253
|
async function startAudioPlayer(
|
|
@@ -512,6 +520,14 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
512
520
|
|
|
513
521
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
514
522
|
|
|
523
|
+
// When video recording is enabled, the server sends a Daily meeting
|
|
524
|
+
// token that auto-starts the cloud recording on join — more reliable
|
|
525
|
+
// than calling startRecording() from a weak client network. Older
|
|
526
|
+
// servers don't send one; then the client starts the recording itself.
|
|
527
|
+
const callToken = (
|
|
528
|
+
webCall?.transport as { callToken?: string } | undefined
|
|
529
|
+
)?.callToken;
|
|
530
|
+
|
|
515
531
|
// Stage 2: Create Daily call object
|
|
516
532
|
this.emit('call-start-progress', {
|
|
517
533
|
stage: 'daily-call-object-creation',
|
|
@@ -573,6 +589,8 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
573
589
|
this.hasEmittedCallEndedStatus = true;
|
|
574
590
|
}
|
|
575
591
|
if (isVideoRecordingEnabled) {
|
|
592
|
+
// When using a token, the recording doesn't stop automatically on
|
|
593
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
576
594
|
this.call?.stopRecording();
|
|
577
595
|
}
|
|
578
596
|
this.cleanup().catch(console.error);
|
|
@@ -651,6 +669,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
651
669
|
this.detachAudioPlayer(e.participant.session_id);
|
|
652
670
|
});
|
|
653
671
|
|
|
672
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
673
|
+
// while join() is still in flight, so a listener added after join
|
|
674
|
+
// could miss the event.
|
|
675
|
+
let recordingRequestedTime = 0;
|
|
676
|
+
if (isVideoRecordingEnabled) {
|
|
677
|
+
this.call.once('recording-started', () => {
|
|
678
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
679
|
+
this.emit('call-start-progress', {
|
|
680
|
+
stage: 'video-recording-started',
|
|
681
|
+
status: 'completed',
|
|
682
|
+
timestamp: new Date().toISOString(),
|
|
683
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
this.send({
|
|
687
|
+
type: 'control',
|
|
688
|
+
control: 'say-first-message',
|
|
689
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
654
694
|
// Stage 3: Mobile device handling and permissions
|
|
655
695
|
const isMobile = this.isMobileDevice();
|
|
656
696
|
this.emit('call-start-progress', {
|
|
@@ -688,11 +728,15 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
688
728
|
});
|
|
689
729
|
|
|
690
730
|
const joinStartTime = Date.now();
|
|
691
|
-
|
|
731
|
+
recordingRequestedTime = joinStartTime;
|
|
732
|
+
|
|
692
733
|
try {
|
|
693
734
|
await this.call.join({
|
|
694
735
|
// @ts-expect-error This exists
|
|
695
736
|
url: webCall.webCallUrl,
|
|
737
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
738
|
+
// only include it when the server actually sent one.
|
|
739
|
+
...(callToken ? { token: callToken } : {}),
|
|
696
740
|
subscribeToTracksAutomatically: false,
|
|
697
741
|
});
|
|
698
742
|
|
|
@@ -724,25 +768,27 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
724
768
|
}
|
|
725
769
|
|
|
726
770
|
// Stage 5: Video recording setup (if enabled)
|
|
727
|
-
if (isVideoRecordingEnabled) {
|
|
771
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
772
|
+
// The meeting token auto-started the recording at join, so there is
|
|
773
|
+
// nothing to request from the client.
|
|
774
|
+
this.emit('call-start-progress', {
|
|
775
|
+
stage: 'video-recording-setup',
|
|
776
|
+
status: 'completed',
|
|
777
|
+
timestamp: new Date().toISOString(),
|
|
778
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
779
|
+
});
|
|
780
|
+
} else if (isVideoRecordingEnabled) {
|
|
728
781
|
this.emit('call-start-progress', {
|
|
729
782
|
stage: 'video-recording-setup',
|
|
730
783
|
status: 'started',
|
|
731
784
|
timestamp: new Date().toISOString()
|
|
732
785
|
});
|
|
733
|
-
|
|
734
|
-
const recordingRequestedTime = new Date().getTime();
|
|
786
|
+
|
|
735
787
|
const recordingStartTime = Date.now();
|
|
736
788
|
|
|
737
789
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
height: 720,
|
|
741
|
-
backgroundColor: '#FF1F2D3D',
|
|
742
|
-
layout: {
|
|
743
|
-
preset: 'default',
|
|
744
|
-
},
|
|
745
|
-
});
|
|
790
|
+
recordingRequestedTime = new Date().getTime();
|
|
791
|
+
this.startRecording();
|
|
746
792
|
|
|
747
793
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
748
794
|
this.emit('call-start-progress', {
|
|
@@ -751,22 +797,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
751
797
|
duration: recordingSetupDuration,
|
|
752
798
|
timestamp: new Date().toISOString()
|
|
753
799
|
});
|
|
754
|
-
|
|
755
|
-
this.call.on('recording-started', () => {
|
|
756
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
757
|
-
this.emit('call-start-progress', {
|
|
758
|
-
stage: 'video-recording-started',
|
|
759
|
-
status: 'completed',
|
|
760
|
-
timestamp: new Date().toISOString(),
|
|
761
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
this.send({
|
|
765
|
-
type: 'control',
|
|
766
|
-
control: 'say-first-message',
|
|
767
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
768
|
-
});
|
|
769
|
-
});
|
|
770
800
|
} catch (error) {
|
|
771
801
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
772
802
|
const serializedError = serializeError(error);
|
|
@@ -1350,6 +1380,37 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1350
1380
|
this.call?.stopScreenShare();
|
|
1351
1381
|
}
|
|
1352
1382
|
|
|
1383
|
+
/**
|
|
1384
|
+
* Starts (or retries) a cloud recording of the call.
|
|
1385
|
+
*
|
|
1386
|
+
* Recording start is asynchronous: success is signaled by the
|
|
1387
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
1388
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
1389
|
+
* recording start timed out), you can call this method again a few seconds
|
|
1390
|
+
* later to retry.
|
|
1391
|
+
*/
|
|
1392
|
+
public startRecording(): void {
|
|
1393
|
+
if (!this.call) {
|
|
1394
|
+
throw new Error('Call object is not available.');
|
|
1395
|
+
}
|
|
1396
|
+
this.call.startRecording({
|
|
1397
|
+
width: 1280,
|
|
1398
|
+
height: 720,
|
|
1399
|
+
backgroundColor: '#FF1F2D3D',
|
|
1400
|
+
layout: {
|
|
1401
|
+
preset: 'default',
|
|
1402
|
+
},
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
1408
|
+
* 'recording-stopped' event.
|
|
1409
|
+
*/
|
|
1410
|
+
public stopRecording(): void {
|
|
1411
|
+
this.call?.stopRecording();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1353
1414
|
/**
|
|
1354
1415
|
* Reconnects to an active call.
|
|
1355
1416
|
*
|
|
@@ -1399,6 +1460,11 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1399
1460
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1400
1461
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1401
1462
|
|
|
1463
|
+
// Same auto-start token as in start(). Rejoining with it while the
|
|
1464
|
+
// recording is still running is safe; if the recording stopped when the
|
|
1465
|
+
// user left, the token starts a new one.
|
|
1466
|
+
const callToken = webCall?.transport?.callToken;
|
|
1467
|
+
|
|
1402
1468
|
// Stage 1: Create Daily call object
|
|
1403
1469
|
this.emit('call-start-progress', {
|
|
1404
1470
|
stage: 'daily-call-object-creation',
|
|
@@ -1441,6 +1507,8 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1441
1507
|
this.hasEmittedCallEndedStatus = true;
|
|
1442
1508
|
}
|
|
1443
1509
|
if (isVideoRecordingEnabled) {
|
|
1510
|
+
// When using a token, the recording doesn't stop automatically on
|
|
1511
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
1444
1512
|
this.call?.stopRecording();
|
|
1445
1513
|
}
|
|
1446
1514
|
this.cleanup().catch(console.error);
|
|
@@ -1552,6 +1620,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1552
1620
|
}
|
|
1553
1621
|
});
|
|
1554
1622
|
|
|
1623
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
1624
|
+
// while join() is still in flight, so a listener added after join
|
|
1625
|
+
// could miss the event.
|
|
1626
|
+
let recordingRequestedTime = 0;
|
|
1627
|
+
if (isVideoRecordingEnabled) {
|
|
1628
|
+
this.call.once('recording-started', () => {
|
|
1629
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1630
|
+
this.emit('call-start-progress', {
|
|
1631
|
+
stage: 'video-recording-started',
|
|
1632
|
+
status: 'completed',
|
|
1633
|
+
timestamp: new Date().toISOString(),
|
|
1634
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
this.send({
|
|
1638
|
+
type: 'control',
|
|
1639
|
+
control: 'say-first-message',
|
|
1640
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1641
|
+
});
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1555
1645
|
// Stage 2: Mobile device handling and permissions
|
|
1556
1646
|
const isMobile = this.isMobileDevice();
|
|
1557
1647
|
this.emit('call-start-progress', {
|
|
@@ -1589,8 +1679,12 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1589
1679
|
});
|
|
1590
1680
|
|
|
1591
1681
|
const joinStartTime = Date.now();
|
|
1682
|
+
recordingRequestedTime = joinStartTime;
|
|
1592
1683
|
await this.call.join({
|
|
1593
1684
|
url: webCall.webCallUrl,
|
|
1685
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
1686
|
+
// only include it when the server actually sent one.
|
|
1687
|
+
...(callToken ? { token: callToken } : {}),
|
|
1594
1688
|
subscribeToTracksAutomatically: false,
|
|
1595
1689
|
});
|
|
1596
1690
|
|
|
@@ -1603,25 +1697,27 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1603
1697
|
});
|
|
1604
1698
|
|
|
1605
1699
|
// Stage 4: Video recording setup (if enabled)
|
|
1606
|
-
if (isVideoRecordingEnabled) {
|
|
1700
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
1701
|
+
// The meeting token auto-started the recording at join, so there is
|
|
1702
|
+
// nothing to request from the client.
|
|
1703
|
+
this.emit('call-start-progress', {
|
|
1704
|
+
stage: 'video-recording-setup',
|
|
1705
|
+
status: 'completed',
|
|
1706
|
+
timestamp: new Date().toISOString(),
|
|
1707
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
1708
|
+
});
|
|
1709
|
+
} else if (isVideoRecordingEnabled) {
|
|
1607
1710
|
this.emit('call-start-progress', {
|
|
1608
1711
|
stage: 'video-recording-setup',
|
|
1609
1712
|
status: 'started',
|
|
1610
1713
|
timestamp: new Date().toISOString()
|
|
1611
1714
|
});
|
|
1612
|
-
|
|
1715
|
+
|
|
1613
1716
|
const recordingStartTime = Date.now();
|
|
1614
|
-
const recordingRequestedTime = new Date().getTime();
|
|
1615
1717
|
|
|
1616
1718
|
try {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
height: 720,
|
|
1620
|
-
backgroundColor: '#FF1F2D3D',
|
|
1621
|
-
layout: {
|
|
1622
|
-
preset: 'default',
|
|
1623
|
-
},
|
|
1624
|
-
});
|
|
1719
|
+
recordingRequestedTime = new Date().getTime();
|
|
1720
|
+
this.startRecording();
|
|
1625
1721
|
|
|
1626
1722
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1627
1723
|
this.emit('call-start-progress', {
|
|
@@ -1630,22 +1726,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1630
1726
|
duration: recordingSetupDuration,
|
|
1631
1727
|
timestamp: new Date().toISOString()
|
|
1632
1728
|
});
|
|
1633
|
-
|
|
1634
|
-
this.call.on('recording-started', () => {
|
|
1635
|
-
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1636
|
-
this.emit('call-start-progress', {
|
|
1637
|
-
stage: 'video-recording-started',
|
|
1638
|
-
status: 'completed',
|
|
1639
|
-
timestamp: new Date().toISOString(),
|
|
1640
|
-
metadata: { delaySeconds: totalRecordingDelay }
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
this.send({
|
|
1644
|
-
type: 'control',
|
|
1645
|
-
control: 'say-first-message',
|
|
1646
|
-
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1647
|
-
});
|
|
1648
|
-
});
|
|
1649
1729
|
} catch (error) {
|
|
1650
1730
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1651
1731
|
const serializedError = serializeError(error);
|