@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
@@ -7,31 +7,36 @@ import Combine
7
7
  import Foundation
8
8
 
9
9
  /// A controller class for picture-in-picture whenever that is possible.
10
- @objc final class StreamPictureInPictureController: NSObject, AVPictureInPictureControllerDelegate {
11
-
10
+ ///
11
+ /// This controller manages the Picture-in-Picture window state and handles transitions
12
+ /// between foreground and background states. It uses the `PictureInPictureContentState`
13
+ /// for centralized state management and a delegate proxy pattern to enable reactive
14
+ /// handling of PiP lifecycle events.
15
+ @objc final class StreamPictureInPictureController: NSObject {
16
+
12
17
  // MARK: - Properties
13
-
18
+
14
19
  /// The RTCVideoTrack for which the picture-in-picture session is created.
15
20
  @objc public var track: RTCVideoTrack? {
16
21
  didSet {
17
22
  didUpdate(track) // Called when the `track` property changes
18
23
  }
19
24
  }
20
-
25
+
21
26
  /// The UIView that contains the video content.
22
27
  @objc public var sourceView: UIView? {
23
28
  didSet {
24
29
  didUpdate(sourceView) // Called when the `sourceView` property changes
25
30
  }
26
31
  }
27
-
32
+
28
33
  /// A closure called when the picture-in-picture view's size changes.
29
34
  public var onSizeUpdate: ((CGSize) -> Void)? {
30
35
  didSet {
31
36
  contentViewController?.onSizeUpdate = onSizeUpdate // Updates the onSizeUpdate closure of the content view controller
32
37
  }
33
38
  }
34
-
39
+
35
40
  /// A closure called when the picture-in-picture state changes.
36
41
  public var onPiPStateChange: ((Bool) -> Void)?
37
42
 
@@ -44,28 +49,118 @@ import Foundation
44
49
 
45
50
  /// A boolean value indicating whether the picture-in-picture session should start automatically when the app enters background.
46
51
  public var canStartPictureInPictureAutomaticallyFromInline: Bool
47
-
52
+
53
+ // MARK: - Content State Properties
54
+ // These properties update the centralized content state, which manages view switching
55
+
56
+ /// The participant's name for the avatar placeholder
57
+ @objc public var participantName: String? {
58
+ didSet {
59
+ syncContentStateIfNeeded()
60
+ }
61
+ }
62
+
63
+ /// The URL string for the participant's profile image
64
+ @objc public var participantImageURL: String? {
65
+ didSet {
66
+ syncContentStateIfNeeded()
67
+ }
68
+ }
69
+
70
+ /// Whether video is enabled - when false, shows avatar placeholder
71
+ @objc public var isVideoEnabled: Bool = true {
72
+ didSet {
73
+ syncContentStateIfNeeded()
74
+ }
75
+ }
76
+
77
+ /// Whether the call is reconnecting - when true, shows reconnection view
78
+ @objc public var isReconnecting: Bool = false {
79
+ didSet {
80
+ syncContentStateIfNeeded()
81
+ }
82
+ }
83
+
84
+ /// Whether screen sharing is active (used for content state tracking)
85
+ @objc public var isScreenSharing: Bool = false {
86
+ didSet {
87
+ syncContentStateIfNeeded()
88
+ }
89
+ }
90
+
91
+ /// Whether the participant has audio enabled (shown in participant overlay)
92
+ @objc public var hasAudio: Bool = true {
93
+ didSet {
94
+ contentViewController?.hasAudio = hasAudio
95
+ }
96
+ }
97
+
98
+ /// Whether the video track is paused (shown in participant overlay)
99
+ @objc public var isTrackPaused: Bool = false {
100
+ didSet {
101
+ contentViewController?.isTrackPaused = isTrackPaused
102
+ }
103
+ }
104
+
105
+ /// Whether the participant is pinned (shown in participant overlay)
106
+ @objc public var isPinned: Bool = false {
107
+ didSet {
108
+ contentViewController?.isPinned = isPinned
109
+ }
110
+ }
111
+
112
+ /// Whether the participant is currently speaking (shows border highlight)
113
+ @objc public var isSpeaking: Bool = false {
114
+ didSet {
115
+ contentViewController?.isSpeaking = isSpeaking
116
+ }
117
+ }
118
+
119
+ /// The connection quality level (0: unknown, 1: poor, 2: good, 3: excellent)
120
+ @objc public var connectionQuality: Int = 0 {
121
+ didSet {
122
+ contentViewController?.connectionQuality = connectionQuality
123
+ }
124
+ }
125
+
48
126
  // MARK: - Private Properties
49
-
127
+
50
128
  /// The AVPictureInPictureController object.
51
129
  private var pictureInPictureController: AVPictureInPictureController?
52
-
130
+
53
131
  /// The StreamAVPictureInPictureViewControlling object that manages the picture-in-picture view.
54
132
  private var contentViewController: StreamAVPictureInPictureViewControlling?
55
-
133
+
134
+ /// Centralized content state manager for unified state handling.
135
+ /// This manages the content switching between video, avatar, reconnection, and screen share views.
136
+ private let contentState = PictureInPictureContentState()
137
+
56
138
  /// A set of `AnyCancellable` objects used to manage subscriptions.
57
139
  private var cancellableBag: Set<AnyCancellable> = []
58
-
59
- /// A `AnyCancellable` object used to ensure that the active track is enabled while in picture-in-picture
60
- /// mode.
61
- private var ensureActiveTrackIsEnabledCancellable: AnyCancellable?
62
-
140
+
141
+ /// Delegate proxy that publishes PiP lifecycle events via Combine.
142
+ private let delegateProxy = PictureInPictureDelegateProxy()
143
+
144
+ /// Adapter responsible for enforcing the stop of PiP when the app returns to foreground.
145
+ private var enforcedStopAdapter: PictureInPictureEnforcedStopAdapter?
146
+
63
147
  /// A `StreamPictureInPictureTrackStateAdapter` object that manages the state of the
64
148
  /// active track.
65
149
  private let trackStateAdapter: StreamPictureInPictureTrackStateAdapter = .init()
150
+
151
+ /// When true, multiple content fields are being updated as one transition.
152
+ private var isApplyingContentSnapshot = false
66
153
 
154
+ // MARK: - Content State Access
155
+
156
+ /// Returns the current content being displayed in the PiP window.
157
+ /// This is useful for debugging and logging purposes.
158
+ var currentContent: PictureInPictureContent {
159
+ contentState.content
160
+ }
161
+
67
162
  // MARK: - Lifecycle
68
-
163
+
69
164
  /// Initializes the controller and creates the content view
70
165
  ///
71
166
  /// - Parameter canStartPictureInPictureAutomaticallyFromInline A boolean value
@@ -77,7 +172,7 @@ import Foundation
77
172
  guard AVPictureInPictureController.isPictureInPictureSupported() else {
78
173
  return nil
79
174
  }
80
-
175
+
81
176
  let contentViewController: StreamAVPictureInPictureViewControlling? = {
82
177
  if #available(iOS 15.0, *) {
83
178
  return StreamAVPictureInPictureVideoCallViewController()
@@ -85,60 +180,121 @@ import Foundation
85
180
  return nil
86
181
  }
87
182
  }()
88
- // contentViewController?.preferredContentSize = .init(width: 400, height: 320)
183
+ // Set a default preferred content size to avoid iOS PGPegasus code:-1003 error
184
+ // This will be updated later when track dimensions become available
185
+ contentViewController?.preferredContentSize = .init(width: 640, height: 480)
89
186
  self.contentViewController = contentViewController
90
187
  self.contentViewController?.isMirrored = isMirrored
91
188
  self.canStartPictureInPictureAutomaticallyFromInline = canStartPictureInPictureAutomaticallyFromInline
92
189
  super.init()
190
+
191
+ // Wire up the content state to the view controller for reactive updates (US-008)
192
+ // This enables the unified content view system where contentState changes
193
+ // automatically drive view switching in the renderer
194
+ contentViewController?.contentState = contentState
195
+ syncContentState()
196
+
197
+ // Subscribe to delegate proxy events for reactive PiP state handling
198
+ setupDelegateProxySubscriptions()
199
+
200
+ // Subscribe to content state changes for logging
201
+ setupContentStateSubscriptions()
93
202
  }
