@fishjam-cloud/react-native-client 0.23.0-RC.0 → 0.24.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 (73) hide show
  1. package/android/src/main/java/io/fishjam/reactnative/PipContainerView.kt +282 -0
  2. package/android/src/main/java/io/fishjam/reactnative/PipContainerViewModule.kt +41 -0
  3. package/android/src/main/java/io/fishjam/reactnative/PipTrackSelector.kt +98 -0
  4. package/android/src/main/java/io/fishjam/reactnative/PipViewFactory.kt +186 -0
  5. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +9 -1
  6. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +2 -0
  7. package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +2 -0
  8. package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +152 -11
  9. package/android/src/main/java/io/fishjam/reactnative/helpers/PictureInPictureHelperFragment.kt +20 -0
  10. package/android/src/main/java/io/fishjam/reactnative/managers/ListenerManager.kt +3 -1
  11. package/build/RNFishjamClientModule.d.ts +2 -0
  12. package/build/RNFishjamClientModule.d.ts.map +1 -1
  13. package/build/RNFishjamClientModule.js.map +1 -1
  14. package/build/components/FishjamRoom/index.d.ts.map +1 -1
  15. package/build/components/FishjamRoom/index.js +0 -1
  16. package/build/components/FishjamRoom/index.js.map +1 -1
  17. package/build/components/PipContainerView.d.ts +48 -0
  18. package/build/components/PipContainerView.d.ts.map +1 -0
  19. package/build/components/PipContainerView.js +34 -0
  20. package/build/components/PipContainerView.js.map +1 -0
  21. package/build/hooks/useAppScreenShare.d.ts +0 -2
  22. package/build/hooks/useAppScreenShare.d.ts.map +1 -1
  23. package/build/hooks/useAppScreenShare.js +0 -8
  24. package/build/hooks/useAppScreenShare.js.map +1 -1
  25. package/build/hooks/useCamera.d.ts +2 -2
  26. package/build/hooks/useCamera.d.ts.map +1 -1
  27. package/build/hooks/useCamera.js +1 -3
  28. package/build/hooks/useCamera.js.map +1 -1
  29. package/build/hooks/useScreenShare.d.ts +2 -3
  30. package/build/hooks/useScreenShare.d.ts.map +1 -1
  31. package/build/hooks/useScreenShare.js +0 -7
  32. package/build/hooks/useScreenShare.js.map +1 -1
  33. package/build/index.d.ts +2 -0
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +1 -0
  36. package/build/index.js.map +1 -1
  37. package/build/livestream/components/LivestreamStreamer.d.ts +3 -30
  38. package/build/livestream/components/LivestreamStreamer.d.ts.map +1 -1
  39. package/build/livestream/components/LivestreamStreamer.js +3 -27
  40. package/build/livestream/components/LivestreamStreamer.js.map +1 -1
  41. package/build/livestream/components/LivestreamViewer.d.ts +2 -7
  42. package/build/livestream/components/LivestreamViewer.d.ts.map +1 -1
  43. package/build/livestream/components/LivestreamViewer.js +1 -1
  44. package/build/livestream/components/LivestreamViewer.js.map +1 -1
  45. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts +49 -0
  46. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts.map +1 -0
  47. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js +58 -0
  48. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js.map +1 -0
  49. package/build/livestream/hooks/useLivestreamStreamer.d.ts +40 -3
  50. package/build/livestream/hooks/useLivestreamStreamer.d.ts.map +1 -1
  51. package/build/livestream/hooks/useLivestreamStreamer.js +44 -3
  52. package/build/livestream/hooks/useLivestreamStreamer.js.map +1 -1
  53. package/build/livestream/hooks/useLivestreamViewer.d.ts +7 -0
  54. package/build/livestream/hooks/useLivestreamViewer.d.ts.map +1 -1
  55. package/build/livestream/hooks/useLivestreamViewer.js +39 -11
  56. package/build/livestream/hooks/useLivestreamViewer.js.map +1 -1
  57. package/build/livestream/index.d.ts +2 -1
  58. package/build/livestream/index.d.ts.map +1 -1
  59. package/build/livestream/index.js +2 -1
  60. package/build/livestream/index.js.map +1 -1
  61. package/expo-module.config.json +4 -2
  62. package/ios/PictureInPictureManager.swift +215 -0
  63. package/ios/PipContainerView.swift +56 -0
  64. package/ios/PipContainerViewModule.swift +48 -0
  65. package/ios/RNFishjamClient.swift +1 -0
  66. package/ios/RNFishjamClientModule.swift +4 -3
  67. package/package.json +2 -2
  68. package/plugin/broadcastExtensionFiles/FishjamBroadcastSampleHandler.swift +1 -1
  69. package/plugin/broadcastExtensionFiles/Info.plist +1 -1
  70. package/plugin/build/types.d.ts +1 -0
  71. package/plugin/build/withFishjam.js +2 -2
  72. package/plugin/build/withFishjamAndroid.js +44 -5
  73. package/plugin/build/withFishjamIos.js +7 -2
