@stream-io/video-react-native-sdk 1.29.4 → 1.30.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/CHANGELOG.md +11 -0
  2. package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
  3. package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
  4. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
  5. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  6. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
  7. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  8. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  9. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  10. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
  11. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  12. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
  13. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  14. package/dist/commonjs/utils/hooks/index.js +0 -11
  15. package/dist/commonjs/utils/hooks/index.js.map +1 -1
  16. package/dist/commonjs/version.js +1 -1
  17. package/dist/module/components/Call/CallContent/CallContent.js +10 -4
  18. package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
  19. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
  20. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  21. package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
  22. package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  23. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  24. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  25. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
  26. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  27. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
  28. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  29. package/dist/module/utils/hooks/index.js +0 -1
  30. package/dist/module/utils/hooks/index.js.map +1 -1
  31. package/dist/module/version.js +1 -1
  32. package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
  33. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
  34. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
  35. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
  36. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
  37. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
  38. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
  39. package/dist/typescript/utils/hooks/index.d.ts +0 -1
  40. package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
  41. package/dist/typescript/version.d.ts +1 -1
  42. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
  43. package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
  44. package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
  45. package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
  46. package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
  47. package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
  48. package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
  49. package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
  50. package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
  51. package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
  52. package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
  53. package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
  54. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
  55. package/ios/RTCViewPip.swift +187 -21
  56. package/ios/RTCViewPipManager.mm +9 -0
  57. package/ios/RTCViewPipManager.swift +3 -3
  58. package/package.json +3 -3
  59. package/src/components/Call/CallContent/CallContent.tsx +16 -8
  60. package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
  61. package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
  62. package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
  63. package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
  64. package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
  65. package/src/utils/hooks/index.ts +0 -1
  66. package/src/version.ts +1 -1
  67. package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
  68. package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
  69. package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
  70. package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
  71. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
  72. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
  73. package/src/utils/hooks/useDebouncedValue.ts +0 -21
@@ -10,11 +10,85 @@ import React
10
10
 
11
11
  @objc(RTCViewPip)