94
-
95
- func setPreferredContentSize(_ size: CGSize) {
96
- contentViewController?.preferredContentSize = size
97
- }
98
-
99
- // MARK: - AVPictureInPictureControllerDelegate
100
-
101
- func pictureInPictureController(
102
- _ pictureInPictureController: AVPictureInPictureController,
103
- restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
104
- ) {
105
- completionHandler(true)
106
- }
107
-
108
- public func pictureInPictureControllerWillStartPictureInPicture(
109
- _ pictureInPictureController: AVPictureInPictureController
110
- ) {
111
- }
112
-
113
- public func pictureInPictureControllerDidStartPictureInPicture(
114
- _ pictureInPictureController: AVPictureInPictureController
115
- ) {
116
- onPiPStateChange?(true)
203
+
204
+ // MARK: - Private Setup
205
+
206
+ /// Sets up subscriptions to the delegate proxy's event publisher.
207
+ private func setupDelegateProxySubscriptions() {
208
+ delegateProxy.publisher
209
+ .sink { [weak self] event in
210
+ self?.handleDelegateEvent(event)
211
+ }
212
+ .store(in: &cancellableBag)
117
213
  }
118
-
119
- public func pictureInPictureController(
120
- _ pictureInPictureController: AVPictureInPictureController,
121
- failedToStartPictureInPictureWithError error: Error
122
- ) {
123
- NSLog("PiP - failedToStartPictureInPictureWithError:\(error)")
214
+
215
+ /// Sets up subscriptions to content state changes for logging and debugging.
216
+ private func setupContentStateSubscriptions() {
217
+ contentState.contentPublisher
218
+ .removeDuplicates()
219
+ .sink { content in
220
+ PictureInPictureLogger.log("Content state changed to: \(content)")
221
+ }
222
+ .store(in: &cancellableBag)
124
223
  }
125
-
126
- public func pictureInPictureControllerWillStopPictureInPicture(
127
- _ pictureInPictureController: AVPictureInPictureController
128
- ) {
224
+
225
+ /// Handles events from the delegate proxy.
226
+ private func handleDelegateEvent(_ event: PictureInPictureDelegateProxy.Event) {
227
+ switch event {
228
+ case .didStart:
229
+ onPiPStateChange?(true)
230
+ case .didStop:
231
+ onPiPStateChange?(false)
232
+ case let .failedToStart(_, error):
233
+ PictureInPictureLogger.log("failedToStartPictureInPictureWithError: \(error.localizedDescription)")
234
+ // Notify JS that PiP failed to start so it can update its state accordingly
235
+ onPiPStateChange?(false)
236
+ case let .restoreUI(_, completionHandler):
237
+ completionHandler(true)
238
+ case .willStart, .willStop:
239
+ // No action needed for will start/stop events
240
+ break
241
+ }
129
242
  }
130
243
 
131
- public func pictureInPictureControllerDidStopPictureInPicture(
132
- _ pictureInPictureController: AVPictureInPictureController
133
- ) {
134
- onPiPStateChange?(false)
244
+ func setPreferredContentSize(_ size: CGSize) {
245
+ // Guard against setting zero size to avoid iOS PGPegasus code:-1003 error
246
+ guard size.width > 0, size.height > 0 else {
247
+ PictureInPictureLogger.log("Ignoring setPreferredContentSize with zero dimensions: \(size)")
248
+ return
249
+ }
250
+ contentViewController?.preferredContentSize = size
135
251
  }
136
-
252
+
137
253
  // MARK: - Private helpers
138
-
254
+
139
255
  private func didUpdate(_ track: RTCVideoTrack?) {
140
- contentViewController?.track = track
141
256
  trackStateAdapter.activeTrack = track
257
+ syncContentStateIfNeeded()
258
+ }
259
+
260
+ private func syncContentStateIfNeeded() {
261
+ guard !isApplyingContentSnapshot else { return }
262
+ syncContentState()
263
+ }
264
+
265
+ private func syncContentState() {
266
+ let snapshot = PictureInPictureContentState.Snapshot(
267
+ track: track,
268
+ participantName: participantName,
269
+ participantImageURL: participantImageURL,
270
+ isVideoEnabled: isVideoEnabled,
271
+ isScreenSharing: isScreenSharing,
272
+ isReconnecting: isReconnecting
273
+ )
274
+ contentState.apply(snapshot)
275
+ }
276
+
277
+ /// Applies all content-driving fields as a single state transition.
278
+ func applyContentSnapshot(
279
+ track: RTCVideoTrack?,
280
+ participantName: String?,
281
+ participantImageURL: String?,
282
+ isVideoEnabled: Bool,
283
+ isScreenSharing: Bool,
284
+ isReconnecting: Bool
285
+ ) {
286
+ isApplyingContentSnapshot = true
287
+ defer {
288
+ isApplyingContentSnapshot = false
289
+ syncContentState()
290
+ }
291
+
292
+ self.track = track
293
+ self.participantName = participantName
294
+ self.participantImageURL = participantImageURL
295
+ self.isVideoEnabled = isVideoEnabled
296
+ self.isScreenSharing = isScreenSharing
297
+ self.isReconnecting = isReconnecting
142
298
  }
143
299
 
144
300
  @objc private func didUpdate(_ sourceView: UIView?) {
@@ -149,7 +305,7 @@ import Foundation
149
305
 
150
306
  pictureInPictureController?
151
307
  .publisher(for: \.isPictureInPicturePossible)
152
- .sink { NSLog("PiP - isPictureInPicturePossible:\($0)") }
308
+ .sink { PictureInPictureLogger.log("isPictureInPicturePossible:\($0)") }
153
309
  .store(in: &cancellableBag)
154
310
 
155
311
  pictureInPictureController?
@@ -169,6 +325,19 @@ import Foundation
169
325
  }
170
326
 
171
327
  @objc func cleanup() {
328
+ // Cancel all Combine subscriptions
329
+ cancellableBag.removeAll()
330
+
331
+ // Reset the content state to inactive
332
+ contentState.reset()
333
+
334
+ // Disable the track state adapter to stop its timer
335
+ trackStateAdapter.isEnabled = false
336
+ trackStateAdapter.activeTrack = nil
337
+
338
+ // Release the enforced stop adapter
339
+ enforcedStopAdapter = nil
340
+
172
341
  sourceView = nil
173
342
  contentViewController?.track = nil
174
343
  contentViewController = nil
@@ -177,7 +346,6 @@ import Foundation
177
346
  }
178
347
  pictureInPictureController?.delegate = nil
179
348
  pictureInPictureController = nil
180
-
181
349
  }
182
350
 
183
351
  private func makePictureInPictureController(with sourceView: UIView) {
@@ -190,13 +358,19 @@ import Foundation
190
358
  )
191
359
  )
192
360
  }
193
-
361
+
194
362
  if #available(iOS 14.2, *) {
195
363
  pictureInPictureController?
196
364
  .canStartPictureInPictureAutomaticallyFromInline = canStartPictureInPictureAutomaticallyFromInline
197
365
  }
