@voltkit-rtc/react-native 0.0.0-stage → 0.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.
Files changed (225) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1608 -2
  4. package/android/build.gradle +70 -0
  5. package/android/consumer-rules.pro +21 -0
  6. package/android/src/main/AndroidManifest.xml +25 -0
  7. package/android/src/main/java/com/voltkit/rtc/reactnative/AppVisibility.kt +67 -0
  8. package/android/src/main/java/com/voltkit/rtc/reactnative/AudioDevices.kt +67 -0
  9. package/android/src/main/java/com/voltkit/rtc/reactnative/AudioRouter.kt +487 -0
  10. package/android/src/main/java/com/voltkit/rtc/reactnative/CallServiceTypes.kt +120 -0
  11. package/android/src/main/java/com/voltkit/rtc/reactnative/NativeTimers.kt +106 -0
  12. package/android/src/main/java/com/voltkit/rtc/reactnative/NetworkWatch.kt +120 -0
  13. package/android/src/main/java/com/voltkit/rtc/reactnative/OutputChoice.kt +65 -0
  14. package/android/src/main/java/com/voltkit/rtc/reactnative/RtcConfig.kt +83 -0
  15. package/android/src/main/java/com/voltkit/rtc/reactnative/ScoLink.kt +61 -0
  16. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitCallService.kt +287 -0
  17. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcInitProvider.kt +215 -0
  18. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcModule.kt +313 -0
  19. package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcPackage.kt +30 -0
  20. package/android/src/main/res/drawable/voltkit_rtc_default_call_icon.xml +15 -0
  21. package/android/src/main/res/raw/voltkit_rtc_keep.xml +7 -0
  22. package/app.plugin.js +17 -0
  23. package/lib/commonjs/audio.js +215 -0
  24. package/lib/commonjs/audio.js.map +1 -0
  25. package/lib/commonjs/components.js +207 -0
  26. package/lib/commonjs/components.js.map +1 -0
  27. package/lib/commonjs/create-client.js +47 -0
  28. package/lib/commonjs/create-client.js.map +1 -0
  29. package/lib/commonjs/errors.js +99 -0
  30. package/lib/commonjs/errors.js.map +1 -0
  31. package/lib/commonjs/globals.js +61 -0
  32. package/lib/commonjs/globals.js.map +1 -0
  33. package/lib/commonjs/index.js +234 -0
  34. package/lib/commonjs/index.js.map +1 -0
  35. package/lib/commonjs/local-media.js +392 -0
  36. package/lib/commonjs/local-media.js.map +1 -0
  37. package/lib/commonjs/native-session.js +365 -0
  38. package/lib/commonjs/native-session.js.map +1 -0
  39. package/lib/commonjs/native.js +99 -0
  40. package/lib/commonjs/native.js.map +1 -0
  41. package/lib/commonjs/package.json +1 -0
  42. package/lib/commonjs/permissions.js +166 -0
  43. package/lib/commonjs/permissions.js.map +1 -0
  44. package/lib/commonjs/platform/android.js +126 -0
  45. package/lib/commonjs/platform/android.js.map +1 -0
  46. package/lib/commonjs/platform/common.js +138 -0
  47. package/lib/commonjs/platform/common.js.map +1 -0
  48. package/lib/commonjs/platform/devices.js +151 -0
  49. package/lib/commonjs/platform/devices.js.map +1 -0
  50. package/lib/commonjs/platform/index.js +35 -0
  51. package/lib/commonjs/platform/index.js.map +1 -0
  52. package/lib/commonjs/platform/ios.js +54 -0
  53. package/lib/commonjs/platform/ios.js.map +1 -0
  54. package/lib/commonjs/platform/streams.js +273 -0
  55. package/lib/commonjs/platform/streams.js.map +1 -0
  56. package/lib/commonjs/platform/timers.js +71 -0
  57. package/lib/commonjs/platform/timers.js.map +1 -0
  58. package/lib/commonjs/platform/visibility.js +60 -0
  59. package/lib/commonjs/platform/visibility.js.map +1 -0
  60. package/lib/commonjs/preflight.js +77 -0
  61. package/lib/commonjs/preflight.js.map +1 -0
  62. package/lib/commonjs/provider.js +75 -0
  63. package/lib/commonjs/provider.js.map +1 -0
  64. package/lib/commonjs/specs/NativeVoltkitRtc.js +29 -0
  65. package/lib/commonjs/specs/NativeVoltkitRtc.js.map +1 -0
  66. package/lib/module/audio.js +207 -0
  67. package/lib/module/audio.js.map +1 -0
  68. package/lib/module/components.js +200 -0
  69. package/lib/module/components.js.map +1 -0
  70. package/lib/module/create-client.js +42 -0
  71. package/lib/module/create-client.js.map +1 -0
  72. package/lib/module/errors.js +87 -0
  73. package/lib/module/errors.js.map +1 -0
  74. package/lib/module/globals.js +53 -0
  75. package/lib/module/globals.js.map +1 -0
  76. package/lib/module/index.js +33 -0
  77. package/lib/module/index.js.map +1 -0
  78. package/lib/module/local-media.js +387 -0
  79. package/lib/module/local-media.js.map +1 -0
  80. package/lib/module/native-session.js +360 -0
  81. package/lib/module/native-session.js.map +1 -0
  82. package/lib/module/native.js +87 -0
  83. package/lib/module/native.js.map +1 -0
  84. package/lib/module/package.json +1 -0
  85. package/lib/module/permissions.js +160 -0
  86. package/lib/module/permissions.js.map +1 -0
  87. package/lib/module/platform/android.js +122 -0
  88. package/lib/module/platform/android.js.map +1 -0
  89. package/lib/module/platform/common.js +128 -0
  90. package/lib/module/platform/common.js.map +1 -0
  91. package/lib/module/platform/devices.js +142 -0
  92. package/lib/module/platform/devices.js.map +1 -0
  93. package/lib/module/platform/index.js +24 -0
  94. package/lib/module/platform/index.js.map +1 -0
  95. package/lib/module/platform/ios.js +49 -0
  96. package/lib/module/platform/ios.js.map +1 -0
  97. package/lib/module/platform/streams.js +268 -0
  98. package/lib/module/platform/streams.js.map +1 -0
  99. package/lib/module/platform/timers.js +67 -0
  100. package/lib/module/platform/timers.js.map +1 -0
  101. package/lib/module/platform/visibility.js +54 -0
  102. package/lib/module/platform/visibility.js.map +1 -0
  103. package/lib/module/preflight.js +72 -0
  104. package/lib/module/preflight.js.map +1 -0
  105. package/lib/module/provider.js +70 -0
  106. package/lib/module/provider.js.map +1 -0
  107. package/lib/module/specs/NativeVoltkitRtc.js +37 -0
  108. package/lib/module/specs/NativeVoltkitRtc.js.map +1 -0
  109. package/lib/typescript/commonjs/audio.d.ts +58 -0
  110. package/lib/typescript/commonjs/audio.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components.d.ts +58 -0
  112. package/lib/typescript/commonjs/components.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/create-client.d.ts +35 -0
  114. package/lib/typescript/commonjs/create-client.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/errors.d.ts +34 -0
  116. package/lib/typescript/commonjs/errors.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/globals.d.ts +16 -0
  118. package/lib/typescript/commonjs/globals.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/index.d.ts +24 -0
  120. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/local-media.d.ts +86 -0
  122. package/lib/typescript/commonjs/local-media.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/native-session.d.ts +38 -0
  124. package/lib/typescript/commonjs/native-session.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/native.d.ts +18 -0
  126. package/lib/typescript/commonjs/native.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/package.json +1 -0
  128. package/lib/typescript/commonjs/permissions.d.ts +30 -0
  129. package/lib/typescript/commonjs/permissions.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/platform/android.d.ts +4 -0
  131. package/lib/typescript/commonjs/platform/android.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/platform/common.d.ts +101 -0
  133. package/lib/typescript/commonjs/platform/common.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/platform/devices.d.ts +42 -0
  135. package/lib/typescript/commonjs/platform/devices.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/platform/index.d.ts +6 -0
  137. package/lib/typescript/commonjs/platform/index.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/platform/ios.d.ts +4 -0
  139. package/lib/typescript/commonjs/platform/ios.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/platform/streams.d.ts +82 -0
  141. package/lib/typescript/commonjs/platform/streams.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/platform/timers.d.ts +19 -0
  143. package/lib/typescript/commonjs/platform/timers.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/platform/visibility.d.ts +11 -0
  145. package/lib/typescript/commonjs/platform/visibility.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/preflight.d.ts +19 -0
  147. package/lib/typescript/commonjs/preflight.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider.d.ts +35 -0
  149. package/lib/typescript/commonjs/provider.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts +157 -0
  151. package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts.map +1 -0
  152. package/lib/typescript/module/audio.d.ts +58 -0
  153. package/lib/typescript/module/audio.d.ts.map +1 -0
  154. package/lib/typescript/module/components.d.ts +58 -0
  155. package/lib/typescript/module/components.d.ts.map +1 -0
  156. package/lib/typescript/module/create-client.d.ts +35 -0
  157. package/lib/typescript/module/create-client.d.ts.map +1 -0
  158. package/lib/typescript/module/errors.d.ts +34 -0
  159. package/lib/typescript/module/errors.d.ts.map +1 -0
  160. package/lib/typescript/module/globals.d.ts +16 -0
  161. package/lib/typescript/module/globals.d.ts.map +1 -0
  162. package/lib/typescript/module/index.d.ts +24 -0
  163. package/lib/typescript/module/index.d.ts.map +1 -0
  164. package/lib/typescript/module/local-media.d.ts +86 -0
  165. package/lib/typescript/module/local-media.d.ts.map +1 -0
  166. package/lib/typescript/module/native-session.d.ts +38 -0
  167. package/lib/typescript/module/native-session.d.ts.map +1 -0
  168. package/lib/typescript/module/native.d.ts +18 -0
  169. package/lib/typescript/module/native.d.ts.map +1 -0
  170. package/lib/typescript/module/package.json +1 -0
  171. package/lib/typescript/module/permissions.d.ts +30 -0
  172. package/lib/typescript/module/permissions.d.ts.map +1 -0
  173. package/lib/typescript/module/platform/android.d.ts +4 -0
  174. package/lib/typescript/module/platform/android.d.ts.map +1 -0
  175. package/lib/typescript/module/platform/common.d.ts +101 -0
  176. package/lib/typescript/module/platform/common.d.ts.map +1 -0
  177. package/lib/typescript/module/platform/devices.d.ts +42 -0
  178. package/lib/typescript/module/platform/devices.d.ts.map +1 -0
  179. package/lib/typescript/module/platform/index.d.ts +6 -0
  180. package/lib/typescript/module/platform/index.d.ts.map +1 -0
  181. package/lib/typescript/module/platform/ios.d.ts +4 -0
  182. package/lib/typescript/module/platform/ios.d.ts.map +1 -0
  183. package/lib/typescript/module/platform/streams.d.ts +82 -0
  184. package/lib/typescript/module/platform/streams.d.ts.map +1 -0
  185. package/lib/typescript/module/platform/timers.d.ts +19 -0
  186. package/lib/typescript/module/platform/timers.d.ts.map +1 -0
  187. package/lib/typescript/module/platform/visibility.d.ts +11 -0
  188. package/lib/typescript/module/platform/visibility.d.ts.map +1 -0
  189. package/lib/typescript/module/preflight.d.ts +19 -0
  190. package/lib/typescript/module/preflight.d.ts.map +1 -0
  191. package/lib/typescript/module/provider.d.ts +35 -0
  192. package/lib/typescript/module/provider.d.ts.map +1 -0
  193. package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts +157 -0
  194. package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts.map +1 -0
  195. package/package.json +140 -4
  196. package/plugin/build/android.d.ts +36 -0
  197. package/plugin/build/android.js +265 -0
  198. package/plugin/build/index.d.ts +9 -0
  199. package/plugin/build/index.js +54 -0
  200. package/plugin/build/ios.d.ts +4 -0
  201. package/plugin/build/ios.js +43 -0
  202. package/plugin/build/options.d.ts +93 -0
  203. package/plugin/build/options.js +140 -0
  204. package/react-native.config.js +11 -0
  205. package/src/audio.ts +221 -0
  206. package/src/components.tsx +233 -0
  207. package/src/create-client.ts +53 -0
  208. package/src/errors.ts +81 -0
  209. package/src/globals.ts +54 -0
  210. package/src/index.ts +95 -0
  211. package/src/local-media.ts +420 -0
  212. package/src/native-session.tsx +384 -0
  213. package/src/native.ts +84 -0
  214. package/src/permissions.ts +173 -0
  215. package/src/platform/android.ts +128 -0
  216. package/src/platform/common.ts +157 -0
  217. package/src/platform/devices.ts +148 -0
  218. package/src/platform/index.ts +22 -0
  219. package/src/platform/ios.ts +48 -0
  220. package/src/platform/streams.ts +285 -0
  221. package/src/platform/timers.ts +72 -0
  222. package/src/platform/visibility.ts +59 -0
  223. package/src/preflight.ts +60 -0
  224. package/src/provider.tsx +88 -0
  225. package/src/specs/NativeVoltkitRtc.ts +180 -0
