@fishjam-cloud/react-native-client 0.2.1 → 0.2.2

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 (24) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/io/fishjam/reactnative/AudioDeviceKind.kt +34 -0
  4. package/android/src/main/java/io/fishjam/reactnative/AudioSwitchManager.kt +61 -0
  5. package/android/src/main/java/io/fishjam/reactnative/EmitableEvents.kt +14 -0
  6. package/android/src/main/java/io/fishjam/reactnative/Errors.kt +31 -0
  7. package/android/src/main/java/io/fishjam/reactnative/FishjamForegroundService.kt +90 -0
  8. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +870 -0
  9. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClientModule.kt +370 -0
  10. package/android/src/main/java/io/fishjam/reactnative/Utils.kt +11 -0
  11. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +40 -0
  12. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewViewModule.kt +17 -0
  13. package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +58 -0
  14. package/android/src/main/java/io/fishjam/reactnative/VideoRendererViewModule.kt +25 -0
  15. package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +77 -0
  16. package/package.json +4 -4
  17. package/plugin/build/types.d.ts +9 -0
  18. package/plugin/build/types.js +2 -0
  19. package/plugin/build/withFishjam.d.ts +4 -0
  20. package/plugin/build/withFishjam.js +13 -0
  21. package/plugin/build/withFishjamAndroid.d.ts +3 -0
  22. package/plugin/build/withFishjamAndroid.js +32 -0
  23. package/plugin/build/withFishjamIos.d.ts +9 -0
  24. package/plugin/build/withFishjamIos.js +222 -0