@@ -0,0 +1,282 @@
1
+ package io.fishjam.reactnative
2
+
3
+ import android.app.PictureInPictureParams
4
+ import android.content.Context
5
+ import android.os.Build
6
+ import android.util.Log
7
+ import android.view.View
8
+ import android.view.ViewGroup
9
+ import androidx.annotation.RequiresApi
10
+ import androidx.fragment.app.FragmentActivity
11
+ import expo.modules.kotlin.AppContext
12
+ import expo.modules.kotlin.views.ExpoView
13
+ import io.fishjam.reactnative.helpers.PictureInPictureHelperFragment
14
+ import io.fishjam.reactnative.managers.TrackUpdateListener
15
+ import java.lang.ref.WeakReference
16
+
17
+ data class RemoteTrackInfo(
18
+ val videoTrack: com.fishjamcloud.client.media.VideoTrack?,
19
+ val displayName: String?,
20
+ val videoTrackActive: Boolean
21
+ )
22
+
23
+ class PipContainerView(
24
+ context: Context,
25
+ appContext: AppContext
26
+ ) : ExpoView(context, appContext), TrackUpdateListener {
27
+
28
+ companion object {
29
+ private const val TAG = "PipContainerView"
30
+ }
31
+
32
+ private val activityRef: WeakReference<FragmentActivity?> = WeakReference(
33
+ appContext.currentActivity as? FragmentActivity
34
+ )
35
+
36
+ private val decorView = activityRef.get()?.window?.decorView
37
+ private val rootView = decorView?.findViewById<ViewGroup>(android.R.id.content)
38
+ private val rootViewChildrenOriginalVisibility: ArrayList<Int> = arrayListOf()
39
+
40
+ private var pictureInPictureHelperTag: String? = null
41
+ private var pipViews: PipViewContainer? = null
42
+ private var isPipActive = false
43
+
44
+ private val trackSelector = PipTrackSelector()
45
+ private val viewFactory by lazy { PipViewFactory(context, appContext) }
46
+
47
+ @RequiresApi(Build.VERSION_CODES.O)
48
+ private val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
49
+
50
+ var startAutomatically: Boolean = true
51
+ set(value) {
52
+ field = value
53
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
54
+ updateAutoEnterEnabled()
55
+ }
56
+ }
57
+
58
+ var stopAutomatically: Boolean = true
59
+
60
+ var primaryPlaceholderText: String = "No camera"
61
+ set(value) {
62
+ field = value
63
+ pipViews?.primaryPlaceholder?.text = value
64
+ }
65
+
66
+ var secondaryPlaceholderText: String = "No active speaker"
67
+ set(value) {
68
+ field = value
69
+ pipViews?.secondaryPlaceholder?.text = value
70
+ }
71
+
72
+ private var remoteTrackInfo: RemoteTrackInfo? = null
73
+
74
+ @RequiresApi(Build.VERSION_CODES.O)
75
+ private fun updatePictureInPictureParams() {
76
+ val activity = activityRef.get()
77
+ if (activity == null) {
78
+ Log.w(TAG, "Cannot update PiP params: activity reference is null")
79
+ return
80
+ }
81
+
82
+ try {
83
+ activity.setPictureInPictureParams(pictureInPictureParamsBuilder.build())
84
+ } catch (e: IllegalStateException) {
85
+ Log.e(TAG, "Failed to update PiP params", e)
86
+ emitPipNotSupportedWarning()
87
+ }
88
+ }
89
+
90
+ private fun emitPipNotSupportedWarning() {
91
+ RNFishjamClient.sendEvent(
92
+ EmitableEvent.warning(
93
+ "Picture-in-Picture is not supported. Enable it in app.json."
94
+ )
95
+ )
96
+ }
97
+
98
+ @RequiresApi(Build.VERSION_CODES.S)
99
+ private fun updateAutoEnterEnabled() {
100
+ pictureInPictureParamsBuilder.setAutoEnterEnabled(startAutomatically)
101
+ updatePictureInPictureParams()
102
+ }
103
+
104
+ private fun updatePipViews() {
105
+ val views = pipViews ?: run {
106
+ Log.w(TAG, "Cannot update PiP views: pipViews is null")
107
+ return
108
+ }
109
+
110
+ val localCameraTrack = trackSelector.findLocalCameraTrack()
111
+ updatePrimaryView(views, localCameraTrack)
112
+
113
+ remoteTrackInfo = trackSelector.findSecondaryRemoteTrack(remoteTrackInfo)
114
+ updateSecondaryView(views, remoteTrackInfo)
115
+ }
116
+
117
+ private fun updatePrimaryView(
118
+ views: PipViewContainer,
119
+ localCameraTrack: com.fishjamcloud.client.media.VideoTrack?
120
+ ) {
121
+ if (localCameraTrack != null && localCameraTrack.isEnabled()) {
122
+ views.primaryVideoView.init(localCameraTrack.id())
123
+ views.primaryVideoView.visibility = View.VISIBLE
124
+ views.primaryPlaceholder.visibility = View.GONE
125
+ } else {
126
+ views.primaryVideoView.visibility = View.GONE
127
+ views.primaryPlaceholder.visibility = View.VISIBLE
128
+ }
129
+ }
130
+
131
+ private fun updateSecondaryView(
132
+ views: PipViewContainer,
133
+ trackInfo: RemoteTrackInfo?
134
+ ) {
135
+ trackInfo?.let { info ->
136
+ if (info.videoTrackActive && info.videoTrack != null) {
137
+ views.secondaryVideoView.init(info.videoTrack.id())
138
+ views.secondaryVideoView.visibility = View.VISIBLE
139
+ views.secondaryPlaceholder.visibility = View.GONE
140
+
141
+ info.displayName?.let { displayName ->
142
+ views.secondaryNameOverlay.text = displayName
143
+ views.secondaryNameOverlay.visibility = View.VISIBLE
144
+ } ?: run {
145
+ views.secondaryNameOverlay.visibility = View.GONE
146
+ }
147
+ } else {
148
+ views.secondaryVideoView.visibility = View.GONE
149
+ views.secondaryPlaceholder.text = info.displayName
150
+ views.secondaryPlaceholder.visibility = View.VISIBLE
151
+ views.secondaryNameOverlay.visibility = View.GONE
152
+ }
153
+ } ?: run {
154
+ views.secondaryVideoView.visibility = View.GONE
155
+ views.secondaryPlaceholder.text = secondaryPlaceholderText
156
+ views.secondaryPlaceholder.visibility = View.VISIBLE
157
+ views.secondaryNameOverlay.visibility = View.GONE
158
+ }
159
+ views.secondaryContainer.visibility = View.VISIBLE
160
+ }
161
+
162
+ @RequiresApi(Build.VERSION_CODES.O)
163
+ fun startPictureInPicture() {
164
+ val activity = activityRef.get()
165
+ if (activity == null) {
166
+ Log.w(TAG, "Cannot start PiP: activity reference is null")
167
+ return
168
+ }
169
+
170
+ try {
171
+ activity.enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
172
+ } catch (e: IllegalStateException) {
173
+ Log.e(TAG, "Failed to enter PiP mode", e)
174
+ emitPipNotSupportedWarning()
175
+ }
176
+ }
177
+
178
+ fun stopPictureInPicture() {
179
+ Log.d(TAG, "stopPictureInPicture called, but no-op on Android")
180
+ }
181
+
182
+ override fun onAttachedToWindow() {
183
+ super.onAttachedToWindow()
184
+
185
+ activityRef.get()?.let { activity ->
186
+ val fragment = PictureInPictureHelperFragment(this)
187
+ pictureInPictureHelperTag = fragment.id
188
+ activity.supportFragmentManager.beginTransaction()
189
+ .add(fragment, fragment.id)
190
+ .commitAllowingStateLoss()
191
+ } ?: Log.w(TAG, "Cannot attach PiP helper: activity reference is null")
192
+
193
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
194
+ updateAutoEnterEnabled()
195
+ }
196
+
197
+ RNFishjamClient.trackUpdateListenersManager.add(this)
198
+ }
199
+
200
+ override fun onDetachedFromWindow() {
201
+ super.onDetachedFromWindow()
202
+
203
+ activityRef.get()?.let { activity ->
204
+ pictureInPictureHelperTag?.let { tag ->
205
+ activity.supportFragmentManager.findFragmentByTag(tag)?.let { fragment ->
206
+ activity.supportFragmentManager.beginTransaction()
207
+ .remove(fragment)
208
+ .commitAllowingStateLoss()
209
+ }
210
+ }
211
+ }
212
+
213
+ startAutomatically = false
214
+ RNFishjamClient.trackUpdateListenersManager.remove(this)
215
+ }
216
+
217
+ override fun onTracksUpdate() {
218
+ if (isPipActive) {
219
+ updatePipViews()
220
+ }
221
+ }
222
+
223
+ fun layoutForPiPEnter() {
224
+ val root = rootView
225
+ if (root == null) {
226
+ Log.w(TAG, "Cannot enter PiP layout: rootView is null")
227
+ return
228
+ }
229
+
230
+ isPipActive = true
231
+
232
+ hideAllRootViewChildren(root)
233
+ pipViews = viewFactory.createPipViews(primaryPlaceholderText, secondaryPlaceholderText)
234
+ pipViews?.let { views ->
235
+ root.addView(views.splitScreenContainer)
236
+ }
237
+
238
+ updatePipViews()
239
+ }
240
+
241
+ fun layoutForPiPExit() {
242
+ val root = rootView
243
+ if (root == null) {
244
+ Log.w(TAG, "Cannot exit PiP layout: rootView is null")
245
+ return
246
+ }
247
+
248
+ isPipActive = false
249
+
250
+ pipViews?.let { views ->
251
+ root.removeView(views.splitScreenContainer)
252
+ cleanUpViews(views)
253
+ }
254
+ pipViews = null
255
+
256
+ restoreRootViewChildren(root)
257
+ }
258
+
259
+ private fun cleanUpViews(views: PipViewContainer) {
260
+ views.primaryVideoView.dispose()
261
+ views.secondaryVideoView.dispose()
262
+ }
263
+
264
+ private fun hideAllRootViewChildren(rootView: ViewGroup) {
265
+ rootViewChildrenOriginalVisibility.clear()
266
+ (0 until rootView.childCount).forEach { i ->
267
+ val child = rootView.getChildAt(i)
268
+ rootViewChildrenOriginalVisibility.add(child.visibility)
269
+ child.visibility = View.GONE
270
+ }
271
+ }
272
+
273
+ private fun restoreRootViewChildren(rootView: ViewGroup) {
274
+ rootViewChildrenOriginalVisibility.forEachIndexed { index, originalVisibility ->
275
+ if (index < rootView.childCount) {
276
+ rootView.getChildAt(index).visibility = originalVisibility
277
+ }
278
+ }
279
+ rootViewChildrenOriginalVisibility.clear()
280
+ }
281
+ }
282
+
@@ -0,0 +1,41 @@
1
+ package io.fishjam.reactnative
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class PipContainerViewModule : Module() {
7
+ override fun definition() =
8
+ ModuleDefinition {
9
+ Name("PipContainerViewModule")
10
+
11
+ View(PipContainerView::class) {
12
+ Prop("startAutomatically") { view: PipContainerView, value: Boolean ->
13
+ view.startAutomatically = value
14
+ }
15
+
16
+ Prop("stopAutomatically") { view: PipContainerView, value: Boolean ->
17
+ view.stopAutomatically = value
18
+ }
19
+
20
+ // iOS-only property, no-op on Android
21
+ Prop("allowsCameraInBackground") { _: PipContainerView, _: Boolean -> }
22
+
23
+ Prop("primaryPlaceholderText") { view: PipContainerView, value: String ->
24
+ view.primaryPlaceholderText = value
25
+ }
26
+
27
+ Prop("secondaryPlaceholderText") { view: PipContainerView, value: String ->
28
+ view.secondaryPlaceholderText = value
29
+ }
30
+
31
+ AsyncFunction("startPictureInPicture") { view: PipContainerView ->
32
+ view.startPictureInPicture()
33
+ }
34
+
35
+ AsyncFunction("stopPictureInPicture") { view: PipContainerView ->
36
+ view.stopPictureInPicture()
37
+ }
38
+ }
39
+ }
40
+ }
41
+
@@ -0,0 +1,98 @@
1
+ package io.fishjam.reactnative
2
+
3
+ import com.fishjamcloud.client.media.RemoteAudioTrack
4
+ import com.fishjamcloud.client.media.Track
5
+ import com.fishjamcloud.client.media.VideoTrack
6
+ import com.fishjamcloud.client.models.Peer
7
+ import fishjam.media_events.server.Server
8
+
9
+ class PipTrackSelector {
10
+ companion object {
11
+ private const val METADATA_TYPE = "type"
12
+ private const val METADATA_CAMERA = "camera"
13
+ private const val METADATA_DISPLAY_NAME = "displayName"
14
+ private const val METADATA_NAME = "name"
15
+ }
16
+
17
+ fun findLocalCameraTrack(): VideoTrack? {
18
+ val peers = RNFishjamClient.getAllPeers()
19
+ val localEndpointId = RNFishjamClient.fishjamClient.getLocalEndpoint().id
20
+ val localPeer = peers.find { it.id == localEndpointId } ?: return null
21
+
22
+ return localPeer.tracks.values
23
+ .filterIsInstance<VideoTrack>()
24
+ .firstOrNull { track ->
25
+ (track.metadata as? Map<*, *>)?.get(METADATA_TYPE) == METADATA_CAMERA
26
+ }
27
+ }
28
+
29
+ fun findSecondaryRemoteTrack(currentTrackInfo: RemoteTrackInfo?): RemoteTrackInfo? {
30
+ val peers = RNFishjamClient.getAllPeers()
31
+ val localEndpointId = RNFishjamClient.fishjamClient.getLocalEndpoint().id
32
+ val remotePeers = peers.filter { it.id != localEndpointId }
33
+
34
+ if (remotePeers.isEmpty()) {
35
+ return null
36
+ }
37
+
38
+ remotePeers.firstOrNull { it.hasActiveVoiceActivity() }?.let { activeSpeaker ->
39
+ return activeSpeaker.toRemoteTrackInfo()
40
+ }
41
+
42
+ currentTrackInfo?.let { currentInfo ->
43
+ val existingPeer = remotePeers.firstOrNull() { peer -> peer.hasTrack(currentInfo.videoTrack) }
44
+ if (existingPeer != null) {
45
+ return existingPeer.toRemoteTrackInfo()
46
+ }
47
+ }
48
+
49
+ remotePeers.firstOrNull { it.hasVideoTrack() }?.let { peerWithVideo ->
50
+ return peerWithVideo.toRemoteTrackInfo()
51
+ }
52
+
53
+ return null
54
+ }
55
+ }
56
+
57
+ private fun Peer.hasActiveVoiceActivity(): Boolean {
58
+ return tracks.values
59
+ .filterIsInstance<RemoteAudioTrack>()
60
+ .any { it.vadStatus == Server.MediaEvent.VadNotification.Status.STATUS_SPEECH }
61
+ }
62
+
63
+ private fun Peer.hasTrack(videoTrack: VideoTrack?): Boolean {
64
+ return videoTrack != null && tracks.values.contains(videoTrack)
65
+ }
66
+
67
+ private fun Peer.hasVideoTrack(): Boolean {
68
+ return tracks.values.any { it is VideoTrack }
69
+ }
70
+
71
+ private fun Peer.getDisplayName(): String {
72
+ val serverMetadata = metadata?.get("peer") as? Map<*, *>
73
+ val displayName = serverMetadata?.get("displayName") as? String
74
+ return displayName ?: id
75
+ }
76
+
77
+ private fun Track.getTrackActive(): Boolean {
78
+
79
+ if (metadata["active"] as? Boolean == false) {
80
+ return false
81
+ }
82
+
83
+ if (metadata["paused"] as? Boolean == true) {
84
+ return false
85
+ }
86
+
87
+ return true
88
+ }
89
+
90
+ private fun Peer.toRemoteTrackInfo(): RemoteTrackInfo {
91
+ val videoTrack = tracks.values.filterIsInstance<VideoTrack>().firstOrNull()
92
+ return RemoteTrackInfo(
93
+ videoTrack = videoTrack,
94
+ displayName = getDisplayName(),
95
+ videoTrackActive = videoTrack?.getTrackActive() ?: false
96
+ )
97
+ }
98
+
@@ -0,0 +1,186 @@
1
+ package io.fishjam.reactnative
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.text.TextUtils
7
+ import android.util.TypedValue
8
+ import android.view.Gravity
9
+ import android.view.View
10
+ import android.view.ViewGroup
11
+ import android.widget.FrameLayout
12
+ import android.widget.LinearLayout
13
+ import android.widget.RelativeLayout
14
+ import android.widget.TextView
15
+ import expo.modules.kotlin.AppContext
16
+
17
+ data class PipViewContainer(
18
+ val splitScreenContainer: LinearLayout,
19
+ val primaryContainer: RelativeLayout,
20
+ val secondaryContainer: RelativeLayout,
21
+ val primaryVideoView: VideoRendererView,
22
+ val secondaryVideoView: VideoRendererView,
23
+ val primaryPlaceholder: TextView,
24
+ val secondaryPlaceholder: TextView,
25
+ val secondaryNameOverlay: TextView
26
+ )
27
+
28
+ class PipViewFactory(
29
+ private val context: Context,
30
+ private val appContext: AppContext
31
+ ) {
32
+ companion object {
33
+ private const val PLACEHOLDER_COLOR = "#000000"
34
+ private const val PLACEHOLDER_TEXT_COLOR = Color.WHITE
35
+ private const val PLACEHOLDER_TEXT_SIZE_SP = 16f
36
+ private const val VIDEO_LAYOUT_MODE = "FILL"
37
+
38
+ private const val NAME_OVERLAY_TEXT_SIZE_SP = 12f
39
+ private const val NAME_OVERLAY_PADDING_DP = 5
40
+ private const val NAME_OVERLAY_MAX_WIDTH_DP = 80
41
+ private const val NAME_OVERLAY_BG_COLOR = 0x4D000000
42
+ private const val NAME_OVERLAY_CORNER_RADIUS_DP = 5f
43
+ }
44
+
45
+ fun createPipViews(
46
+ primaryPlaceholderText: String,
47
+ secondaryPlaceholderText: String
48
+ ): PipViewContainer {
49
+ val primaryContainer = createRelativeLayoutContainer()
50
+ val secondaryContainer = createRelativeLayoutContainer()
51
+
52
+ val primaryPlaceholder = createPlaceholderTextView(primaryPlaceholderText)
53
+ val secondaryPlaceholder = createPlaceholderTextView(secondaryPlaceholderText)
54
+
55
+ val primaryVideoView = createVideoView()
56
+ val secondaryVideoView = createVideoView()
57
+
58
+ val secondaryNameOverlay = createNameOverlayTextView()
59
+
60
+ primaryContainer.apply {
61
+ addView(primaryPlaceholder, createMatchParentLayoutParams())
62
+ addView(primaryVideoView, createCenteredLayoutParams())
63
+ }
64
+
65
+ secondaryContainer.apply {
66
+ addView(secondaryPlaceholder, createMatchParentLayoutParams())
67
+ addView(secondaryVideoView, createCenteredLayoutParams())
68
+ addView(secondaryNameOverlay, createNameOverlayLayoutParams())
69
+ }
70
+
71
+ val splitScreenContainer = createSplitScreenContainer().apply {
72
+ addView(primaryContainer)
73
+ addView(secondaryContainer)
74
+ }
75
+
76
+ return PipViewContainer(
77
+ splitScreenContainer = splitScreenContainer,
78
+ primaryContainer = primaryContainer,
79
+ secondaryContainer = secondaryContainer,
80
+ primaryVideoView = primaryVideoView,
81
+ secondaryVideoView = secondaryVideoView,
82
+ primaryPlaceholder = primaryPlaceholder,
83
+ secondaryPlaceholder = secondaryPlaceholder,
84
+ secondaryNameOverlay = secondaryNameOverlay
85
+ )
86
+ }
87
+
88
+ private fun createPlaceholderTextView(text: String): TextView {
89
+ return TextView(context).apply {
90
+ this.text = text
91
+ setTextColor(PLACEHOLDER_TEXT_COLOR)
92
+ gravity = Gravity.CENTER
93
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, PLACEHOLDER_TEXT_SIZE_SP)
94
+ setBackgroundColor(Color.parseColor(PLACEHOLDER_COLOR))
95
+ }
96
+ }
97
+
98
+ private fun createVideoView(): VideoRendererView {
99
+ return VideoRendererView(context, appContext).apply {
100
+ setVideoLayout(VIDEO_LAYOUT_MODE)
101
+ visibility = View.GONE
102
+ }
103
+ }
104
+
105
+ private fun createSplitScreenContainer(): LinearLayout {
106
+ return LinearLayout(context).apply {
107
+ orientation = LinearLayout.HORIZONTAL
108
+ layoutParams = FrameLayout.LayoutParams(
109
+ ViewGroup.LayoutParams.MATCH_PARENT,
110
+ ViewGroup.LayoutParams.MATCH_PARENT
111
+ )
112
+ }
113
+ }
114
+
115
+ private fun createRelativeLayoutContainer(): RelativeLayout {
116
+ return RelativeLayout(context).apply {
117
+ layoutParams = LinearLayout.LayoutParams(
118
+ 0,
119
+ ViewGroup.LayoutParams.MATCH_PARENT,
120
+ 1f
121
+ )
122
+ }
123
+ }
124
+
125
+ private fun createMatchParentLayoutParams(): RelativeLayout.LayoutParams {
126
+ return RelativeLayout.LayoutParams(
127
+ ViewGroup.LayoutParams.MATCH_PARENT,
128
+ ViewGroup.LayoutParams.MATCH_PARENT
129
+ )
130
+ }
131
+
132
+ private fun createCenteredLayoutParams(): RelativeLayout.LayoutParams {
133
+ return RelativeLayout.LayoutParams(
134
+ ViewGroup.LayoutParams.MATCH_PARENT,
135
+ ViewGroup.LayoutParams.MATCH_PARENT
136
+ ).apply {
137
+ addRule(RelativeLayout.CENTER_IN_PARENT)
138
+ }
139
+ }
140
+
141
+ private fun createNameOverlayTextView(): TextView {
142
+ val paddingPx = dpToPx(NAME_OVERLAY_PADDING_DP)
143
+ val maxWidthPx = dpToPx(NAME_OVERLAY_MAX_WIDTH_DP)
144
+ val cornerRadiusPx = dpToPx(NAME_OVERLAY_CORNER_RADIUS_DP.toInt()).toFloat()
145
+
146
+ val background = GradientDrawable().apply {
147
+ setColor(NAME_OVERLAY_BG_COLOR.toInt())
148
+ cornerRadii = floatArrayOf(
149
+ cornerRadiusPx, cornerRadiusPx,
150
+ 0f, 0f,
151
+ 0f, 0f,
152
+ 0f, 0f
153
+ )
154
+ }
155
+
156
+ return TextView(context).apply {
157
+ setTextColor(PLACEHOLDER_TEXT_COLOR)
158
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, NAME_OVERLAY_TEXT_SIZE_SP)
159
+ setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
160
+ maxWidth = maxWidthPx
161
+ maxLines = 1
162
+ ellipsize = TextUtils.TruncateAt.END
163
+ setBackground(background)
164
+ visibility = View.GONE
165
+ }
166
+ }
167
+
168
+ private fun createNameOverlayLayoutParams(): RelativeLayout.LayoutParams {
169
+ return RelativeLayout.LayoutParams(
170
+ ViewGroup.LayoutParams.WRAP_CONTENT,
171
+ ViewGroup.LayoutParams.WRAP_CONTENT
172
+ ).apply {
173
+ addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
174
+ addRule(RelativeLayout.ALIGN_PARENT_END)
175
+ }
176
+ }
177
+
178
+ private fun dpToPx(dp: Int): Int {
179
+ return TypedValue.applyDimension(
180
+ TypedValue.COMPLEX_UNIT_DIP,
181
+ dp.toFloat(),
182
+ context.resources.displayMetrics
183
+ ).toInt()
184
+ }
185
+ }
186
+
@@ -905,29 +905,37 @@ class RNFishjamClient(
905
905
  }
