@vapi-ai/web 2.6.3 → 2.7.1
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 +44 -0
- package/dist/vapi.js +171 -58
- package/package.json +1 -1
- package/vapi.ts +208 -78
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(
|
|
@@ -415,6 +423,36 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
415
423
|
});
|
|
416
424
|
}
|
|
417
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Starts Daily's local (microphone) audio level observer.
|
|
428
|
+
*
|
|
429
|
+
* Only runs when something is listening for 'local-volume-level'. The observer
|
|
430
|
+
* costs an AudioContext plus an AudioWorklet for the lifetime of the call and
|
|
431
|
+
* nothing else in the SDK reads the level, so starting it unconditionally
|
|
432
|
+
* charges every consumer for a feature almost none of them use. Listeners
|
|
433
|
+
* attached after the call is under way should call the public
|
|
434
|
+
* `startLocalAudioLevelObserver()`.
|
|
435
|
+
*
|
|
436
|
+
* Must run after the noise-cancellation processor has settled. Krisp replaces
|
|
437
|
+
* the microphone track, Daily reacts to any local track change by closing the
|
|
438
|
+
* AudioContext its in-flight `audioWorklet.addModule()` is still loading into,
|
|
439
|
+
* and Chrome and Firefox reject that load with "AbortError: Unable to load a
|
|
440
|
+
* worklet's module". Daily answers by stopping the observer for the rest of
|
|
441
|
+
* the call, so losing this race costs the feature, not just console noise.
|
|
442
|
+
* Tracked upstream at https://github.com/daily-co/daily-js/issues/317.
|
|
443
|
+
*/
|
|
444
|
+
private async maybeStartLocalAudioLevelObserver(): Promise<void> {
|
|
445
|
+
if (!this.call || this.listenerCount('local-volume-level') === 0) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
await this.call.startLocalAudioLevelObserver(100);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
this.emit('local-audio-level-observer-error', serializeError(error));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
418
456
|
async start(
|
|
419
457
|
assistant?: CreateAssistantDTO | string,
|
|
420
458
|
assistantOverrides?: AssistantOverrides,
|
|
@@ -512,6 +550,14 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
512
550
|
|
|
513
551
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
514
552
|
|
|
553
|
+
// When video recording is enabled, the server sends a Daily meeting
|
|
554
|
+
// token that auto-starts the cloud recording on join — more reliable
|
|
555
|
+
// than calling startRecording() from a weak client network. Older
|
|
556
|
+
// servers don't send one; then the client starts the recording itself.
|
|
557
|
+
const callToken = (
|
|
558
|
+
webCall?.transport as { callToken?: string } | undefined
|
|
559
|
+
)?.callToken;
|
|
560
|
+
|
|
515
561
|
// Stage 2: Create Daily call object
|
|
516
562
|
this.emit('call-start-progress', {
|
|
517
563
|
stage: 'daily-call-object-creation',
|
|
@@ -573,6 +619,8 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
573
619
|
this.hasEmittedCallEndedStatus = true;
|
|
574
620
|
}
|
|
575
621
|
if (isVideoRecordingEnabled) {
|
|
622
|
+
// When using a token, the recording doesn't stop automatically on
|
|
623
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
576
624
|
this.call?.stopRecording();
|
|
577
625
|
}
|
|
578
626
|
this.cleanup().catch(console.error);
|
|
@@ -651,6 +699,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
651
699
|
this.detachAudioPlayer(e.participant.session_id);
|
|
652
700
|
});
|
|
653
701
|
|
|
702
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
703
|
+
// while join() is still in flight, so a listener added after join
|
|
704
|
+
// could miss the event.
|
|
705
|
+
let recordingRequestedTime = 0;
|
|
706
|
+
if (isVideoRecordingEnabled) {
|
|
707
|
+
this.call.once('recording-started', () => {
|
|
708
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
709
|
+
this.emit('call-start-progress', {
|
|
710
|
+
stage: 'video-recording-started',
|
|
711
|
+
status: 'completed',
|
|
712
|
+
timestamp: new Date().toISOString(),
|
|
713
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
this.send({
|
|
717
|
+
type: 'control',
|
|
718
|
+
control: 'say-first-message',
|
|
719
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
654
724
|
// Stage 3: Mobile device handling and permissions
|
|
655
725
|
const isMobile = this.isMobileDevice();
|
|
656
726
|
this.emit('call-start-progress', {
|
|
@@ -688,11 +758,15 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
688
758
|
});
|
|
689
759
|
|
|
690
760
|
const joinStartTime = Date.now();
|
|
691
|
-
|
|
761
|
+
recordingRequestedTime = joinStartTime;
|
|
762
|
+
|
|
692
763
|
try {
|
|
693
764
|
await this.call.join({
|
|
694
765
|
// @ts-expect-error This exists
|
|
695
766
|
url: webCall.webCallUrl,
|
|
767
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
768
|
+
// only include it when the server actually sent one.
|
|
769
|
+
...(callToken ? { token: callToken } : {}),
|
|
696
770
|
subscribeToTracksAutomatically: false,
|
|
697
771
|
});
|
|
698
772
|
|
|
@@ -724,25 +798,27 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
724
798
|
}
|
|
725
799
|
|
|
726
800
|
// Stage 5: Video recording setup (if enabled)
|
|
727
|
-
if (isVideoRecordingEnabled) {
|
|
801
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
802
|
+
// The meeting token auto-started the recording at join, so there is
|
|
803
|
+
// nothing to request from the client.
|
|
804
|
+
this.emit('call-start-progress', {
|
|
805
|
+
stage: 'video-recording-setup',
|
|
806
|
+
status: 'completed',
|
|
807
|
+
timestamp: new Date().toISOString(),
|
|
808
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
809
|
+
});
|
|
810
|
+
} else if (isVideoRecordingEnabled) {
|
|
728
811
|
this.emit('call-start-progress', {
|
|
729
812
|
stage: 'video-recording-setup',
|
|
730
813
|
status: 'started',
|
|
731
814
|
timestamp: new Date().toISOString()
|
|
732
815
|
});
|
|
733
|
-
|
|
734
|
-
const recordingRequestedTime = new Date().getTime();
|
|
816
|
+
|
|
735
817
|
const recordingStartTime = Date.now();
|
|
736
818
|
|
|
737
819
|
try {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
height: 720,
|
|
741
|
-
backgroundColor: '#FF1F2D3D',
|
|
742
|
-
layout: {
|
|
743
|
-
preset: 'default',
|
|
744
|
-
},
|
|
745
|
-
});
|
|
820
|
+
recordingRequestedTime = new Date().getTime();
|
|
821
|
+
this.startRecording();
|
|
746
822
|
|
|
747
823
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
748
824
|
this.emit('call-start-progress', {
|
|
@@ -751,22 +827,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
751
827
|
duration: recordingSetupDuration,
|
|
752
828
|
timestamp: new Date().toISOString()
|
|
753
829
|
});
|
|
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
830
|
} catch (error) {
|
|
771
831
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
772
832
|
const serializedError = serializeError(error);
|
|
@@ -805,7 +865,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
805
865
|
|
|
806
866
|
try {
|
|
807
867
|
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
808
|
-
this.call.startLocalAudioLevelObserver(100);
|
|
809
868
|
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
810
869
|
this.emit('call-start-progress', {
|
|
811
870
|
stage: 'audio-observer-setup',
|
|
@@ -875,17 +934,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
875
934
|
const audioProcessingStartTime = Date.now();
|
|
876
935
|
|
|
877
936
|
try {
|
|
878
|
-
this.call
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
type: 'noise-cancellation',
|
|
883
|
-
},
|
|
937
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
938
|
+
audio: {
|
|
939
|
+
processor: {
|
|
940
|
+
type: 'noise-cancellation',
|
|
884
941
|
},
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
audioProcessingUpdate.catch((error) => {
|
|
946
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// The observer waits for this update rather than starting alongside the
|
|
950
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
951
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
952
|
+
//
|
|
953
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
954
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
955
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
956
|
+
audioProcessingUpdate
|
|
957
|
+
.catch(() => {})
|
|
958
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
889
959
|
|
|
890
960
|
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
891
961
|
this.emit('call-start-progress', {
|
|
@@ -1350,6 +1420,37 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1350
1420
|
this.call?.stopScreenShare();
|
|
1351
1421
|
}
|
|
1352
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Starts (or retries) a cloud recording of the call.
|
|
1425
|
+
*
|
|
1426
|
+
* Recording start is asynchronous: success is signaled by the
|
|
1427
|
+
* 'recording-started' event and failure by 'recording-error'. Daily does not
|
|
1428
|
+
* retry automatically, so if you receive 'recording-error' (e.g. the
|
|
1429
|
+
* recording start timed out), you can call this method again a few seconds
|
|
1430
|
+
* later to retry.
|
|
1431
|
+
*/
|
|
1432
|
+
public startRecording(): void {
|
|
1433
|
+
if (!this.call) {
|
|
1434
|
+
throw new Error('Call object is not available.');
|
|
1435
|
+
}
|
|
1436
|
+
this.call.startRecording({
|
|
1437
|
+
width: 1280,
|
|
1438
|
+
height: 720,
|
|
1439
|
+
backgroundColor: '#FF1F2D3D',
|
|
1440
|
+
layout: {
|
|
1441
|
+
preset: 'default',
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Stops the in-progress recording. Completion is signaled by the
|
|
1448
|
+
* 'recording-stopped' event.
|
|
1449
|
+
*/
|
|
1450
|
+
public stopRecording(): void {
|
|
1451
|
+
this.call?.stopRecording();
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1353
1454
|
/**
|
|
1354
1455
|
* Reconnects to an active call.
|
|
1355
1456
|
*
|
|
@@ -1399,6 +1500,11 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1399
1500
|
const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
|
|
1400
1501
|
const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
|
|
1401
1502
|
|
|
1503
|
+
// Same auto-start token as in start(). Rejoining with it while the
|
|
1504
|
+
// recording is still running is safe; if the recording stopped when the
|
|
1505
|
+
// user left, the token starts a new one.
|
|
1506
|
+
const callToken = webCall?.transport?.callToken;
|
|
1507
|
+
|
|
1402
1508
|
// Stage 1: Create Daily call object
|
|
1403
1509
|
this.emit('call-start-progress', {
|
|
1404
1510
|
stage: 'daily-call-object-creation',
|
|
@@ -1441,6 +1547,8 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1441
1547
|
this.hasEmittedCallEndedStatus = true;
|
|
1442
1548
|
}
|
|
1443
1549
|
if (isVideoRecordingEnabled) {
|
|
1550
|
+
// When using a token, the recording doesn't stop automatically on
|
|
1551
|
+
// leave, so still stop it manually to preserve existing behavior.
|
|
1444
1552
|
this.call?.stopRecording();
|
|
1445
1553
|
}
|
|
1446
1554
|
this.cleanup().catch(console.error);
|
|
@@ -1552,6 +1660,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1552
1660
|
}
|
|
1553
1661
|
});
|
|
1554
1662
|
|
|
1663
|
+
// Registered before join(): a token-auto-started recording can begin
|
|
1664
|
+
// while join() is still in flight, so a listener added after join
|
|
1665
|
+
// could miss the event.
|
|
1666
|
+
let recordingRequestedTime = 0;
|
|
1667
|
+
if (isVideoRecordingEnabled) {
|
|
1668
|
+
this.call.once('recording-started', () => {
|
|
1669
|
+
const totalRecordingDelay = (new Date().getTime() - recordingRequestedTime) / 1000;
|
|
1670
|
+
this.emit('call-start-progress', {
|
|
1671
|
+
stage: 'video-recording-started',
|
|
1672
|
+
status: 'completed',
|
|
1673
|
+
timestamp: new Date().toISOString(),
|
|
1674
|
+
metadata: { delaySeconds: totalRecordingDelay }
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
this.send({
|
|
1678
|
+
type: 'control',
|
|
1679
|
+
control: 'say-first-message',
|
|
1680
|
+
videoRecordingStartDelaySeconds: totalRecordingDelay,
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1555
1685
|
// Stage 2: Mobile device handling and permissions
|
|
1556
1686
|
const isMobile = this.isMobileDevice();
|
|
1557
1687
|
this.emit('call-start-progress', {
|
|
@@ -1589,8 +1719,12 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1589
1719
|
});
|
|
1590
1720
|
|
|
1591
1721
|
const joinStartTime = Date.now();
|
|
1722
|
+
recordingRequestedTime = joinStartTime;
|
|
1592
1723
|
await this.call.join({
|
|
1593
1724
|
url: webCall.webCallUrl,
|
|
1725
|
+
// daily-js rejects a `token` key that is present but undefined, so
|
|
1726
|
+
// only include it when the server actually sent one.
|
|
1727
|
+
...(callToken ? { token: callToken } : {}),
|
|
1594
1728
|
subscribeToTracksAutomatically: false,
|
|
1595
1729
|
});
|
|
1596
1730
|
|
|
@@ -1603,25 +1737,27 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1603
1737
|
});
|
|
1604
1738
|
|
|
1605
1739
|
// Stage 4: Video recording setup (if enabled)
|
|
1606
|
-
if (isVideoRecordingEnabled) {
|
|
1740
|
+
if (isVideoRecordingEnabled && callToken) {
|
|
1741
|
+
// The meeting token auto-started the recording at join, so there is
|
|
1742
|
+
// nothing to request from the client.
|
|
1743
|
+
this.emit('call-start-progress', {
|
|
1744
|
+
stage: 'video-recording-setup',
|
|
1745
|
+
status: 'completed',
|
|
1746
|
+
timestamp: new Date().toISOString(),
|
|
1747
|
+
metadata: { action: 'auto-started-via-meeting-token' }
|
|
1748
|
+
});
|
|
1749
|
+
} else if (isVideoRecordingEnabled) {
|
|
1607
1750
|
this.emit('call-start-progress', {
|
|
1608
1751
|
stage: 'video-recording-setup',
|
|
1609
1752
|
status: 'started',
|
|
1610
1753
|
timestamp: new Date().toISOString()
|
|
1611
1754
|
});
|
|
1612
|
-
|
|
1755
|
+
|
|
1613
1756
|
const recordingStartTime = Date.now();
|
|
1614
|
-
const recordingRequestedTime = new Date().getTime();
|
|
1615
1757
|
|
|
1616
1758
|
try {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
height: 720,
|
|
1620
|
-
backgroundColor: '#FF1F2D3D',
|
|
1621
|
-
layout: {
|
|
1622
|
-
preset: 'default',
|
|
1623
|
-
},
|
|
1624
|
-
});
|
|
1759
|
+
recordingRequestedTime = new Date().getTime();
|
|
1760
|
+
this.startRecording();
|
|
1625
1761
|
|
|
1626
1762
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1627
1763
|
this.emit('call-start-progress', {
|
|
@@ -1630,22 +1766,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1630
1766
|
duration: recordingSetupDuration,
|
|
1631
1767
|
timestamp: new Date().toISOString()
|
|
1632
1768
|
});
|
|
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
1769
|
} catch (error) {
|
|
1650
1770
|
const recordingSetupDuration = Date.now() - recordingStartTime;
|
|
1651
1771
|
const serializedError = serializeError(error);
|
|
@@ -1678,7 +1798,6 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1678
1798
|
|
|
1679
1799
|
try {
|
|
1680
1800
|
this.call.startRemoteParticipantsAudioLevelObserver(100);
|
|
1681
|
-
this.call.startLocalAudioLevelObserver(100);
|
|
1682
1801
|
const audioObserverDuration = Date.now() - audioObserverStartTime;
|
|
1683
1802
|
this.emit('call-start-progress', {
|
|
1684
1803
|
stage: 'audio-observer-setup',
|
|
@@ -1709,17 +1828,28 @@ export default class Vapi extends VapiEventEmitter {
|
|
|
1709
1828
|
const audioProcessingStartTime = Date.now();
|
|
1710
1829
|
|
|
1711
1830
|
try {
|
|
1712
|
-
this.call
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
type: 'noise-cancellation',
|
|
1717
|
-
},
|
|
1831
|
+
const audioProcessingUpdate = this.call.updateInputSettings({
|
|
1832
|
+
audio: {
|
|
1833
|
+
processor: {
|
|
1834
|
+
type: 'noise-cancellation',
|
|
1718
1835
|
},
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1836
|
+
},
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
audioProcessingUpdate.catch((error) => {
|
|
1840
|
+
this.emitAudioProcessingError('audio-processing-setup', error);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
// The observer waits for this update rather than starting alongside the
|
|
1844
|
+
// remote one above, so that Krisp has already swapped the microphone
|
|
1845
|
+
// track. See maybeStartLocalAudioLevelObserver().
|
|
1846
|
+
//
|
|
1847
|
+
// A separate chain, not another link on the one above: EventEmitter
|
|
1848
|
+
// rethrows out of emit('error') when a consumer registered no 'error'
|
|
1849
|
+
// listener, and whether the observer starts must not hinge on that.
|
|
1850
|
+
audioProcessingUpdate
|
|
1851
|
+
.catch(() => {})
|
|
1852
|
+
.then(() => this.maybeStartLocalAudioLevelObserver());
|
|
1723
1853
|
|
|
1724
1854
|
const audioProcessingDuration = Date.now() - audioProcessingStartTime;
|
|
1725
1855
|
this.emit('call-start-progress', {
|