@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,8 +7,42 @@ import Foundation
|
|
|
7
7
|
import UIKit
|
|
8
8
|
|
|
9
9
|
/// A view that can be used to render an instance of `RTCVideoTrack`
|
|
10
|
+
///
|
|
11
|
+
/// This view manages the display of different content types in the PiP window:
|
|
12
|
+
/// - Video content from a participant's camera or screen share
|
|
13
|
+
/// - Avatar placeholder when video is disabled
|
|
14
|
+
/// - Screen sharing indicator overlay
|
|
15
|
+
/// - Reconnection view during connection recovery
|
|
16
|
+
///
|
|
17
|
+
/// The content can be managed either through individual properties (legacy approach)
|
|
18
|
+
/// or through the unified `content` property using `PictureInPictureContent` enum.
|
|
10
19
|
final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
11
|
-
|
|
20
|
+
|
|
21
|
+
// MARK: - Content State (New unified approach)
|
|
22
|
+
|
|
23
|
+
/// The current content being displayed, using the unified content enum.
|
|
24
|
+
/// Setting this property automatically updates all overlay views and the video track.
|
|
25
|
+
var content: PictureInPictureContent = .inactive {
|
|
26
|
+
didSet {
|
|
27
|
+
guard content != oldValue else { return }
|
|
28
|
+
applyContent(content)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// The content state manager for reactive state updates.
|
|
33
|
+
/// When set, the renderer subscribes to content changes automatically.
|
|
34
|
+
var contentState: PictureInPictureContentState? {
|
|
35
|
+
didSet {
|
|
36
|
+
subscribeToContentState()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Cancellable for content state subscription
|
|
41
|
+
private var contentStateCancellable: AnyCancellable?
|
|
42
|
+
private var isApplyingContentBatch = false
|
|
43
|
+
|
|
44
|
+
// MARK: - Individual Properties (Legacy approach - still supported)
|
|
45
|
+
|
|
12
46
|
/// The rendering track.
|
|
13
47
|
var track: RTCVideoTrack? {
|
|
14
48
|
didSet {
|
|
@@ -16,11 +50,17 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
16
50
|
// - stopFrameStreaming for the old track
|
|
17
51
|
// - startFrameStreaming for the new track and only if we are already
|
|
18
52
|
// in picture-in-picture.
|
|
19
|
-
|
|
53
|
+
PictureInPictureLogger.log("Renderer: track changed from \(oldValue?.trackId ?? "nil") to \(track?.trackId ?? "nil")")
|
|
54
|
+
guard !isSameTrackInstance(oldValue, track) else { return }
|
|
55
|
+
trackSize = .zero
|
|
20
56
|
prepareForTrackRendering(oldValue)
|
|
57
|
+
if !isApplyingContentBatch {
|
|
58
|
+
// Track changes coming from non-content flows should still refresh overlays immediately.
|
|
59
|
+
updateOverlayVisibility()
|
|
60
|
+
}
|
|
21
61
|
}
|
|
22
62
|
}
|
|
23
|
-
|
|
63
|
+
|
|
24
64
|
/// The layer that renders the track's frames.
|
|
25
65
|
var displayLayer: CALayer { contentView.layer }
|
|
26
66
|
|
|
@@ -36,7 +76,90 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
36
76
|
/// A policy defining how the Picture in Picture window should be resized in order to better fit
|
|
37
77
|
/// the rendering frame size.
|
|
38
78
|
var pictureInPictureWindowSizePolicy: PictureInPictureWindowSizePolicy
|
|
39
|
-
|
|
79
|
+
|
|
80
|
+
// MARK: - Avatar Placeholder Properties
|
|
81
|
+
|
|
82
|
+
/// The participant's name for the avatar and overlay
|
|
83
|
+
var participantName: String? {
|
|
84
|
+
didSet {
|
|
85
|
+
PictureInPictureLogger.log("Renderer.participantName didSet: '\(participantName ?? "nil")', forwarding to avatarView")
|
|
86
|
+
avatarView.participantName = participantName
|
|
87
|
+
participantOverlayView.participantName = participantName
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The URL string for the participant's profile image
|
|
92
|
+
var participantImageURL: String? {
|
|
93
|
+
didSet {
|
|
94
|
+
avatarView.imageURL = participantImageURL
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Whether video is enabled - when false, shows avatar placeholder
|
|
99
|
+
var isVideoEnabled: Bool = true {
|
|
100
|
+
didSet {
|
|
101
|
+
PictureInPictureLogger.log("Renderer: isVideoEnabled changed from \(oldValue) to \(isVideoEnabled), avatarView.participantName='\(avatarView.participantName ?? "nil")'")
|
|
102
|
+
if !isApplyingContentBatch {
|
|
103
|
+
updateOverlayVisibility()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Whether the call is reconnecting - when true, shows reconnection view
|
|
109
|
+
var isReconnecting: Bool = false {
|
|
110
|
+
didSet {
|
|
111
|
+
reconnectionView.isReconnecting = isReconnecting
|
|
112
|
+
if !isApplyingContentBatch {
|
|
113
|
+
updateOverlayVisibility()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Whether screen sharing is active (used for content state tracking)
|
|
119
|
+
var isScreenSharing: Bool = false
|
|
120
|
+
|
|
121
|
+
/// Whether the participant has audio enabled (shown in participant overlay)
|
|
122
|
+
var hasAudio: Bool = true {
|
|
123
|
+
didSet {
|
|
124
|
+
participantOverlayView.hasAudio = hasAudio
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Whether the video track is paused (shown in participant overlay)
|
|
129
|
+
var isTrackPaused: Bool = false {
|
|
130
|
+
didSet {
|
|
131
|
+
participantOverlayView.isTrackPaused = isTrackPaused
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Whether the participant is pinned (shown in participant overlay)
|
|
136
|
+
var isPinned: Bool = false {
|
|
137
|
+
didSet {
|
|
138
|
+
participantOverlayView.isPinned = isPinned
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Whether the participant is currently speaking (shows border highlight)
|
|
143
|
+
var isSpeaking: Bool = false {
|
|
144
|
+
didSet {
|
|
145
|
+
updateSpeakingIndicator()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// The connection quality level (0: unknown, 1: poor, 2: good, 3: excellent)
|
|
150
|
+
var connectionQuality: Int = 0 {
|
|
151
|
+
didSet {
|
|
152
|
+
connectionQualityIndicator.connectionQuality = PictureInPictureConnectionQualityIndicator.ConnectionQuality(rawValue: connectionQuality) ?? .unspecified
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Whether the participant overlay is enabled
|
|
157
|
+
var isParticipantOverlayEnabled: Bool = true {
|
|
158
|
+
didSet {
|
|
159
|
+
participantOverlayView.isOverlayEnabled = isParticipantOverlayEnabled
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
40
163
|
/// The publisher which is used to streamline the frames received from the track.
|
|
41
164
|
private let bufferPublisher: PassthroughSubject<CMSampleBuffer, Never> = .init()
|
|
42
165
|
|
|
@@ -68,7 +191,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
68
191
|
didUpdateTrackSize()
|
|
69
192
|
}
|
|
70
193
|
}
|
|
71
|
-
|
|
194
|
+
|
|
72
195
|
/// A property that defines if the RTCVideoFrame instances that will be rendered need to be resized
|
|
73
196
|
/// to fid the view's contentSize.
|
|
74
197
|
private var requiresResize = false {
|
|
@@ -95,7 +218,59 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
95
218
|
|
|
96
219
|
/// A size ratio threshold used to determine if skipping frames is required.
|
|
97
220
|
private let sizeRatioThreshold: CGFloat = 15
|
|
98
|
-
|
|
221
|
+
|
|
222
|
+
/// The avatar view shown when video is disabled
|
|
223
|
+
/// Note: Uses alpha=0 for visibility instead of isHidden to match upstream SwiftUI behavior
|
|
224
|
+
/// and ensure layoutSubviews is always called for proper constraint layout.
|
|
225
|
+
private lazy var avatarView: PictureInPictureAvatarView = {
|
|
226
|
+
let view = PictureInPictureAvatarView()
|
|
227
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
228
|
+
view.alpha = 0 // Initially invisible (video enabled by default)
|
|
229
|
+
return view
|
|
230
|
+
}()
|
|
231
|
+
|
|
232
|
+
/// The reconnection view shown when connection is being recovered
|
|
233
|
+
private lazy var reconnectionView: PictureInPictureReconnectionView = {
|
|
234
|
+
let view = PictureInPictureReconnectionView()
|
|
235
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
236
|
+
view.isHidden = true // Initially hidden (not reconnecting by default)
|
|
237
|
+
return view
|
|
238
|
+
}()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
/// The participant overlay view showing name and mute status
|
|
242
|
+
private lazy var participantOverlayView: PictureInPictureParticipantOverlayView = {
|
|
243
|
+
let view = PictureInPictureParticipantOverlayView()
|
|
244
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
245
|
+
return view
|
|
246
|
+
}()
|
|
247
|
+
|
|
248
|
+
/// Connection quality indicator view (bottom-right)
|
|
249
|
+
private lazy var connectionQualityIndicator: PictureInPictureConnectionQualityIndicator = {
|
|
250
|
+
let view = PictureInPictureConnectionQualityIndicator()
|
|
251
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
252
|
+
return view
|
|
253
|
+
}()
|
|
254
|
+
|
|
255
|
+
/// Speaking indicator border layer
|
|
256
|
+
private lazy var speakingBorderLayer: CAShapeLayer = {
|
|
257
|
+
let layer = CAShapeLayer()
|
|
258
|
+
layer.fillColor = UIColor.clear.cgColor
|
|
259
|
+
layer.strokeColor = UIColor(red: 0.0, green: 0.8, blue: 0.6, alpha: 1.0).cgColor // Teal green
|
|
260
|
+
layer.lineWidth = 2
|
|
261
|
+
layer.isHidden = true
|
|
262
|
+
return layer
|
|
263
|
+
}()
|
|
264
|
+
|
|
265
|
+
/// The speaking indicator corner radius (matches upstream)
|
|
266
|
+
private var speakingCornerRadius: CGFloat {
|
|
267
|
+
if #available(iOS 26.0, *) {
|
|
268
|
+
return 32
|
|
269
|
+
} else {
|
|
270
|
+
return 16
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
99
274
|
// MARK: - Lifecycle
|
|
100
275
|
|
|
101
276
|
@available(*, unavailable)
|
|
@@ -112,15 +287,28 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
112
287
|
// Depending on the window we are moving we either start or stop
|
|
113
288
|
// streaming frames from the track.
|
|
114
289
|
if newWindow != nil {
|
|
290
|
+
PictureInPictureLogger.log("Renderer: willMove(toWindow:) - added to window, track=\(track?.trackId ?? "nil"), isVideoEnabled=\(isVideoEnabled)")
|
|
291
|
+
trackSize = .zero
|
|
292
|
+
updateOverlayVisibility()
|
|
115
293
|
startFrameStreaming(for: track, on: newWindow)
|
|
116
294
|
} else {
|
|
295
|
+
PictureInPictureLogger.log("Renderer: willMove(toWindow:) - removed from window")
|
|
117
296
|
stopFrameStreaming(for: track)
|
|
297
|
+
trackSize = .zero
|
|
298
|
+
updateOverlayVisibility()
|
|
118
299
|
}
|
|
119
300
|
}
|
|
120
301
|
|
|
121
302
|
override func layoutSubviews() {
|
|
122
303
|
super.layoutSubviews()
|
|
123
304
|
contentSize = frame.size
|
|
305
|
+
|
|
306
|
+
// Update speaking border frame
|
|
307
|
+
CATransaction.begin()
|
|
308
|
+
CATransaction.setDisableActions(true)
|
|
309
|
+
speakingBorderLayer.frame = bounds
|
|
310
|
+
speakingBorderLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), cornerRadius: speakingCornerRadius).cgPath
|
|
311
|
+
CATransaction.commit()
|
|
124
312
|
}
|
|
125
313
|
|
|
126
314
|
// MARK: - Rendering lifecycle
|
|
@@ -134,7 +322,12 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
134
322
|
guard let frame = frame else {
|
|
135
323
|
return
|
|
136
324
|
}
|
|
137
|
-
|
|
325
|
+
|
|
326
|
+
// Ignore empty frames
|
|
327
|
+
if frame.width <= 0 || frame.height <= 0 {
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
138
331
|
// Update the trackSize and re-calculate rendering properties if the size
|
|
139
332
|
// has changed.
|
|
140
333
|
trackSize = .init(width: Int(frame.width), height: Int(frame.height))
|
|
@@ -159,15 +352,96 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
159
352
|
|
|
160
353
|
/// Set up the view's hierarchy.
|
|
161
354
|
private func setUp() {
|
|
355
|
+
// Add speaking border layer first (behind everything else)
|
|
356
|
+
layer.addSublayer(speakingBorderLayer)
|
|
357
|
+
|
|
162
358
|
addSubview(contentView)
|
|
359
|
+
addSubview(avatarView)
|
|
360
|
+
addSubview(reconnectionView)
|
|
361
|
+
addSubview(participantOverlayView)
|
|
362
|
+
addSubview(connectionQualityIndicator)
|
|
363
|
+
|
|
163
364
|
NSLayoutConstraint.activate([
|
|
164
365
|
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
165
366
|
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
166
367
|
contentView.topAnchor.constraint(equalTo: topAnchor),
|
|
167
|
-
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
368
|
+
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
369
|
+
|
|
370
|
+
avatarView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
371
|
+
avatarView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
372
|
+
avatarView.topAnchor.constraint(equalTo: topAnchor),
|
|
373
|
+
avatarView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
374
|
+
|
|
375
|
+
reconnectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
376
|
+
reconnectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
377
|
+
reconnectionView.topAnchor.constraint(equalTo: topAnchor),
|
|
378
|
+
reconnectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
379
|
+
|
|
380
|
+
// Participant overlay positioned at bottom
|
|
381
|
+
participantOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
382
|
+
participantOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
383
|
+
participantOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
384
|
+
participantOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
385
|
+
|
|
386
|
+
// Connection quality indicator at bottom-right
|
|
387
|
+
connectionQualityIndicator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
388
|
+
connectionQualityIndicator.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
389
|
+
connectionQualityIndicator.widthAnchor.constraint(equalToConstant: 28),
|
|
390
|
+
connectionQualityIndicator.heightAnchor.constraint(equalToConstant: 28)
|
|
168
391
|
])
|
|
169
392
|
}
|
|
170
|
-
|
|
393
|
+
|
|
394
|
+
/// Updates the visibility of overlay views based on current state.
|
|
395
|
+
/// Priority: reconnection view > avatar view > video content
|
|
396
|
+
///
|
|
397
|
+
/// The avatar view is shown when:
|
|
398
|
+
/// - Video is explicitly disabled (isVideoEnabled = false), OR
|
|
399
|
+
/// - Track is nil
|
|
400
|
+
///
|
|
401
|
+
/// IMPORTANT: Participant overlay (name, mic, connection quality) is shown on top of BOTH
|
|
402
|
+
/// video AND avatar views, matching the upstream stream-video-swift implementation.
|
|
403
|
+
/// The overlay is only hidden during reconnection.
|
|
404
|
+
private func updateOverlayVisibility() {
|
|
405
|
+
// Reconnection view takes highest priority
|
|
406
|
+
if isReconnecting {
|
|
407
|
+
PictureInPictureLogger.log("updateOverlayVisibility: isReconnecting=true, hiding avatar, showing reconnection")
|
|
408
|
+
reconnectionView.isHidden = false
|
|
409
|
+
avatarView.alpha = 0
|
|
410
|
+
avatarView.isVideoEnabled = true
|
|
411
|
+
// Hide participant overlay ONLY during reconnection (matches upstream)
|
|
412
|
+
participantOverlayView.isOverlayEnabled = false
|
|
413
|
+
} else {
|
|
414
|
+
reconnectionView.isHidden = true
|
|
415
|
+
// Avatar view shows when video is disabled OR when we don't have a track
|
|
416
|
+
let shouldShowVideo = isVideoEnabled && track != nil
|
|
417
|
+
let shouldShowAvatar = !shouldShowVideo
|
|
418
|
+
PictureInPictureLogger.log("updateOverlayVisibility: isVideoEnabled=\(isVideoEnabled), track=\(track?.trackId ?? "nil"), shouldShowAvatar=\(shouldShowAvatar)")
|
|
419
|
+
|
|
420
|
+
// Update avatar visibility - setting isVideoEnabled triggers internal layout
|
|
421
|
+
avatarView.isVideoEnabled = !shouldShowAvatar
|
|
422
|
+
avatarView.alpha = shouldShowAvatar ? 1 : 0
|
|
423
|
+
|
|
424
|
+
// Force layout when avatar becomes visible to ensure proper sizing
|
|
425
|
+
if shouldShowAvatar {
|
|
426
|
+
PictureInPictureLogger.log("updateOverlayVisibility: showing avatar, forcing layout. participantName=\(participantName ?? "nil"), avatarView.participantName='\(avatarView.participantName ?? "nil")'")
|
|
427
|
+
avatarView.setNeedsLayout()
|
|
428
|
+
avatarView.layoutIfNeeded()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Participant overlay shows on BOTH video and avatar (matches upstream)
|
|
432
|
+
// Only hide during reconnection
|
|
433
|
+
participantOverlayView.isOverlayEnabled = true
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Updates the speaking indicator border visibility based on isSpeaking state.
|
|
438
|
+
/// The border is shown when the participant is speaking, on BOTH video and avatar views
|
|
439
|
+
/// (matching upstream behavior). Only hidden during reconnection.
|
|
440
|
+
private func updateSpeakingIndicator() {
|
|
441
|
+
let shouldShowBorder = isSpeaking && !isReconnecting
|
|
442
|
+
speakingBorderLayer.isHidden = !shouldShowBorder
|
|
443
|
+
}
|
|
444
|
+
|
|
171
445
|
/// A method used to process the frame's buffer and enqueue on the rendering view.
|
|
172
446
|
private func process(_ buffer: CMSampleBuffer) {
|
|
173
447
|
guard
|
|
@@ -198,14 +472,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
198
472
|
on window: UIWindow?
|
|
199
473
|
) {
|
|
200
474
|
guard window != nil, let track else { return }
|
|
201
|
-
|
|
475
|
+
|
|
202
476
|
bufferUpdatesCancellable = bufferPublisher
|
|
203
477
|
.receive(on: DispatchQueue.main)
|
|
204
478
|
.sink { [weak self] in self?.process($0) }
|
|
205
|
-
|
|
479
|
+
|
|
206
480
|
track.add(self)
|
|
207
481
|
}
|
|
208
|
-
|
|
482
|
+
|
|
209
483
|
/// A method that stops the frame consumption from the track. Used automatically when the rendering
|
|
210
484
|
/// view move's away from the window or when the track changes.
|
|
211
485
|
private func stopFrameStreaming(for track: RTCVideoTrack?) {
|
|
@@ -257,4 +531,102 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
257
531
|
requiresResize = false
|
|
258
532
|
startFrameStreaming(for: track, on: window)
|
|
259
533
|
}
|
|
534
|
+
|
|
535
|
+
private func isSameTrackInstance(_ lhs: RTCVideoTrack?, _ rhs: RTCVideoTrack?) -> Bool {
|
|
536
|
+
switch (lhs, rhs) {
|
|
537
|
+
case (nil, nil):
|
|
538
|
+
return true
|
|
539
|
+
case let (lhsTrack?, rhsTrack?):
|
|
540
|
+
return lhsTrack === rhsTrack
|
|
541
|
+
default:
|
|
542
|
+
return false
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// MARK: - Content State System
|
|
547
|
+
|
|
548
|
+
/// Subscribes to the content state manager for reactive updates.
|
|
549
|
+
private func subscribeToContentState() {
|
|
550
|
+
contentStateCancellable?.cancel()
|
|
551
|
+
contentStateCancellable = nil
|
|
552
|
+
|
|
553
|
+
guard let contentState = contentState else { return }
|
|
554
|
+
|
|
555
|
+
contentStateCancellable = contentState.contentPublisher
|
|
556
|
+
.receive(on: DispatchQueue.main)
|
|
557
|
+
.sink { [weak self] newContent in
|
|
558
|
+
self?.content = newContent
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// Applies the given content state to update all view components.
|
|
563
|
+
/// This method synchronizes the unified content enum with the individual properties
|
|
564
|
+
/// for backward compatibility while providing a cleaner API.
|
|
565
|
+
private func applyContent(_ content: PictureInPictureContent) {
|
|
566
|
+
isApplyingContentBatch = true
|
|
567
|
+
defer {
|
|
568
|
+
isApplyingContentBatch = false
|
|
569
|
+
updateOverlayVisibility()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
switch content {
|
|
573
|
+
case .inactive:
|
|
574
|
+
// Clear everything
|
|
575
|
+
track = nil
|
|
576
|
+
participantName = nil
|
|
577
|
+
participantImageURL = nil
|
|
578
|
+
isVideoEnabled = true
|
|
579
|
+
isReconnecting = false
|
|
580
|
+
isScreenSharing = false
|
|
581
|
+
|
|
582
|
+
case let .video(newTrack, name, imageURL):
|
|
583
|
+
// Show video content
|
|
584
|
+
track = newTrack
|
|
585
|
+
participantName = name
|
|
586
|
+
participantImageURL = imageURL
|
|
587
|
+
isVideoEnabled = true
|
|
588
|
+
isReconnecting = false
|
|
589
|
+
isScreenSharing = false
|
|
590
|
+
|
|
591
|
+
case let .screenSharing(newTrack, name):
|
|
592
|
+
// Show screen sharing content with indicator
|
|
593
|
+
track = newTrack
|
|
594
|
+
participantName = name
|
|
595
|
+
participantImageURL = nil
|
|
596
|
+
isVideoEnabled = true
|
|
597
|
+
isReconnecting = false
|
|
598
|
+
isScreenSharing = true
|
|
599
|
+
|
|
600
|
+
case let .avatar(name, imageURL):
|
|
601
|
+
// Show avatar placeholder (video disabled)
|
|
602
|
+
// Keep existing track for potential quick re-enable
|
|
603
|
+
participantName = name
|
|
604
|
+
participantImageURL = imageURL
|
|
605
|
+
isVideoEnabled = false
|
|
606
|
+
isReconnecting = false
|
|
607
|
+
isScreenSharing = false
|
|
608
|
+
|
|
609
|
+
case .reconnecting:
|
|
610
|
+
// Show reconnection view
|
|
611
|
+
// Keep existing track and participant info for recovery
|
|
612
|
+
isReconnecting = true
|
|
613
|
+
isScreenSharing = false
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Returns the current content as a `PictureInPictureContent` enum value.
|
|
618
|
+
/// This is useful for reading the current state in a unified way.
|
|
619
|
+
func getCurrentContent() -> PictureInPictureContent {
|
|
620
|
+
if isReconnecting {
|
|
621
|
+
return .reconnecting
|
|
622
|
+
} else if !isVideoEnabled {
|
|
623
|
+
return .avatar(participantName: participantName, participantImageURL: participantImageURL)
|
|
624
|
+
} else if isScreenSharing {
|
|
625
|
+
return .screenSharing(track: track, participantName: participantName)
|
|
626
|
+
} else if track != nil {
|
|
627
|
+
return .video(track: track, participantName: participantName, participantImageURL: participantImageURL)
|
|
628
|
+
} else {
|
|
629
|
+
return .avatar(participantName: participantName, participantImageURL: participantImageURL)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
260
632
|
}
|