@poolse/react-native-calls 0.1.3 → 0.2.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/android/build.gradle +7 -0
- package/android/consumer-rules.pro +11 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/dev/poolse/callcore/CallCoreRuntime.kt +245 -110
- package/android/src/main/java/dev/poolse/callcore/CallMachine.kt +113 -18
- package/android/src/main/java/dev/poolse/callcore/Ports.kt +17 -2
- package/android/src/main/java/dev/poolse/callcore/android/AndroidRingSurface.kt +51 -4
- package/android/src/main/java/dev/poolse/callcore/android/CallActionReceiver.kt +4 -3
- package/android/src/main/java/dev/poolse/callcore/android/CallCore.kt +253 -26
- package/android/src/main/java/dev/poolse/callcore/android/CallForegroundService.kt +228 -47
- package/android/src/main/java/dev/poolse/callcore/android/CallHttpClient.kt +97 -46
- package/android/src/main/java/dev/poolse/callcore/android/CallMessagingService.kt +59 -13
- package/android/src/main/java/dev/poolse/callcore/android/IncomingCallActivity.kt +100 -12
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +75 -17
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +190 -16
- package/android/src/test/java/dev/poolse/callcore/android/CallHttpClientTest.kt +61 -0
- package/dist/index.cjs +10 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +39 -2
|
@@ -2,9 +2,14 @@ package dev.poolse.callcore.android
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.content.Intent
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.os.PowerManager
|
|
8
|
+
import android.util.Log
|
|
5
9
|
import dev.poolse.callcore.CallCoreRuntime
|
|
6
10
|
import dev.poolse.callcore.MachineState
|
|
7
11
|
import dev.poolse.callcore.MediaPort
|
|
12
|
+
import dev.poolse.callcore.Phase
|
|
8
13
|
import dev.poolse.callcore.RingContext
|
|
9
14
|
import dev.poolse.callcore.SnapshotSink
|
|
10
15
|
|
|
@@ -16,40 +21,98 @@ import dev.poolse.callcore.SnapshotSink
|
|
|
16
21
|
* bridge — comes through here, so there is exactly one runtime and one
|
|
17
22
|
* answer/decline path on the device. Process-singleton, created lazily
|
|
18
23
|
* from the first caller's application context.
|
|
24
|
+
*
|
|
25
|
+
* It also owns what the pure machine deliberately does not carry but a
|
|
26
|
+
* truthful snapshot needs: the remote roster, the conversation name,
|
|
27
|
+
* self mute/camera, and the audio route. All of it lives on the
|
|
28
|
+
* runtime's single thread and is merged into every snapshot.
|
|
19
29
|
*/
|
|
20
30
|
class CallCore private constructor(context: Context) {
|
|
21
31
|
|
|
22
32
|
private val appContext = context.applicationContext
|
|
33
|
+
private val main = Handler(Looper.getMainLooper())
|
|
23
34
|
|
|
24
35
|
// The SFU is injected, not baked in: the core stays media-engine
|
|
25
36
|
// agnostic (CONTRACT.md — "LiveKit or any SFU"), and the library
|
|
26
37
|
// compiles without a specific media SDK on the classpath. The host
|
|
27
38
|
// supplies the factory; LiveKitMediaPort is the first implementation.
|
|
28
39
|
private val media: MediaPort = requireConfig().mediaFactory(appContext, mediaEvents())
|
|
29
|
-
|
|
40
|
+
|
|
41
|
+
// Surface + HTTP read the config on every use, so a later configure()
|
|
42
|
+
// (another API origin, icon, ringback) applies without a rebuild.
|
|
43
|
+
private val surface = AndroidRingSurface(appContext) {
|
|
44
|
+
peekConfig()?.ringSurface ?: RingSurfaceConfig(appContext.applicationInfo.icon, 0)
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
private val runtime = CallCoreRuntime(
|
|
31
|
-
http = CallHttpClient(
|
|
48
|
+
http = CallHttpClient({ peekConfig()?.apiBaseUrl ?: "" }),
|
|
32
49
|
media = media,
|
|
33
50
|
surface = surface,
|
|
34
51
|
session = CallForegroundService.also { it.attach(appContext) },
|
|
35
|
-
timers = HandlerTimerPort(),
|
|
36
|
-
sink = SnapshotSink {
|
|
52
|
+
timers = HandlerTimerPort(main),
|
|
53
|
+
sink = SnapshotSink { onState(it) },
|
|
54
|
+
// One thread for every input: the main looper — where LiveKit,
|
|
55
|
+
// the timers, the notification and the service lifecycle already
|
|
56
|
+
// live. Already on it? Run now; the runtime's trampoline still
|
|
57
|
+
// queues anything produced mid-dispatch.
|
|
58
|
+
executor = { r -> if (Looper.myLooper() == Looper.getMainLooper()) r.run() else main.post(r) },
|
|
59
|
+
onError = { Log.e(TAG, "call runtime task failed", it) },
|
|
37
60
|
)
|
|
38
61
|
|
|
62
|
+
// ── snapshot extras (runtime thread only) ───────────────────────────
|
|
63
|
+
|
|
64
|
+
private var extrasCallId: String? = null
|
|
65
|
+
private var extrasMedia: String? = null
|
|
66
|
+
private var roster: List<MediaParticipant> = emptyList()
|
|
67
|
+
private var conversationName: String? = null
|
|
68
|
+
private var mutedSelf = false
|
|
69
|
+
private var cameraOn = false
|
|
70
|
+
private var audioRoute: String? = null
|
|
71
|
+
|
|
72
|
+
/** Last snapshot emitted; read by the bridge's getCallState from any thread. */
|
|
73
|
+
@Volatile private var lastSnapshot: Map<String, Any?>? = null
|
|
74
|
+
|
|
75
|
+
private val proximity: PowerManager.WakeLock? = runCatching {
|
|
76
|
+
val pm = appContext.getSystemService(PowerManager::class.java)
|
|
77
|
+
if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
|
78
|
+
pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "poolse:call-proximity")
|
|
79
|
+
.apply { setReferenceCounted(false) }
|
|
80
|
+
} else null
|
|
81
|
+
}.getOrNull()
|
|
82
|
+
|
|
39
83
|
// ── native entry points ─────────────────────────────────────────────
|
|
40
84
|
|
|
41
85
|
/** A ring arrived — from FCM (closed app) or the socket (open app). */
|
|
42
86
|
fun onRing(ring: RingContext) = runtime.onRing(ring)
|
|
43
87
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The caller withdrew, or the ring resolved elsewhere. Beyond the
|
|
90
|
+
* machine (which only knows the ring it is showing), ALWAYS tear down
|
|
91
|
+
* any surface for this call: after a process restart the notification
|
|
92
|
+
* and full-screen activity outlive the in-memory state that would
|
|
93
|
+
* otherwise dismiss them.
|
|
94
|
+
*/
|
|
95
|
+
fun onRingEnded(callId: String, reason: String) {
|
|
96
|
+
runtime.onRingEnded(callId, reason)
|
|
97
|
+
runtime.post { tearDownRing(callId) }
|
|
98
|
+
}
|
|
46
99
|
|
|
47
|
-
|
|
100
|
+
/** The server finished the call (push `finished` or socket `call:finished`). */
|
|
101
|
+
fun onCallFinished(callId: String, reason: String? = null) {
|
|
102
|
+
runtime.onCallFinished(callId, reason)
|
|
103
|
+
runtime.post { tearDownRing(callId) }
|
|
104
|
+
}
|
|
48
105
|
|
|
49
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Answer from a native surface. [from] is the activity the tap
|
|
108
|
+
* landed in: the app must be brought forward from THAT context — a
|
|
109
|
+
* background start from the application context is blocked on
|
|
110
|
+
* Android 10+ and left audio connected behind the lock screen.
|
|
111
|
+
*/
|
|
112
|
+
fun onUserAnswer(ring: RingContext, from: Context? = null): Boolean {
|
|
50
113
|
runtime.onRing(ring) // idempotent: dedupes if already showing
|
|
51
114
|
runtime.answer()
|
|
52
|
-
bringAppToFront(ring)
|
|
115
|
+
return bringAppToFront(ring.callId, from)
|
|
53
116
|
}
|
|
54
117
|
|
|
55
118
|
fun onUserDecline(ring: RingContext) {
|
|
@@ -57,11 +120,16 @@ class CallCore private constructor(context: Context) {
|
|
|
57
120
|
runtime.decline()
|
|
58
121
|
}
|
|
59
122
|
|
|
60
|
-
fun onUserOpen(ring: RingContext) {
|
|
123
|
+
fun onUserOpen(ring: RingContext, from: Context? = null) {
|
|
61
124
|
runtime.onRing(ring)
|
|
62
|
-
bringAppToFront(ring)
|
|
125
|
+
bringAppToFront(ring.callId, from)
|
|
63
126
|
}
|
|
64
127
|
|
|
128
|
+
/** False once this ring is resolved here or past its deadline (+ clock slack). */
|
|
129
|
+
fun isRingLive(ring: RingContext): Boolean =
|
|
130
|
+
!runtime.isResolved(ring.callId) &&
|
|
131
|
+
ring.ringDeadlineEpochMs + CallCoreRuntime.CLOCK_SLACK_MS > System.currentTimeMillis()
|
|
132
|
+
|
|
65
133
|
// ── bridge-driven actions ───────────────────────────────────────────
|
|
66
134
|
|
|
67
135
|
fun answer() = runtime.answer()
|
|
@@ -71,46 +139,182 @@ class CallCore private constructor(context: Context) {
|
|
|
71
139
|
runtime.place(conversationId, media, sessionToken)
|
|
72
140
|
fun join(callId: String, conversationId: String?, sessionToken: String) =
|
|
73
141
|
runtime.join(callId, conversationId, sessionToken)
|
|
74
|
-
|
|
75
|
-
fun
|
|
142
|
+
|
|
143
|
+
fun setMuted(muted: Boolean) {
|
|
144
|
+
runtime.post { mutedSelf = muted }
|
|
145
|
+
runtime.setMuted(muted)
|
|
146
|
+
runtime.reemit()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fun setCamera(on: Boolean) {
|
|
150
|
+
runtime.post { cameraOn = on }
|
|
151
|
+
runtime.setCamera(on)
|
|
152
|
+
runtime.reemit()
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
fun switchCamera() = runtime.switchCamera()
|
|
77
156
|
|
|
78
|
-
fun
|
|
157
|
+
fun setSpeaker(on: Boolean) {
|
|
158
|
+
// Optimistic; the media adapter reports the route it actually got.
|
|
159
|
+
runtime.post { audioRoute = if (on) "speaker" else "earpiece" }
|
|
160
|
+
runtime.setSpeaker(on)
|
|
161
|
+
runtime.reemit()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fun allRingsResolved(outcome: String) = runtime.allRingsResolved(outcome)
|
|
79
165
|
|
|
80
|
-
|
|
81
|
-
@Volatile var snapshotListener: ((MachineState) -> Unit)? = null
|
|
166
|
+
fun state(): MachineState = runtime.currentState()
|
|
82
167
|
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
/** The contract Snapshot (`null` = idle), safe from any thread. */
|
|
169
|
+
fun snapshot(): Map<String, Any?>? = lastSnapshot
|
|
170
|
+
|
|
171
|
+
// ── snapshot assembly (runtime thread) ──────────────────────────────
|
|
172
|
+
|
|
173
|
+
private fun onState(state: MachineState) {
|
|
174
|
+
if (state.phase == Phase.IDLE) {
|
|
175
|
+
if (extrasCallId != null) {
|
|
176
|
+
// Idle: no call owns media any more. Clears the port's per-call
|
|
177
|
+
// mute/camera/speaker choices even when the call ended on a path
|
|
178
|
+
// with no MEDIA_LEAVE (e.g. refused while connecting).
|
|
179
|
+
media.leave()
|
|
180
|
+
}
|
|
181
|
+
resetExtras(null, null)
|
|
182
|
+
} else if (state.callId != extrasCallId) {
|
|
183
|
+
// `pending:<conv>` → the server-minted id is the same call.
|
|
184
|
+
if (extrasCallId?.startsWith("pending:") == true) extrasCallId = state.callId
|
|
185
|
+
else resetExtras(state.callId, state.media)
|
|
186
|
+
}
|
|
187
|
+
if (state.phase != Phase.IDLE && state.media != extrasMedia) {
|
|
188
|
+
// The server corrected the media (a join defaults to audio until
|
|
189
|
+
// its bundle says otherwise): re-derive what depends on it.
|
|
190
|
+
if (extrasMedia != null) {
|
|
191
|
+
cameraOn = state.media == "video"
|
|
192
|
+
audioRoute = null
|
|
193
|
+
CallForegroundService.updateCall(state.callId ?: "", state.media)
|
|
194
|
+
}
|
|
195
|
+
extrasMedia = state.media
|
|
196
|
+
}
|
|
197
|
+
runtime.conversationName?.let { conversationName = it }
|
|
198
|
+
|
|
199
|
+
val snap = state.toSnapshot(
|
|
200
|
+
conversationName = conversationName,
|
|
201
|
+
participants = roster.map {
|
|
202
|
+
mapOf(
|
|
203
|
+
"userId" to it.identity,
|
|
204
|
+
"externalId" to it.externalId,
|
|
205
|
+
"speaking" to it.speaking,
|
|
206
|
+
"muted" to it.muted,
|
|
207
|
+
"cameraOn" to it.cameraOn,
|
|
208
|
+
)
|
|
209
|
+
},
|
|
210
|
+
audioRoute = audioRoute ?: if (state.media == "video") "speaker" else "earpiece",
|
|
211
|
+
mutedSelf = mutedSelf,
|
|
212
|
+
cameraOn = cameraOn,
|
|
213
|
+
)
|
|
214
|
+
lastSnapshot = snap
|
|
215
|
+
reconcileAndroid(state)
|
|
216
|
+
runCatching { listener?.invoke(snap) }.onFailure { Log.w(TAG, "snapshot listener failed", it) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun resetExtras(callId: String?, media: String?) {
|
|
220
|
+
extrasCallId = callId
|
|
221
|
+
extrasMedia = media
|
|
222
|
+
roster = emptyList()
|
|
223
|
+
conversationName = null
|
|
224
|
+
mutedSelf = false
|
|
225
|
+
// Video calls publish the camera on join (LiveKitMediaPort).
|
|
226
|
+
cameraOn = media == "video"
|
|
227
|
+
audioRoute = null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* OS-level consequences of the state, applied after the FINAL state
|
|
232
|
+
* of each dispatch — so an answer's DISMISS_RING → CALL_SESSION_START
|
|
233
|
+
* hands the foreground service from ring to call without a gap.
|
|
234
|
+
*/
|
|
235
|
+
private fun reconcileAndroid(state: MachineState) {
|
|
236
|
+
// Keep the process alive while ringing (OEM freezers otherwise kill
|
|
237
|
+
// the ringtone and the deadline timer of a closed app).
|
|
238
|
+
if (state.phase == Phase.RINGING_IN) {
|
|
239
|
+
CallForegroundService.startRing(state.callId ?: "")
|
|
240
|
+
} else {
|
|
241
|
+
CallForegroundService.stopRing()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (state.phase == Phase.IDLE || state.phase == Phase.ENDED) surface.setCommunicationReady(false)
|
|
245
|
+
|
|
246
|
+
// Screen off at the ear: only for an audio call on the earpiece.
|
|
247
|
+
val inCall = state.phase == Phase.ACTIVE || state.phase == Phase.RECONNECTING ||
|
|
248
|
+
state.phase == Phase.RINGING_OUT || state.phase == Phase.CONNECTING
|
|
249
|
+
val earpiece = (audioRoute ?: if (state.media == "video") "speaker" else "earpiece") == "earpiece"
|
|
250
|
+
setProximity(inCall && state.media == "audio" && earpiece)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun setProximity(on: Boolean) {
|
|
254
|
+
val lock = proximity ?: return
|
|
255
|
+
runCatching {
|
|
256
|
+
if (on && !lock.isHeld) lock.acquire(4 * 60 * 60 * 1000L)
|
|
257
|
+
else if (!on && lock.isHeld) lock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY)
|
|
258
|
+
}.onFailure { Log.w(TAG, "proximity wake lock", it) }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun tearDownRing(callId: String) {
|
|
262
|
+
val s = runtime.currentState()
|
|
263
|
+
surface.dismiss(callId)
|
|
264
|
+
// Never silence a DIFFERENT call that is ringing right now.
|
|
265
|
+
if (!(s.phase == Phase.RINGING_IN && s.callId != callId)) surface.ringtoneStop()
|
|
266
|
+
if (s.phase != Phase.RINGING_IN) CallForegroundService.stopRing()
|
|
85
267
|
}
|
|
86
268
|
|
|
87
269
|
private fun mediaEvents() = object : MediaEvents {
|
|
88
|
-
override fun onConnected()
|
|
270
|
+
override fun onConnected() {
|
|
271
|
+
// The communication audio mode is set by now: ringback may play
|
|
272
|
+
// through the call route (earpiece), not the loudspeaker.
|
|
273
|
+
runtime.post { surface.setCommunicationReady(true) }
|
|
274
|
+
runtime.onMediaConnected()
|
|
275
|
+
}
|
|
89
276
|
override fun onFailed() = runtime.onMediaFailed()
|
|
90
277
|
override fun onLost() = runtime.onMediaLost()
|
|
278
|
+
override fun onReconnecting() = runtime.onMediaReconnecting()
|
|
91
279
|
override fun onRecovered() = runtime.onMediaRecovered()
|
|
92
280
|
override fun onRemoteJoined() = runtime.onRemoteJoined()
|
|
93
281
|
override fun onParticipants(participants: List<MediaParticipant>) {
|
|
94
|
-
|
|
95
|
-
|
|
282
|
+
runtime.post { roster = participants }
|
|
283
|
+
runtime.reemit()
|
|
284
|
+
}
|
|
285
|
+
override fun onAudioRoute(route: String) {
|
|
286
|
+
runtime.post { audioRoute = route }
|
|
287
|
+
runtime.reemit()
|
|
96
288
|
}
|
|
97
289
|
}
|
|
98
290
|
|
|
99
|
-
private fun bringAppToFront(
|
|
291
|
+
private fun bringAppToFront(callId: String, from: Context?): Boolean {
|
|
100
292
|
val launch = appContext.packageManager.getLaunchIntentForPackage(appContext.packageName)
|
|
101
|
-
?: return
|
|
293
|
+
?: return false
|
|
102
294
|
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
103
|
-
|
|
104
|
-
|
|
295
|
+
// The host's MainActivity reads this to show over the keyguard and
|
|
296
|
+
// dismiss it, and to route straight to the call screen.
|
|
297
|
+
launch.putExtra(EXTRA_LAUNCH_CALL, callId)
|
|
298
|
+
return runCatching { (from ?: appContext).startActivity(launch) }
|
|
299
|
+
.onFailure { Log.w(TAG, "could not bring the app forward", it) }
|
|
300
|
+
.isSuccess
|
|
105
301
|
}
|
|
106
302
|
|
|
107
303
|
companion object {
|
|
108
304
|
const val EXTRA_LAUNCH_CALL = "poolse_launch_call"
|
|
305
|
+
private const val TAG = "PoolseCallCore"
|
|
109
306
|
private const val PREFS = "dev.poolse.callcore"
|
|
110
307
|
|
|
111
308
|
@Volatile private var instance: CallCore? = null
|
|
112
309
|
@Volatile private var config: CallCoreConfig? = null
|
|
113
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The bridge's snapshot subscriber. Process-level (not per instance)
|
|
313
|
+
* so the JS side may subscribe before configure() — the core picks
|
|
314
|
+
* it up when it is created.
|
|
315
|
+
*/
|
|
316
|
+
@Volatile var listener: ((Map<String, Any?>?) -> Unit)? = null
|
|
317
|
+
|
|
114
318
|
/**
|
|
115
319
|
* Configure the core and PERSIST the inputs. A ring can wake a killed
|
|
116
320
|
* app: the OS spawns the process cold to run [CallMessagingService],
|
|
@@ -119,6 +323,10 @@ class CallCore private constructor(context: Context) {
|
|
|
119
323
|
* that cold start, so closed-app ringing works. The bridge calls this
|
|
120
324
|
* each time the app opens; the resource names (not the resolved ids)
|
|
121
325
|
* are stored, since ids are not stable across installs.
|
|
326
|
+
*
|
|
327
|
+
* Re-configuring a live core swaps the config in place: HTTP and the
|
|
328
|
+
* ring surface read it per use, so a new API origin takes effect on
|
|
329
|
+
* the next request.
|
|
122
330
|
*/
|
|
123
331
|
fun configure(context: Context, apiBaseUrl: String, smallIconName: String?, ringbackName: String?) {
|
|
124
332
|
val ctx = context.applicationContext
|
|
@@ -130,14 +338,22 @@ class CallCore private constructor(context: Context) {
|
|
|
130
338
|
config = build(ctx, apiBaseUrl, smallIconName, ringbackName)
|
|
131
339
|
}
|
|
132
340
|
|
|
341
|
+
/** The core; throws if never configured (not even persisted). Prefer [getOrNull]. */
|
|
133
342
|
fun get(context: Context): CallCore =
|
|
343
|
+
getOrNull(context) ?: error("CallCore.configure() must be called before use")
|
|
344
|
+
|
|
345
|
+
/** The core, or null if this install was never configured — callers no-op. */
|
|
346
|
+
fun getOrNull(context: Context): CallCore? =
|
|
134
347
|
instance ?: synchronized(this) {
|
|
135
348
|
instance ?: run {
|
|
136
349
|
if (config == null) restoreConfig(context.applicationContext)
|
|
137
|
-
CallCore(context).also { instance = it }
|
|
350
|
+
if (config == null) null else CallCore(context).also { instance = it }
|
|
138
351
|
}
|
|
139
352
|
}
|
|
140
353
|
|
|
354
|
+
/** The live core if one exists — never creates (cheap reads, e.g. getCallState). */
|
|
355
|
+
fun peek(): CallCore? = instance
|
|
356
|
+
|
|
141
357
|
internal fun peekConfig(): CallCoreConfig? = config
|
|
142
358
|
|
|
143
359
|
internal fun requireConfig(): CallCoreConfig =
|
|
@@ -196,6 +412,7 @@ class CallCore private constructor(context: Context) {
|
|
|
196
412
|
override fun setMuted(muted: Boolean) {}
|
|
197
413
|
override fun setCamera(on: Boolean) {}
|
|
198
414
|
override fun switchCamera() {}
|
|
415
|
+
override fun setSpeaker(on: Boolean) {}
|
|
199
416
|
}
|
|
200
417
|
}
|
|
201
418
|
}
|
|
@@ -223,14 +440,24 @@ data class CallCoreConfig(
|
|
|
223
440
|
interface MediaEvents {
|
|
224
441
|
fun onConnected()
|
|
225
442
|
fun onFailed()
|
|
443
|
+
/** Unexpected disconnect — the SFU gave up; the core asks the server and rejoins. */
|
|
226
444
|
fun onLost()
|
|
445
|
+
/** The SFU is resuming the session itself; only reflected in the UI. */
|
|
446
|
+
fun onReconnecting()
|
|
227
447
|
fun onRecovered()
|
|
228
448
|
fun onRemoteJoined()
|
|
449
|
+
/** The REMOTE roster (the local participant excluded). */
|
|
229
450
|
fun onParticipants(participants: List<MediaParticipant>)
|
|
451
|
+
/** `earpiece` | `speaker` | `bluetooth` | `headset`. */
|
|
452
|
+
fun onAudioRoute(route: String)
|
|
230
453
|
}
|
|
231
454
|
|
|
232
455
|
data class MediaParticipant(
|
|
456
|
+
/** The SFU identity — the poolse user id. */
|
|
233
457
|
val identity: String,
|
|
234
458
|
val speaking: Boolean,
|
|
235
459
|
val muted: Boolean,
|
|
460
|
+
val cameraOn: Boolean = false,
|
|
461
|
+
/** The host app's own id for the user, when the SFU carries it; else the identity. */
|
|
462
|
+
val externalId: String = identity,
|
|
236
463
|
)
|