@voltkit-rtc/react-native 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +1606 -2
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +21 -0
- package/android/src/main/AndroidManifest.xml +25 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AppVisibility.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioDevices.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioRouter.kt +487 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/CallServiceTypes.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NativeTimers.kt +106 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NetworkWatch.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/OutputChoice.kt +65 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/RtcConfig.kt +83 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/ScoLink.kt +61 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitCallService.kt +287 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcInitProvider.kt +215 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcModule.kt +313 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcPackage.kt +30 -0
- package/android/src/main/res/drawable/voltkit_rtc_default_call_icon.xml +15 -0
- package/android/src/main/res/raw/voltkit_rtc_keep.xml +7 -0
- package/app.plugin.js +17 -0
- package/lib/commonjs/audio.js +215 -0
- package/lib/commonjs/audio.js.map +1 -0
- package/lib/commonjs/components.js +207 -0
- package/lib/commonjs/components.js.map +1 -0
- package/lib/commonjs/create-client.js +47 -0
- package/lib/commonjs/create-client.js.map +1 -0
- package/lib/commonjs/errors.js +99 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/globals.js +61 -0
- package/lib/commonjs/globals.js.map +1 -0
- package/lib/commonjs/index.js +234 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/local-media.js +392 -0
- package/lib/commonjs/local-media.js.map +1 -0
- package/lib/commonjs/native-session.js +314 -0
- package/lib/commonjs/native-session.js.map +1 -0
- package/lib/commonjs/native.js +99 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/permissions.js +166 -0
- package/lib/commonjs/permissions.js.map +1 -0
- package/lib/commonjs/platform/android.js +126 -0
- package/lib/commonjs/platform/android.js.map +1 -0
- package/lib/commonjs/platform/common.js +138 -0
- package/lib/commonjs/platform/common.js.map +1 -0
- package/lib/commonjs/platform/devices.js +151 -0
- package/lib/commonjs/platform/devices.js.map +1 -0
- package/lib/commonjs/platform/index.js +35 -0
- package/lib/commonjs/platform/index.js.map +1 -0
- package/lib/commonjs/platform/ios.js +54 -0
- package/lib/commonjs/platform/ios.js.map +1 -0
- package/lib/commonjs/platform/streams.js +273 -0
- package/lib/commonjs/platform/streams.js.map +1 -0
- package/lib/commonjs/platform/timers.js +71 -0
- package/lib/commonjs/platform/timers.js.map +1 -0
- package/lib/commonjs/platform/visibility.js +60 -0
- package/lib/commonjs/platform/visibility.js.map +1 -0
- package/lib/commonjs/preflight.js +77 -0
- package/lib/commonjs/preflight.js.map +1 -0
- package/lib/commonjs/provider.js +73 -0
- package/lib/commonjs/provider.js.map +1 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js +29 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/module/audio.js +207 -0
- package/lib/module/audio.js.map +1 -0
- package/lib/module/components.js +200 -0
- package/lib/module/components.js.map +1 -0
- package/lib/module/create-client.js +42 -0
- package/lib/module/create-client.js.map +1 -0
- package/lib/module/errors.js +87 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/globals.js +53 -0
- package/lib/module/globals.js.map +1 -0
- package/lib/module/index.js +33 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/local-media.js +387 -0
- package/lib/module/local-media.js.map +1 -0
- package/lib/module/native-session.js +309 -0
- package/lib/module/native-session.js.map +1 -0
- package/lib/module/native.js +87 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.js +160 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/platform/android.js +122 -0
- package/lib/module/platform/android.js.map +1 -0
- package/lib/module/platform/common.js +128 -0
- package/lib/module/platform/common.js.map +1 -0
- package/lib/module/platform/devices.js +142 -0
- package/lib/module/platform/devices.js.map +1 -0
- package/lib/module/platform/index.js +24 -0
- package/lib/module/platform/index.js.map +1 -0
- package/lib/module/platform/ios.js +49 -0
- package/lib/module/platform/ios.js.map +1 -0
- package/lib/module/platform/streams.js +268 -0
- package/lib/module/platform/streams.js.map +1 -0
- package/lib/module/platform/timers.js +67 -0
- package/lib/module/platform/timers.js.map +1 -0
- package/lib/module/platform/visibility.js +54 -0
- package/lib/module/platform/visibility.js.map +1 -0
- package/lib/module/preflight.js +72 -0
- package/lib/module/preflight.js.map +1 -0
- package/lib/module/provider.js +68 -0
- package/lib/module/provider.js.map +1 -0
- package/lib/module/specs/NativeVoltkitRtc.js +37 -0
- package/lib/module/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/typescript/commonjs/audio.d.ts +58 -0
- package/lib/typescript/commonjs/audio.d.ts.map +1 -0
- package/lib/typescript/commonjs/components.d.ts +58 -0
- package/lib/typescript/commonjs/components.d.ts.map +1 -0
- package/lib/typescript/commonjs/create-client.d.ts +35 -0
- package/lib/typescript/commonjs/create-client.d.ts.map +1 -0
- package/lib/typescript/commonjs/errors.d.ts +34 -0
- package/lib/typescript/commonjs/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/globals.d.ts +16 -0
- package/lib/typescript/commonjs/globals.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +24 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/local-media.d.ts +86 -0
- package/lib/typescript/commonjs/local-media.d.ts.map +1 -0
- package/lib/typescript/commonjs/native-session.d.ts +38 -0
- package/lib/typescript/commonjs/native-session.d.ts.map +1 -0
- package/lib/typescript/commonjs/native.d.ts +18 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/permissions.d.ts +30 -0
- package/lib/typescript/commonjs/permissions.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/android.d.ts +4 -0
- package/lib/typescript/commonjs/platform/android.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/common.d.ts +101 -0
- package/lib/typescript/commonjs/platform/common.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/devices.d.ts +42 -0
- package/lib/typescript/commonjs/platform/devices.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/index.d.ts +6 -0
- package/lib/typescript/commonjs/platform/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/ios.d.ts +4 -0
- package/lib/typescript/commonjs/platform/ios.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/streams.d.ts +82 -0
- package/lib/typescript/commonjs/platform/streams.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/timers.d.ts +19 -0
- package/lib/typescript/commonjs/platform/timers.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts +11 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/commonjs/preflight.d.ts +19 -0
- package/lib/typescript/commonjs/preflight.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider.d.ts +35 -0
- package/lib/typescript/commonjs/provider.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/lib/typescript/module/audio.d.ts +58 -0
- package/lib/typescript/module/audio.d.ts.map +1 -0
- package/lib/typescript/module/components.d.ts +58 -0
- package/lib/typescript/module/components.d.ts.map +1 -0
- package/lib/typescript/module/create-client.d.ts +35 -0
- package/lib/typescript/module/create-client.d.ts.map +1 -0
- package/lib/typescript/module/errors.d.ts +34 -0
- package/lib/typescript/module/errors.d.ts.map +1 -0
- package/lib/typescript/module/globals.d.ts +16 -0
- package/lib/typescript/module/globals.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +24 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/local-media.d.ts +86 -0
- package/lib/typescript/module/local-media.d.ts.map +1 -0
- package/lib/typescript/module/native-session.d.ts +38 -0
- package/lib/typescript/module/native-session.d.ts.map +1 -0
- package/lib/typescript/module/native.d.ts +18 -0
- package/lib/typescript/module/native.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/permissions.d.ts +30 -0
- package/lib/typescript/module/permissions.d.ts.map +1 -0
- package/lib/typescript/module/platform/android.d.ts +4 -0
- package/lib/typescript/module/platform/android.d.ts.map +1 -0
- package/lib/typescript/module/platform/common.d.ts +101 -0
- package/lib/typescript/module/platform/common.d.ts.map +1 -0
- package/lib/typescript/module/platform/devices.d.ts +42 -0
- package/lib/typescript/module/platform/devices.d.ts.map +1 -0
- package/lib/typescript/module/platform/index.d.ts +6 -0
- package/lib/typescript/module/platform/index.d.ts.map +1 -0
- package/lib/typescript/module/platform/ios.d.ts +4 -0
- package/lib/typescript/module/platform/ios.d.ts.map +1 -0
- package/lib/typescript/module/platform/streams.d.ts +82 -0
- package/lib/typescript/module/platform/streams.d.ts.map +1 -0
- package/lib/typescript/module/platform/timers.d.ts +19 -0
- package/lib/typescript/module/platform/timers.d.ts.map +1 -0
- package/lib/typescript/module/platform/visibility.d.ts +11 -0
- package/lib/typescript/module/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/module/preflight.d.ts +19 -0
- package/lib/typescript/module/preflight.d.ts.map +1 -0
- package/lib/typescript/module/provider.d.ts +35 -0
- package/lib/typescript/module/provider.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/package.json +140 -4
- package/plugin/build/android.d.ts +36 -0
- package/plugin/build/android.js +265 -0
- package/plugin/build/index.d.ts +9 -0
- package/plugin/build/index.js +54 -0
- package/plugin/build/ios.d.ts +4 -0
- package/plugin/build/ios.js +43 -0
- package/plugin/build/options.d.ts +93 -0
- package/plugin/build/options.js +140 -0
- package/react-native.config.js +11 -0
- package/src/audio.ts +221 -0
- package/src/components.tsx +233 -0
- package/src/create-client.ts +53 -0
- package/src/errors.ts +81 -0
- package/src/globals.ts +54 -0
- package/src/index.ts +95 -0
- package/src/local-media.ts +420 -0
- package/src/native-session.tsx +338 -0
- package/src/native.ts +84 -0
- package/src/permissions.ts +173 -0
- package/src/platform/android.ts +128 -0
- package/src/platform/common.ts +157 -0
- package/src/platform/devices.ts +148 -0
- package/src/platform/index.ts +22 -0
- package/src/platform/ios.ts +48 -0
- package/src/platform/streams.ts +285 -0
- package/src/platform/timers.ts +72 -0
- package/src/platform/visibility.ts +59 -0
- package/src/preflight.ts +60 -0
- package/src/provider.tsx +86 -0
- 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
|
+
}
|