@voltkit-rtc/react-native 0.1.5 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7
4
+
5
+ 2026-11-09. The echo canceller tuned for loudspeakers. Same peer dependencies as 0.1.6. Build the app again after upgrading:
6
+ the setting is read from the manifest when the app starts, and the version check reads this package's version.
7
+
8
+ - **WebRTC's canceller tuned for phones whose loudspeaker leaks into their own microphone** (`android.echoSuppression`
9
+ in the Expo plugin, meta-data `in.voltkit.rtc.ECHO_SUPPRESSION`: `"auto"`, the default, applies the tuning on phones whose
10
+ own canceller is Android's software one or absent, read from Android's effect descriptors, the gate Agora's SDK applies to
11
+ the phone's canceller; `"strong"` applies it everywhere; `"webrtc"` is 0.1.6's behaviour everywhere).
12
+ Measured on an OPPO CPH2477 at full loudspeaker volume, whose speaker compresses the sound and whose microphone clips: with
13
+ no processing the far voice came back 15 dB louder than it was sent, with the phone's own canceller 10 dB louder, with 0.1.6's
14
+ canceller 6 dB louder, and with this tuning 3 dB quieter (measured through the Flutter SDK; this SDK runs the same
15
+ AEC3 and the same tuning). Why it had let the echo through: AEC3 estimates the echo from what it plays with a path gain of 1
16
+ until its filter converges; on this phone the real gain was about +15 dB, so the residual looked like near-end speech and the
17
+ canceller went transparent. The tuning (WebRTC's `WebRTC-Aec3SuppressorTuningOverride` field trial, `Aec3Tuning.kt`) lowers
18
+ the suppressor's echo-to-near-end thresholds and makes its dominant-near-end detector insensitive. A phone whose canceller
19
+ coped before (a POCO X3 Pro) stays as good. The price: someone speaking into such a phone while the room is loud is cut more
20
+ often than before. The init provider adds the trial to `WebRTCModuleOptions.fieldTrials` (an app's own trials stay); it does
21
+ nothing while the phone's canceller is in use (`hardwareEchoCancellation: true`). Measurement:
22
+ `docs/implementation-progress/ECHO_LAB_2026-11-09.md`. Tests: `RtcConfigTest`, `Aec3TuningTest`, the plugin's.
23
+
24
+ ## 0.1.6
25
+
26
+ 2026-11-06. WebRTC's own echo canceller by default on Android. Same peer dependencies as 0.1.5. Build the app again after
27
+ upgrading: the setting is read from the manifest when the app starts, and the version check reads this package's version.
28
+
29
+ - **WebRTC's echo canceller (AEC3) and noise suppressor by default** (`android.hardwareEchoCancellation` now defaults to
30
+ `false`; meta-data `in.voltkit.rtc.HW_AEC`). On Android, while WebRTC uses the phone's own canceller it switches its own
31
+ off, so the result depended on the phone: on an Oppo on the loudspeaker the person on the other phone heard themselves
32
+ with distortion, in this SDK and the Flutter one (a developer's finding, 2026-11-06). Agora and ZEGO tell developers to
33
+ use the SDK's canceller for such phones; theirs keep one, WebRTC does not. AEC3 is the canceller browsers use, and the
34
+ one this SDK's `react-native-webrtc` build carries. To keep the phone's, as before 0.1.6, set
35
+ `android: { hardwareEchoCancellation: true }` in the plugin options (or `in.voltkit.rtc.HW_AEC` to `true`). iOS is
36
+ unchanged: it uses the system's voice processing.
37
+
3
38
  ## 0.1.5
4
39
 
5
40
  2026-11-05. A developer's findings on 0.1.4, and the round of 2026-11-05 (decisions D2 and D10). Peer dependencies
package/README.md CHANGED
@@ -134,7 +134,7 @@ you edit no native file:
134
134
  | `android.callServiceCamera` | `false` | Adds the `camera` type and `FOREGROUND_SERVICE_CAMERA`, for an app that keeps the camera on in the background (`background={{ pauseCamera: false }}`) |
135
135
  | `android.screenShare` | `false` | Turns on react-native-webrtc's screen-capture service for [screen sharing](#screen-sharing), with `FOREGROUND_SERVICE_MEDIA_PROJECTION` and the `mediaProjection` type on the call service |
136
136
  | `android.audioMode` | `"communication"` | `"media"` for an app whose users only listen: see [Audio routing](#audio-routing) |
137
- | `android.hardwareEchoCancellation` | `true` | The phone's own echo canceller and noise suppressor, where it has them |
137
+ | `android.hardwareEchoCancellation` | `false` | `false` (the default from 0.1.6): WebRTC's own echo canceller and noise suppressor (AEC3). `true`: the phone's own, where it has them, as before 0.1.6. See [Echo on Android](#echo-on-android) |
138
138
  | `ios.backgroundAudio` | `true` | Adds `audio` to `UIBackgroundModes`, so a call keeps its sound in the background |
139
139
  | `ios.voip` | `false` | Adds `voip` to `UIBackgroundModes`, for an app that integrates CallKit itself |
140
140
 
@@ -741,11 +741,12 @@ export function OutputPicker() {
741
741
  tried again until the user picks it, a device is added, or the call ends.
742
742
  - **No Bluetooth permission** is needed: the routing goes through Android's
743
743
  `AudioManager`.
744
- - **Echo on one phone model.** The phone's own echo canceller and noise
745
- suppressor are used where it has them. If one model echoes, turn them off with
746
- the Expo plugin's `android.hardwareEchoCancellation: false` (or the
747
- `in.voltkit.rtc.HW_AEC` meta-data, below), and WebRTC's own echo canceller
748
- does the work.
744
+ - **Echo cancellation.** From 0.1.6, WebRTC's own echo canceller and noise
745
+ suppressor (AEC3, the one browsers use), not the phone's built-in ones, which
746
+ some phones cannot run well on the loudspeaker.
747
+ `android: { hardwareEchoCancellation: true }` in the Expo plugin's options (or
748
+ the `in.voltkit.rtc.HW_AEC` meta-data, below) brings the phone's back. See
749
+ [Echo on Android](#echo-on-android).
749
750
  - **Apps whose users only listen** can set the audio mode to `media` (the
750
751
  plugin's `android.audioMode`, or the `in.voltkit.rtc.AUDIO_MODE` meta-data):
751
752
  sound plays at the media volume and on Bluetooth's high-quality profile, with
@@ -765,8 +766,8 @@ In a React Native app without Expo, the audio settings are meta-data in
765
766
  <application>
766
767
  <!-- communication (calls and audio rooms) or media (listen-only apps) -->
767
768
  <meta-data android:name="in.voltkit.rtc.AUDIO_MODE" android:value="communication" />
768
- <!-- the phone's own echo canceller and noise suppressor, where it has them -->
769
- <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="true" />
769
+ <!-- false: WebRTC's own echo canceller and noise suppressor (from 0.1.6); true: the phone's own, where it has them -->
770
+ <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="false" />
770
771
  <!-- react-native-webrtc's screen-capture service: see Screen sharing -->
771
772
  <meta-data android:name="in.voltkit.rtc.SCREEN_SHARE" android:value="false" />
772
773
  </application>
@@ -1433,12 +1434,62 @@ The emulator has only the loudspeaker, so `useAudioOutput().outputs` lists one
1433
1434
  output and `setSpeakerphoneOn(false)` has nowhere else to go. Test the earpiece
1434
1435
  and headsets on a phone.
1435
1436
 
1436
- ### Echo on one phone model
1437
+ ### Echo on Android
1437
1438
 
1438
- Turn off the phone's own echo canceller with the Expo plugin's
1439
- `android.hardwareEchoCancellation: false` (or `in.voltkit.rtc.HW_AEC` set to
1440
- `false`) and build again; WebRTC's own canceller takes over. A headset or the
1441
- earpiece removes echo entirely.
1439
+ From 0.1.6 the SDK uses WebRTC's own echo canceller and noise suppressor (AEC3,
1440
+ the one browsers use) on Android, not the phone's built-in ones. While WebRTC
1441
+ uses a phone's canceller it switches its own off, and some phones' cancellers
1442
+ do not cope with the loudspeaker: the person on the other phone hears their own
1443
+ voice back, even in another room. An app on 0.1.5 or earlier that sees this
1444
+ should upgrade and build again.
1445
+
1446
+ To use the phone's own canceller instead, as before 0.1.6, set it in the Expo
1447
+ plugin's options, in `app.json` or `app.config.js`:
1448
+
1449
+ ```json
1450
+ {
1451
+ "expo": {
1452
+ "plugins": [
1453
+ ["@voltkit-rtc/react-native", { "android": { "hardwareEchoCancellation": true } }]
1454
+ ]
1455
+ }
1456
+ }
1457
+ ```
1458
+
1459
+ Then make a new build of the app (`npx expo run:android`, or a new EAS build):
1460
+ the setting is read when the app starts, so reloading the JavaScript does not
1461
+ apply it. In a React Native app without Expo, set the meta-data in
1462
+ `AndroidManifest.xml` instead, and build again:
1463
+
1464
+ ```xml
1465
+ <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="true" />
1466
+ ```
1467
+
1468
+ **Tuned for loudspeakers (0.1.7).** WebRTC's canceller, as tuned for laptops,
1469
+ still let the echo through on a phone whose loudspeaker leaks into its own
1470
+ microphone: measured on an OPPO CPH2477 at full volume, the far voice came back
1471
+ 6 dB louder than it was sent, because the canceller underestimated the echo some
1472
+ thirty times and took the rest for someone speaking. From 0.1.7 the SDK hands
1473
+ WebRTC a tuning for such phones: on that phone the echo comes back 3 dB
1474
+ quieter than it was sent. The default, `android.echoSuppression: "auto"`, applies
1475
+ it only on phones whose own echo canceller is Android's software one or absent,
1476
+ which are the phones that echo (Android's effect descriptors say which; the gate
1477
+ Agora's SDK applies to the phone's canceller); phones with a vendor's canceller
1478
+ keep WebRTC's own tuning and full duplex. The price on the tuned phones is that
1479
+ someone speaking into them while the room is loud is cut more often. `"strong"`
1480
+ applies the tuning everywhere; `"webrtc"` keeps WebRTC's own tuning everywhere,
1481
+ as 0.1.6 had. Set it in the plugin's options
1482
+ (`{ "android": { "echoSuppression": "webrtc" } }`), or the meta-data
1483
+ `in.voltkit.rtc.ECHO_SUPPRESSION` without Expo, and build again. The tuning is WebRTC's `WebRTC-Aec3SuppressorTuningOverride` field
1484
+ trial, handed to react-native-webrtc through `WebRTCModuleOptions.fieldTrials`
1485
+ (field trials your app set itself stay); it does nothing while the phone's own
1486
+ canceller is in use. The measurement: docs/implementation-progress/ECHO_LAB_2026-11-09.md.
1487
+
1488
+ An Expo app uses the plugin option, not a hand edit: `expo prebuild` writes the
1489
+ manifest again, and the edit is lost. Test either choice on the phone that
1490
+ echoed and on another before you ship it. A headset or the earpiece on the
1491
+ phone that causes the echo also stops it. Where echo comes from, including two
1492
+ phones in one room: https://voltkit.in/docs/troubleshooting#echo.
1442
1493
 
1443
1494
  ### Google Play rejects the app for 16 KB page sizes
1444
1495
 
@@ -0,0 +1,60 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ /**
4
+ * The WebRTC field trial behind the SDK's echo suppression (0.1.7): AEC3's suppressor thresholds and its dominant-
5
+ * near-end detector, in WebRTC's override format (`WebRTC-Aec3SuppressorTuningOverride`). AEC3 estimates the echo from
6
+ * what it plays with an assumed path gain of 1 until its filter converges; on the measured phone (an OPPO CPH2477,
7
+ * whose loudspeaker compresses the sound and whose microphone clips) the real gain was about +15 dB, so the echo was
8
+ * underestimated some thirty times, the residual looked like near-end speech and the canceller went transparent: the
9
+ * people on the other phones heard themselves, louder than they had spoken. These thresholds are WebRTC's defaults
10
+ * divided by that factor, and the near-end detector made as much less sensitive. From the lab of 2026-11-09
11
+ * (docs/implementation-progress/ECHO_LAB_2026-11-09.md). The same string is used by the Flutter and Kotlin SDKs.
12
+ * react-native-webrtc hands `WebRTCModuleOptions.fieldTrials` to WebRTC when it starts.
13
+ */
14
+ internal object Aec3Tuning {
15
+ const val LOUDSPEAKER: String = "WebRTC-Aec3SuppressorTuningOverride/" +
16
+ "normal_tuning_mask_lf_enr_transparent:0.01,normal_tuning_mask_lf_enr_suppress:0.013," +
17
+ "normal_tuning_mask_hf_enr_transparent:0.0023,normal_tuning_mask_hf_enr_suppress:0.0033," +
18
+ "nearend_tuning_mask_lf_enr_transparent:0.03,nearend_tuning_mask_lf_enr_suppress:0.04," +
19
+ "nearend_tuning_mask_hf_enr_transparent:0.003,nearend_tuning_mask_hf_enr_suppress:0.01," +
20
+ "dominant_nearend_detection_enr_threshold:0.008,dominant_nearend_detection_hold_duration:25/"
21
+
22
+ /**
23
+ * The phone's own echo canceller as Android describes it (`AudioEffect.queryEffects()`): a vendor's DSP canceller
24
+ * (Qualcomm Fluence, Samsung, ...) or the AOSP software one (`libaudiopreprocessing`, the old WebRTC mobile canceller
25
+ * run by the audio server), which the phones that echo on the loudspeaker ship (the OPPO CPH2477 of the 2026-11-09
26
+ * lab: MediaTek's build, uuid 9f35ed76-...; the stock one is bb392ec0-...). Agora's SDK gates the phone's canceller
27
+ * on the same descriptor.
28
+ */
29
+ data class PhoneCanceller(val available: Boolean, val implementor: String?, val uuid: String?) {
30
+ val aosp: Boolean
31
+ get() = (implementor?.contains("Android Open Source") == true) ||
32
+ uuid.equals("bb392ec0-8d4d-11e0-a896-0002a5d5c51b", ignoreCase = true) ||
33
+ uuid.equals("9f35ed76-0b66-4330-8f79-e39ca266dc7c", ignoreCase = true)
34
+
35
+ fun describe(): String = if (!available) "none" else "${implementor ?: "unknown"} ($uuid)"
36
+ }
37
+
38
+ fun phoneCanceller(): PhoneCanceller {
39
+ val available = runCatching { android.media.audiofx.AcousticEchoCanceler.isAvailable() }.getOrDefault(false)
40
+ val d = runCatching {
41
+ android.media.audiofx.AudioEffect.queryEffects().firstOrNull { it.type == android.media.audiofx.AudioEffect.EFFECT_TYPE_AEC }
42
+ }.getOrNull()
43
+ return PhoneCanceller(available, d?.implementor, d?.uuid?.toString())
44
+ }
45
+
46
+ /** Whether the loudspeaker tuning goes in: `strong` always, `webrtc` never, `auto` on the phones that echo (the AOSP software canceller, or none). */
47
+ fun tunes(suppression: EchoSuppression, phone: PhoneCanceller): Boolean = when (suppression) {
48
+ EchoSuppression.strong -> true
49
+ EchoSuppression.webrtc -> false
50
+ EchoSuppression.auto -> !phone.available || phone.aosp
51
+ }
52
+
53
+ /** [LOUDSPEAKER] added to field trials the app already set, once; an app's own AEC3 tuning stays. */
54
+ fun merge(existing: String?): String {
55
+ val have = existing?.trim().orEmpty()
56
+ if (have.contains("WebRTC-Aec3SuppressorTuningOverride/")) return have
57
+ if (have.isEmpty()) return LOUDSPEAKER
58
+ return (if (have.endsWith("/")) have else "$have/") + LOUDSPEAKER
59
+ }
60
+ }
@@ -20,22 +20,53 @@ internal enum class AudioMode {
20
20
  media,
21
21
  }
22
22
 
23
+ /**
24
+ * How hard WebRTC's own echo canceller (AEC3) suppresses what is left of the echo (meta-data in.voltkit.rtc.ECHO_SUPPRESSION;
25
+ * 0.1.7). In force only while WebRTC's canceller is in use (HW_AEC false, the default): with the phone's own, WebRTC's is off.
26
+ */
27
+ @Suppress("EnumEntryName")
28
+ internal enum class EchoSuppression {
29
+ /**
30
+ * The default: [strong] on phones whose own echo canceller is Android's software one or absent (the phones that echo
31
+ * on the loudspeaker), [webrtc] on phones with a vendor's DSP canceller (which do not echo and keep full duplex).
32
+ */
33
+ auto,
34
+
35
+ /**
36
+ * AEC3 tuned for phones whose loudspeaker leaks into their own microphone (an OPPO CPH2477 returned the far voice
37
+ * louder than it arrived, and WebRTC's canceller as tuned for laptops mistook that echo for someone speaking and let
38
+ * it through). Someone speaking into the phone while the room is loud is cut more often than with [webrtc].
39
+ */
40
+ strong,
41
+
42
+ /** WebRTC's own tuning, as 0.1.6 used. */
43
+ webrtc,
44
+ }
45
+
23
46
  /**
24
47
  * The process-wide settings the init provider reads from the app's manifest before react-native-webrtc starts
25
48
  * (plan §5.3), as the Expo config plugin writes them or a hand-written manifest gives them:
26
49
  *
27
50
  * <meta-data android:name="in.voltkit.rtc.AUDIO_MODE" android:value="communication" /> communication | media
28
- * <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="true" /> the handset's own echo
29
- * canceller and noise suppressor
51
+ * <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="false" /> the handset's own echo
52
+ * canceller and noise suppressor;
53
+ * false (the default from 0.1.6):
54
+ * WebRTC's own (AEC3)
30
55
  * <meta-data android:name="in.voltkit.rtc.SCREEN_SHARE" android:value="false" /> react-native-webrtc's
31
56
  * screen-share service
57
+ * <meta-data android:name="in.voltkit.rtc.ECHO_SUPPRESSION" android:value="auto" /> auto (the default from 0.1.7:
58
+ * AEC3 tuned for loudspeakers
59
+ * on phones whose own canceller
60
+ * is Android's software one) |
61
+ * strong | webrtc
32
62
  *
33
63
  * Parsing only, so it can be tested off a device. A value that cannot be read keeps its default and is reported.
34
64
  */
35
65
  internal data class RtcConfig(
36
66
  val audioMode: AudioMode = AudioMode.communication,
37
- val hardwareEchoCancellation: Boolean = true,
67
+ val hardwareEchoCancellation: Boolean = false,
38
68
  val screenShare: Boolean = false,
69
+ val echoSuppression: EchoSuppression = EchoSuppression.auto,
39
70
  /** What could not be read, with the default used instead; the init provider logs it. */
40
71
  val problems: List<String> = emptyList(),
41
72
  ) {
@@ -43,6 +74,7 @@ internal data class RtcConfig(
43
74
  const val AUDIO_MODE = "in.voltkit.rtc.AUDIO_MODE"
44
75
  const val HW_AEC = "in.voltkit.rtc.HW_AEC"
45
76
  const val SCREEN_SHARE = "in.voltkit.rtc.SCREEN_SHARE"
77
+ const val ECHO_SUPPRESSION = "in.voltkit.rtc.ECHO_SUPPRESSION"
46
78
 
47
79
  /** From the application's meta-data: [meta] gives one key's value (a Bundle's `get`), or null. */
48
80
  fun from(meta: (String) -> Any?): RtcConfig {
@@ -53,10 +85,17 @@ internal data class RtcConfig(
53
85
  ?: AudioMode.communication.also { problems += "$AUDIO_MODE: '$raw' is not communication or media; using communication" }
54
86
  else -> AudioMode.communication.also { problems += "$AUDIO_MODE: '$raw' is not communication or media; using communication" }
55
87
  }
88
+ val suppression = when (val raw = meta(ECHO_SUPPRESSION)) {
89
+ null -> EchoSuppression.auto
90
+ is String -> EchoSuppression.values().firstOrNull { it.name == raw.trim().lowercase() }
91
+ ?: EchoSuppression.auto.also { problems += "$ECHO_SUPPRESSION: '$raw' is not auto, strong or webrtc; using auto" }
92
+ else -> EchoSuppression.auto.also { problems += "$ECHO_SUPPRESSION: '$raw' is not auto, strong or webrtc; using auto" }
93
+ }
56
94
  return RtcConfig(
57
95
  audioMode = mode,
58
- hardwareEchoCancellation = flag(meta(HW_AEC), HW_AEC, default = true, problems),
96
+ hardwareEchoCancellation = flag(meta(HW_AEC), HW_AEC, default = false, problems),
59
97
  screenShare = flag(meta(SCREEN_SHARE), SCREEN_SHARE, default = false, problems),
98
+ echoSuppression = suppression,
60
99
  problems = problems,
61
100
  )
62
101
  }
@@ -49,6 +49,15 @@ class VoltkitRtcInitProvider : ContentProvider() {
49
49
  config.problems.forEach { Log.w(TAG, "manifest meta-data: $it") }
50
50
  val options = WebRTCModuleOptions.getInstance()
51
51
  if (config.screenShare) options.enableMediaProjectionService = true
52
+ // WebRTC's canceller tuned for loudspeakers (0.1.7): a field trial react-native-webrtc hands WebRTC when it
53
+ // starts, read when the canceller is built. Nothing to tune while the phone's own canceller is in use (WebRTC's
54
+ // is then off). Field trials the app set itself stay; this one is added.
55
+ if (!config.hardwareEchoCancellation) {
56
+ val phone = Aec3Tuning.phoneCanceller()
57
+ val tune = Aec3Tuning.tunes(config.echoSuppression, phone)
58
+ if (tune) options.fieldTrials = Aec3Tuning.merge(options.fieldTrials)
59
+ Log.i(TAG, "echo suppression ${config.echoSuppression.name}: the phone's canceller is ${phone.describe()}; WebRTC's canceller ${if (tune) "tuned for loudspeakers" else "with its own tuning"}")
60
+ }
52
61
  if (options.audioDeviceModule != null) {
53
62
  Log.i(TAG, "react-native-webrtc already has an audio device module; keeping it")
54
63
  return
@@ -21,7 +21,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
21
21
  */
22
22
 
23
23
  /** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
24
- const SDK_VERSION = exports.SDK_VERSION = '0.1.5';
24
+ const SDK_VERSION = exports.SDK_VERSION = '0.1.7';
25
25
 
26
26
  /** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
27
27
  function nativeModule() {
@@ -9,7 +9,7 @@ import { Platform } from 'react-native';
9
9
  import NativeVoltkitRtc from "./specs/NativeVoltkitRtc.js";
10
10
  import { iosUnsupported, logError, nativeMissing } from "./errors.js";
11
11
  /** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
12
- export const SDK_VERSION = '0.1.5';
12
+ export const SDK_VERSION = '0.1.7';
13
13
 
14
14
  /** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
15
15
  export function nativeModule() {
@@ -2,7 +2,7 @@ import type { EventSubscription } from 'react-native';
2
2
  import { type AudioDevice, type Constants, type Spec } from './specs/NativeVoltkitRtc';
3
3
  import type { MediaDevice } from '@voltkit-rtc/client';
4
4
  /** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
5
- export declare const SDK_VERSION = "0.1.5";
5
+ export declare const SDK_VERSION = "0.1.7";
6
6
  /** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
7
7
  export declare function nativeModule(): Spec | null;
8
8
  /** The module's constants, read once: the audio mode, screen sharing, the call service as the manifest declares them. */
@@ -2,7 +2,7 @@ import type { EventSubscription } from 'react-native';
2
2
  import { type AudioDevice, type Constants, type Spec } from './specs/NativeVoltkitRtc.js';
3
3
  import type { MediaDevice } from '@voltkit-rtc/client';
4
4
  /** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
5
- export declare const SDK_VERSION = "0.1.5";
5
+ export declare const SDK_VERSION = "0.1.7";
6
6
  /** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
7
7
  export declare function nativeModule(): Spec | null;
8
8
  /** The module's constants, read once: the audio mode, screen sharing, the call service as the manifest declares them. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltkit-rtc/react-native",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "React Native SDK for VoltKit RTC: calls, audio rooms and live video on Android (iOS in preview), with the hooks of @voltkit-rtc/react, background calls, audio routing and an Expo config plugin.",
5
5
  "keywords": [
6
6
  "voltkit",
@@ -6,6 +6,7 @@ export declare const CALL_SERVICE_CLASS = "com.voltkit.rtc.reactnative.VoltkitCa
6
6
  export declare const META_AUDIO_MODE = "in.voltkit.rtc.AUDIO_MODE";
7
7
  export declare const META_HW_AEC = "in.voltkit.rtc.HW_AEC";
8
8
  export declare const META_SCREEN_SHARE = "in.voltkit.rtc.SCREEN_SHARE";
9
+ export declare const META_ECHO_SUPPRESSION = "in.voltkit.rtc.ECHO_SUPPRESSION";
9
10
  /** A service type by its manifest name (CallServiceTypes.kt), with what it needs on Android 14 and later. */
10
11
  type ServiceType = 'microphone' | 'mediaPlayback' | 'camera' | 'mediaProjection';
11
12
  /** What the plugin writes for these options, given the permissions the app blocks. */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.withVoltkitAndroid = exports.META_SCREEN_SHARE = exports.META_HW_AEC = exports.META_AUDIO_MODE = exports.CALL_SERVICE_CLASS = void 0;
3
+ exports.withVoltkitAndroid = exports.META_ECHO_SUPPRESSION = exports.META_SCREEN_SHARE = exports.META_HW_AEC = exports.META_AUDIO_MODE = exports.CALL_SERVICE_CLASS = void 0;
4
4
  exports.blockedPermissions = blockedPermissions;
5
5
  exports.planAndroid = planAndroid;
6
6
  exports.setVoltkitManifest = setVoltkitManifest;
@@ -25,8 +25,9 @@ exports.setVoltkitManifest = setVoltkitManifest;
25
25
  * <uses-feature android:name="android.hardware.microphone" android:required="false" /> with RECORD_AUDIO
26
26
  * <application>
27
27
  * <meta-data android:name="in.voltkit.rtc.AUDIO_MODE" android:value="communication" /> RtcConfig.kt
28
- * <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="true" />
28
+ * <meta-data android:name="in.voltkit.rtc.HW_AEC" android:value="false" />
29
29
  * <meta-data android:name="in.voltkit.rtc.SCREEN_SHARE" android:value="false" />
30
+ * <meta-data android:name="in.voltkit.rtc.ECHO_SUPPRESSION" android:value="auto" /> auto | strong | webrtc (0.1.7)
30
31
  * <service android:name="com.voltkit.rtc.reactnative.VoltkitCallService" android:exported="false"
31
32
  * android:foregroundServiceType="microphone|mediaPlayback" /> VoltkitCallService.kt
32
33
  * </application>
@@ -85,6 +86,7 @@ exports.CALL_SERVICE_CLASS = 'com.voltkit.rtc.reactnative.VoltkitCallService';
85
86
  exports.META_AUDIO_MODE = 'in.voltkit.rtc.AUDIO_MODE';
86
87
  exports.META_HW_AEC = 'in.voltkit.rtc.HW_AEC';
87
88
  exports.META_SCREEN_SHARE = 'in.voltkit.rtc.SCREEN_SHARE';
89
+ exports.META_ECHO_SUPPRESSION = 'in.voltkit.rtc.ECHO_SUPPRESSION';
88
90
  const permission = (name) => `android.permission.${name}`;
89
91
  const CAMERA = permission('CAMERA');
90
92
  const RECORD_AUDIO = permission('RECORD_AUDIO');
@@ -246,6 +248,7 @@ function setVoltkitManifest(manifest, options, configBlocked = []) {
246
248
  config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(application, exports.META_AUDIO_MODE, options.audioMode);
247
249
  config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(application, exports.META_HW_AEC, String(options.hardwareEchoCancellation));
248
250
  config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(application, exports.META_SCREEN_SHARE, String(plan.screenShare));
251
+ config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(application, exports.META_ECHO_SUPPRESSION, options.echoSuppression);
249
252
  setCallService(application, plan.serviceTypes);
250
253
  if (plan.serviceDropped) {
251
254
  config_plugins_1.WarningAggregator.addWarningAndroid(`${options_1.PLUGIN_NAME} android.callService`, `VoltkitCallService is not declared: ${plan.serviceDropped}. Calls will drop a few seconds after the app leaves ` +
@@ -2,8 +2,8 @@ import type { ConfigPlugin } from 'expo/config-plugins';
2
2
  import type { VoltkitRtcPluginProps } from './options';
3
3
  declare const _default: ConfigPlugin<void | VoltkitRtcPluginProps | null>;
4
4
  export default _default;
5
- export { CALL_SERVICE_CLASS, META_AUDIO_MODE, META_HW_AEC, META_SCREEN_SHARE, blockedPermissions, planAndroid, setVoltkitManifest, withVoltkitAndroid, } from './android';
5
+ export { CALL_SERVICE_CLASS, META_AUDIO_MODE, META_ECHO_SUPPRESSION, META_HW_AEC, META_SCREEN_SHARE, blockedPermissions, planAndroid, setVoltkitManifest, withVoltkitAndroid, } from './android';
6
6
  export type { AndroidPlan } from './android';
7
7
  export { setVoltkitInfoPlist, withVoltkitIos } from './ios';
8
8
  export { DEFAULT_CAMERA_PERMISSION, DEFAULT_MICROPHONE_PERMISSION, PLUGIN_NAME, resolveOptions } from './options';
9
- export type { AndroidOptions, AudioMode, IosOptions, PluginOptions, VoltkitRtcAndroidProps, VoltkitRtcIosProps, VoltkitRtcPluginProps, } from './options';
9
+ export type { AndroidOptions, AudioMode, IosOptions, PluginOptions, VoltkitRtcAndroidProps, VoltkitRtcIosProps, VoltkitRtcPluginProps, EchoSuppression, } from './options';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveOptions = exports.PLUGIN_NAME = exports.DEFAULT_MICROPHONE_PERMISSION = exports.DEFAULT_CAMERA_PERMISSION = exports.withVoltkitIos = exports.setVoltkitInfoPlist = exports.withVoltkitAndroid = exports.setVoltkitManifest = exports.planAndroid = exports.blockedPermissions = exports.META_SCREEN_SHARE = exports.META_HW_AEC = exports.META_AUDIO_MODE = exports.CALL_SERVICE_CLASS = void 0;
3
+ exports.resolveOptions = exports.PLUGIN_NAME = exports.DEFAULT_MICROPHONE_PERMISSION = exports.DEFAULT_CAMERA_PERMISSION = exports.withVoltkitIos = exports.setVoltkitInfoPlist = exports.withVoltkitAndroid = exports.setVoltkitManifest = exports.planAndroid = exports.blockedPermissions = exports.META_SCREEN_SHARE = exports.META_HW_AEC = exports.META_ECHO_SUPPRESSION = exports.META_AUDIO_MODE = exports.CALL_SERVICE_CLASS = void 0;
4
4
  /**
5
5
  * The Expo config plugin of @voltkit-rtc/react-native (plan §5.5). An app lists it in app.json:
6
6
  *
@@ -38,6 +38,7 @@ exports.default = (0, config_plugins_1.createRunOncePlugin)(withVoltkitRtc, opti
38
38
  var android_2 = require("./android");
39
39
  Object.defineProperty(exports, "CALL_SERVICE_CLASS", { enumerable: true, get: function () { return android_2.CALL_SERVICE_CLASS; } });
40
40
  Object.defineProperty(exports, "META_AUDIO_MODE", { enumerable: true, get: function () { return android_2.META_AUDIO_MODE; } });
41
+ Object.defineProperty(exports, "META_ECHO_SUPPRESSION", { enumerable: true, get: function () { return android_2.META_ECHO_SUPPRESSION; } });
41
42
  Object.defineProperty(exports, "META_HW_AEC", { enumerable: true, get: function () { return android_2.META_HW_AEC; } });
42
43
  Object.defineProperty(exports, "META_SCREEN_SHARE", { enumerable: true, get: function () { return android_2.META_SCREEN_SHARE; } });
43
44
  Object.defineProperty(exports, "blockedPermissions", { enumerable: true, get: function () { return android_2.blockedPermissions; } });
@@ -17,6 +17,8 @@
17
17
  export declare const PLUGIN_NAME = "@voltkit-rtc/react-native";
18
18
  /** How the app plays call audio (meta-data in.voltkit.rtc.AUDIO_MODE; RtcConfig.kt). */
19
19
  export type AudioMode = 'communication' | 'media';
20
+ /** How hard WebRTC's echo canceller suppresses what is left of the echo (meta-data in.voltkit.rtc.ECHO_SUPPRESSION; RtcConfig.kt). */
21
+ export type EchoSuppression = 'auto' | 'strong' | 'webrtc';
20
22
  export interface VoltkitRtcAndroidProps {
21
23
  /**
22
24
  * "communication" (default): call audio, echo cancellation, the earpiece, the loudspeaker and headsets. "media", for
@@ -51,8 +53,22 @@ export interface VoltkitRtcAndroidProps {
51
53
  * android.blockedPermissions blocks what it needs.
52
54
  */
53
55
  screenShare?: boolean;
54
- /** The phone's own echo canceller and noise suppressor, where it has them (meta-data in.voltkit.rtc.HW_AEC; default true). */
56
+ /**
57
+ * The phone's own echo canceller and noise suppressor, where it has them (meta-data in.voltkit.rtc.HW_AEC). Default false
58
+ * from 0.1.6: WebRTC's own (AEC3, the one browsers use), because WebRTC switches its own off while it uses the phone's and
59
+ * some phones' cancellers do not cope with the loudspeaker. True for the phone's, as before 0.1.6.
60
+ */
55
61
  hardwareEchoCancellation?: boolean;
62
+ /**
63
+ * How hard WebRTC's echo canceller suppresses what is left of the echo (meta-data in.voltkit.rtc.ECHO_SUPPRESSION).
64
+ * "auto" (default from 0.1.7): "strong" on phones whose own canceller is Android's software one or absent, which are the
65
+ * phones that echo on the loudspeaker; "webrtc" on phones with a vendor's canceller. "strong": AEC3 tuned for phones
66
+ * whose loudspeaker leaks into their own microphone, everywhere (measured on one whose untuned canceller returned the
67
+ * far voice louder than it arrived; the price is that someone speaking into the phone while the room is loud is cut
68
+ * more often). "webrtc": WebRTC's own tuning everywhere, as 0.1.6 used. In force only with WebRTC's canceller
69
+ * (hardwareEchoCancellation false).
70
+ */
71
+ echoSuppression?: EchoSuppression;
56
72
  }
57
73
  export interface VoltkitRtcIosProps {
58
74
  /** Adds `audio` to UIBackgroundModes, so a call keeps its sound in the background (default true). */
@@ -75,6 +91,7 @@ export interface AndroidOptions {
75
91
  callServiceCamera: boolean;
76
92
  screenShare: boolean;
77
93
  hardwareEchoCancellation: boolean;
94
+ echoSuppression: EchoSuppression;
78
95
  }
79
96
  export interface IosOptions {
80
97
  /** Absent: keep the Info.plist's own text, or write the default. */
@@ -22,13 +22,15 @@ exports.PLUGIN_NAME = '@voltkit-rtc/react-native';
22
22
  exports.DEFAULT_CAMERA_PERMISSION = 'Allow $(PRODUCT_NAME) to use your camera in calls.';
23
23
  exports.DEFAULT_MICROPHONE_PERMISSION = 'Allow $(PRODUCT_NAME) to use your microphone in calls.';
24
24
  const AUDIO_MODES = ['communication', 'media'];
25
+ const ECHO_SUPPRESSIONS = ['auto', 'strong', 'webrtc'];
25
26
  const ANDROID_DEFAULTS = {
26
27
  audioMode: 'communication',
27
28
  camera: true,
28
29
  callService: true,
29
30
  callServiceCamera: false,
30
31
  screenShare: false,
31
- hardwareEchoCancellation: true,
32
+ hardwareEchoCancellation: false,
33
+ echoSuppression: 'auto',
32
34
  };
33
35
  const IOS_DEFAULTS = { backgroundAudio: true, voip: false };
34
36
  const TOP_KEYS = ['cameraPermission', 'microphonePermission', 'android', 'ios'];
@@ -112,8 +114,17 @@ function resolveOptions(props) {
112
114
  }
113
115
  audioMode = value;
114
116
  }
117
+ let echoSuppression = ANDROID_DEFAULTS.echoSuppression;
118
+ if (androidProps.echoSuppression !== undefined) {
119
+ const value = androidProps.echoSuppression;
120
+ if (!ECHO_SUPPRESSIONS.includes(value)) {
121
+ fail(`android.echoSuppression must be ${ECHO_SUPPRESSIONS.map((mode) => `"${mode}"`).join(' or ')}, not ${show(value)}`);
122
+ }
123
+ echoSuppression = value;
124
+ }
115
125
  const android = {
116
126
  audioMode,
127
+ echoSuppression,
117
128
  camera: bool(androidProps, 'camera', 'android', ANDROID_DEFAULTS.camera),
118
129
  callService: bool(androidProps, 'callService', 'android', ANDROID_DEFAULTS.callService),
119
130
  callServiceCamera: bool(androidProps, 'callServiceCamera', 'android', ANDROID_DEFAULTS.callServiceCamera),
package/src/native.ts CHANGED
@@ -10,7 +10,7 @@ import { iosUnsupported, logError, nativeMissing } from './errors';
10
10
  import type { AudioDeviceType, DeviceKind, MediaDevice } from '@voltkit-rtc/client';
11
11
 
12
12
  /** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
13
- export const SDK_VERSION = '0.1.5';
13
+ export const SDK_VERSION = '0.1.7';
14
14
 
15
15
  /** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
16
16
  export function nativeModule(): Spec | null {