@@ -0,0 +1,106 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ import android.os.Handler
4
+ import android.os.HandlerThread
5
+
6
+ /**
7
+ * Timers for the client's own work -- reconnect backoff, socket liveness, request deadlines, sampling -- that keep
8
+ * running in the background and with the screen off (plan §2, PLAN_UPDATES 3). React Native's JavaScript timers run
9
+ * on Choreographer frames, which stop at onHostPause and with the screen off even while the app is in front (Phase 0
10
+ * S5). These wait on their own thread and tell JavaScript through the module's onTimer event.
11
+ *
12
+ * A fire can be on its way when JavaScript stops a timer, so JavaScript ignores a fire for an id it no longer has, and
13
+ * never reuses an id. Here, a timer stopped or replaced before its fire ran never fires.
14
+ */
15
+ internal class NativeTimers(
16
+ private val scheduler: Scheduler,
17
+ private val fire: (id: Double) -> Unit,
18
+ ) {
19
+ /** Where timers wait: a Handler thread in the app ([HandlerScheduler]), a virtual clock in tests. */
20
+ interface Scheduler {
21
+ /** Run [task] after [delayMillis]; returns how to cancel it. */
22
+ fun schedule(delayMillis: Long, task: Runnable): Cancel
23
+ }
24
+
25
+ fun interface Cancel {
26
+ fun cancel()
27
+ }
28
+
29
+ private class Entry(val id: Double, val delayMillis: Long, val repeat: Boolean) {
30
+ var cancel: Cancel? = null
31
+ }
32
+
33
+ private val lock = Any()
34
+ private val live = HashMap<Double, Entry>()
35
+
36
+ /** How many timers are live. */
37
+ val size: Int
38
+ get() = synchronized(lock) { live.size }
39
+
40
+ /** Fire [id] after [ms], and every [ms] after that when [repeat]. A live [id] is replaced. */
41
+ fun start(id: Double, ms: Double, repeat: Boolean) {
42
+ val entry = Entry(id, delayOf(ms, repeat), repeat)
43
+ synchronized(lock) {
44
+ live.remove(id)?.cancel?.cancel()
45
+ live[id] = entry
46
+ entry.cancel = scheduler.schedule(entry.delayMillis) { tick(entry) }
47
+ }
48
+ }
49
+
50
+ fun stop(id: Double) {
51
+ synchronized(lock) { live.remove(id) }?.cancel?.cancel()
52
+ }
53
+
54
+ /** Every timer ends (the React instance went away). */
55
+ fun stopAll() {
56
+ val all = synchronized(lock) { live.values.toList().also { live.clear() } }
57
+ all.forEach { it.cancel?.cancel() }
58
+ }
59
+
60
+ private fun tick(entry: Entry) {
61
+ synchronized(lock) {
62
+ // Stopped or replaced while this fire was on its way.
63
+ if (live[entry.id] !== entry) return
64
+ if (entry.repeat) {
65
+ entry.cancel = scheduler.schedule(entry.delayMillis) { tick(entry) }
66
+ } else {
67
+ live.remove(entry.id)
68
+ }
69
+ }
70
+ fire(entry.id)
71
+ }
72
+
73
+ companion object {
74
+ /** An interval of 0 would spin: held to the 4 ms browsers clamp nested timers to. */
75
+ const val MIN_INTERVAL_MILLIS = 4L
76
+
77
+ /** The longest wait: Handler adds the delay to the uptime clock, and a larger one would wrap around. */
78
+ const val MAX_DELAY_MILLIS = Int.MAX_VALUE.toLong()
79
+
80
+ /** Negative and NaN fire at once (as in browsers); infinite and huge wait the longest; fractions round down. */
81
+ fun delayOf(ms: Double, repeat: Boolean): Long {
82
+ val millis = when {
83
+ ms.isNaN() || ms <= 0.0 -> 0L
84
+ ms >= MAX_DELAY_MILLIS.toDouble() -> MAX_DELAY_MILLIS
85
+ else -> ms.toLong()
86
+ }
87
+ return if (repeat) maxOf(millis, MIN_INTERVAL_MILLIS) else millis
88
+ }
89
+ }
90
+ }
91
+
92
+ /** The app's [NativeTimers.Scheduler]: a thread of its own, so a busy main or JavaScript thread does not delay fires. */
93
+ internal class HandlerScheduler(name: String) : NativeTimers.Scheduler {
94
+ private val thread = HandlerThread(name).apply { start() }
95
+ private val handler = Handler(thread.looper)
96
+
97
+ override fun schedule(delayMillis: Long, task: Runnable): NativeTimers.Cancel {
98
+ handler.postDelayed(task, delayMillis)
99
+ return NativeTimers.Cancel { handler.removeCallbacks(task) }
100
+ }
101
+
102
+ fun quit() {
103
+ handler.removeCallbacksAndMessages(null)
104
+ thread.quitSafely()
105
+ }
106
+ }
@@ -0,0 +1,120 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.Network
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.util.Log
9
+
10
+ /**
11
+ * The network coming back, for the client's `onOnline` hook (a browser's `online` event): a reconnect waiting out its
12
+ * backoff tries at once, and the socket is checked. Told when the default network returns after there was none
13
+ * (`handover` false), and when it changes to another one, say Wi-Fi to mobile data (`handover` true), since the old
14
+ * network's sockets are then on their way out. Needs ACCESS_NETWORK_STATE, which the library's manifest declares.
15
+ *
16
+ * Not told at once: Android hands the default callback's onAvailable out before it has switched the app's routes to
17
+ * that network (on the API 37 emulator "Switching to new default network" followed it by 40 to 80 ms, and a connect
18
+ * made at once failed in 7 ms: Phase 3, C11). The news waits [NetworkWatchLogic.GRACE_MILLIS] and goes out only if the
19
+ * network is still the default then; a newer network's own onAvailable covers the case where it is not.
20
+ */
21
+ internal class NetworkWatch(context: Context, onAvailable: (handover: Boolean) -> Unit) {
22
+ private val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
23
+ private val main = Handler(Looper.getMainLooper())
24
+ private val lock = Any()
25
+ private var registered = false
26
+ private val logic = NetworkWatchLogic<Network>(
27
+ schedule = { delayMillis, task -> main.postDelayed(task, delayMillis) },
28
+ // Still the app's default network when the grace ends; when that cannot be read, taken as yes.
29
+ isDefault = { network -> runCatching { cm?.activeNetwork == network }.getOrDefault(true) },
30
+ tell = onAvailable,
31
+ )
32
+
33
+ private val callback = object : ConnectivityManager.NetworkCallback() {
34
+ override fun onAvailable(network: Network) = logic.available(network)
35
+
36
+ override fun onLost(network: Network) = logic.lost(network)
37
+ }
38
+
39
+ fun start() {
40
+ val manager = cm ?: return
41
+ synchronized(lock) {
42
+ if (registered) return
43
+ registered = true
44
+ }
45
+ logic.reset(runCatching { manager.activeNetwork }.getOrNull())
46
+ try {
47
+ manager.registerDefaultNetworkCallback(callback)
48
+ } catch (t: Throwable) {
49
+ Log.w(TAG, "could not watch the network: ${t.message}")
50
+ synchronized(lock) { registered = false }
51
+ }
52
+ }
53
+
54
+ fun stop() {
55
+ val manager = cm ?: return
56
+ synchronized(lock) {
57
+ if (!registered) return
58
+ registered = false
59
+ }
60
+ runCatching { manager.unregisterNetworkCallback(callback) }
61
+ }
62
+
63
+ private companion object {
64
+ const val TAG = "VoltKitRtc"
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The decision behind [NetworkWatch], without Android: which onAvailable is news (the first network after none, or a
70
+ * handover), and when it may be told (after [GRACE_MILLIS], if the network is still the default and nothing newer
71
+ * came). [N] is the network's type: android.net.Network in the app, anything in a test.
72
+ */
73
+ internal class NetworkWatchLogic<N : Any>(
74
+ private val schedule: (delayMillis: Long, task: () -> Unit) -> Unit,
75
+ private val isDefault: (N) -> Boolean,
76
+ private val tell: (handover: Boolean) -> Unit,
77
+ ) {
78
+ private val lock = Any()
79
+ private var current: N? = null
80
+ /** Bumped by every change: a grace that ends after a later change says nothing. */
81
+ private var generation = 0L
82
+
83
+ /** The default network at the start (registration replays it, which is not news). */
84
+ fun reset(active: N?) {
85
+ synchronized(lock) {
86
+ current = active
87
+ generation += 1
88
+ }
89
+ }
90
+
91
+ fun available(network: N) {
92
+ val handover: Boolean
93
+ val gen: Long
94
+ synchronized(lock) {
95
+ val was = current
96
+ if (was == network) return
97
+ current = network
98
+ handover = was != null
99
+ generation += 1
100
+ gen = generation
101
+ }
102
+ schedule(GRACE_MILLIS) {
103
+ val still = synchronized(lock) { generation == gen && current == network }
104
+ if (still && isDefault(network)) runCatching { tell(handover) }
105
+ }
106
+ }
107
+
108
+ fun lost(network: N) {
109
+ synchronized(lock) {
110
+ if (current != network) return
111
+ current = null
112
+ generation += 1
113
+ }
114
+ }
115
+
116
+ companion object {
117
+ /** How long after onAvailable the routes are trusted to have followed (observed 40 to 80 ms; a wide margin). */
118
+ const val GRACE_MILLIS = 500L
119
+ }
120
+ }
@@ -0,0 +1,65 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ /**
4
+ * Where call audio should play: the output picked by hand, the loudspeaker or earpiece preference, and the Bluetooth
5
+ * headsets that refused call audio. Decisions only, so they can be tested off a device; [AudioRouter] keeps one and
6
+ * touches it on its own thread. Ported unchanged from the Android SDK (android-sdk/.../OutputChoice.kt).
7
+ */
8
+ internal class OutputChoice {
9
+ /** Loudspeaker (true) or earpiece (false) when no headset is connected. */
10
+ var speakerPreferred: Boolean = true
11
+
12
+ /** The output picked by hand ([AudioRouter.selectOutput]), while it stays connected. */
13
+ var picked: String? = null
14
+ private set
15
+
16
+ /**
17
+ * Every headset that refused call audio, skipped until it is picked again, a device is added, or the call ends.
18
+ * Every one, not the last: with one remembered, two refusing headsets were switched between forever (K096-1).
19
+ */
20
+ private val refused = HashSet<String>()
21
+
22
+ /** Picked by hand: it wins while it stays connected, and a headset that refused is tried again. */
23
+ fun pick(deviceId: String) {
24
+ picked = deviceId
25
+ refused.remove(deviceId)
26
+ }
27
+
28
+ /** The preference changed: a built-in output picked by hand gives way to it; a headset picked by hand stays. */
29
+ fun preferSpeaker(on: Boolean) {
30
+ speakerPreferred = on
31
+ if (picked == AudioRouter.SPEAKER || picked == AudioRouter.EARPIECE) picked = null
32
+ }
33
+
34
+ /** The headset [deviceId] refused call audio: route elsewhere. */
35
+ fun refusedBy(deviceId: String?) {
36
+ deviceId?.let { refused.add(it) }
37
+ }
38
+
39
+ /** A device was added: every headset gets another try. */
40
+ fun devicesAdded() {
41
+ refused.clear()
42
+ }
43
+
44
+ /** The call audio was let go: nothing picked, nothing refused. */
45
+ fun reset() {
46
+ picked = null
47
+ refused.clear()
48
+ }
49
+
50
+ /** The outputs now: a pick no longer among them is forgotten. */
51
+ fun outputsNow(outputs: List<MediaDevice>) {
52
+ if (picked != null && outputs.none { it.deviceId == picked }) picked = null
53
+ }
54
+
55
+ fun choose(outputs: List<MediaDevice>): MediaDevice? {
56
+ // A pick that refused is passed over: picked first regardless, it was started again every 3 seconds, and call
57
+ // audio had no working output (K096-1).
58
+ picked?.takeIf { it !in refused }?.let { id -> outputs.firstOrNull { it.deviceId == id }?.let { return it } }
59
+ outputs.firstOrNull { it.type == AudioDeviceType.bluetooth && it.deviceId !in refused }?.let { return it }
60
+ outputs.firstOrNull { it.type == AudioDeviceType.wiredHeadset || it.type == AudioDeviceType.usb }?.let { return it }
61
+ val speaker = outputs.firstOrNull { it.type == AudioDeviceType.speaker }
62
+ val earpiece = outputs.firstOrNull { it.type == AudioDeviceType.earpiece }
63
+ return if (speakerPreferred) speaker ?: earpiece else earpiece ?: speaker
64
+ }
65
+ }
@@ -0,0 +1,83 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ /**
4
+ * How this process plays call audio, chosen once in the app's manifest (meta-data in.voltkit.rtc.AUDIO_MODE) and
5
+ * fixed for the life of the audio engine, as the Android SDK's `RtcOptions.audioMode`.
6
+ */
7
+ @Suppress("EnumEntryName")
8
+ internal enum class AudioMode {
9
+ /**
10
+ * A call: echo cancellation, the call volume, Bluetooth headsets as two-way headsets, and the earpiece or
11
+ * loudspeaker by choice. For anyone who speaks -- calls and audio rooms, listeners included, since a listener
12
+ * brought on stage must not echo.
13
+ */
14
+ communication,
15
+
16
+ /**
17
+ * Media playback, for an app whose users only ever listen: the media volume, Bluetooth headphones in their
18
+ * high-quality mode, no earpiece. Not for anyone who may speak.
19
+ */
20
+ media,
21
+ }
22
+
23
+ /**
24
+ * The process-wide settings the init provider reads from the app's manifest before react-native-webrtc starts
25
+ * (plan §5.3), as the Expo config plugin writes them or a hand-written manifest gives them:
26
+ *
27
+ * <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
30
+ * <meta-data android:name="in.voltkit.rtc.SCREEN_SHARE" android:value="false" /> react-native-webrtc's
31
+ * screen-share service
32
+ *
33
+ * Parsing only, so it can be tested off a device. A value that cannot be read keeps its default and is reported.
34
+ */
35
+ internal data class RtcConfig(
36
+ val audioMode: AudioMode = AudioMode.communication,
37
+ val hardwareEchoCancellation: Boolean = true,
38
+ val screenShare: Boolean = false,
39
+ /** What could not be read, with the default used instead; the init provider logs it. */
40
+ val problems: List<String> = emptyList(),
41
+ ) {
42
+ companion object {
43
+ const val AUDIO_MODE = "in.voltkit.rtc.AUDIO_MODE"
44
+ const val HW_AEC = "in.voltkit.rtc.HW_AEC"
45
+ const val SCREEN_SHARE = "in.voltkit.rtc.SCREEN_SHARE"
46
+
47
+ /** From the application's meta-data: [meta] gives one key's value (a Bundle's `get`), or null. */
48
+ fun from(meta: (String) -> Any?): RtcConfig {
49
+ val problems = ArrayList<String>()
50
+ val mode = when (val raw = meta(AUDIO_MODE)) {
51
+ null -> AudioMode.communication
52
+ is String -> AudioMode.values().firstOrNull { it.name == raw.trim().lowercase() }
53
+ ?: AudioMode.communication.also { problems += "$AUDIO_MODE: '$raw' is not communication or media; using communication" }
54
+ else -> AudioMode.communication.also { problems += "$AUDIO_MODE: '$raw' is not communication or media; using communication" }
55
+ }
56
+ return RtcConfig(
57
+ audioMode = mode,
58
+ hardwareEchoCancellation = flag(meta(HW_AEC), HW_AEC, default = true, problems),
59
+ screenShare = flag(meta(SCREEN_SHARE), SCREEN_SHARE, default = false, problems),
60
+ problems = problems,
61
+ )
62
+ }
63
+
64
+ private fun flag(raw: Any?, key: String, default: Boolean, problems: MutableList<String>): Boolean {
65
+ val value = when (raw) {
66
+ null -> return default
67
+ is Boolean -> raw
68
+ is String -> when (raw.trim().lowercase()) {
69
+ "true" -> true
70
+ "false" -> false
71
+ else -> null
72
+ }
73
+ is Number -> when (raw.toInt()) {
74
+ 1 -> true
75
+ 0 -> false
76
+ else -> null
77
+ }
78
+ else -> null
79
+ }
80
+ return value ?: default.also { problems += "$key: '$raw' is not true or false; using $default" }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,61 @@
1
+ package com.voltkit.rtc.reactnative
2
+
3
+ /**
4
+ * What the Bluetooth call link's state broadcasts mean (API 23 to 30, where call audio reaches a headset through
5
+ * `startBluetoothSco`). Decisions only, so they can be tested off a device; [AudioRouter] acts on them. Ported
6
+ * unchanged from the Android SDK (android-sdk/.../ScoLink.kt).
7
+ *
8
+ * The broadcast also reports the link stopped before: switching from Bluetooth to the speaker and back quickly, that
9
+ * link's DISCONNECTED arrived after the new start and was taken for the new link's refusal, and the headset was routed
10
+ * away until devices changed (SR-1). So after a start, DISCONNECTED says nothing until the link has said CONNECTING or
11
+ * CONNECTED. A refusal can say DISCONNECTED and nothing else: a start that hears nothing of its link within
12
+ * [START_TIMEOUT_MILLIS] failed too ([silent]). ERROR is always a failure.
13
+ */
14
+ internal class ScoLink {
15
+ /** The call link is asked for: started and not stopped since. */
16
+ var wanted: Boolean = false
17
+ private set
18
+
19
+ /** Starts so far; [silent] checks the one it was given. */
20
+ private var starts = 0
21
+
22
+ /** Since the last start, the link said CONNECTING or CONNECTED: a DISCONNECTED is about this link. */
23
+ private var heard = false
24
+
25
+ /** Asked for; returns this start's number, for [silent]. */
26
+ fun start(): Int {
27
+ wanted = true
28
+ heard = false
29
+ return ++starts
30
+ }
31
+
32
+ fun stop() {
33
+ wanted = false
34
+ heard = false
35
+ }
36
+
37
+ /** One state broadcast: true when the link asked for failed. */
38
+ fun failed(state: Int): Boolean {
39
+ if (!wanted) return false
40
+ return when (state) {
41
+ CONNECTING, CONNECTED -> {
42
+ heard = true
43
+ false
44
+ }
45
+ DISCONNECTED -> heard
46
+ else -> true
47
+ }
48
+ }
49
+
50
+ /** [START_TIMEOUT_MILLIS] after start [start]: still asked for, and nothing heard of the link. */
51
+ fun silent(start: Int): Boolean = wanted && start == starts && !heard
52
+
53
+ companion object {
54
+ // AudioManager.SCO_AUDIO_STATE_*, as the broadcast carries them.
55
+ const val ERROR = -1
56
+ const val DISCONNECTED = 0
57
+ const val CONNECTED = 1
58
+ const val CONNECTING = 2
59
+ const val START_TIMEOUT_MILLIS = 3_000L
60
+ }
61
+ }