@@ -0,0 +1,870 @@
1
+ package io.fishjam.reactnative
2
+
3
+ import android.app.Activity
4
+ import android.content.ActivityNotFoundException
5
+ import android.content.Intent
6
+ import android.media.projection.MediaProjectionManager
7
+ import androidx.appcompat.app.AppCompatActivity
8
+ import com.fishjamcloud.client.FishjamClient
9
+ import com.fishjamcloud.client.FishjamClientListener
10
+ import com.fishjamcloud.client.media.LocalAudioTrack
11
+ import com.fishjamcloud.client.media.LocalScreencastTrack
12
+ import com.fishjamcloud.client.media.LocalVideoTrack
13
+ import com.fishjamcloud.client.media.RemoteAudioTrack
14
+ import com.fishjamcloud.client.media.RemoteVideoTrack
15
+ import com.fishjamcloud.client.media.Track
16
+ import com.fishjamcloud.client.models.AuthError
17
+ import com.fishjamcloud.client.models.Endpoint
18
+ import com.fishjamcloud.client.models.Metadata
19
+ import com.fishjamcloud.client.models.Peer
20
+ import com.fishjamcloud.client.models.RTCInboundStats
21
+ import com.fishjamcloud.client.models.RTCOutboundStats
22
+ import com.fishjamcloud.client.models.ReconnectConfig
23
+ import com.fishjamcloud.client.models.SimulcastConfig
24
+ import com.fishjamcloud.client.models.TrackBandwidthLimit
25
+ import com.fishjamcloud.client.models.VideoParameters
26
+ import com.twilio.audioswitch.AudioDevice
27
+ import expo.modules.kotlin.AppContext
28
+ import expo.modules.kotlin.Promise
29
+ import expo.modules.kotlin.exception.CodedException
30
+ import kotlinx.coroutines.CoroutineScope
31
+ import kotlinx.coroutines.Dispatchers
32
+ import kotlinx.coroutines.SupervisorJob
33
+ import kotlinx.coroutines.launch
34
+ import org.webrtc.Logging
35
+
36
+ class RNFishjamClient(
37
+ val sendEvent: (name: String, data: Map<String, Any?>) -> Unit
38
+ ) : FishjamClientListener {
39
+ private val SCREENCAST_REQUEST = 1
40
+
41
+ var isMicrophoneOn = false
42
+ var isCameraOn = false
43
+ var isScreencastOn = false
44
+ var isConnected = false
45
+
46
+ private var connectPromise: Promise? = null
47
+ private var screencastPermissionPromise: Promise? = null
48
+
49
+ var videoSimulcastConfig: SimulcastConfig = SimulcastConfig()
50
+ private var localUserMetadata: Metadata = mutableMapOf()
51
+
52
+ var screencastQuality: String? = null
53
+ var screencastSimulcastConfig: SimulcastConfig = SimulcastConfig()
54
+ var screencastMaxBandwidth: TrackBandwidthLimit = TrackBandwidthLimit.BandwidthLimit(0)
55
+
56
+ var screencastMetadata: Map<String, Any> = mutableMapOf()
57
+
58
+ private var mediaProjectionIntent: Intent? = null
59
+
60
+ var audioSwitchManager: AudioSwitchManager? = null
61
+
62
+ var appContext: AppContext? = null
63
+
64
+ interface OnTrackUpdateListener {
65
+ fun onTracksUpdate()
66
+ }
67
+
68
+ interface OnLocalTrackSwitchListener {
69
+ suspend fun onLocalTrackWillSwitch()
70
+
71
+ suspend fun onLocalTrackSwitched()
72
+ }
73
+
74
+ companion object {
75
+ var onTracksUpdateListeners: MutableList<OnTrackUpdateListener> = mutableListOf()
76
+ val onLocalTrackSwitchListener: MutableList<OnLocalTrackSwitchListener> = mutableListOf()
77
+ lateinit var fishjamClient: FishjamClient
78
+
79
+ fun getAllPeers(): List<Peer> {
80
+ val listOfPeers = fishjamClient.getRemotePeers().toMutableList()
81
+ val localEndpoint = fishjamClient.getLocalEndpoint()
82
+ listOfPeers.add(localEndpoint)
83
+ return listOfPeers
84
+ }
85
+ }
86
+
87
+ fun onModuleCreate(appContext: AppContext) {
88
+ this.appContext = appContext
89
+ this.audioSwitchManager = AudioSwitchManager(appContext.reactContext!!)
90
+ create()
91
+ }
92
+
93
+ fun onModuleDestroy() {
94
+ audioSwitchManager?.stop()
95
+ }
96
+
97
+ private val coroutineScope: CoroutineScope =
98
+ CoroutineScope(SupervisorJob() + Dispatchers.Default)
99
+
100
+ fun onActivityResult(
101
+ requestCode: Int,
102
+ resultCode: Int,
103
+ data: Intent?
104
+ ) {
105
+ coroutineScope.launch(Dispatchers.Main) {
106
+ if (requestCode != SCREENCAST_REQUEST) return@launch
107
+ if (resultCode != Activity.RESULT_OK) {
108
+ screencastPermissionPromise?.resolve("denied")
109
+ screencastPermissionPromise = null
110
+ return@launch
111
+ }
112
+
113
+ mediaProjectionIntent = data
114
+ screencastPermissionPromise?.resolve("granted")
115
+ screencastPermissionPromise = null
116
+ }
117
+ }
118
+
119
+ private fun getSimulcastConfigFromOptions(simulcastConfigMap: io.fishjam.reactnative.SimulcastConfig): SimulcastConfig {
120
+ val simulcastEnabled = simulcastConfigMap.enabled
121
+ val activeEncodings = simulcastConfigMap.activeEncodings.map { e -> e.toTrackEncoding() }
122
+ return SimulcastConfig(
123
+ enabled = simulcastEnabled,
124
+ activeEncodings = activeEncodings
125
+ )
126
+ }
127
+
128
+ private fun getMaxBandwidthFromOptions(
129
+ maxBandwidthMap: Map<String, Int>?,
130
+ maxBandwidthInt: Int
131
+ ): TrackBandwidthLimit {
132
+ if (maxBandwidthMap != null) {
133
+ val maxBandwidthSimulcast = mutableMapOf<String, TrackBandwidthLimit.BandwidthLimit>()
134
+ maxBandwidthMap.forEach {
135
+ maxBandwidthSimulcast[it.key] = TrackBandwidthLimit.BandwidthLimit(it.value)
136
+ }
137
+ return TrackBandwidthLimit.SimulcastBandwidthLimit(maxBandwidthSimulcast)
138
+ }
139
+ return TrackBandwidthLimit.BandwidthLimit(maxBandwidthInt)
140
+ }
141
+
142
+ private fun create() {
143
+ audioSwitchManager = AudioSwitchManager(appContext?.reactContext!!)
144
+ fishjamClient =
145
+ FishjamClient(
146
+ appContext = appContext?.reactContext!!,
147
+ listener = this
148
+ )
149
+ }
150
+
151
+ private fun getVideoParametersFromOptions(createOptions: CameraConfig): VideoParameters {
152
+ val videoMaxBandwidth =
153
+ getMaxBandwidthFromOptions(createOptions.maxBandwidthMap, createOptions.maxBandwidthInt)
154
+ var videoParameters =
155
+ when (createOptions.quality) {
156
+ "QVGA169" -> VideoParameters.presetQVGA169
157
+ "VGA169" -> VideoParameters.presetVGA169
158
+ "QHD169" -> VideoParameters.presetQHD169
159
+ "HD169" -> VideoParameters.presetHD169
160
+ "FHD169" -> VideoParameters.presetFHD169
161
+ "QVGA43" -> VideoParameters.presetQVGA43
162
+ "VGA43" -> VideoParameters.presetVGA43
163
+ "QHD43" -> VideoParameters.presetQHD43
164
+ "HD43" -> VideoParameters.presetHD43
165
+ "FHD43" -> VideoParameters.presetFHD43
166
+ else -> VideoParameters.presetVGA169
167
+ }
168
+ videoParameters =
169
+ videoParameters.copy(
170
+ dimensions = if (createOptions.flipVideo) videoParameters.dimensions.flip() else videoParameters.dimensions,
171
+ simulcastConfig = getSimulcastConfigFromOptions(createOptions.simulcastConfig),
172
+ maxBitrate = videoMaxBandwidth
173
+ )
174
+ return videoParameters
175
+ }
176
+
177
+ private fun getLocalVideoTrack(): LocalVideoTrack? =
178
+ fishjamClient
179
+ .getLocalEndpoint()
180
+ .tracks
181
+ .values
182
+ .filterIsInstance<LocalVideoTrack>()
183
+ .firstOrNull()
184
+
185
+ private fun getLocalAudioTrack(): LocalAudioTrack? =
186
+ fishjamClient
187
+ .getLocalEndpoint()
188
+ .tracks
189
+ .values
190
+ .filterIsInstance<LocalAudioTrack>()
191
+ .firstOrNull()
192
+
193
+ private fun getLocalScreencastTrack(): LocalScreencastTrack? =
194
+ fishjamClient
195
+ .getLocalEndpoint()
196
+ .tracks
197
+ .values
198
+ .filterIsInstance<LocalScreencastTrack>()
199
+ .firstOrNull()
200
+
201
+ private fun ensureConnected() {
202
+ if (!isConnected) {
203
+ throw ClientNotConnectedError()
204
+ }
205
+ }
206
+
207
+ private fun ensureVideoTrack() {
208
+ if (getLocalVideoTrack() == null) {
209
+ throw NoLocalVideoTrackError()
210
+ }
211
+ }
212
+
213
+ private fun ensureAudioTrack() {
214
+ if (getLocalAudioTrack() == null) {
215
+ throw NoLocalAudioTrackError()
216
+ }
217
+ }
218
+
219
+ private fun ensureScreencastTrack() {
220
+ if (getLocalScreencastTrack() == null) {
221
+ throw NoScreencastTrackError()
222
+ }
223
+ }
224
+
225
+ override fun onAuthError(reason: AuthError) {
226
+ CoroutineScope(Dispatchers.Main).launch {
227
+ connectPromise?.reject(ConnectionError(reason))
228
+ connectPromise = null
229
+ }
230
+ }
231
+
232
+ fun joinRoom(
233
+ url: String,
234
+ participantToken: String,
235
+ participantMetadata: Map<String, Any>,
236
+ config: ConnectConfig,
237
+ promise: Promise
238
+ ) {
239
+ connectPromise = promise
240
+ localUserMetadata = participantMetadata
241
+ fishjamClient.connect(
242
+ com.fishjamcloud.client.ConnectConfig(
243
+ url,
244
+ participantToken,
245
+ participantMetadata,
246
+ ReconnectConfig(
247
+ config.reconnectConfig.maxAttempts,
248
+ config.reconnectConfig.initialDelayMs,
249
+ config.reconnectConfig.delayMs
250
+ )
251
+ )
252
+ )
253
+ }
254
+
255
+ fun leaveRoom() {
256
+ if (isScreencastOn) {
257
+ stopScreencast()
258
+ }
259
+ isMicrophoneOn = false
260
+ isCameraOn = false
261
+ isScreencastOn = false
262
+ isConnected = false
263
+ fishjamClient.leave()
264
+ }
265
+
266
+ suspend fun startCamera(config: CameraConfig) {
267
+ val cameraTrack = createCameraTrack(config)
268
+ setCameraTrackState(cameraTrack, config.cameraEnabled)
269
+ emitEndpoints()
270
+ }
271
+
272
+ private suspend fun createCameraTrack(config: CameraConfig): LocalVideoTrack {
273
+ val videoParameters = getVideoParametersFromOptions(config)
274
+ videoSimulcastConfig = getSimulcastConfigFromOptions(config.simulcastConfig)
275
+ return fishjamClient.createVideoTrack(
276
+ videoParameters,
277
+ config.videoTrackMetadata,
278
+ config.cameraId
279
+ )
280
+ }
281
+
282
+ private fun setCameraTrackState(
283
+ cameraTrack: LocalVideoTrack,
284
+ isEnabled: Boolean
285
+ ) {
286
+ cameraTrack.setEnabled(isEnabled)
287
+ isCameraOn = isEnabled
288
+ val eventName = EmitableEvents.IsCameraOn
289
+ val isCameraOnMap = mapOf(eventName to isEnabled)
290
+ emitEvent(eventName, isCameraOnMap)
291
+ }
292
+
293
+ fun toggleCamera(): Boolean {
294
+ ensureVideoTrack()
295
+ getLocalVideoTrack()?.let { setCameraTrackState(it, !isCameraOn) }
296
+ return isCameraOn
297
+ }
298
+
299
+ suspend fun flipCamera() {
300
+ ensureVideoTrack()
301
+ onLocalTrackSwitchListener.forEach { it.onLocalTrackWillSwitch() }
302
+ getLocalVideoTrack()?.flipCamera()
303
+ onLocalTrackSwitchListener.forEach { it.onLocalTrackSwitched() }
304
+ }
305
+
306
+ suspend fun switchCamera(cameraId: String) {
307
+ ensureVideoTrack()
308
+ onLocalTrackSwitchListener.forEach { it.onLocalTrackWillSwitch() }
309
+ getLocalVideoTrack()?.switchCamera(cameraId)
310
+ onLocalTrackSwitchListener.forEach {
311
+ it.onLocalTrackSwitched()
312
+ }
313
+ }
314
+
315
+ private suspend fun startMicrophone() {
316
+ val microphoneTrack = fishjamClient.createAudioTrack(emptyMap())
317
+ setMicrophoneTrackState(microphoneTrack, true)
318
+ emitEndpoints()
319
+ }
320
+
321
+ private fun setMicrophoneTrackState(
322
+ microphoneTrack: LocalAudioTrack,
323
+ isEnabled: Boolean
324
+ ) {
325
+ microphoneTrack.setEnabled(isEnabled)
326
+ isMicrophoneOn = isEnabled
327
+ val eventName = EmitableEvents.IsMicrophoneOn
328
+ val isMicrophoneOnMap = mapOf(eventName to isEnabled)
329
+ emitEvent(eventName, isMicrophoneOnMap)
330
+ }
331
+
332
+ suspend fun toggleMicrophone(): Boolean {
333
+ if (getLocalAudioTrack() == null) {
334
+ startMicrophone()
335
+ } else {
336
+ getLocalAudioTrack()?.let { setMicrophoneTrackState(it, !isMicrophoneOn) }
337
+ }
338
+ return isMicrophoneOn
339
+ }
340
+
341
+ fun handleScreencastPermission(promise: Promise) {
342
+ screencastPermissionPromise = promise
343
+ if (!isScreencastOn) {
344
+ ensureConnected()
345
+ val currentActivity = appContext?.currentActivity ?: throw ActivityNotFoundException()
346
+
347
+ val mediaProjectionManager =
348
+ appContext?.reactContext!!.getSystemService(
349
+ AppCompatActivity.MEDIA_PROJECTION_SERVICE
350
+ ) as MediaProjectionManager
351
+ val intent = mediaProjectionManager.createScreenCaptureIntent()
352
+ currentActivity.startActivityForResult(intent, SCREENCAST_REQUEST)
353
+ }
354
+ }
355
+
356
+ suspend fun toggleScreencast(screencastOptions: ScreencastOptions) {
357
+ this.screencastMetadata = screencastOptions.screencastMetadata
358
+ this.screencastQuality = screencastOptions.quality
359
+ this.screencastSimulcastConfig =
360
+ getSimulcastConfigFromOptions(screencastOptions.simulcastConfig)
361
+ this.screencastMaxBandwidth =
362
+ getMaxBandwidthFromOptions(
363
+ screencastOptions.maxBandwidthMap,
364
+ screencastOptions.maxBandwidthInt
365
+ )
366
+ if (!isScreencastOn) {
367
+ ensureConnected()
368
+ startScreencast()
369
+ } else {
370
+ stopScreencast()
371
+ }
372
+ }
373
+
374
+ fun getPeers(): List<Map<String, Any?>> =
375
+ getAllPeers().map { endpoint ->
376
+ mapOf(
377
+ "id" to endpoint.id,
378
+ "isLocal" to (endpoint.id == fishjamClient.getLocalEndpoint().id),
379
+ "type" to endpoint.type,
380
+ "metadata" to endpoint.metadata,
381
+ "tracks" to
382
+ endpoint.tracks.values.mapNotNull { track ->
383
+ when (track) {
384
+ is RemoteVideoTrack ->
385
+ mapOf(
386
+ "id" to track.id(),
387
+ "type" to "Video",
388
+ "metadata" to track.metadata,
389
+ "encoding" to track.encoding?.rid,
390
+ "encodingReason" to track.encodingReason?.value
391
+ )
392
+
393
+ is RemoteAudioTrack ->
394
+ mapOf(
395
+ "id" to track.id(),
396
+ "type" to "Audio",
397
+ "metadata" to track.metadata,
398
+ "vadStatus" to track.vadStatus.value
399
+ )
400
+
401
+ is LocalVideoTrack ->
402
+ mapOf(
403
+ "id" to track.id(),
404
+ "type" to "Video",
405
+ "metadata" to track.metadata
406
+ )
407
+
408
+ is LocalScreencastTrack ->
409
+ mapOf(
410
+ "id" to track.id(),
411
+ "type" to "Video",
412
+ "metadata" to track.metadata
413
+ )
414
+
415
+ is LocalAudioTrack ->
416
+ mapOf(
417
+ "id" to track.id(),
418
+ "type" to "Audio",
419
+ "metadata" to track.metadata
420
+ )
421
+
422
+ else -> {
423
+ null
424
+ }
425
+ }
426
+ }
427
+ )
428
+ }
429
+
430
+ fun getCaptureDevices(): List<Map<String, Any>> {
431
+ val devices = LocalVideoTrack.getCaptureDevices(appContext?.reactContext!!)
432
+ return devices.map { device ->
433
+ mapOf<String, Any>(
434
+ "id" to device.deviceName,
435
+ "name" to device.deviceName,
436
+ "facingDirection" to
437
+ when (true) {
438
+ device.isFrontFacing -> "front"
439
+ device.isBackFacing -> "back"
440
+ else -> "unspecified"
441
+ }
442
+ )
443
+ }
444
+ }
445
+
446
+ fun updatePeerMetadata(metadata: Metadata) {
447
+ ensureConnected()
448
+ fishjamClient.updatePeerMetadata(metadata)
449
+ }
450
+
451
+ private fun updateTrackMetadata(
452
+ trackId: String,
453
+ metadata: Metadata
454
+ ) {
455
+ fishjamClient.updateTrackMetadata(trackId, metadata)
456
+ emitEndpoints()
457
+ }
458
+
459
+ fun updateLocalVideoTrackMetadata(metadata: Metadata) {
460
+ ensureVideoTrack()
461
+ getLocalVideoTrack()?.let {
462
+ updateTrackMetadata(it.id(), metadata)
463
+ }
464
+ }
465
+
466
+ fun updateLocalAudioTrackMetadata(metadata: Metadata) {
467
+ ensureAudioTrack()
468
+ getLocalAudioTrack()?.let {
469
+ updateTrackMetadata(it.id(), metadata)
470
+ }
471
+ }
472
+
473
+ fun updateLocalScreencastTrackMetadata(metadata: Metadata) {
474
+ ensureScreencastTrack()
475
+ getLocalScreencastTrack()?.let {
476
+ updateTrackMetadata(it.id(), metadata)
477
+ }
478
+ }
479
+
480
+ fun setOutputAudioDevice(audioDevice: String) {
481
+ audioSwitchManager?.selectAudioOutput(AudioDeviceKind.fromTypeName(audioDevice))
482
+ }
483
+
484
+ fun startAudioSwitcher() {
485
+ audioSwitchManager?.let {
486
+ it.start(this::emitAudioDeviceEvent)
487
+ emitAudioDeviceEvent(
488
+ it.availableAudioDevices(),
489
+ it.selectedAudioDevice()
490
+ )
491
+ }
492
+ }
493
+
494
+ fun stopAudioSwitcher() {
495
+ audioSwitchManager?.stop()
496
+ }
497
+
498
+ private fun toggleTrackEncoding(
499
+ encoding: String,
500
+ trackId: String,
501
+ simulcastConfig: SimulcastConfig
502
+ ): SimulcastConfig {
503
+ val trackEncoding = encoding.toTrackEncoding()
504
+
505
+ val isTrackEncodingActive = simulcastConfig.activeEncodings.contains(trackEncoding)
506
+
507
+ if (isTrackEncodingActive) {
508
+ fishjamClient.disableTrackEncoding(trackId, trackEncoding)
509
+ } else {
510
+ fishjamClient.enableTrackEncoding(trackId, trackEncoding)
511
+ }
512
+
513
+ val updatedActiveEncodings =
514
+ if (isTrackEncodingActive) {
515
+ simulcastConfig.activeEncodings.filter { it != trackEncoding }
516
+ } else {
517
+ simulcastConfig.activeEncodings + trackEncoding
518
+ }
519
+
520
+ return SimulcastConfig(
521
+ enabled = true,
522
+ activeEncodings = updatedActiveEncodings
523
+ )
524
+ }
525
+
526
+ fun toggleScreencastTrackEncoding(encoding: String): Map<String, Any> {
527
+ ensureScreencastTrack()
528
+ getLocalScreencastTrack()?.let {
529
+ screencastSimulcastConfig = toggleTrackEncoding(encoding, it.id(), screencastSimulcastConfig)
530
+ }
531
+ return getSimulcastConfigAsRNMap(screencastSimulcastConfig)
532
+ }
533
+
534
+ fun setScreencastTrackBandwidth(bandwidth: Int) {
535
+ ensureScreencastTrack()
536
+ getLocalScreencastTrack()?.let {
537
+ fishjamClient.setTrackBandwidth(it.id(), TrackBandwidthLimit.BandwidthLimit(bandwidth))
538
+ }
539
+ }
540
+
541
+ fun setScreencastTrackEncodingBandwidth(
542
+ encoding: String,
543
+ bandwidth: Int
544
+ ) {
545
+ ensureScreencastTrack()
546
+ getLocalScreencastTrack()?.let {
547
+ fishjamClient.setEncodingBandwidth(
548
+ it.id(),
549
+ encoding,
550
+ TrackBandwidthLimit.BandwidthLimit(bandwidth)
551
+ )
552
+ }
553
+ }
554
+
555
+ fun setTargetTrackEncoding(
556
+ trackId: String,
557
+ encoding: String
558
+ ) {
559
+ ensureConnected()
560
+ fishjamClient.setTargetTrackEncoding(trackId, encoding.toTrackEncoding())
561
+ }
562
+
563
+ fun toggleVideoTrackEncoding(encoding: String): Map<String, Any> {
564
+ ensureVideoTrack()
565
+ val trackId = getLocalVideoTrack()?.id() ?: return emptyMap()
566
+ videoSimulcastConfig = toggleTrackEncoding(encoding, trackId, videoSimulcastConfig)
567
+ val eventName = EmitableEvents.SimulcastConfigUpdate
568
+ emitEvent(eventName, getSimulcastConfigAsRNMap(videoSimulcastConfig))
569
+ return getSimulcastConfigAsRNMap(videoSimulcastConfig)
570
+ }
571
+
572
+ fun setVideoTrackEncodingBandwidth(
573
+ encoding: String,
574
+ bandwidth: Int
575
+ ) {
576
+ ensureVideoTrack()
577
+ getLocalVideoTrack()?.let {
578
+ fishjamClient.setEncodingBandwidth(
579
+ it.id(),
580
+ encoding,
581
+ TrackBandwidthLimit.BandwidthLimit(bandwidth)
582
+ )
583
+ }
584
+ }
585
+
586
+ fun setVideoTrackBandwidth(bandwidth: Int) {
587
+ ensureVideoTrack()
588
+ getLocalVideoTrack()?.let {
589
+ fishjamClient.setTrackBandwidth(it.id(), TrackBandwidthLimit.BandwidthLimit(bandwidth))
590
+ }
591
+ }
592
+
593
+ fun changeWebRTCLoggingSeverity(severity: String) {
594
+ when (severity) {
595
+ "verbose" -> fishjamClient.changeWebRTCLoggingSeverity(Logging.Severity.LS_VERBOSE)
596
+
597
+ "info" -> fishjamClient.changeWebRTCLoggingSeverity(Logging.Severity.LS_INFO)
598
+
599
+ "error" -> fishjamClient.changeWebRTCLoggingSeverity(Logging.Severity.LS_ERROR)
600
+
601
+ "warning" -> fishjamClient.changeWebRTCLoggingSeverity(Logging.Severity.LS_WARNING)
602
+
603
+ "none" -> fishjamClient.changeWebRTCLoggingSeverity(Logging.Severity.LS_NONE)
604
+
605
+ else -> {
606
+ throw CodedException("Severity with name=$severity not found")
607
+ }
608
+ }
609
+ }
610
+
611
+ private fun rtcOutboundStatsToRNMap(stats: RTCOutboundStats): Map<String, Any?> {
612
+ val innerMap = mutableMapOf<String, Double>()
613
+ innerMap["bandwidth"] = stats.qualityLimitationDurations?.bandwidth ?: 0.0
614
+ innerMap["cpu"] = stats.qualityLimitationDurations?.cpu ?: 0.0
615
+ innerMap["none"] = stats.qualityLimitationDurations?.none ?: 0.0
616
+ innerMap["other"] = stats.qualityLimitationDurations?.other ?: 0.0
617
+
618
+ val res = mutableMapOf<String, Any?>()
619
+ res["kind"] = stats.kind
620
+ res["rid"] = stats.rid
621
+ res["bytesSent"] = stats.bytesSent?.toInt() ?: 0
622
+ res["targetBitrate"] = stats.targetBitrate ?: 0.0
623
+ res["packetsSent"] = stats.packetsSent?.toInt() ?: 0
624
+ res["framesEncoded"] = stats.framesEncoded?.toInt() ?: 0
625
+ res["framesPerSecond"] = stats.framesPerSecond ?: 0.0
626
+ res["frameWidth"] = stats.frameWidth?.toInt() ?: 0
627
+ res["frameHeight"] = stats.frameHeight?.toInt() ?: 0
628
+ res["qualityLimitationDurations"] = innerMap
629
+
630
+ return res
631
+ }
632
+
633
+ private fun rtcInboundStatsToRNMap(stats: RTCInboundStats): Map<String, Any?> {
634
+ val res = mutableMapOf<String, Any?>()
635
+ res["kind"] = stats.kind
636
+ res["jitter"] = stats.jitter ?: 0.0
637
+ res["packetsLost"] = stats.packetsLost ?: 0
638
+ res["packetsReceived"] = stats.packetsReceived?.toInt() ?: 0
639
+ res["bytesReceived"] = stats.bytesReceived?.toInt() ?: 0
640
+ res["framesReceived"] = stats.framesReceived ?: 0
641
+ res["frameWidth"] = stats.frameWidth?.toInt() ?: 0
642
+ res["frameHeight"] = stats.frameHeight?.toInt() ?: 0
643
+ res["framesPerSecond"] = stats.framesPerSecond ?: 0.0
644
+ res["framesDropped"] = stats.framesDropped?.toInt() ?: 0
645
+
646
+ return res
647
+ }
648
+
649
+ fun getStatistics(): MutableMap<String, Map<String, Any?>> {
650
+ val newMap = mutableMapOf<String, Map<String, Any?>>()
651
+ fishjamClient.getStats().forEach { entry ->
652
+ newMap[entry.key] =
653
+ if (entry.value is RTCInboundStats) {
654
+ rtcInboundStatsToRNMap(
655
+ entry.value as RTCInboundStats
656
+ )
657
+ } else {
658
+ rtcOutboundStatsToRNMap(entry.value as RTCOutboundStats)
659
+ }
660
+ }
661
+ return newMap
662
+ }
663
+
664
+ private suspend fun startScreencast() {
665
+ val videoParameters = getScreencastVideoParameters()
666
+ if (mediaProjectionIntent == null) {
667
+ throw MissingScreencastPermission()
668
+ }
669
+ fishjamClient.createScreencastTrack(
670
+ mediaProjectionIntent!!,
671
+ videoParameters,
672
+ screencastMetadata
673
+ )
674
+ mediaProjectionIntent = null
675
+
676
+ setScreencastTrackState(true)
677
+ emitEndpoints()
678
+ }
679
+
680
+ private fun getScreencastVideoParameters(): VideoParameters {
681
+ val videoParameters =
682
+ when (screencastQuality) {
683
+ "VGA" -> VideoParameters.presetScreenShareVGA
684
+ "HD5" -> VideoParameters.presetScreenShareHD5
685
+ "HD15" -> VideoParameters.presetScreenShareHD15
686
+ "FHD15" -> VideoParameters.presetScreenShareFHD15
687
+ "FHD30" -> VideoParameters.presetScreenShareFHD30
688
+ else -> VideoParameters.presetScreenShareHD15
689
+ }
690
+ val dimensions = videoParameters.dimensions.flip()
691
+ return videoParameters.copy(
692
+ dimensions = dimensions,
693
+ simulcastConfig = screencastSimulcastConfig,
694
+ maxBitrate = screencastMaxBandwidth
695
+ )
696
+ }
697
+
698
+ private fun setScreencastTrackState(isEnabled: Boolean) {
699
+ isScreencastOn = isEnabled
700
+ val eventName = EmitableEvents.IsScreencastOn
701
+ emitEvent(eventName, mapOf(eventName to isEnabled))
702
+ }
703
+
704
+ private fun stopScreencast() {
705
+ ensureScreencastTrack()
706
+ coroutineScope.launch {
707
+ val screencastTrack =
708
+ fishjamClient.getLocalEndpoint().tracks.values.first { track ->
709
+ track is LocalScreencastTrack
710
+ } as? LocalScreencastTrack
711
+ if (screencastTrack != null) {
712
+ fishjamClient.removeTrack(screencastTrack.id())
713
+ }
714
+ setScreencastTrackState(false)
715
+ emitEndpoints()
716
+ }
717
+ }
718
+
719
+ private fun emitEvent(
720
+ eventName: String,
721
+ data: Map<String, Any?>
722
+ ) {
723
+ sendEvent(eventName, data)
724
+ }
725
+
726
+ private fun emitEndpoints() {
727
+ val eventName = EmitableEvents.PeersUpdate
728
+ val map = mapOf(eventName to getPeers())
729
+ emitEvent(eventName, map)
730
+ }
731
+
732
+ private fun audioDeviceAsRNMap(audioDevice: AudioDevice): Map<String, String?> =
733
+ mapOf(
734
+ "name" to audioDevice.name,
735
+ "type" to AudioDeviceKind.fromAudioDevice(audioDevice)?.typeName
736
+ )
737
+
738
+ private fun emitAudioDeviceEvent(
739
+ audioDevices: List<AudioDevice>,
740
+ selectedDevice: AudioDevice?
741
+ ) {
742
+ val eventName = EmitableEvents.AudioDeviceUpdate
743
+ val map =
744
+ mapOf(
745
+ eventName to
746
+ mapOf(
747
+ "selectedDevice" to (
748
+ if (selectedDevice != null) {
749
+ audioDeviceAsRNMap(
750
+ selectedDevice
751
+ )
752
+ } else {
753
+ null
754
+ }
755
+ ),
756
+ "availableDevices" to
757
+ audioDevices.map { audioDevice ->
758
+ audioDeviceAsRNMap(
759
+ audioDevice
760
+ )
761
+ }
762
+ )
763
+ )
764
+
765
+ emitEvent(eventName, map)
766
+ }
767
+
768
+ private fun getSimulcastConfigAsRNMap(simulcastConfig: SimulcastConfig): Map<String, Any> =
769
+ mapOf(
770
+ "enabled" to simulcastConfig.enabled,
771
+ "activeEncodings" to
772
+ simulcastConfig.activeEncodings.map {
773
+ it.rid
774
+ }
775
+ )
776
+
777
+ override fun onJoined(
778
+ peerID: String,
779
+ peersInRoom: MutableMap<String, Endpoint>
780
+ ) {
781
+ CoroutineScope(Dispatchers.Main).launch {
782
+ isConnected = true
783
+ connectPromise?.resolve(null)
784
+ connectPromise = null
785
+ emitEndpoints()
786
+ }
787
+ }
788
+
789
+ override fun onJoinError(metadata: Any) {
790
+ CoroutineScope(Dispatchers.Main).launch {
791
+ connectPromise?.reject(JoinError(metadata))
792
+ connectPromise = null
793
+ }
794
+ }
795
+
796
+ private fun addOrUpdateTrack(track: Track) {
797
+ emitEndpoints()
798
+ onTracksUpdateListeners.forEach { it.onTracksUpdate() }
799
+ }
800
+
801
+ override fun onTrackReady(track: Track) {
802
+ CoroutineScope(Dispatchers.Main).launch {
803
+ addOrUpdateTrack(track)
804
+ }
805
+ }
806
+
807
+ override fun onTrackAdded(track: Track) {}
808
+
809
+ override fun onTrackRemoved(track: Track) {
810
+ CoroutineScope(Dispatchers.Main).launch {
811
+ emitEndpoints()
812
+ }
813
+ }
814
+
815
+ override fun onTrackUpdated(track: Track) {
816
+ CoroutineScope(Dispatchers.Main).launch {
817
+ emitEndpoints()
818
+ }
819
+ }
820
+
821
+ override fun onPeerJoined(peer: Peer) {
822
+ CoroutineScope(Dispatchers.Main).launch {
823
+ emitEndpoints()
824
+ }
825
+ }
826
+
827
+ override fun onPeerLeft(peer: Peer) {
828
+ CoroutineScope(Dispatchers.Main).launch {
829
+ emitEndpoints()
830
+ }
831
+ }
832
+
833
+ override fun onPeerUpdated(peer: Peer) {}
834
+
835
+ override fun onBandwidthEstimationChanged(estimation: Long) {
836
+ val eventName = EmitableEvents.BandwidthEstimation
837
+ emitEvent(eventName, mapOf(eventName to estimation.toFloat()))
838
+ }
839
+
840
+ override fun onDisconnected() {}
841
+
842
+ override fun onSocketClose(
843
+ code: Int,
844
+ reason: String
845
+ ) {
846
+ CoroutineScope(Dispatchers.Main).launch {
847
+ connectPromise?.reject(SocketClosedError(code, reason))
848
+ connectPromise = null
849
+ }
850
+ }
851
+
852
+ override fun onSocketError(t: Throwable) {
853
+ CoroutineScope(Dispatchers.Main).launch {
854
+ connectPromise?.reject(SocketError(t.message ?: t.toString()))
855
+ connectPromise = null
856
+ }
857
+ }
858
+
859
+ override fun onReconnected() {
860
+ emitEvent(EmitableEvents.Reconnected, emptyMap())
861
+ }
862
+
863
+ override fun onReconnectionStarted() {
864
+ emitEvent(EmitableEvents.ReconnectionStarted, emptyMap())
865
+ }
866
+
867
+ override fun onReconnectionRetriesLimitReached() {
868
+ emitEvent(EmitableEvents.ReconnectionRetriesLimitReached, emptyMap())
869
+ }
870
+ }