@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.
- package/android/src/main/java/io/fishjam/reactnative/PipContainerView.kt +282 -0
- package/android/src/main/java/io/fishjam/reactnative/PipContainerViewModule.kt +41 -0
- package/android/src/main/java/io/fishjam/reactnative/PipTrackSelector.kt +98 -0
- package/android/src/main/java/io/fishjam/reactnative/PipViewFactory.kt +186 -0
- package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +9 -1
- package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +2 -0
- package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +2 -0
- package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +152 -11
- package/android/src/main/java/io/fishjam/reactnative/helpers/PictureInPictureHelperFragment.kt +20 -0
- package/android/src/main/java/io/fishjam/reactnative/managers/ListenerManager.kt +3 -1
- package/build/RNFishjamClientModule.d.ts +2 -0
- package/build/RNFishjamClientModule.d.ts.map +1 -1
- package/build/RNFishjamClientModule.js.map +1 -1
- package/build/components/FishjamRoom/index.d.ts.map +1 -1
- package/build/components/FishjamRoom/index.js +0 -1
- package/build/components/FishjamRoom/index.js.map +1 -1
- package/build/components/PipContainerView.d.ts +48 -0
- package/build/components/PipContainerView.d.ts.map +1 -0
- package/build/components/PipContainerView.js +34 -0
- package/build/components/PipContainerView.js.map +1 -0
- package/build/hooks/useAppScreenShare.d.ts +0 -2
- package/build/hooks/useAppScreenShare.d.ts.map +1 -1
- package/build/hooks/useAppScreenShare.js +0 -8
- package/build/hooks/useAppScreenShare.js.map +1 -1
- package/build/hooks/useCamera.d.ts +2 -2
- package/build/hooks/useCamera.d.ts.map +1 -1
- package/build/hooks/useCamera.js +1 -3
- package/build/hooks/useCamera.js.map +1 -1
- package/build/hooks/useScreenShare.d.ts +2 -3
- package/build/hooks/useScreenShare.d.ts.map +1 -1
- package/build/hooks/useScreenShare.js +0 -7
- package/build/hooks/useScreenShare.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/livestream/components/LivestreamStreamer.d.ts +3 -30
- package/build/livestream/components/LivestreamStreamer.d.ts.map +1 -1
- package/build/livestream/components/LivestreamStreamer.js +3 -27
- package/build/livestream/components/LivestreamStreamer.js.map +1 -1
- package/build/livestream/components/LivestreamViewer.d.ts +2 -7
- package/build/livestream/components/LivestreamViewer.d.ts.map +1 -1
- package/build/livestream/components/LivestreamViewer.js +1 -1
- package/build/livestream/components/LivestreamViewer.js.map +1 -1
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts +49 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts.map +1 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js +58 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js.map +1 -0
- package/build/livestream/hooks/useLivestreamStreamer.d.ts +40 -3
- package/build/livestream/hooks/useLivestreamStreamer.d.ts.map +1 -1
- package/build/livestream/hooks/useLivestreamStreamer.js +44 -3
- package/build/livestream/hooks/useLivestreamStreamer.js.map +1 -1
- package/build/livestream/hooks/useLivestreamViewer.d.ts +7 -0
- package/build/livestream/hooks/useLivestreamViewer.d.ts.map +1 -1
- package/build/livestream/hooks/useLivestreamViewer.js +39 -11
- package/build/livestream/hooks/useLivestreamViewer.js.map +1 -1
- package/build/livestream/index.d.ts +2 -1
- package/build/livestream/index.d.ts.map +1 -1
- package/build/livestream/index.js +2 -1
- package/build/livestream/index.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/PictureInPictureManager.swift +215 -0
- package/ios/PipContainerView.swift +56 -0
- package/ios/PipContainerViewModule.swift +48 -0
- package/ios/RNFishjamClient.swift +1 -0
- package/ios/RNFishjamClientModule.swift +4 -3
- package/package.json +2 -2
- package/plugin/broadcastExtensionFiles/FishjamBroadcastSampleHandler.swift +1 -1
- package/plugin/broadcastExtensionFiles/Info.plist +1 -1
- package/plugin/build/types.d.ts +1 -0
- package/plugin/build/withFishjam.js +2 -2
- package/plugin/build/withFishjamAndroid.js +44 -5
- 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
|
|