12
12
  class RTCViewPip: UIView {
13
-
14
- private var pictureInPictureController = StreamPictureInPictureController()
13
+
14
+ private var pictureInPictureController: StreamPictureInPictureController? = StreamPictureInPictureController()
15
15
  private var webRtcModule: WebRTCModule?
16
-
16
+
17
17
  @objc var onPiPChange: RCTBubblingEventBlock?
18
+
19
+ // MARK: - Avatar Placeholder Properties
20
+
21
+ /// The participant's name for the avatar placeholder
22
+ @objc public var participantName: NSString? = nil {
23
+ didSet {
24
+ PictureInPictureLogger.log("RTCViewPip.participantName didSet: \(participantName as String? ?? "nil"), controller exists: \(pictureInPictureController != nil)")
25
+ pictureInPictureController?.participantName = participantName as String?
26
+ }
27
+ }
28
+
29
+ /// The URL string for the participant's profile image
30
+ @objc public var participantImageURL: NSString? = nil {
31
+ didSet {
32
+ PictureInPictureLogger.log("RTCViewPip.participantImageURL didSet: \(participantImageURL as String? ?? "nil"), controller exists: \(pictureInPictureController != nil)")
33
+ pictureInPictureController?.participantImageURL = participantImageURL as String?
34
+ }
35
+ }
36
+
37
+ // MARK: - Reconnection Properties
38
+
39
+ /// Whether the call is reconnecting - when true, shows reconnection view
40
+ @objc public var isReconnecting: Bool = false {
41
+ didSet {
42
+ pictureInPictureController?.isReconnecting = isReconnecting
43
+ }
44
+ }
45
+
46
+ // MARK: - Screen Sharing Properties
47
+
48
+ /// Whether screen sharing is active (used for content state tracking)
49
+ @objc public var isScreenSharing: Bool = false {
50
+ didSet {
51
+ pictureInPictureController?.isScreenSharing = isScreenSharing
52
+ }
53
+ }
54
+
55
+ // MARK: - Participant Overlay Properties
56
+
57
+ /// Whether the participant has audio enabled (shown in participant overlay)
58
+ @objc public var hasAudio: Bool = true {
59
+ didSet {
60
+ pictureInPictureController?.hasAudio = hasAudio
61
+ }
62
+ }
63
+
64
+ /// Whether the video track is paused (shown in participant overlay)
65
+ @objc public var isTrackPaused: Bool = false {
66
+ didSet {
67
+ pictureInPictureController?.isTrackPaused = isTrackPaused
68
+ }
69
+ }
70
+
71
+ /// Whether the participant is pinned (shown in participant overlay)
72
+ @objc public var isPinned: Bool = false {
73
+ didSet {
74
+ pictureInPictureController?.isPinned = isPinned
75
+ }
76
+ }
77
+
78
+ /// Whether the participant is currently speaking (shows border highlight)
79
+ @objc public var isSpeaking: Bool = false {
80
+ didSet {
81
+ pictureInPictureController?.isSpeaking = isSpeaking
82
+ }
83
+ }
84
+
85
+ /// The connection quality level (0: unknown, 1: poor, 2: good, 3: excellent)
86
+ @objc public var connectionQuality: Int = 0 {
87
+ didSet {
88
+ pictureInPictureController?.connectionQuality = connectionQuality
89
+ }
90
+ }
91
+
18
92
  @objc public var mirror: Bool = false {
19
93
  didSet {
20
94
  self.pictureInPictureController?.isMirrored = mirror
@@ -38,27 +112,36 @@ class RTCViewPip: UIView {
38
112
  didSet {
39
113
  // https://github.com/react-native-webrtc/react-native-webrtc/blob/8dfc9c394b4bf627c0214255466ebd3b160ca563/ios/RCTWebRTC/RTCVideoViewManager.m#L405-L418
40
114
  guard let streamURLString = streamURL as String? else {
41
- NSLog("PiP - No streamURL set")
115
+ PictureInPictureLogger.log("No streamURL set, clearing track")
116
+ DispatchQueue.main.async {
117
+ self.applyTrackStateToController(track: nil, isVideoEnabled: false)
118
+ }
42
119
  return
43
120
  }
44
-
121
+
45
122
  guard let stream = self.webRtcModule?.stream(forReactTag: streamURLString) else {
46
- NSLog("PiP - No stream for streamURL: -\(streamURLString)")
123
+ PictureInPictureLogger.log("No stream for streamURL: -\(streamURLString), clearing track")
124
+ DispatchQueue.main.async {
125
+ self.applyTrackStateToController(track: nil, isVideoEnabled: false)
126
+ }
47
127
  return
48
128
  }
49
-
129
+
50
130
  guard let videoTrack = stream.videoTracks.first else {
51
- NSLog("PiP - No video track for streamURL: -\(streamURLString)")
131
+ PictureInPictureLogger.log("No video track for streamURL: -\(streamURLString), clearing track")
132
+ DispatchQueue.main.async {
133
+ self.applyTrackStateToController(track: nil, isVideoEnabled: false)
134
+ }
52
135
  return
53
136
  }
54
- if (self.pictureInPictureController?.track == videoTrack) {
55
- NSLog("PiP - Skipping video track for streamURL: -\(streamURLString)")
137
+ if isSameTrackInstance(self.pictureInPictureController?.track, videoTrack) {
138
+ PictureInPictureLogger.log("Skipping video track for streamURL: -\(streamURLString)")
56
139
  return
57
140
  }
58
-
141
+
59
142
  DispatchQueue.main.async {
60
- NSLog("PiP - Setting video track for streamURL: -\(streamURLString) trackId: \(videoTrack.trackId)")
61
- self.pictureInPictureController?.track = videoTrack
143
+ PictureInPictureLogger.log("Setting video track for streamURL: -\(streamURLString) trackId: \(videoTrack.trackId)")
144
+ self.applyTrackStateToController(track: videoTrack, isVideoEnabled: true)
62
145
  }
63
146
  }
64
147
  }
@@ -70,30 +153,40 @@ class RTCViewPip: UIView {
70
153
 
71
154
  @objc
72
155
  func onCallClosed() {
73
- NSLog("PiP - pictureInPictureController cleanup called")
156
+ PictureInPictureLogger.log("pictureInPictureController cleanup called")
74
157
  self.pictureInPictureController?.cleanup()
75
158
  self.pictureInPictureController = nil
76
159
  }
77
160
 
78
161
  @objc
79
162
  func setPreferredContentSize(_ size: CGSize) {
80
- NSLog("PiP - RTCViewPip setPreferredContentSize \(size)")
163
+ PictureInPictureLogger.log("RTCViewPip setPreferredContentSize \(size)")
81
164
  self.pictureInPictureController?.setPreferredContentSize(size)
82
165
  }
83
166
 
84
167
  override func didMoveToSuperview() {
85
168
  super.didMoveToSuperview()
86
169
  if self.superview == nil {
87
- NSLog("PiP - RTCViewPip has been removed from its superview.")
170
+ PictureInPictureLogger.log("RTCViewPip has been removed from its superview.")
88
171
  NotificationCenter.default.removeObserver(self)
89
172
  DispatchQueue.main.async {
90
- NSLog("PiP - onCallClosed called due to view detaching")
173
+ PictureInPictureLogger.log("onCallClosed called due to view detaching")
91
174
  self.onCallClosed()
92
175
  }
93
176
  } else {
94
- NSLog("PiP - RTCViewPip has been added to a superview.")
177
+ PictureInPictureLogger.log("RTCViewPip has been added to a superview.")
95
178
  setupNotificationObserver()
96
179
  DispatchQueue.main.async {
180
+ // Recreate controller if it was previously cleaned up
181
+ // This allows PiP to work again for subsequent calls
182
+ let wasNil = self.pictureInPictureController == nil
183
+ if wasNil {
184
+ PictureInPictureLogger.log("Recreating pictureInPictureController for new session")
185
+ self.pictureInPictureController = StreamPictureInPictureController()
186
+ // Re-apply all current properties to the new controller
187
+ // This is necessary because React Native may have set props while controller was nil
188
+ self.applyCurrentPropertiesToController()
189
+ }
97
190
  self.pictureInPictureController?.sourceView = self
98
191
  self.pictureInPictureController?.isMirrored = self.mirror
99
192
  // Set up PiP state change callback
@@ -103,20 +196,93 @@ class RTCViewPip: UIView {
103
196
  if let reactTag = self.reactTag, let bridge = self.webRtcModule?.bridge {
104
197
  if let manager = bridge.module(for: RTCViewPipManager.self) as? RTCViewPipManager,
105
198
  let size = manager.getCachedSize(for: reactTag) {
106
- NSLog("PiP - Applying cached size \(size) for reactTag \(reactTag)")
199
+ PictureInPictureLogger.log("Applying cached size \(size) for reactTag \(reactTag)")
107
200
  self.setPreferredContentSize(size)
108
201
  }
109
202
  }
110
203
  }
111
204
  }
112
205
  }
206
+
207
+ /// Re-applies all current property values to the controller.
208
+ /// This is needed after controller recreation because didSet doesn't fire
209
+ /// when the property values haven't changed on the React Native side.
210
+ ///
211
+ /// NOTE: This reads from RTCViewPip's own properties (self.participantName, etc.)
212
+ /// which retain their values even after controller cleanup.
213
+ private func applyCurrentPropertiesToController() {
214
+ guard let controller = pictureInPictureController else {
215
+ PictureInPictureLogger.log("applyCurrentPropertiesToController: controller is nil, skipping")
216
+ return
217
+ }
218
+
219
+ PictureInPictureLogger.log("applyCurrentPropertiesToController STARTING:")
220
+ PictureInPictureLogger.log(" participantName: '\(participantName as String? ?? "nil")'")
221
+ PictureInPictureLogger.log(" participantImageURL: '\(participantImageURL as String? ?? "nil")'")
222
+ PictureInPictureLogger.log(" streamURL: '\(streamURL as String? ?? "nil")'")
223
+
224
+ let resolvedTrack: RTCVideoTrack?
225
+ let isVideoEnabled: Bool
226
+ if let streamURLString = streamURL as String?,
227
+ let stream = webRtcModule?.stream(forReactTag: streamURLString),
228
+ let videoTrack = stream.videoTracks.first {
229
+ PictureInPictureLogger.log("Re-applying track from streamURL: \(streamURLString), trackId: \(videoTrack.trackId)")
230
+ resolvedTrack = videoTrack
231
+ isVideoEnabled = true
232
+ } else {
233
+ // No stream URL or no track means video is disabled - show avatar
234
+ PictureInPictureLogger.log("No valid stream/track, setting isVideoEnabled=false for avatar")
235
+ resolvedTrack = nil
236
+ isVideoEnabled = false
237
+ }
238
+
239
+ // Keep PiP content transitions store-driven with one snapshot update.
240
+ controller.applyContentSnapshot(
241
+ track: resolvedTrack,
242
+ participantName: participantName as String?,
243
+ participantImageURL: participantImageURL as String?,
244
+ isVideoEnabled: isVideoEnabled,
245
+ isScreenSharing: isScreenSharing,
246
+ isReconnecting: isReconnecting
247
+ )
248
+
249
+ controller.hasAudio = hasAudio
250
+ controller.isTrackPaused = isTrackPaused
251
+ controller.isPinned = isPinned
252
+ controller.isSpeaking = isSpeaking
253
+ controller.connectionQuality = connectionQuality
254
+ PictureInPictureLogger.log("applyCurrentPropertiesToController COMPLETED")
255
+ }
256
+
257
+ /// Applies track/video availability without splitting a single change into multiple setters.
258
+ private func applyTrackStateToController(track: RTCVideoTrack?, isVideoEnabled: Bool) {
259
+ pictureInPictureController?.applyContentSnapshot(
260
+ track: track,
261
+ participantName: participantName as String?,
262
+ participantImageURL: participantImageURL as String?,
263
+ isVideoEnabled: isVideoEnabled,
264
+ isScreenSharing: isScreenSharing,
265
+ isReconnecting: isReconnecting
266
+ )
267
+ }
268
+
269
+ private func isSameTrackInstance(_ lhs: RTCVideoTrack?, _ rhs: RTCVideoTrack?) -> Bool {
270
+ switch (lhs, rhs) {
271
+ case (nil, nil):
272
+ return true
273
+ case let (lhsTrack?, rhsTrack?):
274
+ return lhsTrack === rhsTrack
275
+ default:
276
+ return false
277
+ }
278
+ }
113
279
 
114
280
  private func sendPiPChangeEvent(isActive: Bool) {
115
281
  guard let onPiPChange = onPiPChange else {
116
282
  return
117
283
  }
118
-
119
- NSLog("PiP - Sending PiP state change event: \(isActive)")
284
+
285
+ PictureInPictureLogger.log("Sending PiP state change event: \(isActive)")
120
286
  onPiPChange(["active": isActive])
121
287
  }
122
288
  }
@@ -13,6 +13,15 @@
13
13
  RCT_EXPORT_VIEW_PROPERTY(streamURL, NSString)
14
14
  RCT_EXPORT_VIEW_PROPERTY(mirror, BOOL)
15
15
  RCT_EXPORT_VIEW_PROPERTY(onPiPChange, RCTBubblingEventBlock)
16
+ RCT_EXPORT_VIEW_PROPERTY(participantName, NSString)
17
+ RCT_EXPORT_VIEW_PROPERTY(participantImageURL, NSString)
18
+ RCT_EXPORT_VIEW_PROPERTY(isReconnecting, BOOL)
19
+ RCT_EXPORT_VIEW_PROPERTY(isScreenSharing, BOOL)
20
+ RCT_EXPORT_VIEW_PROPERTY(hasAudio, BOOL)
21
+ RCT_EXPORT_VIEW_PROPERTY(isTrackPaused, BOOL)
22
+ RCT_EXPORT_VIEW_PROPERTY(isPinned, BOOL)
23
+ RCT_EXPORT_VIEW_PROPERTY(isSpeaking, BOOL)
24
+ RCT_EXPORT_VIEW_PROPERTY(connectionQuality, NSInteger)
16
25
  RCT_EXTERN_METHOD(onCallClosed:(nonnull NSNumber*) reactTag)
17
26
  RCT_EXTERN_METHOD(setPreferredContentSize:(nonnull NSNumber *)reactTag width:(CGFloat)w height:(CGFloat)h);
18
27
 
@@ -32,7 +32,7 @@ class RTCViewPipManager: RCTViewManager {
32
32
  pipView.onCallClosed()
33
33
  }
34
34
  } else {
35
- NSLog("PiP - onCallClosed cant be called, Invalid view returned from registry, expecting RTCViewPip")
35
+ PictureInPictureLogger.log("onCallClosed cant be called, Invalid view returned from registry, expecting RTCViewPip")
36
36
  }
37
37
  })
38
38
  }
@@ -51,7 +51,7 @@ class RTCViewPipManager: RCTViewManager {
51
51
  } else {
52
52
  // If the view is not found, cache the size.
53
53
  // this happens when this method is called before the view can attach react super view
54
- NSLog("PiP - View not found for reactTag \(reactTag), caching size.")
54
+ PictureInPictureLogger.log("View not found for reactTag \(reactTag), caching size.")
55
55
  self.cachedSizes[reactTag] = size
56
56
  }
57
57
  })
