@fishjam-cloud/react-native-client 0.3.0 → 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 (58) hide show
  1. package/README.md +2 -2
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/io/fishjam/reactnative/Errors.kt +1 -2
  4. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +52 -27
  5. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClientModule.kt +40 -79
  6. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +4 -3
  7. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewViewModule.kt +5 -0
  8. package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +4 -3
  9. package/android/src/main/java/io/fishjam/reactnative/VideoRendererViewModule.kt +2 -0
  10. package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +3 -2
  11. package/android/src/main/java/io/fishjam/reactnative/{FishjamForegroundService.kt → foregroundService/FishjamForegroundService.kt} +7 -2
  12. package/android/src/main/java/io/fishjam/reactnative/foregroundService/ForegroundServiceManager.kt +147 -0
  13. package/android/src/main/java/io/fishjam/reactnative/managers/LocalCameraTracksChangedListenersManager.kt +21 -0
  14. package/android/src/main/java/io/fishjam/reactnative/managers/LocalTracksSwitchListenersManager.kt +27 -0
  15. package/android/src/main/java/io/fishjam/reactnative/managers/TracksUpdateListenersManager.kt +21 -0
  16. package/android/src/main/java/io/fishjam/reactnative/utils/PermissionUtils.kt +45 -0
  17. package/build/RNFishjamClientModule.d.ts +6 -2
  18. package/build/RNFishjamClientModule.d.ts.map +1 -1
  19. package/build/RNFishjamClientModule.js.map +1 -1
  20. package/build/hooks/useAppScreenShare.d.ts +11 -0
  21. package/build/hooks/useAppScreenShare.d.ts.map +1 -0
  22. package/build/hooks/useAppScreenShare.js +54 -0
  23. package/build/hooks/useAppScreenShare.js.map +1 -0
  24. package/build/hooks/useFishjamEvent.d.ts +1 -0
  25. package/build/hooks/useFishjamEvent.d.ts.map +1 -1
  26. package/build/hooks/useFishjamEvent.js +1 -0
  27. package/build/hooks/useFishjamEvent.js.map +1 -1
  28. package/build/hooks/useForegroundService.d.ts +44 -0
  29. package/build/hooks/useForegroundService.d.ts.map +1 -0
  30. package/build/hooks/useForegroundService.js +57 -0
  31. package/build/hooks/useForegroundService.js.map +1 -0
  32. package/build/hooks/useMicrophone.d.ts +1 -1
  33. package/build/hooks/useMicrophone.js +1 -1
  34. package/build/hooks/useMicrophone.js.map +1 -1
  35. package/build/hooks/useScreenShare.d.ts +1 -0
  36. package/build/hooks/useScreenShare.d.ts.map +1 -1
  37. package/build/hooks/useScreenShare.js +20 -13
  38. package/build/hooks/useScreenShare.js.map +1 -1
  39. package/build/index.d.ts +4 -3
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +2 -2
  42. package/build/index.js.map +1 -1
  43. package/build/types.d.ts +0 -25
  44. package/build/types.d.ts.map +1 -1
  45. package/build/types.js +1 -10
  46. package/build/types.js.map +1 -1
  47. package/ios/Events.swift +1 -0
  48. package/ios/RNFishjamClient.podspec +1 -1
  49. package/ios/RNFishjamClient.swift +144 -57
  50. package/ios/RNFishjamClientModule.swift +32 -24
  51. package/ios/VideoPreviewView.swift +6 -10
  52. package/ios/VideoRendererView.swift +5 -14
  53. package/package.json +6 -6
  54. package/plugin/build/withFishjamAndroid.js +1 -1
  55. package/build/utils/foregroundService.d.ts +0 -32
  56. package/build/utils/foregroundService.d.ts.map +0 -1
  57. package/build/utils/foregroundService.js +0 -43
  58. package/build/utils/foregroundService.js.map +0 -1
package/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  # Installation
8
8
 