198
-
199
- pictureInPictureController?.delegate = self
366
+
367
+ // Use the delegate proxy for reactive event handling
368
+ pictureInPictureController?.delegate = delegateProxy
369
+
370
+ // Create the enforced stop adapter to handle app foreground transitions
371
+ if let pipController = pictureInPictureController {
372
+ enforcedStopAdapter = PictureInPictureEnforcedStopAdapter(pipController)
373
+ }
200
374
  }
201
375
 
202
376
  private func didUpdatePictureInPictureActiveState(_ isActive: Bool) {
@@ -0,0 +1,30 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVKit
6
+ import Combine
7
+ import Foundation
8
+
9
+ /// Protocol defining the interface for Picture-in-Picture controller functionality.
10
+ ///
11
+ /// This abstraction allows for easier testing and decouples components from the
12
+ /// concrete `AVPictureInPictureController` implementation.
13
+ protocol StreamPictureInPictureControllerProtocol: AnyObject {
14
+ /// Publisher that emits whenever the Picture-in-Picture active state changes.
15
+ /// Consumers should rely on this stream instead of synchronous snapshots so
16
+ /// lifecycle adapters can react to state transitions deterministically.
17
+ var isPictureInPictureActivePublisher: AnyPublisher<Bool, Never> { get }
18
+
19
+ /// Stops the Picture-in-Picture playback if it is currently active.
20
+ func stopPictureInPicture()
21
+ }
22
+
23
+ /// Extends `AVPictureInPictureController` to conform to `StreamPictureInPictureControllerProtocol`.
24
+ ///
25
+ /// This extension provides a Combine publisher for observing the `isPictureInPictureActive` property.
26
+ extension AVPictureInPictureController: StreamPictureInPictureControllerProtocol {
27
+ var isPictureInPictureActivePublisher: AnyPublisher<Bool, Never> {
28
+ publisher(for: \.isPictureInPictureActive).eraseToAnyPublisher()
29
+ }
30
+ }