@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
|
@@ -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
|
-
|
|
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
|
-
///
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|