@@ -19,6 +19,12 @@ import fishjam.media_events.server.Server
1919import io.fishjam.reactnative.helpers.PictureInPictureHelperFragment
2020import io.fishjam.reactnative.managers.TrackUpdateListener
2121
22+ data class RemoteTrackInfo (
23+ val videoTrack : com.fishjamcloud.client.media.VideoTrack ? ,
24+ val displayName : String? ,
25+ val hasVideoTrack : Boolean
26+ )
27+
2228class PipContainerView (
2329 context : Context ,
2430 appContext : AppContext
@@ -34,6 +40,8 @@ class PipContainerView(
3440 private var primaryPlaceholder: android.widget.TextView ? = null
3541 private var secondaryPlaceholder: android.widget.TextView ? = null
3642 private var splitScreenContainer: android.widget.LinearLayout ? = null
43+ private var primaryContainer: FrameLayout ? = null
44+ private var secondaryContainer: FrameLayout ? = null
3745
3846 @RequiresApi(Build .VERSION_CODES .O )
3947 private var pictureInPictureParamsBuilder = PictureInPictureParams .Builder ()
@@ -85,78 +93,93 @@ class PipContainerView(
8593 val peers = RNFishjamClient .getAllPeers()
8694 val localEndpointId = RNFishjamClient .fishjamClient.getLocalEndpoint().id
8795 val localPeer = peers.firstOrNull { it.id == localEndpointId } ? : return null
88-
96+
8997 return localPeer.tracks.values.firstOrNull { track ->
90- track is com.fishjamcloud.client.media.VideoTrack &&
98+ track is com.fishjamcloud.client.media.VideoTrack &&
9199 (track.metadata as ? Map <* , * >)?.get(" type" ) == " camera"
92100 } as ? com.fishjamcloud.client.media.VideoTrack
93101 }
94102
95- private fun findRemoteVadActiveTrack (): com.fishjamcloud.client.media. VideoTrack ? {
103+ private fun findRemoteVadActiveTrack (): RemoteTrackInfo ? {
96104 val peers = RNFishjamClient .getAllPeers()
97105 val localEndpointId = RNFishjamClient .fishjamClient.getLocalEndpoint().id
98106 val remotePeers = peers.filter { it.id != localEndpointId }
99-
107+
100108 // First pass: look for active VAD
101109 for (peer in remotePeers) {
102110 // Find if this peer has an active VAD audio track
103111 val hasActiveVad = peer.tracks.values.any { track ->
104- track is com.fishjamcloud.client.media.RemoteAudioTrack &&
112+ track is com.fishjamcloud.client.media.RemoteAudioTrack &&
105113 track.vadStatus == Server .MediaEvent .VadNotification .Status .STATUS_SPEECH
106114 }
107-
115+
108116 if (hasActiveVad) {
109- // Return the first video track from this peer
110- return peer.tracks.values.firstOrNull { track ->
117+ val videoTrack = peer.tracks.values.firstOrNull { track ->
111118 track is com.fishjamcloud.client.media.VideoTrack
112119 } as ? com.fishjamcloud.client.media.VideoTrack
120+
121+ val displayName = (peer.metadata?.get(" displayName" ) ? : peer.metadata?.get(" name" ) ? : peer.id) as ? String ? : peer.id
122+
123+ return RemoteTrackInfo (
124+ videoTrack = videoTrack,
125+ displayName = displayName,
126+ hasVideoTrack = videoTrack != null
127+ )
113128 }
114129 }
115-
116- // Fallback: return first available remote video track
130+
131+ // Fallback: return first available remote peer with video track
117132 for (peer in remotePeers) {
118133 val videoTrack = peer.tracks.values.firstOrNull { track ->
119134 track is com.fishjamcloud.client.media.VideoTrack
120135 } as ? com.fishjamcloud.client.media.VideoTrack
121-
136+
122137 if (videoTrack != null ) {
123- return videoTrack
138+ val displayName = (peer.metadata?.get(" displayName" ) ? : peer.metadata?.get(" name" ) ? : peer.id) as ? String ? : peer.id
139+ return RemoteTrackInfo (
140+ videoTrack = videoTrack,
141+ displayName = displayName,
142+ hasVideoTrack = true
143+ )
124144 }
125145 }
126-
146+
127147 return null
128148 }
129149
130150 private fun updatePipViews () {
131151 val localCameraTrack = findLocalCameraTrack()
132- val remoteVadTrack = findRemoteVadActiveTrack()
133-
152+ val remoteTrackInfo = findRemoteVadActiveTrack()
153+
134154 // Update primary view (local camera)
135155 if (localCameraTrack != null ) {
136- if (primaryVideoView == null ) {
137- primaryVideoView = VideoRendererView (context, appContext)
138- }
139- primaryVideoView?.let { view ->
140- view.init (localCameraTrack.id())
141- }
156+ primaryVideoView?.init (localCameraTrack.id())
157+ primaryVideoView?.visibility = View .VISIBLE
142158 primaryPlaceholder?.visibility = View .GONE
143159 } else {
144- primaryVideoView = null
160+ primaryVideoView?.visibility = View . GONE
145161 primaryPlaceholder?.visibility = View .VISIBLE
146162 }
147-
163+
148164 // Update secondary view (remote VAD)
149- if (remoteVadTrack != null ) {
150- if (secondaryVideoView == null ) {
151- secondaryVideoView = VideoRendererView (context, appContext)
165+ if (remoteTrackInfo != null ) {
166+ if (remoteTrackInfo.hasVideoTrack) {
167+ // Has video track - show video
168+ secondaryVideoView?.init (remoteTrackInfo.videoTrack!! .id())
169+ secondaryVideoView?.visibility = View .VISIBLE
170+ secondaryPlaceholder?.visibility = View .GONE
171+ } else {
172+ // Has audio only - show display name
173+ secondaryVideoView?.visibility = View .GONE
174+ secondaryPlaceholder?.text = remoteTrackInfo.displayName
175+ secondaryPlaceholder?.visibility = View .VISIBLE
152176 }
153- secondaryVideoView?.let { view ->
154- view.init (remoteVadTrack.id())
155- }
156- secondaryPlaceholder?.visibility = View .GONE
177+ secondaryContainer?.visibility = View .VISIBLE
157178 } else {
158- secondaryVideoView = null
159- secondaryPlaceholder?.visibility = View .VISIBLE
179+ // No remote track - hide secondary container entirely
180+ secondaryVideoView?.visibility = View .GONE
181+ secondaryPlaceholder?.visibility = View .GONE
182+ secondaryContainer?.visibility = View .GONE
160183 }
161184 }
162185
@@ -187,8 +210,7 @@ class PipContainerView(
187210 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
188211 updateAutoEnterEnabled()
189212 }
190-
191- // Register for track updates
213+
192214 RNFishjamClient .trackUpdateListenersManager.add(this )
193215 }
194216
@@ -201,8 +223,7 @@ class PipContainerView(
201223 .remove(fragment)
202224 .commitAllowingStateLoss()
203225 }
204-
205- // Unregister from track updates
226+
206227 RNFishjamClient .trackUpdateListenersManager.remove(this )
207228 }
208229
@@ -229,79 +250,74 @@ class PipContainerView(
229250 )
230251 }
231252
232- // Create primary container (left)
233- val primaryContainer = FrameLayout (context).apply {
253+ primaryContainer = FrameLayout (context).apply {
234254 layoutParams = LinearLayout .LayoutParams (
235255 0 ,
236256 ViewGroup .LayoutParams .MATCH_PARENT ,
237257 1f
238258 )
239259 }
240260
241- // Create secondary container (right)
242- val secondaryContainer = FrameLayout (context).apply {
261+ secondaryContainer = FrameLayout (context).apply {
243262 layoutParams = LinearLayout .LayoutParams (
244263 0 ,
245264 ViewGroup .LayoutParams .MATCH_PARENT ,
246265 1f
247266 )
248267 }
249268
250- // Create placeholders
251269 primaryPlaceholder = createPlaceholderTextView(primaryPlaceholderText)
252270 secondaryPlaceholder = createPlaceholderTextView(secondaryPlaceholderText)
253271
254- primaryContainer.addView(
272+ primaryVideoView = VideoRendererView (context, appContext).apply {
273+ setVideoLayout(" FILL" )
274+ visibility = View .GONE
275+ }
276+
277+ secondaryVideoView = VideoRendererView (context, appContext).apply {
278+ setVideoLayout(" FILL" )
279+ visibility = View .GONE
280+ }
281+
282+ primaryContainer?.addView(
255283 primaryPlaceholder,
256284 FrameLayout .LayoutParams (
257285 ViewGroup .LayoutParams .MATCH_PARENT ,
258286 ViewGroup .LayoutParams .MATCH_PARENT
259287 )
260288 )
261-
262- secondaryContainer.addView(
263- secondaryPlaceholder,
289+ primaryContainer?.addView(
290+ primaryVideoView,
264291 FrameLayout .LayoutParams (
265292 ViewGroup .LayoutParams .MATCH_PARENT ,
266293 ViewGroup .LayoutParams .MATCH_PARENT
267294 )
268295 )
269296
270- // Add video views if available
271- primaryVideoView?.let {
272- primaryContainer.addView(
273- it,
274- FrameLayout .LayoutParams (
275- ViewGroup .LayoutParams .MATCH_PARENT ,
276- ViewGroup .LayoutParams .MATCH_PARENT
277- )
297+ secondaryContainer?.addView(
298+ secondaryPlaceholder,
299+ FrameLayout .LayoutParams (
300+ ViewGroup .LayoutParams .MATCH_PARENT ,
301+ ViewGroup .LayoutParams .MATCH_PARENT
278302 )
279- primaryPlaceholder?.visibility = View .GONE
280- }
281-
282- secondaryVideoView?.let {
283- secondaryContainer.addView(
284- it,
285- FrameLayout .LayoutParams (
286- ViewGroup .LayoutParams .MATCH_PARENT ,
287- ViewGroup .LayoutParams .MATCH_PARENT
288- )
303+ )
304+ secondaryContainer?.addView(
305+ secondaryVideoView,
306+ FrameLayout .LayoutParams (
307+ ViewGroup .LayoutParams .MATCH_PARENT ,
308+ ViewGroup .LayoutParams .MATCH_PARENT
289309 )
290- secondaryPlaceholder?.visibility = View .GONE
291- }
310+ )
292311
293- container.addView(primaryContainer)
294- container.addView(secondaryContainer)
312+ primaryContainer?. let { container.addView(it) }
313+ secondaryContainer?. let { container.addView(it) }
295314
296315 return container
297316 }
298317
299318 fun layoutForPiPEnter () {
300319 if (rootView == null ) return
301320
302- // Update tracks before entering PiP
303- updatePipViews()
304-
305321 // Hide all root view children
306322 for (i in 0 until rootView.childCount) {
307323 val child = rootView.getChildAt(i)
@@ -312,6 +328,8 @@ class PipContainerView(
312328 // Create and add split screen container
313329 splitScreenContainer = createSplitScreenContainer()
314330 rootView.addView(splitScreenContainer)
331+
332+ updatePipViews()
315333 }
316334
317335 fun layoutForPiPExit () {
@@ -330,6 +348,8 @@ class PipContainerView(
330348 primaryPlaceholder = null
331349 secondaryPlaceholder = null
332350 splitScreenContainer = null
351+ primaryContainer = null
352+ secondaryContainer = null
333353
334354 // Restore visibility of other root view children
335355 var visibilityIndex = 0
0 commit comments