9
- Check out the [installation](https://fishjam-cloud.github.io/documentation/guide/react-native/installation) section of our docs for the detailed installation instructions.
9
+ Check out the [installation](https://docs.fishjam.io/react-native/installation) section of our docs for the detailed installation instructions.
10
10
 
11
11
  # Documentation
12
12
 
13
- Check out our [documentation](https://fishjam-cloud.github.io/documentation/) on how Fishjam Cloud work and how to integrate our service with your app.
13
+ Check out our [documentation](https://docs.fishjam.io/) on how Fishjam Cloud work and how to integrate our service with your app.
14
14
 
15
15
  There is also [API documentation](https://fishjam-cloud.github.io/mobile-client-sdk/modules/_fishjam_cloud_react_native_client.html) available.
16
16
 
@@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
2
2
  apply plugin: 'kotlin-android'
3
3
  apply plugin: 'maven-publish'
4
4
 
5
- def packageVersion = '0.3.0'
5
+ def packageVersion = '0.4.0'
6
6
 
7
7
  group = 'io.fishjam.reactnative'
8
8
  version = "${packageVersion}"
@@ -86,6 +86,6 @@ dependencies {
86
86
  // update version number when releasing
87
87
  implementation "com.github.fishjam-cloud:mobile-client-sdk:${packageVersion}"
88
88
  }
89
- api 'com.github.davidliu:audioswitch:8edf84ee46cdbc84c2b7725aa8f8d66363e19876'
89
+ implementation 'com.twilio:audioswitch:1.2.0'
90
90
  implementation 'androidx.appcompat:appcompat:1.6.1'
91
91
  }
@@ -11,8 +11,7 @@ class ConnectionError(
11
11
  reason: AuthError
12
12
  ) : CodedException(message = "Connection error: ${reason.error}")
13
13
 
14
- class MissingScreenSharePermission :
15
- CodedException(message = "No permission to start screen share, call handleScreenSharePermission first.")
14
+ class MissingScreenSharePermission : CodedException(message = "No permission to start screen share.")
16
15
 
17
16
  class ClientNotConnectedError : CodedException(message = "Client not connected to server yet. Make sure to call connect() first!")
18
17
 
@@ -27,6 +27,11 @@ import com.twilio.audioswitch.AudioDevice
27
27
  import expo.modules.kotlin.AppContext
28
28
  import expo.modules.kotlin.Promise
29
29
  import expo.modules.kotlin.exception.CodedException
30
+ import io.fishjam.reactnative.foregroundService.ForegroundServiceManager
31
+ import io.fishjam.reactnative.managers.LocalCameraTracksChangedListenersManager
32
+ import io.fishjam.reactnative.managers.LocalTracksSwitchListenersManager
33
+ import io.fishjam.reactnative.managers.TracksUpdateListenersManager
34
+ import io.fishjam.reactnative.utils.PermissionUtils
30
35
  import kotlinx.coroutines.CoroutineScope
31
36
  import kotlinx.coroutines.Dispatchers
32
37
  import kotlinx.coroutines.SupervisorJob
@@ -70,24 +75,15 @@ class RNFishjamClient(
70
75
  emitEvent(event, mapOf(event.name to value))
71
76
  }
72
77
 
73
- interface OnTrackUpdateListener {
74
- fun onTracksUpdate()
75
- }
78
+ private var foregroundServiceManager: ForegroundServiceManager? = null
76
79
 
77
- interface OnLocalTrackSwitchListener {
78
- suspend fun onLocalTrackWillSwitch()
80
+ companion object {
81
+ val trackUpdateListenersManager = TracksUpdateListenersManager()
79
82
 
80
- suspend fun onLocalTrackSwitched()
81
- }
83
+ val localTracksSwitchListenerManager = LocalTracksSwitchListenersManager()
82
84
 
83
- interface OnLocalCameraTrackChangedListener {
84
- fun onLocalCameraTrackChanged()
85
- }
85
+ val localCameraTracksChangedListenersManager = LocalCameraTracksChangedListenersManager()
86
86
 
87
- companion object {
88
- var onTracksUpdateListeners: MutableList<OnTrackUpdateListener> = mutableListOf()
89
- val onLocalTrackSwitchListener: MutableList<OnLocalTrackSwitchListener> = mutableListOf()
90
- var localCameraTrackListeners: MutableList<OnLocalCameraTrackChangedListener> = mutableListOf()
91
87
  lateinit var fishjamClient: FishjamClient
92
88
 
93
89
  fun getAllPeers(): List<Peer> {
@@ -253,12 +249,12 @@ class RNFishjamClient(
253
249
  ) {
254
250
  peerStatus = PeerStatus.connecting
255
251
  connectPromise = promise
256
- localUserMetadata = peerMetadata
252
+ localUserMetadata = mapOf("server" to emptyMap(), "peer" to peerMetadata)
257
253
  fishjamClient.connect(
258
254
  com.fishjamcloud.client.ConnectConfig(
259
255
  url,
260
256
  peerToken,
261
- peerMetadata,
257
+ localUserMetadata,
262
258
  ReconnectConfig(
263
259
  config.reconnectConfig.maxAttempts,
264
260
  config.reconnectConfig.initialDelayMs,
@@ -287,6 +283,11 @@ class RNFishjamClient(
287
283
  emitWarning("Camera already started. You may only call startCamera once before leaveRoom is called.")
288
284
  return
289
285
  }
286
+ if (!PermissionUtils.requestCameraPermission(appContext)) {
287
+ emitWarning("Camera permission not granted.")
288
+ return
289
+ }
290
+
290
291
  val cameraTrack = createCameraTrack(config)
291
292
  setCameraTrackState(cameraTrack, config.cameraEnabled)
292
293
  emitEndpoints()
@@ -311,7 +312,7 @@ class RNFishjamClient(
311
312
  isCameraOn = isEnabled
312
313
  val event = EmitableEvents.IsCameraOn
313
314
  emitEvent(event, mapOf(event.name to isEnabled))
314
- localCameraTrackListeners.forEach { it.onLocalCameraTrackChanged() }
315
+ localCameraTracksChangedListenersManager.notifyListeners()
315
316
  }
316
317
 
317
318
  fun toggleCamera(): Boolean {
@@ -322,21 +323,24 @@ class RNFishjamClient(
322
323
 
323
324
  suspend fun flipCamera() {
324
325
  ensureVideoTrack()
325
- onLocalTrackSwitchListener.forEach { it.onLocalTrackWillSwitch() }
326
+ localTracksSwitchListenerManager.notifyWillSwitch()
326
327
  getLocalVideoTrack()?.flipCamera()
327
- onLocalTrackSwitchListener.forEach { it.onLocalTrackSwitched() }
328
+ localTracksSwitchListenerManager.notifySwitched()
328
329
  }
329
330
 
330
331
  suspend fun switchCamera(cameraId: String) {
331
332
  ensureVideoTrack()
332
- onLocalTrackSwitchListener.forEach { it.onLocalTrackWillSwitch() }
333
+ localTracksSwitchListenerManager.notifyWillSwitch()
333
334
  getLocalVideoTrack()?.switchCamera(cameraId)
334
- onLocalTrackSwitchListener.forEach {
335
- it.onLocalTrackSwitched()
336
- }
335
+ localTracksSwitchListenerManager.notifySwitched()
337
336
  }
338
337
 
339
338
  private suspend fun startMicrophone() {
339
+ if (!PermissionUtils.requestMicrophonePermission(appContext)) {
340
+ emitWarning("Microphone permission not granted.")
341
+ return
342
+ }
343
+
340
344
  val microphoneTrack = fishjamClient.createAudioTrack(emptyMap())
341
345
  setMicrophoneTrackState(microphoneTrack, true)
342
346
  emitEndpoints()
@@ -376,6 +380,24 @@ class RNFishjamClient(
376
380
  }
377
381
  }
378
382
 
383
+ fun configureForegroundService(config: ForegroundServiceNotificationConfig) {
384
+ if (foregroundServiceManager == null) {
385
+ foregroundServiceManager = ForegroundServiceManager(appContext!!, config)
386
+ }
387
+ }
388
+
389
+ suspend fun startForegroundService(config: ForegroundServicePermissionsConfig) {
390
+ if (foregroundServiceManager == null) {
391
+ throw CodedException("Foreground service not configured.")
392
+ }
393
+
394
+ foregroundServiceManager?.startForegroundService(config)
395
+ }
396
+
397
+ fun stopForegroundService() {
398
+ foregroundServiceManager?.stopForegroundService()
399
+ }
400
+
379
401
  suspend fun toggleScreenShare(screenShareOptions: ScreenShareOptions) {
380
402
  this.screenShareMetadata = screenShareOptions.screenShareMetadata
381
403
  this.screenShareQuality = screenShareOptions.quality
@@ -549,7 +571,8 @@ class RNFishjamClient(
549
571
  fun toggleScreenShareTrackEncoding(encoding: String): Map<String, Any> {
550
572
  ensureScreenShareTrack()
551
573
  getLocalScreenShareTrack()?.let {
552
- screenShareSimulcastConfig = toggleTrackEncoding(encoding, it.id(), screenShareSimulcastConfig)
574
+ screenShareSimulcastConfig =
575
+ toggleTrackEncoding(encoding, it.id(), screenShareSimulcastConfig)
553
576
  }
554
577
  return getSimulcastConfigAsRNMap(screenShareSimulcastConfig)
555
578
  }
@@ -743,10 +766,12 @@ class RNFishjamClient(
743
766
  event: EmitableEvents,
744
767
  data: Map<String, Any?> = mapOf()
745
768
  ) {
746
- sendEvent(event.name, data)
769
+ CoroutineScope(Dispatchers.Main).launch {
770
+ sendEvent(event.name, data)
771
+ }
747
772
  }
748
773
 
749
- private fun emitWarning(warning: String) {
774
+ fun emitWarning(warning: String) {
750
775
  emitEvent(EmitableEvents.Warning, mapOf("message" to warning))
751
776
  }
752
777
 
@@ -822,7 +847,7 @@ class RNFishjamClient(
822
847
 
823
848
  private fun addOrUpdateTrack(track: Track) {
824
849
  emitEndpoints()
825
- onTracksUpdateListeners.forEach { it.onTracksUpdate() }
850
+ trackUpdateListenersManager.notifyListeners()
826
851
  }
827
852
 
828
853
  override fun onTrackReady(track: Track) {
@@ -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,24 +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
101
+
102
+ @Field
103
+ val enableMicrophone: Boolean = false
99
104
 
100
105
  @Field
101
- val foregroundServiceTypes: IntArray = intArrayOf()
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(EmitableEvents.allEvents)
112
+ val mutex = Mutex()
110
113
 
111
114
  val rnFishjamClient =
112
115
  RNFishjamClient { name: String, data: Map<String, Any?> ->
113
116
  sendEvent(name, data)
114
117
  }
115
118
 
116
- val mutex = Mutex()
119
+ Name("RNFishjamClient")
120
+
121
+ Events(EmitableEvents.allEvents)
117
122
 
118
123
  OnCreate {
119
124
  rnFishjamClient.onModuleCreate(appContext)
@@ -131,6 +136,26 @@ class RNFishjamClientModule : Module() {
131
136
  rnFishjamClient.onActivityResult(result.requestCode, result.resultCode, result.data)
132
137
  }
133
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
+
134
159
  AsyncFunction(
135
160
  "joinRoom"
136
161
  ) { url: String, peerToken: String, peerMetadata: Map<String, Any>, config: ConnectConfig, promise: Promise ->
@@ -151,20 +176,12 @@ class RNFishjamClientModule : Module() {
151
176
  }
152
177
  }
153
178
 
154
- Property("isMicrophoneOn") {
155
- return@Property rnFishjamClient.isMicrophoneOn
156
- }
157
-
158
179
  AsyncFunction("toggleMicrophone") Coroutine { ->
159
180
  withContext(Dispatchers.Main) {
160
181
  rnFishjamClient.toggleMicrophone()
161
182
  }
162
183
  }
163
184
 
164
- Property("isCameraOn") {
165
- return@Property rnFishjamClient.isCameraOn
166
- }
167
-
168
185
  AsyncFunction("toggleCamera") Coroutine { ->
169
186
  withContext(Dispatchers.Main) {
170
187
  rnFishjamClient.toggleCamera()
@@ -199,14 +216,6 @@ class RNFishjamClientModule : Module() {
199
216
  }
200
217
  }
201
218
 
202
- Property("cameras") {
203
- return@Property rnFishjamClient.getCaptureDevices()
204
- }
205
-
206
- Property("isScreenShareOn") {
207
- return@Property rnFishjamClient.isScreenShareOn
208
- }
209
-
210
219
  AsyncFunction("getPeers") Coroutine { ->
211
220
  withContext(Dispatchers.Main) {
212
221
  rnFishjamClient.getPeers()
@@ -299,64 +308,16 @@ class RNFishjamClientModule : Module() {
299
308
 
300
309
  AsyncFunction("getStatistics") { rnFishjamClient.getStatistics() }
301
310
 
302
- Function("startForegroundService") { config: ForegroundServiceConfig ->
303
- if (appContext.reactContext == null) {
304
- throw CodedException(message = "reactContext not found")
305
- }
306
-
307
- val channelId =
308
- config.channelId
309
- ?: throw CodedException(message = "Missing `channelId` for startForegroundService")
310
- val channelName =
311
- config.channelName
312
- ?: throw CodedException(message = "Missing `channelName` for startForegroundService")
313
- val notificationContent =
314
- config.notificationContent
315
- ?: throw CodedException(message = "Missing `notificationContent` for startForegroundService")
316
- val notificationTitle =
317
- config.notificationTitle
318
- ?: throw CodedException(message = "Missing `notificationTitle` for startForegroundService")
319
- val foregroundServiceTypes = config.foregroundServiceTypes
320
-
321
- if (foregroundServiceTypes.isEmpty()) {
322
- throw CodedException(message = "`foregroundServiceTypes` cannot be empty")
323
- }
324
-
325
- val serviceIntent =
326
- Intent(
327
- appContext.reactContext,
328
- FishjamForegroundService::class.java
329
- )
330
-
331
- serviceIntent.putExtra("channelId", channelId)
332
- serviceIntent.putExtra("channelName", channelName)
333
- serviceIntent.putExtra("notificationTitle", notificationContent)
334
- serviceIntent.putExtra("notificationContent", notificationTitle)
335
- serviceIntent.putExtra("foregroundServiceTypes", foregroundServiceTypes)
336
-
337
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
338
- appContext.reactContext!!.startForegroundService(serviceIntent)
339
- } else {
340
- appContext.reactContext!!.startService(serviceIntent)
341
- }
311
+ Function("configureForegroundService") { config: ForegroundServiceNotificationConfig ->
312
+ return@Function rnFishjamClient.configureForegroundService(config)
342
313
  }
343
314
 
344
- Function("stopForegroundService") {
345
- if (appContext.reactContext == null) {
346
- throw CodedException(message = "reactContext not found")
347
- }
348
-
349
- val serviceIntent: Intent =
350
- Intent(
351
- appContext.reactContext,
352
- FishjamForegroundService::class.java
353
- )
354
-
355
- appContext.reactContext!!.stopService(serviceIntent)
315
+ AsyncFunction("startForegroundService") Coroutine { config: ForegroundServicePermissionsConfig ->
316
+ rnFishjamClient.startForegroundService(config)
356
317
  }
357
318
 
358
- Property("peerStatus") {
359
- return@Property rnFishjamClient.peerStatus
319
+ Function("stopForegroundService") {
320
+ rnFishjamClient.stopForegroundService()
360
321
  }
361
322
  }
362
323
  }
@@ -4,12 +4,13 @@ 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
12
  ) : VideoView(context, appContext),
12
- RNFishjamClient.OnLocalCameraTrackChangedListener {
13
+ LocalCameraTrackChangedListener {
13
14
  private var localVideoTrack: LocalVideoTrack? = null
14
15
 
15
16
  private fun trySetLocalCameraTrack() {
@@ -31,13 +32,13 @@ class VideoPreviewView(
31
32
  }
32
33
 
33
34
  override fun onDetachedFromWindow() {
34
- RNFishjamClient.localCameraTrackListeners.remove(this)
35
+ RNFishjamClient.localCameraTracksChangedListenersManager.remove(this)
35
36
  super.onDetachedFromWindow()
36
37
  dispose()
37
38
  }
38
39
 
39
40
  override fun onAttachedToWindow() {
40
- RNFishjamClient.localCameraTrackListeners.add(this)
41
+ RNFishjamClient.localCameraTracksChangedListenersManager.add(this)
41
42
  super.onAttachedToWindow()
42
43
  initialize()
43
44
  }
@@ -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")