906
906
  }
907
907
 
908
- override fun onTrackAdded(track: Track) {}
908
+ override fun onTrackAdded(track: Track) {
909
+ CoroutineScope(Dispatchers.Main).launch {
910
+ trackUpdateListenersManager.notifyListeners()
911
+ }
912
+ }
909
913
 
910
914
  override fun onTrackRemoved(track: Track) {
911
915
  CoroutineScope(Dispatchers.Main).launch {
912
916
  emitEndpoints()
917
+ trackUpdateListenersManager.notifyListeners()
913
918
  }
914
919
  }
915
920
 
916
921
  override fun onTrackUpdated(track: Track) {
917
922
  CoroutineScope(Dispatchers.Main).launch {
918
923
  emitEndpoints()
924
+ trackUpdateListenersManager.notifyListeners()
919
925
  }
920
926
  }
921
927
 
922
928
  override fun onPeerJoined(peer: Peer) {
923
929
  CoroutineScope(Dispatchers.Main).launch {
924
930
  emitEndpoints()
931
+ trackUpdateListenersManager.notifyListeners()
925
932
  }
926
933
  }
927
934
 
928
935
  override fun onPeerLeft(peer: Peer) {
929
936
  CoroutineScope(Dispatchers.Main).launch {
930
937
  emitEndpoints()
938
+ trackUpdateListenersManager.notifyListeners()
931
939
  }
932
940
  }
933
941
 
@@ -1,7 +1,9 @@
1
1
  package io.fishjam.reactnative
2
2
 
3
3
  import android.content.Context
4
+ import android.util.Log
4
5
  import com.fishjamcloud.client.media.LocalVideoTrack
6
+ import com.fishjamcloud.client.models.Dimensions
5
7
  import expo.modules.kotlin.AppContext
6
8
  import io.fishjam.reactnative.managers.LocalCameraTrackChangedListener
7
9
 
@@ -71,4 +71,6 @@ open class VideoRendererView(
71
71
  )
72
72
  )
73
73
  }
74
+
75
+ fun getTrackId(): String? = trackId
74
76
  }