@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.
Files changed (8) hide show
  1. package/README.md +14 -0
  2. package/api.ts +25665 -11101
  3. package/dist/api.d.ts +22973 -13375
  4. package/dist/api.js +2171 -697
  5. package/dist/vapi.d.ts +25 -0
  6. package/dist/vapi.js +116 -48
  7. package/package.json +1 -1
  8. 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
- this.call.startRecording({
460
- width: 1280,
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
- this.call.startRecording({
1237
- width: 1280,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "2.6.3",
3
+ "version": "2.7.0",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",
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
- this.call.startRecording({
739
- width: 1280,
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
- this.call.startRecording({
1618
- width: 1280,
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);