@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.
- package/CHANGELOG.md +11 -0
- package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
- package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
- package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
- package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
- package/dist/commonjs/utils/hooks/index.js +0 -11
- package/dist/commonjs/utils/hooks/index.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/module/components/Call/CallContent/CallContent.js +10 -4
- package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
- package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
- package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
- package/dist/module/utils/hooks/index.js +0 -1
- package/dist/module/utils/hooks/index.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
- package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
- package/dist/typescript/utils/hooks/index.d.ts +0 -1
- package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
- package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
- package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
- package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
- package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
- package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
- package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
- package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
- package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
- package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
- package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
- package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
- package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
- package/ios/RTCViewPip.swift +187 -21
- package/ios/RTCViewPipManager.mm +9 -0
- package/ios/RTCViewPipManager.swift +3 -3
- package/package.json +3 -3
- package/src/components/Call/CallContent/CallContent.tsx +16 -8
- package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
- package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
- package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
- package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
- package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
- package/src/utils/hooks/index.ts +0 -1
- package/src/version.ts +1 -1
- package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
- package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
- package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
- package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
- package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
- package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
- package/src/utils/hooks/useDebouncedValue.ts +0 -21
package/ios/RTCViewPip.swift
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
PictureInPictureLogger.log("RTCViewPip has been removed from its superview.")
|
|
88
171
|
NotificationCenter.default.removeObserver(self)
|
|
89
172
|
DispatchQueue.main.async {
|
|
90
|
-
|
|
173
|
+
PictureInPictureLogger.log("onCallClosed called due to view detaching")
|
|
91
174
|
self.onCallClosed()
|
|
92
175
|
}
|
|
93
176
|
} else {
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
+
|
|
285
|
+
PictureInPictureLogger.log("Sending PiP state change event: \(isActive)")
|
|
120
286
|
onPiPChange(["active": isActive])
|
|
121
287
|
}
|
|
122
288
|
}
|
package/ios/RTCViewPipManager.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
54
|
-
"@stream-io/video-react-bindings": "1.13.
|
|
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
|
|
146
|
-
|
|
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,
|
|
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 {
|
|
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 {
|
|
44
|
-
const
|
|
45
|
-
sortBy: speakerLayoutSortPreset,
|
|
46
|
-
});
|
|
47
|
+
const { useCameraState, useCallCallingState } = useCallStateHooks();
|
|
48
|
+
const callingState = useCallCallingState();
|
|
47
49
|
const { direction } = useCameraState();
|
|
48
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
};
|