@vapi-ai/web 3.1.0 → 3.1.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/dist/vapi.js CHANGED
@@ -138,6 +138,7 @@ class Vapi extends VapiEventEmitter {
138
138
  dailyCallObject = {};
139
139
  microphoneTrack = null;
140
140
  cancelMicrophonePreflight = null;
141
+ clientSideNoiseCancellationEnabled = true;
141
142
  async microphoneAcquire() {
142
143
  const source = this.dailyCallObject.audioSource;
143
144
  if (source && typeof source === 'object') {
@@ -280,6 +281,7 @@ class Vapi extends VapiEventEmitter {
280
281
  }
281
282
  });
282
283
  this.started = true;
284
+ this.clientSideNoiseCancellationEnabled = options?.clientSideNoiseCancellationEnabled ?? true;
283
285
  let failureStage = 'microphone-preflight';
284
286
  try {
285
287
  if (this.call) {
@@ -653,59 +655,66 @@ class Vapi extends VapiEventEmitter {
653
655
  });
654
656
  }
655
657
  });
656
- // Stage 7: Audio processing setup
657
- this.emit('call-start-progress', {
658
- stage: 'audio-processing-setup',
659
- status: 'started',
660
- timestamp: new Date().toISOString()
661
- });
662
- const audioProcessingStartTime = Date.now();
663
- try {
664
- const audioProcessingUpdate = this.call.updateInputSettings({
665
- audio: {
666
- processor: {
667
- type: 'noise-cancellation',
668
- },
669
- },
670
- });
671
- audioProcessingUpdate.catch((error) => {
672
- this.emitAudioProcessingError('audio-processing-setup', error);
673
- });
674
- // The observer waits for this update rather than starting alongside the
675
- // remote one above, so that Krisp has already swapped the microphone
676
- // track. See maybeStartLocalAudioLevelObserver().
677
- //
678
- // A separate chain, not another link on the one above: EventEmitter
679
- // rethrows out of emit('error') when a consumer registered no 'error'
680
- // listener, and whether the observer starts must not hinge on that.
681
- audioProcessingUpdate
682
- .catch(() => { })
683
- .then(() => this.maybeStartLocalAudioLevelObserver());
684
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
658
+ if (this.clientSideNoiseCancellationEnabled) {
659
+ // Stage 7: Audio processing setup
685
660
  this.emit('call-start-progress', {
686
661
  stage: 'audio-processing-setup',
687
- status: 'completed',
688
- duration: audioProcessingDuration,
662
+ status: 'started',
689
663
  timestamp: new Date().toISOString()
690
664
  });
665
+ const audioProcessingStartTime = Date.now();
666
+ try {
667
+ const audioProcessingUpdate = this.call.updateInputSettings({
668
+ audio: {
669
+ processor: {
670
+ type: 'noise-cancellation',
671
+ },
672
+ },
673
+ });
674
+ audioProcessingUpdate.catch((error) => {
675
+ this.emitAudioProcessingError('audio-processing-setup', error);
676
+ });
677
+ // The observer waits for this update rather than starting alongside the
678
+ // remote one above, so that Krisp has already swapped the microphone
679
+ // track. See maybeStartLocalAudioLevelObserver().
680
+ //
681
+ // A separate chain, not another link on the one above: EventEmitter
682
+ // rethrows out of emit('error') when a consumer registered no 'error'
683
+ // listener, and whether the observer starts must not hinge on that.
684
+ audioProcessingUpdate
685
+ .catch(() => { })
686
+ .then(() => this.maybeStartLocalAudioLevelObserver());
687
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
688
+ this.emit('call-start-progress', {
689
+ stage: 'audio-processing-setup',
690
+ status: 'completed',
691
+ duration: audioProcessingDuration,
692
+ timestamp: new Date().toISOString()
693
+ });
694
+ }
695
+ catch (error) {
696
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
697
+ const serializedError = serializeError(error);
698
+ this.emit('call-start-progress', {
699
+ stage: 'audio-processing-setup',
700
+ status: 'failed',
701
+ duration: audioProcessingDuration,
702
+ timestamp: new Date().toISOString(),
703
+ metadata: { error: serializedError.message }
704
+ });
705
+ this.emit('error', {
706
+ type: 'audio-processing-setup-error',
707
+ stage: 'audio-processing-setup',
708
+ error: serializedError,
709
+ timestamp: new Date().toISOString()
710
+ });
711
+ // Don't throw here, this is non-critical
712
+ }
691
713
  }
692
- catch (error) {
693
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
694
- const serializedError = serializeError(error);
695
- this.emit('call-start-progress', {
696
- stage: 'audio-processing-setup',
697
- status: 'failed',
698
- duration: audioProcessingDuration,
699
- timestamp: new Date().toISOString(),
700
- metadata: { error: serializedError.message }
701
- });
702
- this.emit('error', {
703
- type: 'audio-processing-setup-error',
704
- stage: 'audio-processing-setup',
705
- error: serializedError,
706
- timestamp: new Date().toISOString()
707
- });
708
- // Don't throw here, this is non-critical
714
+ else {
715
+ // Daily's default processor is 'none', so the microphone track is never
716
+ // swapped and the observer has nothing to wait for.
717
+ void this.maybeStartLocalAudioLevelObserver();
709
718
  }
710
719
  const totalDuration = Date.now() - startTime;
711
720
  this.emit('call-start-success', {
@@ -1163,10 +1172,10 @@ class Vapi extends VapiEventEmitter {
1163
1172
  /**
1164
1173
  * Reconnects to an active call.
1165
1174
  *
1166
- *
1167
1175
  * @param webCall
1176
+ * @param options
1168
1177
  */
1169
- async reconnect(webCall) {
1178
+ async reconnect(webCall, options) {
1170
1179
  const startTime = Date.now();
1171
1180
  if (this.started) {
1172
1181
  throw new Error('Cannot reconnect while a call is already in progress. Call stop() first.');
@@ -1174,6 +1183,9 @@ class Vapi extends VapiEventEmitter {
1174
1183
  if (!webCall.webCallUrl) {
1175
1184
  throw new Error('webCallUrl is required for reconnection.');
1176
1185
  }
1186
+ if (options?.clientSideNoiseCancellationEnabled !== undefined) {
1187
+ this.clientSideNoiseCancellationEnabled = options.clientSideNoiseCancellationEnabled;
1188
+ }
1177
1189
  this.emit('call-start-progress', {
1178
1190
  stage: 'reconnect-initialization',
1179
1191
  status: 'started',
@@ -1477,53 +1489,60 @@ class Vapi extends VapiEventEmitter {
1477
1489
  });
1478
1490
  // Don't throw here, this is non-critical
1479
1491
  }
1480
- // Stage 6: Audio processing setup
1481
- this.emit('call-start-progress', {
1482
- stage: 'audio-processing-setup',
1483
- status: 'started',
1484
- timestamp: new Date().toISOString()
1485
- });
1486
- const audioProcessingStartTime = Date.now();
1487
- try {
1488
- const audioProcessingUpdate = this.call.updateInputSettings({
1489
- audio: {
1490
- processor: {
1491
- type: 'noise-cancellation',
1492
- },
1493
- },
1494
- });
1495
- audioProcessingUpdate.catch((error) => {
1496
- this.emitAudioProcessingError('audio-processing-setup', error);
1497
- });
1498
- // The observer waits for this update rather than starting alongside the
1499
- // remote one above, so that Krisp has already swapped the microphone
1500
- // track. See maybeStartLocalAudioLevelObserver().
1501
- //
1502
- // A separate chain, not another link on the one above: EventEmitter
1503
- // rethrows out of emit('error') when a consumer registered no 'error'
1504
- // listener, and whether the observer starts must not hinge on that.
1505
- audioProcessingUpdate
1506
- .catch(() => { })
1507
- .then(() => this.maybeStartLocalAudioLevelObserver());
1508
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1492
+ if (this.clientSideNoiseCancellationEnabled) {
1493
+ // Stage 6: Audio processing setup
1509
1494
  this.emit('call-start-progress', {
1510
1495
  stage: 'audio-processing-setup',
1511
- status: 'completed',
1512
- duration: audioProcessingDuration,
1496
+ status: 'started',
1513
1497
  timestamp: new Date().toISOString()
1514
1498
  });
1499
+ const audioProcessingStartTime = Date.now();
1500
+ try {
1501
+ const audioProcessingUpdate = this.call.updateInputSettings({
1502
+ audio: {
1503
+ processor: {
1504
+ type: 'noise-cancellation',
1505
+ },
1506
+ },
1507
+ });
1508
+ audioProcessingUpdate.catch((error) => {
1509
+ this.emitAudioProcessingError('audio-processing-setup', error);
1510
+ });
1511
+ // The observer waits for this update rather than starting alongside the
1512
+ // remote one above, so that Krisp has already swapped the microphone
1513
+ // track. See maybeStartLocalAudioLevelObserver().
1514
+ //
1515
+ // A separate chain, not another link on the one above: EventEmitter
1516
+ // rethrows out of emit('error') when a consumer registered no 'error'
1517
+ // listener, and whether the observer starts must not hinge on that.
1518
+ audioProcessingUpdate
1519
+ .catch(() => { })
1520
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1521
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1522
+ this.emit('call-start-progress', {
1523
+ stage: 'audio-processing-setup',
1524
+ status: 'completed',
1525
+ duration: audioProcessingDuration,
1526
+ timestamp: new Date().toISOString()
1527
+ });
1528
+ }
1529
+ catch (error) {
1530
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1531
+ const serializedError = serializeError(error);
1532
+ this.emit('call-start-progress', {
1533
+ stage: 'audio-processing-setup',
1534
+ status: 'failed',
1535
+ duration: audioProcessingDuration,
1536
+ timestamp: new Date().toISOString(),
1537
+ metadata: { error: serializedError.message }
1538
+ });
1539
+ // Don't throw here, this is non-critical
1540
+ }
1515
1541
  }
1516
- catch (error) {
1517
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1518
- const serializedError = serializeError(error);
1519
- this.emit('call-start-progress', {
1520
- stage: 'audio-processing-setup',
1521
- status: 'failed',
1522
- duration: audioProcessingDuration,
1523
- timestamp: new Date().toISOString(),
1524
- metadata: { error: serializedError.message }
1525
- });
1526
- // Don't throw here, this is non-critical
1542
+ else {
1543
+ // Daily's default processor is 'none', so the microphone track is never
1544
+ // swapped and the observer has nothing to wait for.
1545
+ void this.maybeStartLocalAudioLevelObserver();
1527
1546
  }
1528
1547
  const totalDuration = Date.now() - startTime;
1529
1548
  this.emit('call-start-success', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",
package/vapi.ts CHANGED
@@ -224,6 +224,15 @@ type VideoRecordingStartFailedMessage = {
224
224
  type StartCallOptions = {
225
225
  /** Acquire the microphone before creating a backend call. Defaults to true. */
226
226
  microphonePreflightEnabled?: boolean;
227
+ /**
228
+ * Run Daily's Krisp noise cancellation on the microphone
229
+ * in the browser, before the audio leaves the device. Set to `false` to send
230
+ * the raw microphone audio. Defaults to true.
231
+ *
232
+ * Separate from the assistant's server-side `backgroundSpeechDenoisingPlan`,
233
+ * which this option does not change.
234
+ */
235
+ clientSideNoiseCancellationEnabled?: boolean;
227
236
  /**
228
237
  * This determines whether the daily room will be deleted and all participants will be kicked once the user leaves the room.
229
238
  * If set to `false`, the room will be kept alive even after the user leaves, allowing clients to reconnect to the same room.
@@ -254,6 +263,18 @@ type StartCallOptions = {
254
263
  videoRecordingStartMode?: VideoRecordingStartMode;
255
264
  }
256
265
 
266
+ type ReconnectOptions = {
267
+ /**
268
+ * Run Daily's Krisp noise cancellation on the microphone
269
+ * in the browser. Defaults to the value used by the last `start()` or
270
+ * `reconnect()`, or true if neither set it.
271
+ *
272
+ * Separate from the assistant's server-side `backgroundSpeechDenoisingPlan`,
273
+ * which this option does not change.
274
+ */
275
+ clientSideNoiseCancellationEnabled?: boolean;
276
+ }
277
+
257
278
  type WebCall = {
258
279
  /**
259
280
  * The Vapi WebCall URL. This is the URL that the call will be joined on.
@@ -404,6 +425,7 @@ export default class Vapi extends VapiEventEmitter {
404
425
  private dailyCallObject: DailyFactoryOptions = {};
405
426
  private microphoneTrack: MediaStreamTrack | null = null;
406
427
  private cancelMicrophonePreflight: (() => void) | null = null;
428
+ private clientSideNoiseCancellationEnabled: boolean = true;
407
429
 
408
430
  private async microphoneAcquire(): Promise<MediaStreamTrack> {
409
431
  const source = this.dailyCallObject.audioSource;
@@ -579,6 +601,7 @@ export default class Vapi extends VapiEventEmitter {
579
601
  });
580
602
 
581
603
  this.started = true;
604
+ this.clientSideNoiseCancellationEnabled = options?.clientSideNoiseCancellationEnabled ?? true;
582
605
 
583
606
  let failureStage = 'microphone-preflight';
584
607
  try {
@@ -985,63 +1008,69 @@ export default class Vapi extends VapiEventEmitter {
985
1008
  }
986
1009
  });
987
1010
 
988
- // Stage 7: Audio processing setup
989
- this.emit('call-start-progress', {
990
- stage: 'audio-processing-setup',
991
- status: 'started',
992
- timestamp: new Date().toISOString()
993
- });
1011
+ if (this.clientSideNoiseCancellationEnabled) {
1012
+ // Stage 7: Audio processing setup
1013
+ this.emit('call-start-progress', {
1014
+ stage: 'audio-processing-setup',
1015
+ status: 'started',
1016
+ timestamp: new Date().toISOString()
1017
+ });
994
1018
 
995
- const audioProcessingStartTime = Date.now();
1019
+ const audioProcessingStartTime = Date.now();
996
1020
 
997
- try {
998
- const audioProcessingUpdate = this.call.updateInputSettings({
999
- audio: {
1000
- processor: {
1001
- type: 'noise-cancellation',
1021
+ try {
1022
+ const audioProcessingUpdate = this.call.updateInputSettings({
1023
+ audio: {
1024
+ processor: {
1025
+ type: 'noise-cancellation',
1026
+ },
1002
1027
  },
1003
- },
1004
- });
1028
+ });
1005
1029
 
1006
- audioProcessingUpdate.catch((error) => {
1007
- this.emitAudioProcessingError('audio-processing-setup', error);
1008
- });
1030
+ audioProcessingUpdate.catch((error) => {
1031
+ this.emitAudioProcessingError('audio-processing-setup', error);
1032
+ });
1009
1033
 
1010
- // The observer waits for this update rather than starting alongside the
1011
- // remote one above, so that Krisp has already swapped the microphone
1012
- // track. See maybeStartLocalAudioLevelObserver().
1013
- //
1014
- // A separate chain, not another link on the one above: EventEmitter
1015
- // rethrows out of emit('error') when a consumer registered no 'error'
1016
- // listener, and whether the observer starts must not hinge on that.
1017
- audioProcessingUpdate
1018
- .catch(() => {})
1019
- .then(() => this.maybeStartLocalAudioLevelObserver());
1020
-
1021
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1022
- this.emit('call-start-progress', {
1023
- stage: 'audio-processing-setup',
1024
- status: 'completed',
1025
- duration: audioProcessingDuration,
1026
- timestamp: new Date().toISOString()
1027
- });
1028
- } catch (error) {
1029
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1030
- const serializedError = serializeError(error);
1031
- this.emit('call-start-progress', {
1032
- stage: 'audio-processing-setup',
1033
- status: 'failed',
1034
- duration: audioProcessingDuration,
1035
- timestamp: new Date().toISOString(),
1036
- metadata: { error: serializedError.message }
1037
- });
1038
- this.emit('error', {
1039
- type: 'audio-processing-setup-error',
1040
- stage: 'audio-processing-setup',
1041
- error: serializedError,
1042
- timestamp: new Date().toISOString()
1043
- });
1044
- // Don't throw here, this is non-critical
1034
+ // The observer waits for this update rather than starting alongside the
1035
+ // remote one above, so that Krisp has already swapped the microphone
1036
+ // track. See maybeStartLocalAudioLevelObserver().
1037
+ //
1038
+ // A separate chain, not another link on the one above: EventEmitter
1039
+ // rethrows out of emit('error') when a consumer registered no 'error'
1040
+ // listener, and whether the observer starts must not hinge on that.
1041
+ audioProcessingUpdate
1042
+ .catch(() => {})
1043
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1044
+
1045
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1046
+ this.emit('call-start-progress', {
1047
+ stage: 'audio-processing-setup',
1048
+ status: 'completed',
1049
+ duration: audioProcessingDuration,
1050
+ timestamp: new Date().toISOString()
1051
+ });
1052
+ } catch (error) {
1053
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1054
+ const serializedError = serializeError(error);
1055
+ this.emit('call-start-progress', {
1056
+ stage: 'audio-processing-setup',
1057
+ status: 'failed',
1058
+ duration: audioProcessingDuration,
1059
+ timestamp: new Date().toISOString(),
1060
+ metadata: { error: serializedError.message }
1061
+ });
1062
+ this.emit('error', {
1063
+ type: 'audio-processing-setup-error',
1064
+ stage: 'audio-processing-setup',
1065
+ error: serializedError,
1066
+ timestamp: new Date().toISOString()
1067
+ });
1068
+ // Don't throw here, this is non-critical
1069
+ }
1070
+ } else {
1071
+ // Daily's default processor is 'none', so the microphone track is never
1072
+ // swapped and the observer has nothing to wait for.
1073
+ void this.maybeStartLocalAudioLevelObserver();
1045
1074
  }
1046
1075
 
1047
1076
  const totalDuration = Date.now() - startTime;
@@ -1567,11 +1596,11 @@ export default class Vapi extends VapiEventEmitter {
1567
1596
 
1568
1597
  /**
1569
1598
  * Reconnects to an active call.
1570
- *
1571
- *
1572
- * @param webCall
1599
+ *
1600
+ * @param webCall
1601
+ * @param options
1573
1602
  */
1574
- async reconnect(webCall: WebCall): Promise<void> {
1603
+ async reconnect(webCall: WebCall, options?: ReconnectOptions): Promise<void> {
1575
1604
  const startTime = Date.now();
1576
1605
 
1577
1606
  if (this.started) {
@@ -1582,6 +1611,10 @@ export default class Vapi extends VapiEventEmitter {
1582
1611
  throw new Error('webCallUrl is required for reconnection.');
1583
1612
  }
1584
1613
 
1614
+ if (options?.clientSideNoiseCancellationEnabled !== undefined) {
1615
+ this.clientSideNoiseCancellationEnabled = options.clientSideNoiseCancellationEnabled;
1616
+ }
1617
+
1585
1618
  this.emit('call-start-progress', {
1586
1619
  stage: 'reconnect-initialization',
1587
1620
  status: 'started',
@@ -1912,57 +1945,63 @@ export default class Vapi extends VapiEventEmitter {
1912
1945
  // Don't throw here, this is non-critical
1913
1946
  }
1914
1947
 
1915
- // Stage 6: Audio processing setup
1916
- this.emit('call-start-progress', {
1917
- stage: 'audio-processing-setup',
1918
- status: 'started',
1919
- timestamp: new Date().toISOString()
1920
- });
1948
+ if (this.clientSideNoiseCancellationEnabled) {
1949
+ // Stage 6: Audio processing setup
1950
+ this.emit('call-start-progress', {
1951
+ stage: 'audio-processing-setup',
1952
+ status: 'started',
1953
+ timestamp: new Date().toISOString()
1954
+ });
1921
1955
 
1922
- const audioProcessingStartTime = Date.now();
1956
+ const audioProcessingStartTime = Date.now();
1923
1957
 
1924
- try {
1925
- const audioProcessingUpdate = this.call.updateInputSettings({
1926
- audio: {
1927
- processor: {
1928
- type: 'noise-cancellation',
1958
+ try {
1959
+ const audioProcessingUpdate = this.call.updateInputSettings({
1960
+ audio: {
1961
+ processor: {
1962
+ type: 'noise-cancellation',
1963
+ },
1929
1964
  },
1930
- },
1931
- });
1965
+ });
1932
1966
 
1933
- audioProcessingUpdate.catch((error) => {
1934
- this.emitAudioProcessingError('audio-processing-setup', error);
1935
- });
1967
+ audioProcessingUpdate.catch((error) => {
1968
+ this.emitAudioProcessingError('audio-processing-setup', error);
1969
+ });
1936
1970
 
1937
- // The observer waits for this update rather than starting alongside the
1938
- // remote one above, so that Krisp has already swapped the microphone
1939
- // track. See maybeStartLocalAudioLevelObserver().
1940
- //
1941
- // A separate chain, not another link on the one above: EventEmitter
1942
- // rethrows out of emit('error') when a consumer registered no 'error'
1943
- // listener, and whether the observer starts must not hinge on that.
1944
- audioProcessingUpdate
1945
- .catch(() => {})
1946
- .then(() => this.maybeStartLocalAudioLevelObserver());
1947
-
1948
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1949
- this.emit('call-start-progress', {
1950
- stage: 'audio-processing-setup',
1951
- status: 'completed',
1952
- duration: audioProcessingDuration,
1953
- timestamp: new Date().toISOString()
1954
- });
1955
- } catch (error) {
1956
- const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1957
- const serializedError = serializeError(error);
1958
- this.emit('call-start-progress', {
1959
- stage: 'audio-processing-setup',
1960
- status: 'failed',
1961
- duration: audioProcessingDuration,
1962
- timestamp: new Date().toISOString(),
1963
- metadata: { error: serializedError.message }
1964
- });
1965
- // Don't throw here, this is non-critical
1971
+ // The observer waits for this update rather than starting alongside the
1972
+ // remote one above, so that Krisp has already swapped the microphone
1973
+ // track. See maybeStartLocalAudioLevelObserver().
1974
+ //
1975
+ // A separate chain, not another link on the one above: EventEmitter
1976
+ // rethrows out of emit('error') when a consumer registered no 'error'
1977
+ // listener, and whether the observer starts must not hinge on that.
1978
+ audioProcessingUpdate
1979
+ .catch(() => {})
1980
+ .then(() => this.maybeStartLocalAudioLevelObserver());
1981
+
1982
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1983
+ this.emit('call-start-progress', {
1984
+ stage: 'audio-processing-setup',
1985
+ status: 'completed',
1986
+ duration: audioProcessingDuration,
1987
+ timestamp: new Date().toISOString()
1988
+ });
1989
+ } catch (error) {
1990
+ const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1991
+ const serializedError = serializeError(error);
1992
+ this.emit('call-start-progress', {
1993
+ stage: 'audio-processing-setup',
1994
+ status: 'failed',
1995
+ duration: audioProcessingDuration,
1996
+ timestamp: new Date().toISOString(),
1997
+ metadata: { error: serializedError.message }
1998
+ });
1999
+ // Don't throw here, this is non-critical
2000
+ }
2001
+ } else {
2002
+ // Daily's default processor is 'none', so the microphone track is never
2003
+ // swapped and the observer has nothing to wait for.
2004
+ void this.maybeStartLocalAudioLevelObserver();
1966
2005
  }
1967
2006
 
1968
2007
  const totalDuration = Date.now() - startTime;