@fishjam-cloud/react-native-client 0.2.4 → 0.4.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.
Files changed (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +9 -8
  3. package/android/build.gradle +4 -4
  4. package/android/src/main/java/io/fishjam/reactnative/EmitableEvents.kt +25 -12
  5. package/android/src/main/java/io/fishjam/reactnative/Errors.kt +1 -2
  6. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +94 -52
  7. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClientModule.kt +42 -90
  8. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +14 -3
  9. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewViewModule.kt +5 -0
  10. package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +4 -3
  11. package/android/src/main/java/io/fishjam/reactnative/VideoRendererViewModule.kt +2 -0
  12. package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +3 -2
  13. package/android/src/main/java/io/fishjam/reactnative/{FishjamForegroundService.kt → foregroundService/FishjamForegroundService.kt} +7 -2
  14. package/android/src/main/java/io/fishjam/reactnative/foregroundService/ForegroundServiceManager.kt +147 -0
  15. package/android/src/main/java/io/fishjam/reactnative/managers/LocalCameraTracksChangedListenersManager.kt +21 -0
  16. package/android/src/main/java/io/fishjam/reactnative/managers/LocalTracksSwitchListenersManager.kt +27 -0
  17. package/android/src/main/java/io/fishjam/reactnative/managers/TracksUpdateListenersManager.kt +21 -0
  18. package/android/src/main/java/io/fishjam/reactnative/utils/PermissionUtils.kt +45 -0
  19. package/build/RNFishjamClientModule.d.ts +16 -10
  20. package/build/RNFishjamClientModule.d.ts.map +1 -1
  21. package/build/RNFishjamClientModule.js.map +1 -1
  22. package/build/common/client.d.ts +3 -3
  23. package/build/common/client.d.ts.map +1 -1
  24. package/build/common/client.js +4 -4
  25. package/build/common/client.js.map +1 -1
  26. package/build/common/metadata.d.ts +3 -3
  27. package/build/common/metadata.d.ts.map +1 -1
  28. package/build/common/metadata.js +4 -4
  29. package/build/common/metadata.js.map +1 -1
  30. package/build/common/webRTC.d.ts +0 -8
  31. package/build/common/webRTC.d.ts.map +1 -1
  32. package/build/common/webRTC.js +0 -9
  33. package/build/common/webRTC.js.map +1 -1
  34. package/build/components/VideoRendererView.d.ts +1 -1
  35. package/build/components/VideoRendererView.js +1 -1
  36. package/build/components/VideoRendererView.js.map +1 -1
  37. package/build/debug/common/webRTC.d.ts +9 -0
  38. package/build/debug/common/webRTC.d.ts.map +1 -0
  39. package/build/debug/common/webRTC.js +11 -0
  40. package/build/debug/common/webRTC.js.map +1 -0
  41. package/build/debug/index.d.ts +4 -0
  42. package/build/debug/index.d.ts.map +1 -0
  43. package/build/debug/index.js +3 -0
  44. package/build/debug/index.js.map +1 -0
  45. package/build/debug/stats/types.d.ts.map +1 -0
  46. package/build/debug/stats/types.js.map +1 -0
  47. package/build/debug/stats/useRTCStatistics.d.ts.map +1 -0
  48. package/build/{stats → debug/stats}/useRTCStatistics.js +1 -1
  49. package/build/debug/stats/useRTCStatistics.js.map +1 -0
  50. package/build/hooks/useAppScreenShare.d.ts +11 -0
  51. package/build/hooks/useAppScreenShare.d.ts.map +1 -0
  52. package/build/hooks/useAppScreenShare.js +54 -0
  53. package/build/hooks/useAppScreenShare.js.map +1 -0
  54. package/build/hooks/useFishjamEvent.d.ts +2 -0
  55. package/build/hooks/useFishjamEvent.d.ts.map +1 -1
  56. package/build/hooks/useFishjamEvent.js +2 -0
  57. package/build/hooks/useFishjamEvent.js.map +1 -1
  58. package/build/hooks/useForegroundService.d.ts +44 -0
  59. package/build/hooks/useForegroundService.d.ts.map +1 -0
  60. package/build/hooks/useForegroundService.js +57 -0
  61. package/build/hooks/useForegroundService.js.map +1 -0
  62. package/build/hooks/useMicrophone.d.ts +1 -1
  63. package/build/hooks/useMicrophone.js +1 -1
  64. package/build/hooks/useMicrophone.js.map +1 -1
  65. package/build/hooks/usePeerStatus.d.ts +19 -0
  66. package/build/hooks/usePeerStatus.d.ts.map +1 -0
  67. package/build/hooks/usePeerStatus.js +18 -0
  68. package/build/hooks/usePeerStatus.js.map +1 -0
  69. package/build/hooks/{useParticipants.d.ts → usePeers.d.ts} +11 -11
  70. package/build/hooks/usePeers.d.ts.map +1 -0
  71. package/build/hooks/usePeers.js +34 -0
  72. package/build/hooks/usePeers.js.map +1 -0
  73. package/build/hooks/useScreenShare.d.ts +1 -0
  74. package/build/hooks/useScreenShare.d.ts.map +1 -1
  75. package/build/hooks/useScreenShare.js +20 -13
  76. package/build/hooks/useScreenShare.js.map +1 -1
  77. package/build/index.d.ts +8 -7
  78. package/build/index.d.ts.map +1 -1
  79. package/build/index.js +5 -5
  80. package/build/index.js.map +1 -1
  81. package/build/types.d.ts +0 -25
  82. package/build/types.d.ts.map +1 -1
  83. package/build/types.js +1 -10
  84. package/build/types.js.map +1 -1
  85. package/debug/package.json +4 -0
  86. package/ios/Events.swift +30 -13
  87. package/ios/RNFishjamClient.podspec +1 -1
  88. package/ios/RNFishjamClient.swift +196 -106
  89. package/ios/RNFishjamClientModule.swift +34 -34
  90. package/ios/VideoPreviewView.swift +26 -12
  91. package/ios/VideoRendererView.swift +5 -14
  92. package/package.json +7 -6
  93. package/plugin/build/withFishjamAndroid.js +1 -1
  94. package/plugin/build/withFishjamIos.js +2 -2
  95. package/build/hooks/useParticipants.d.ts.map +0 -1
  96. package/build/hooks/useParticipants.js +0 -34
  97. package/build/hooks/useParticipants.js.map +0 -1
  98. package/build/stats/types.d.ts.map +0 -1
  99. package/build/stats/types.js.map +0 -1
  100. package/build/stats/useRTCStatistics.d.ts.map +0 -1
  101. package/build/stats/useRTCStatistics.js.map +0 -1
  102. package/build/utils/foregroundService.d.ts +0 -32
  103. package/build/utils/foregroundService.d.ts.map +0 -1
  104. package/build/utils/foregroundService.js +0 -43
  105. package/build/utils/foregroundService.js.map +0 -1
  106. /package/build/{stats → debug/stats}/types.d.ts +0 -0
  107. /package/build/{stats → debug/stats}/types.js +0 -0
  108. /package/build/{stats → debug/stats}/useRTCStatistics.d.ts +0 -0
@@ -1,9 +1,6 @@
1
1
  package io.fishjam.reactnative
2
2
 
3
- import android.content.Intent
4
- import android.os.Build
5
3
  import expo.modules.kotlin.Promise
6
- import expo.modules.kotlin.exception.CodedException
7
4
  import expo.modules.kotlin.functions.Coroutine
8
5
  import expo.modules.kotlin.modules.Module
9
6
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -84,7 +81,7 @@ class ConnectConfig : Record {
84
81
  val reconnectConfig: ReconnectConfig = ReconnectConfig()
85
82
  }
86
83
 
87
- class ForegroundServiceConfig : Record {
84
+ class ForegroundServiceNotificationConfig : Record {
88
85
  @Field
89
86
  val channelId: String? = null
90
87
 
@@ -96,37 +93,32 @@ class ForegroundServiceConfig : Record {
96
93
 
97
94
  @Field
98
95
  val notificationTitle: String? = null
96
+ }
97
+
98
+ class ForegroundServicePermissionsConfig : Record {
99
+ @Field
100
+ val enableCamera: Boolean = false
99
101
 
100
102
  @Field
101
- val foregroundServiceTypes: IntArray = intArrayOf()
103
+ val enableMicrophone: Boolean = false
104
+
105
+ @Field
106
+ val enableScreenSharing: Boolean = false
102
107
  }
103
108
 
104
109
  class RNFishjamClientModule : Module() {
105
110
  override fun definition() =
106
111
  ModuleDefinition {
107
- Name("RNFishjamClient")
108
-
109
- Events(
110
- "IsCameraOn",
111
- "IsMicrophoneOn",
112
- "IsScreenShareOn",
113
- "SimulcastConfigUpdate",
114
- "PeersUpdate",
115
- "AudioDeviceUpdate",
116
- "SendMediaEvent",
117
- "BandwidthEstimation",
118
- "ReconnectionRetriesLimitReached",
119
- "ReconnectionStarted",
120
- "Reconnected",
121
- "Warning"
122
- )
112
+ val mutex = Mutex()
123
113
 
124
114
  val rnFishjamClient =
125
115
  RNFishjamClient { name: String, data: Map<String, Any?> ->
126
116
  sendEvent(name, data)
127
117
  }
128
118
 
129
- val mutex = Mutex()
119
+ Name("RNFishjamClient")
120
+
121
+ Events(EmitableEvents.allEvents)
130
122
 
131
123
  OnCreate {
132
124
  rnFishjamClient.onModuleCreate(appContext)
@@ -144,11 +136,31 @@ class RNFishjamClientModule : Module() {
144
136
  rnFishjamClient.onActivityResult(result.requestCode, result.resultCode, result.data)
145
137
  }
146
138
 
139
+ Property("isCameraOn") {
140
+ return@Property rnFishjamClient.isCameraOn
141
+ }
142
+
143
+ Property("isMicrophoneOn") {
144
+ return@Property rnFishjamClient.isMicrophoneOn
145
+ }
146
+
147
+ Property("cameras") {
148
+ return@Property rnFishjamClient.getCaptureDevices()
149
+ }
150
+
151
+ Property("isScreenShareOn") {
152
+ return@Property rnFishjamClient.isScreenShareOn
153
+ }
154
+
155
+ Property("peerStatus") {
156
+ return@Property rnFishjamClient.peerStatus
157
+ }
158
+
147
159
  AsyncFunction(
148
160
  "joinRoom"
149
- ) { url: String, participantToken: String, participantMetadata: Map<String, Any>, config: ConnectConfig, promise: Promise ->
161
+ ) { url: String, peerToken: String, peerMetadata: Map<String, Any>, config: ConnectConfig, promise: Promise ->
150
162
  CoroutineScope(Dispatchers.Main).launch {
151
- rnFishjamClient.joinRoom(url, participantToken, participantMetadata, config, promise)
163
+ rnFishjamClient.joinRoom(url, peerToken, peerMetadata, config, promise)
152
164
  }
153
165
  }
154
166
 
@@ -164,20 +176,12 @@ class RNFishjamClientModule : Module() {
164
176
  }
165
177
  }
166
178
 
167
- Property("isMicrophoneOn") {
168
- return@Property rnFishjamClient.isMicrophoneOn
169
- }
170
-
171
179
  AsyncFunction("toggleMicrophone") Coroutine { ->
172
180
  withContext(Dispatchers.Main) {
173
181
  rnFishjamClient.toggleMicrophone()
174
182
  }
175
183
  }
176
184
 
177
- Property("isCameraOn") {
178
- return@Property rnFishjamClient.isCameraOn
179
- }
180
-
181
185
  AsyncFunction("toggleCamera") Coroutine { ->
182
186
  withContext(Dispatchers.Main) {
183
187
  rnFishjamClient.toggleCamera()
@@ -212,14 +216,6 @@ class RNFishjamClientModule : Module() {
212
216
  }
213
217
  }
214
218
 
215
- Property("cameras") {
216
- return@Property rnFishjamClient.getCaptureDevices()
217
- }
218
-
219
- Property("isScreenShareOn") {
220
- return@Property rnFishjamClient.isScreenShareOn
221
- }
222
-
223
219
  AsyncFunction("getPeers") Coroutine { ->
224
220
  withContext(Dispatchers.Main) {
225
221
  rnFishjamClient.getPeers()
@@ -312,60 +308,16 @@ class RNFishjamClientModule : Module() {
312
308
 
313
309
  AsyncFunction("getStatistics") { rnFishjamClient.getStatistics() }
314
310
 
315
- Function("startForegroundService") { config: ForegroundServiceConfig ->
316
- if (appContext.reactContext == null) {
317
- throw CodedException(message = "reactContext not found")
318
- }
319
-
320
- val channelId =
321
- config.channelId
322
- ?: throw CodedException(message = "Missing `channelId` for startForegroundService")
323
- val channelName =
324
- config.channelName
325
- ?: throw CodedException(message = "Missing `channelName` for startForegroundService")
326
- val notificationContent =
327
- config.notificationContent
328
- ?: throw CodedException(message = "Missing `notificationContent` for startForegroundService")
329
- val notificationTitle =
330
- config.notificationTitle
331
- ?: throw CodedException(message = "Missing `notificationTitle` for startForegroundService")
332
- val foregroundServiceTypes = config.foregroundServiceTypes
333
-
334
- if (foregroundServiceTypes.isEmpty()) {
335
- throw CodedException(message = "`foregroundServiceTypes` cannot be empty")
336
- }
311
+ Function("configureForegroundService") { config: ForegroundServiceNotificationConfig ->
312
+ return@Function rnFishjamClient.configureForegroundService(config)
313
+ }
337
314
 
338
- val serviceIntent =
339
- Intent(
340
- appContext.reactContext,
341
- FishjamForegroundService::class.java
342
- )
343
-
344
- serviceIntent.putExtra("channelId", channelId)
345
- serviceIntent.putExtra("channelName", channelName)
346
- serviceIntent.putExtra("notificationTitle", notificationContent)
347
- serviceIntent.putExtra("notificationContent", notificationTitle)
348
- serviceIntent.putExtra("foregroundServiceTypes", foregroundServiceTypes)
349
-
350
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
351
- appContext.reactContext!!.startForegroundService(serviceIntent)
352
- } else {
353
- appContext.reactContext!!.startService(serviceIntent)
354
- }
315
+ AsyncFunction("startForegroundService") Coroutine { config: ForegroundServicePermissionsConfig ->
316
+ rnFishjamClient.startForegroundService(config)
355
317
  }
356
318
 
357
319
  Function("stopForegroundService") {
358
- if (appContext.reactContext == null) {
359
- throw CodedException(message = "reactContext not found")
360
- }
361
-
362
- val serviceIntent: Intent =
363
- Intent(
364
- appContext.reactContext,
365
- FishjamForegroundService::class.java
366
- )
367
-
368
- appContext.reactContext!!.stopService(serviceIntent)
320
+ rnFishjamClient.stopForegroundService()
369
321
  }
370
322
  }
371
323
  }
@@ -4,20 +4,25 @@ import android.content.Context
4
4
  import com.fishjamcloud.client.media.LocalVideoTrack
5
5
  import com.fishjamcloud.client.media.VideoTrack
6
6
  import expo.modules.kotlin.AppContext
7
+ import io.fishjam.reactnative.managers.LocalCameraTrackChangedListener
7
8
 
8
9
  class VideoPreviewView(
9
10
  context: Context,
10
11
  appContext: AppContext
11
- ) : VideoView(context, appContext) {
12
+ ) : VideoView(context, appContext),
13
+ LocalCameraTrackChangedListener {
12
14
  private var localVideoTrack: LocalVideoTrack? = null
13
15
 
14
- private fun initialize() {
16
+ private fun trySetLocalCameraTrack() {
15
17
  localVideoTrack =
16
18
  RNFishjamClient.fishjamClient.getLocalEndpoint().tracks.values.firstOrNull { track ->
17
19
  track is LocalVideoTrack
18
20
  } as? LocalVideoTrack?
19
-
20
21
  videoView.let { localVideoTrack?.addRenderer(it) }
22
+ }
23
+
24
+ private fun initialize() {
25
+ trySetLocalCameraTrack()
21
26
  super.setupTrack()
22
27
  }
23
28
 
@@ -27,14 +32,20 @@ class VideoPreviewView(
27
32
  }
28
33
 
29
34
  override fun onDetachedFromWindow() {
35
+ RNFishjamClient.localCameraTracksChangedListenersManager.remove(this)
30
36
  super.onDetachedFromWindow()
31
37
  dispose()
32
38
  }
33
39
 
34
40
  override fun onAttachedToWindow() {
41
+ RNFishjamClient.localCameraTracksChangedListenersManager.add(this)
35
42
  super.onAttachedToWindow()
36
43
  initialize()
37
44
  }
38
45
 
39
46
  override fun getVideoTrack(): VideoTrack? = localVideoTrack
47
+
48
+ override fun onLocalCameraTrackChanged() {
49
+ if (localVideoTrack == null) trySetLocalCameraTrack()
50
+ }
40
51
  }
@@ -9,6 +9,11 @@ class VideoPreviewViewModule : Module() {
9
9
  Name("VideoPreviewViewModule")
10
10
 
11
11
  View(VideoPreviewView::class) {
12
+ OnViewDestroys { view: VideoPreviewView ->
13
+ view.dispose()
14
+ RNFishjamClient.localTracksSwitchListenerManager.remove(view)
15
+ }
16
+
12
17
  Prop("videoLayout") { view: VideoPreviewView, videoLayout: String ->
13
18
  view.setVideoLayout(videoLayout)
14
19
  }
@@ -3,6 +3,7 @@ package io.fishjam.reactnative
3
3
  import android.content.Context
4
4
  import com.fishjamcloud.client.media.VideoTrack
5
5
  import expo.modules.kotlin.AppContext
6
+ import io.fishjam.reactnative.managers.TrackUpdateListener
6
7
  import kotlinx.coroutines.CoroutineScope
7
8
  import kotlinx.coroutines.Dispatchers
8
9
  import kotlinx.coroutines.launch
@@ -11,12 +12,12 @@ class VideoRendererView(
11
12
  context: Context,
12
13
  appContext: AppContext
13
14
  ) : VideoView(context, appContext),
14
- RNFishjamClient.OnTrackUpdateListener {
15
+ TrackUpdateListener {
15
16
  private var activeVideoTrack: VideoTrack? = null
16
17
  private var trackId: String? = null
17
18
 
18
19
  init {
19
- RNFishjamClient.onTracksUpdateListeners.add(this)
20
+ RNFishjamClient.trackUpdateListenersManager.add(this)
20
21
  }
21
22
 
22
23
  private fun setupTrack(videoTrack: VideoTrack) {
@@ -46,7 +47,7 @@ class VideoRendererView(
46
47
 
47
48
  override fun dispose() {
48
49
  activeVideoTrack?.removeRenderer(videoView)
49
- RNFishjamClient.onTracksUpdateListeners.remove(this)
50
+ RNFishjamClient.trackUpdateListenersManager.add(this)
50
51
  super.dispose()
51
52
  }
52
53
 
@@ -11,6 +11,8 @@ class VideoRendererViewModule : Module() {
11
11
  View(VideoRendererView::class) {
12
12
  OnViewDestroys { view: VideoRendererView ->
13
13
  view.dispose()
14
+ RNFishjamClient.trackUpdateListenersManager.remove(view)
15
+ RNFishjamClient.localTracksSwitchListenerManager.remove(view)
14
16
  }
15
17
 
16
18
  Prop("trackId") { view: VideoRendererView, trackId: String ->
@@ -9,6 +9,7 @@ import com.fishjamcloud.client.media.LocalVideoTrack
9
9
  import com.fishjamcloud.client.media.VideoTrack
10
10
  import expo.modules.kotlin.AppContext
11
11
  import expo.modules.kotlin.views.ExpoView
12
+ import io.fishjam.reactnative.managers.LocalTrackSwitchListener
12
13
  import kotlinx.coroutines.CoroutineScope
13
14
  import kotlinx.coroutines.Dispatchers
14
15
  import kotlinx.coroutines.delay
@@ -18,7 +19,7 @@ abstract class VideoView(
18
19
  context: Context,
19
20
  appContext: AppContext
20
21
  ) : ExpoView(context, appContext),
21
- RNFishjamClient.OnLocalTrackSwitchListener {
22
+ LocalTrackSwitchListener {
22
23
  protected val videoView =
23
24
  RNFishjamClient.fishjamClient.createVideoViewRenderer().also {
24
25
  addView(it)
@@ -37,7 +38,7 @@ abstract class VideoView(
37
38
  val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
38
39
 
39
40
  init {
40
- RNFishjamClient.onLocalTrackSwitchListener.add(this)
41
+ RNFishjamClient.localTracksSwitchListenerManager.add(this)
41
42
  }
42
43
 
43
44
  fun setVideoLayout(videoLayout: String) {
@@ -1,4 +1,4 @@
1
- package io.fishjam.reactnative
1
+ package io.fishjam.reactnative.foregroundService
2
2
 
3
3
  import android.app.Notification
4
4
  import android.app.NotificationChannel
@@ -6,6 +6,7 @@ import android.app.NotificationManager
6
6
  import android.app.PendingIntent
7
7
  import android.app.Service
8
8
  import android.content.Intent
9
+ import android.os.Binder
9
10
  import android.os.Build
10
11
  import android.os.IBinder
11
12
  import androidx.core.app.NotificationCompat
@@ -15,7 +16,11 @@ class FishjamForegroundService : Service() {
15
16
  private const val FOREGROUND_SERVICE_ID = 1668
16
17
  }
17
18
 
18
- override fun onBind(p0: Intent?): IBinder? = null
19
+ private val binder = LocalBinder()
20
+
21
+ inner class LocalBinder : Binder()
22
+
23
+ override fun onBind(intent: Intent): IBinder = binder
19
24
 
20
25
  override fun onStartCommand(
21
26
  intent: Intent?,
@@ -0,0 +1,147 @@
1
+ package io.fishjam.reactnative.foregroundService
2
+
3
+ import android.content.ComponentName
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.ServiceConnection
7
+ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
8
+ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
9
+ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
10
+ import android.os.Build
11
+ import android.os.IBinder
12
+ import expo.modules.kotlin.AppContext
13
+ import expo.modules.kotlin.exception.CodedException
14
+ import io.fishjam.reactnative.ForegroundServiceNotificationConfig
15
+ import io.fishjam.reactnative.ForegroundServicePermissionsConfig
16
+ import io.fishjam.reactnative.utils.PermissionUtils
17
+ import kotlinx.coroutines.CancellableContinuation
18
+ import kotlinx.coroutines.suspendCancellableCoroutine
19
+ import kotlin.coroutines.resume
20
+
21
+ class ForegroundServiceManager(
22
+ private val appContext: AppContext,
23
+ notificationConfig: ForegroundServiceNotificationConfig
24
+ ) {
25
+ private val reactContext by lazy {
26
+ appContext.reactContext ?: throw CodedException("reactContext not found")
27
+ }
28
+ private var isServiceBound: Boolean = false
29
+ private var serviceConnectedContinuation: CancellableContinuation<Unit>? = null
30
+
31
+ private val serviceIntent =
32
+ Intent(
33
+ appContext.reactContext,
34
+ FishjamForegroundService::class.java
35
+ ).apply {
36
+ putExtra(
37
+ "channelId",
38
+ notificationConfig.channelId.orThrow("channelId")
39
+ )
40
+ putExtra(
41
+ "channelName",
42
+ notificationConfig.channelName.orThrow("channelName")
43
+ )
44
+ putExtra(
45
+ "notificationContent",
46
+ notificationConfig.notificationContent.orThrow("notificationContent")
47
+ )
48
+ putExtra(
49
+ "notificationTitle",
50
+ notificationConfig.notificationTitle.orThrow("notificationTitle")
51
+ )
52
+ }
53
+
54
+ private val connection =
55
+ object : ServiceConnection {
56
+ override fun onServiceConnected(
57
+ className: ComponentName,
58
+ service: IBinder
59
+ ) {
60
+ isServiceBound = true
61
+ serviceConnectedContinuation?.resume(Unit)
62
+ serviceConnectedContinuation = null
63
+ }
64
+
65
+ override fun onServiceDisconnected(arg0: ComponentName) {
66
+ isServiceBound = false
67
+ serviceConnectedContinuation?.cancel()
68
+ serviceConnectedContinuation = null
69
+ }
70
+ }
71
+
72
+ suspend fun startForegroundService(permissionsConfig: ForegroundServicePermissionsConfig) {
73
+ val foregroundServiceTypes = buildForegroundServiceTypes(permissionsConfig)
74
+
75
+ if (foregroundServiceTypes.isEmpty()) {
76
+ return
77
+ }
78
+
79
+ serviceIntent.putExtra("foregroundServiceTypes", foregroundServiceTypes.toIntArray())
80
+
81
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
82
+ reactContext.startForegroundService(serviceIntent)
83
+ } else {
84
+ reactContext.startService(serviceIntent)
85
+ }
86
+
87
+ bindServiceIfNeededAndAwait()
88
+ }
89
+
90
+ fun stopForegroundService() {
91
+ if (isServiceBound) {
92
+ appContext.currentActivity?.unbindService(connection)
93
+ ?: throw CodedException("Current activity not found")
94
+ isServiceBound = false
95
+ }
96
+ reactContext.stopService(serviceIntent)
97
+ serviceConnectedContinuation?.cancel()
98
+ serviceConnectedContinuation = null
99
+ }
100
+
101
+ private fun buildForegroundServiceTypes(permissionsConfig: ForegroundServicePermissionsConfig): List<Int> =
102
+ buildList {
103
+ if (permissionsConfig.enableCamera) {
104
+ addIfPermissionGranted(
105
+ FOREGROUND_SERVICE_TYPE_CAMERA,
106
+ "camera",
107
+ PermissionUtils::hasCameraPermission
108
+ )
109
+ }
110
+ if (permissionsConfig.enableMicrophone) {
111
+ addIfPermissionGranted(
112
+ FOREGROUND_SERVICE_TYPE_MICROPHONE,
113
+ "microphone",
114
+ PermissionUtils::hasMicrophonePermission
115
+ )
116
+ }
117
+ if (permissionsConfig.enableScreenSharing) add(FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
118
+ }
119
+
120
+ private fun MutableList<Int>.addIfPermissionGranted(
121
+ serviceType: Int,
122
+ permissionName: String,
123
+ hasPermission: (AppContext) -> Boolean
124
+ ) {
125
+ if (!hasPermission(appContext)) {
126
+ throw CodedException("Cannot start a $permissionName foreground service without $permissionName permission.")
127
+ }
128
+ add(serviceType)
129
+ }
130
+
131
+ private suspend fun bindServiceIfNeededAndAwait() =
132
+ suspendCancellableCoroutine { continuation ->
133
+ if (!isServiceBound) {
134
+ serviceConnectedContinuation = continuation
135
+ runCatching {
136
+ appContext.currentActivity?.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)
137
+ }.onFailure { error ->
138
+ continuation.cancel(CodedException("Failed to bind service: ${error.message}"))
139
+ }
140
+ } else {
141
+ continuation.resume(Unit)
142
+ serviceConnectedContinuation = null
143
+ }
144
+ }
145
+ }
146
+
147
+ private fun String?.orThrow(fieldName: String): String = this ?: throw CodedException("Missing `$fieldName` for startForegroundService")
@@ -0,0 +1,21 @@
1
+ package io.fishjam.reactnative.managers
2
+
3
+ interface LocalCameraTrackChangedListener {
4
+ fun onLocalCameraTrackChanged()
5
+ }
6
+
7
+ class LocalCameraTracksChangedListenersManager {
8
+ private var listeners: MutableList<LocalCameraTrackChangedListener> = mutableListOf()
9
+
10
+ fun add(listener: LocalCameraTrackChangedListener) {
11
+ listeners.add(listener)
12
+ }
13
+
14
+ fun remove(listener: LocalCameraTrackChangedListener) {
15
+ listeners.remove(listener)
16
+ }
17
+
18
+ fun notifyListeners() {
19
+ for (listener in listeners) listener.onLocalCameraTrackChanged()
20
+ }
21
+ }
@@ -0,0 +1,27 @@
1
+ package io.fishjam.reactnative.managers
2
+
3
+ interface LocalTrackSwitchListener {
4
+ suspend fun onLocalTrackWillSwitch()
5
+
6
+ suspend fun onLocalTrackSwitched()
7
+ }
8
+
9
+ class LocalTracksSwitchListenersManager {
10
+ private val listeners: MutableList<LocalTrackSwitchListener> = mutableListOf()
11
+
12
+ fun add(listener: LocalTrackSwitchListener) {
13
+ listeners.add(listener)
14
+ }
15
+
16
+ fun remove(listener: LocalTrackSwitchListener) {
17
+ listeners.remove(listener)
18
+ }
19
+
20
+ suspend fun notifyWillSwitch() {
21
+ for (listener in listeners) listener.onLocalTrackWillSwitch()
22
+ }
23
+
24
+ suspend fun notifySwitched() {
25
+ for (listener in listeners) listener.onLocalTrackSwitched()
26
+ }
27
+ }
@@ -0,0 +1,21 @@
1
+ package io.fishjam.reactnative.managers
2
+
3
+ interface TrackUpdateListener {
4
+ fun onTracksUpdate()
5
+ }
6
+
7
+ class TracksUpdateListenersManager {
8
+ private val listeners: MutableList<TrackUpdateListener> = mutableListOf()
9
+
10
+ fun add(listener: TrackUpdateListener) {
11
+ listeners.add(listener)
12
+ }
13
+
14
+ fun remove(listener: TrackUpdateListener) {
15
+ listeners.remove(listener)
16
+ }
17
+
18
+ fun notifyListeners() {
19
+ for (listener in listeners) listener.onTracksUpdate()
20
+ }
21
+ }
@@ -0,0 +1,45 @@
1
+ package io.fishjam.reactnative.utils
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import expo.modules.interfaces.permissions.Permissions
6
+ import expo.modules.interfaces.permissions.PermissionsStatus
7
+ import expo.modules.kotlin.AppContext
8
+ import expo.modules.kotlin.exception.Exceptions
9
+ import kotlin.coroutines.resume
10
+ import kotlin.coroutines.suspendCoroutine
11
+
12
+ object PermissionUtils {
13
+ fun hasCameraPermission(appContext: AppContext?): Boolean = hasPermission(appContext, Manifest.permission.CAMERA)
14
+
15
+ fun hasMicrophonePermission(appContext: AppContext?): Boolean = hasPermission(appContext, Manifest.permission.RECORD_AUDIO)
16
+
17
+ suspend fun requestCameraPermission(appContext: AppContext?): Boolean = requestAccessIfNeeded(appContext, Manifest.permission.CAMERA)
18
+
19
+ suspend fun requestMicrophonePermission(appContext: AppContext?): Boolean =
20
+ requestAccessIfNeeded(appContext, Manifest.permission.RECORD_AUDIO)
21
+
22
+ private suspend fun requestAccessIfNeeded(
23
+ appContext: AppContext?,
24
+ permission: String
25
+ ): Boolean {
26
+ val permissionsManager: Permissions =
27
+ appContext?.permissions
28
+ ?: throw Exceptions.PermissionsModuleNotFound()
29
+
30
+ if (hasPermission(appContext, permission)) {
31
+ return true
32
+ }
33
+
34
+ return suspendCoroutine { continuation ->
35
+ permissionsManager.askForPermissions({ result ->
36
+ continuation.resume(result[permission]?.status == PermissionsStatus.GRANTED)
37
+ }, permission)
38
+ }
39
+ }
40
+
41
+ private fun hasPermission(
42
+ appContext: AppContext?,
43
+ permission: String
44
+ ): Boolean = appContext?.reactContext?.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
45
+ }
@@ -1,29 +1,34 @@
1
1
  import { EventEmitter } from 'expo-modules-core';
2
2
  import { NativeModule } from 'react-native';
3
- import type { RTCStats } from './stats/types';
4
- import type { ForegroundServiceOptions, SimulcastConfig } from './types';
3
+ import type { RTCStats } from './debug/stats/types';
4
+ import type { SimulcastConfig } from './types';
5
5
  import type { CameraConfigInternal, Camera } from './hooks/useCamera';
6
- import type { Participant } from './hooks/useParticipants';
6
+ import type { Peer } from './hooks/usePeers';
7
7
  import type { ScreenShareOptionsInternal } from './hooks/useScreenShare';
8
8
  import type { ConnectionConfig } from './common/client';
9
+ import { PeerStatus } from './hooks/usePeerStatus';
10
+ import { ForegroundServiceNotificationConfig, ForegroundServicePermissionsConfigInternal } from './hooks/useForegroundService';
9
11
  type Metadata = {
10
12
  [key: string]: any;
11
13
  };
12
14
  type RNFishjamClient = {
13
- joinRoom: (url: string, participantToken: string, participantMetadata: Metadata, config: ConnectionConfig) => Promise<void>;
15
+ isMicrophoneOn: boolean;
16
+ isCameraOn: boolean;
17
+ isScreenShareOn: boolean;
18
+ isAppScreenShareOn: boolean;
19
+ cameras: ReadonlyArray<Camera>;
20
+ peerStatus: PeerStatus;
21
+ joinRoom: (url: string, peerToken: string, peerMetadata: Metadata, config: ConnectionConfig) => Promise<void>;
14
22
  leaveRoom: () => Promise<void>;
15
23
  startCamera: (config: CameraConfigInternal) => Promise<void>;
16
- isMicrophoneOn: boolean;
17
24
  toggleMicrophone: () => Promise<boolean>;
18
- isCameraOn: boolean;
19
25
  toggleCamera: () => Promise<boolean>;
20
26
  flipCamera: () => Promise<void>;
21
27
  switchCamera: (cameraId: string) => Promise<void>;
22
- cameras: ReadonlyArray<Camera>;
23
28
  handleScreenSharePermission: () => Promise<'granted' | 'denied'>;
24
29
  toggleScreenShare: (screenShareOptions: Partial<ScreenShareOptionsInternal>) => Promise<void>;
25
- isScreenShareOn: boolean;
26
- getPeers: <PeerMetadataType extends Metadata>() => Promise<Participant<PeerMetadataType>[]>;
30
+ toggleAppScreenShare: (screenShareOptions: Partial<ScreenShareOptionsInternal>) => Promise<void>;
31
+ getPeers: <PeerMetadataType extends Metadata>() => Promise<Peer<PeerMetadataType>[]>;
27
32
  updatePeerMetadata: <MetadataType extends Metadata>(metadata: MetadataType) => Promise<void>;
28
33
  updateVideoTrackMetadata: <MetadataType extends Metadata>(metadata: MetadataType) => Promise<void>;
29
34
  updateAudioTrackMetadata: <MetadataType extends Metadata>(metadata: MetadataType) => Promise<void>;
@@ -42,7 +47,8 @@ type RNFishjamClient = {
42
47
  setVideoTrackBandwidth: (bandwidth: number) => Promise<void>;
43
48
  changeWebRTCLoggingSeverity: (severity: string) => Promise<void>;
44
49
  getStatistics: () => Promise<RTCStats>;
45
- startForegroundService: (options: ForegroundServiceOptions) => void;
50
+ configureForegroundService: (config: ForegroundServiceNotificationConfig) => void;
51
+ startForegroundService: (config: ForegroundServicePermissionsConfigInternal) => Promise<void>;
46
52
  stopForegroundService: () => void;
47
53
  };
48
54
  export declare const nativeModuleEventEmitter: EventEmitter;