@@ -60,7 +60,7 @@ class RTCViewPipManager: RCTViewManager {
60
60
  func getCachedSize(for reactTag: NSNumber) -> CGSize? {
61
61
  let size = self.cachedSizes.removeValue(forKey: reactTag)
62
62
  if size != nil {
63
- NSLog("PiP - Found and removed cached size for reactTag \(reactTag).")
63
+ PictureInPictureLogger.log("Found and removed cached size for reactTag \(reactTag).")
64
64
  }
65
65
  return size
66
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "1.29.4",
3
+ "version": "1.30.0",
4
4
  "description": "Stream Video SDK for React Native",
5
5
  "author": "https://getstream.io",
6
6
  "homepage": "https://getstream.io/video/docs/react-native/",
@@ -50,8 +50,8 @@
50
50
  "!**/.*"
51
51
  ],
52
52
  "dependencies": {
53
- "@stream-io/video-client": "1.43.0",
54
- "@stream-io/video-react-bindings": "1.13.8",
53
+ "@stream-io/video-client": "1.44.0",
54
+ "@stream-io/video-react-bindings": "1.13.9",
55
55
  "intl-pluralrules": "2.0.1",
56
56
  "lodash.merge": "^4.6.2",
57
57
  "react-native-url-polyfill": "^3.0.0",
@@ -21,11 +21,12 @@ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
21
21
  import {
22
22
  CallingState,
23
23
  type StreamReaction,
24
+ type StreamVideoParticipant,
24
25
  videoLoggerSystem,
25
26
  } from '@stream-io/video-client';
27
+ import { debounceTime } from 'rxjs';
26
28
 
27
29
  import { Z_INDEX } from '../../../constants';
28
- import { useDebouncedValue } from '../../../utils/hooks';
29
30
  import {
30
31
  FloatingParticipantView as DefaultFloatingParticipantView,
31
32
  type FloatingParticipantViewProps,
@@ -134,16 +135,23 @@ export const CallContent = ({
134
135
  theme: { callContent },
135
136
  } = useTheme();
136
137
  const call = useCall();
137
- const {
138
- useHasOngoingScreenShare,
139
- useRemoteParticipants,
140
- useLocalParticipant,
141
- } = useCallStateHooks();
138
+ const { useHasOngoingScreenShare, useLocalParticipant } = useCallStateHooks();
142
139
 
143
140
  useAutoEnterPiPEffect(disablePictureInPicture);
144
141
 
145
- const _remoteParticipants = useRemoteParticipants();
146
- const remoteParticipants = useDebouncedValue(_remoteParticipants, 300); // we debounce the remote participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously
142
+ const [remoteParticipants, setRemoteParticipants] = useState<
143
+ StreamVideoParticipant[]
144
+ >(() => call?.state.remoteParticipants ?? []);
145
+ useEffect(() => {
146
+ if (!call) {
147
+ setRemoteParticipants([]);
148
+ return;
149
+ }
150
+ const sub = call.state.remoteParticipants$
151
+ .pipe(debounceTime(300))
152
+ .subscribe(setRemoteParticipants);
153
+ return () => sub.unsubscribe();
154
+ }, [call]);
147
155
  const localParticipant = useLocalParticipant();
148
156
  const isInPiPMode = useIsInPiPMode();
149
157
  const hasScreenShare = useHasOngoingScreenShare();
@@ -1,21 +1,25 @@
1
1
  import {
2
2
  CallingState,
3
+ SfuModels,
4
+ hasAudio,
5
+ hasPausedTrack,
3
6
  hasScreenShare,
4
- speakerLayoutSortPreset,
5
7
  type StreamVideoParticipant,
6
8
  videoLoggerSystem,
7
9
  type VideoTrackType,
10
+ hasVideo,
11
+ isPinned,
8
12
  } from '@stream-io/video-client';
9
13
  import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
10
14
  import type { MediaStream } from '@stream-io/react-native-webrtc';
11
- import React, { useEffect, useMemo, useCallback } from 'react';
15
+ import React, { useEffect, useCallback, useState } from 'react';
12
16
  import { findNodeHandle } from 'react-native';
13
17
  import {
14
18
  onNativeCallClosed,
15
19
  onNativeDimensionsUpdated,
16
20
  RTCViewPipNative,
17
21
  } from './RTCViewPipNative';
18
- import { useDebouncedValue } from '../../../utils/hooks';
22
+ import { debounceTime } from 'rxjs';
19
23
  import { shouldDisableIOSLocalVideoOnBackgroundRef } from '../../../utils/internal/shouldDisableIOSLocalVideoOnBackground';
20
24
  import { useTrackDimensions } from '../../../hooks/useTrackDimensions';
21
25
  import { isInPiPMode$ } from '../../../utils/internal/rxSubjects';
@@ -40,12 +44,26 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
40
44
  onPiPChange,
41
45
  } = props;
42
46
  const call = useCall();
43
- const { useParticipants, useCameraState } = useCallStateHooks();
44
- const _allParticipants = useParticipants({
45
- sortBy: speakerLayoutSortPreset,
46
- });
47
+ const { useCameraState, useCallCallingState } = useCallStateHooks();
48
+ const callingState = useCallCallingState();
47
49
  const { direction } = useCameraState();
48
- const allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously
50
+
51
+ const [allParticipants, setAllParticipants] = useState<
52
+ StreamVideoParticipant[]
53
+ >(call?.state.participants ?? []);
54
+
55
+ // we debounce the participants to avoid unnecessary rerenders
56
+ // that happen when participant tracks are all subscribed simultaneously
57
+ useEffect(() => {
58
+ if (!call) {
59
+ setAllParticipants([]);
60
+ return;
61
+ }
62
+ const subscription = call.state.participants$
63
+ .pipe(debounceTime(300))
64
+ .subscribe(setAllParticipants);
65
+ return () => subscription.unsubscribe();
66
+ }, [call]);
49
67
 
50
68
  const [dominantSpeaker, dominantSpeaker2] = allParticipants.filter(
51
69
  (participant) =>
@@ -118,24 +136,63 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
118
136
  ? screenShareStream
119
137
  : videoStream) as unknown as MediaStream | undefined;
120
138
 
139
+ const isPublishingTrack =
140
+ isScreenSharing ||
141
+ (participantInSpotlight && hasVideo(participantInSpotlight));
142
+
143
+ const streamURL = isPublishingTrack
144
+ ? videoStreamToRender?.toURL()
145
+ : undefined;
146
+
121
147
  const mirror = isScreenSharing
122
148
  ? false
123
149
  : mirrorOverride !== undefined
124
150
  ? mirrorOverride
125
151
  : !!participantInSpotlight?.isLocalParticipant && direction === 'front';
126
152
 
127
- const streamURL = useMemo(() => {
128
- if (!videoStreamToRender) {
129
- return undefined;
130
- }
131
- return videoStreamToRender?.toURL();
132
- }, [videoStreamToRender]);
133
-
134
153
  const handlePiPChange = (event: { nativeEvent: { active: boolean } }) => {
135
154
  isInPiPMode$.next(event.nativeEvent.active);
136
155
  onPiPChange?.(event.nativeEvent.active);
137
156
  };
138
157
 
158
+ // Get participant info for avatar placeholder
159
+ const participantName = participantInSpotlight?.name || undefined;
160
+ const participantImageURL = participantInSpotlight?.image || undefined;
161
+
162
+ // Determine if the call is reconnecting or offline
163
+ const isReconnecting =
164
+ callingState === CallingState.MIGRATING ||
165
+ callingState === CallingState.RECONNECTING ||
166
+ callingState === CallingState.RECONNECTING_FAILED ||
167
+ callingState === CallingState.OFFLINE;
168
+
169
+ // Determine if the participant has audio enabled
170
+ const participantHasAudio = participantInSpotlight
171
+ ? hasAudio(participantInSpotlight)
172
+ : true;
173
+
174
+ // Determine if the video track is paused
175
+ const trackType: VideoTrackType = isScreenSharing
176
+ ? 'screenShareTrack'
177
+ : 'videoTrack';
178
+
179
+ const isVideoTrackPaused = participantInSpotlight
180
+ ? hasPausedTrack(participantInSpotlight, trackType)
181
+ : false;
182
+
183
+ // Determine if the participant is pinned
184
+ const participantIsPinned = participantInSpotlight
185
+ ? isPinned(participantInSpotlight)
186
+ : false;
187
+
188
+ // Determine if the participant is speaking
189
+ const participantIsSpeaking = participantInSpotlight?.isSpeaking ?? false;
190
+
191
+ // Get connection quality (convert enum to number: UNSPECIFIED=0, POOR=1, GOOD=2, EXCELLENT=3)
192
+ const participantConnectionQuality =
193
+ participantInSpotlight?.connectionQuality ??
194
+ SfuModels.ConnectionQuality.UNSPECIFIED;
195
+
139
196
  return (
140
197
  <>
141
198
  <RTCViewPipNative
@@ -143,6 +200,15 @@ export const RTCViewPipIOS = React.memo((props: Props) => {
143
200
  mirror={mirror}
144
201
  ref={nativeRef}
145
202
  onPiPChange={handlePiPChange}
203
+ participantName={participantName}
204
+ participantImageURL={participantImageURL}
205
+ isReconnecting={isReconnecting}
206
+ isScreenSharing={isScreenSharing}
207
+ hasAudio={participantHasAudio}
208
+ isTrackPaused={isVideoTrackPaused}
209
+ isPinned={participantIsPinned}
210
+ isSpeaking={participantIsSpeaking}
211
+ connectionQuality={participantConnectionQuality}
146
212
  />
147
213
  {participantInSpotlight && (
148
214
  <DimensionsUpdatedRenderless
@@ -18,6 +18,24 @@ type RTCViewPipNativeProps = {
18
18
  streamURL?: string;
19
19
  mirror?: boolean;
20
20
  onPiPChange?: (event: { nativeEvent: PiPChangeEvent }) => void;
21
+ /** The participant's name for the avatar placeholder when video is disabled */
22
+ participantName?: string;
23
+ /** The URL string for the participant's profile image */
24
+ participantImageURL?: string;
25
+ /** Whether the call is reconnecting - when true, shows reconnection view */
26
+ isReconnecting?: boolean;
27
+ /** Whether screen sharing is active (used for content state tracking) */
28
+ isScreenSharing?: boolean;
29
+ /** Whether the participant has audio enabled (shown in participant overlay) */
30
+ hasAudio?: boolean;
31
+ /** Whether the video track is paused (shown in participant overlay) */
32
+ isTrackPaused?: boolean;
33
+ /** Whether the participant is pinned (shown in participant overlay) */
34
+ isPinned?: boolean;
35
+ /** Whether the participant is currently speaking (shows border highlight) */
36
+ isSpeaking?: boolean;
37
+ /** The connection quality level (0: unknown, 1: poor, 2: good, 3: excellent) */
38
+ connectionQuality?: number;
21
39
  };
22
40
 
23
41
  const NativeComponent: HostComponent<RTCViewPipNativeProps> =
@@ -65,6 +83,24 @@ export const RTCViewPipNative = React.memo(
65
83
  mirror={props.mirror}
66
84
  // eslint-disable-next-line react/prop-types
67
85
  onPiPChange={props.onPiPChange}
86
+ // eslint-disable-next-line react/prop-types
87
+ participantName={props.participantName}
88
+ // eslint-disable-next-line react/prop-types
89
+ participantImageURL={props.participantImageURL}
90
+ // eslint-disable-next-line react/prop-types
91
+ isReconnecting={props.isReconnecting}
92
+ // eslint-disable-next-line react/prop-types
93
+ isScreenSharing={props.isScreenSharing}
94
+ // eslint-disable-next-line react/prop-types
95
+ hasAudio={props.hasAudio}
96
+ // eslint-disable-next-line react/prop-types
97
+ isTrackPaused={props.isTrackPaused}
98
+ // eslint-disable-next-line react/prop-types
99
+ isPinned={props.isPinned}
100
+ // eslint-disable-next-line react/prop-types
101
+ isSpeaking={props.isSpeaking}
102
+ // eslint-disable-next-line react/prop-types
103
+ connectionQuality={props.connectionQuality}
68
104
  // @ts-expect-error - types issue
69
105
  ref={ref}
70
106
  />
@@ -1,7 +1,7 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { StyleSheet, View, type ViewStyle } from 'react-native';
3
- import { useCallStateHooks } from '@stream-io/video-react-bindings';
4
- import { useDebouncedValue } from '../../../utils/hooks/useDebouncedValue';
3
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
4
+ import { debounceTime } from 'rxjs';
5
5
  import {
6
6
  CallParticipantsList as DefaultCallParticipantsList,
7
7
  type CallParticipantsListComponentProps,
@@ -49,19 +49,33 @@ export const CallParticipantsGrid = ({
49
49
  const {
50
50
  theme: { colors, callParticipantsGrid },
51
51
  } = useTheme();
52
- const {
53
- useRemoteParticipants,
54
- useParticipants,
55
- useLocalParticipant,
56
- useDominantSpeaker,
57
- } = useCallStateHooks();
58
- const _remoteParticipants = useRemoteParticipants();
52
+ const call = useCall();
53
+ const { useLocalParticipant, useDominantSpeaker } = useCallStateHooks();
59
54
  const localParticipant = useLocalParticipant();
60
- const _allParticipants = useParticipants();
61
55
  const dominantSpeaker = useDominantSpeaker();
62
- // we debounce the participants arrays to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously
63
- const remoteParticipants = useDebouncedValue(_remoteParticipants, 300);
64
- const allParticipants = useDebouncedValue(_allParticipants, 300);
56
+ const [remoteParticipants, setRemoteParticipants] = useState<
57
+ StreamVideoParticipant[]
58
+ >(() => call?.state.remoteParticipants ?? []);
59
+ const [allParticipants, setAllParticipants] = useState<
60
+ StreamVideoParticipant[]
61
+ >(() => call?.state.participants ?? []);
62
+ useEffect(() => {
63
+ if (!call) {
64
+ setRemoteParticipants([]);
65
+ setAllParticipants([]);
66
+ return;
67
+ }
68
+ const sub1 = call.state.remoteParticipants$
69
+ .pipe(debounceTime(300))
70
+ .subscribe(setRemoteParticipants);
71
+ const sub2 = call.state.participants$
72
+ .pipe(debounceTime(300))
73
+ .subscribe(setAllParticipants);
74
+ return () => {
75
+ sub1.unsubscribe();
76
+ sub2.unsubscribe();
77
+ };
78
+ }, [call]);
65
79
  const landscapeStyles: ViewStyle = {
66
80
  flexDirection: landscape ? 'row' : 'column',
67
81
  };