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

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