@stream-io/video-react-native-sdk 1.37.1-beta.0 → 1.38.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 +20 -0
- package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +0 -81
- package/dist/commonjs/hooks/index.js +0 -11
- package/dist/commonjs/hooks/index.js.map +1 -1
- package/dist/commonjs/modules/call-manager/CallManager.js +13 -0
- package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
- package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js +37 -0
- package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
- package/dist/commonjs/providers/StreamCall/index.js +2 -1
- package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
- package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/utils/push/internal/ios.js +5 -0
- package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/module/hooks/index.js +0 -1
- package/dist/module/hooks/index.js.map +1 -1
- package/dist/module/modules/call-manager/CallManager.js +13 -0
- package/dist/module/modules/call-manager/CallManager.js.map +1 -1
- package/dist/module/providers/StreamCall/AudioInterruptionTracer.js +30 -0
- package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
- package/dist/module/providers/StreamCall/index.js +2 -1
- package/dist/module/providers/StreamCall/index.js.map +1 -1
- package/dist/module/utils/internal/callingx/callingx.js +2 -2
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/utils/push/internal/ios.js +5 -0
- package/dist/module/utils/push/internal/ios.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/module/version.js.map +1 -1
- package/dist/typescript/hooks/index.d.ts +0 -1
- package/dist/typescript/hooks/index.d.ts.map +1 -1
- package/dist/typescript/modules/call-manager/CallManager.d.ts +8 -1
- package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
- package/dist/typescript/modules/call-manager/types.d.ts +6 -0
- package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
- package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts +5 -0
- package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -0
- package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
- package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
- package/dist/typescript/version.d.ts +1 -1
- package/dist/typescript/version.d.ts.map +1 -1
- package/ios/PictureInPicture/StreamBufferTransformer.swift +13 -4
- package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +76 -70
- package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +20 -16
- package/ios/StreamInCallManager.swift +237 -81
- package/ios/StreamVideoReactNative-Bridging-Header.h +0 -2
- package/ios/StreamVideoReactNative.m +0 -81
- package/package.json +11 -11
- package/src/hooks/index.ts +0 -1
- package/src/modules/call-manager/CallManager.ts +25 -1
- package/src/modules/call-manager/types.ts +7 -0
- package/src/providers/StreamCall/AudioInterruptionTracer.tsx +51 -0
- package/src/providers/StreamCall/index.tsx +2 -0
- package/src/utils/internal/callingx/callingx.ts +2 -2
- package/src/utils/push/internal/ios.ts +5 -0
- package/src/version.ts +1 -1
- package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +0 -436
- package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +0 -17
- package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +0 -36
- package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +0 -60
- package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +0 -31
- package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +0 -329
- package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +0 -472
- package/dist/commonjs/hooks/useLoopbackRecording.js +0 -243
- package/dist/commonjs/hooks/useLoopbackRecording.js.map +0 -1
- package/dist/module/hooks/useLoopbackRecording.js +0 -238
- package/dist/module/hooks/useLoopbackRecording.js.map +0 -1
- package/dist/typescript/hooks/useLoopbackRecording.d.ts +0 -85
- package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +0 -1
- package/ios/TracksRecorder/AudioPipeline.swift +0 -270
- package/ios/TracksRecorder/PipelineHost.swift +0 -56
- package/ios/TracksRecorder/RecorderAudioRenderTap.swift +0 -154
- package/ios/TracksRecorder/RecorderVideoSink.swift +0 -137
- package/ios/TracksRecorder/TracksRecorderManager.swift +0 -327
- package/ios/TracksRecorder/VideoPipeline.swift +0 -297
- package/src/hooks/useLoopbackRecording.ts +0 -438
|
@@ -17,9 +17,9 @@ import UIKit
|
|
|
17
17
|
/// The content can be managed either through individual properties (legacy approach)
|
|
18
18
|
/// or through the unified `content` property using `PictureInPictureContent` enum.
|
|
19
19
|
final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
// MARK: - Content State (New unified approach)
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
/// The current content being displayed, using the unified content enum.
|
|
24
24
|
/// Setting this property automatically updates all overlay views and the video track.
|
|
25
25
|
var content: PictureInPictureContent = .inactive {
|
|
@@ -28,7 +28,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
28
28
|
applyContent(content)
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
/// The content state manager for reactive state updates.
|
|
33
33
|
/// When set, the renderer subscribes to content changes automatically.
|
|
34
34
|
var contentState: PictureInPictureContentState? {
|
|
@@ -36,13 +36,13 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
36
36
|
subscribeToContentState()
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
/// Cancellable for content state subscription
|
|
41
41
|
private var contentStateCancellable: AnyCancellable?
|
|
42
42
|
private var isApplyingContentBatch = false
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
// MARK: - Individual Properties (Legacy approach - still supported)
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
/// The rendering track.
|
|
47
47
|
var track: RTCVideoTrack? {
|
|
48
48
|
didSet {
|
|
@@ -60,25 +60,25 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
/// The layer that renders the track's frames.
|
|
65
65
|
var displayLayer: CALayer { contentView.layer }
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
/// Indicates whether the rendered video should be mirrored.
|
|
68
68
|
var isMirrored: Bool = false {
|
|
69
69
|
didSet {
|
|
70
70
|
contentView.transform = isMirrored
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
? CGAffineTransform(scaleX: -1, y: 1)
|
|
72
|
+
: .identity
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
/// A policy defining how the Picture in Picture window should be resized in order to better fit
|
|
77
77
|
/// the rendering frame size.
|
|
78
78
|
var pictureInPictureWindowSizePolicy: PictureInPictureWindowSizePolicy
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
// MARK: - Avatar Placeholder Properties
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
/// The participant's name for the avatar and overlay
|
|
83
83
|
var participantName: String? {
|
|
84
84
|
didSet {
|
|
@@ -87,14 +87,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
87
87
|
participantOverlayView.participantName = participantName
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
/// The URL string for the participant's profile image
|
|
92
92
|
var participantImageURL: String? {
|
|
93
93
|
didSet {
|
|
94
94
|
avatarView.imageURL = participantImageURL
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
/// Whether video is enabled - when false, shows avatar placeholder
|
|
99
99
|
var isVideoEnabled: Bool = true {
|
|
100
100
|
didSet {
|
|
@@ -104,7 +104,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
/// Whether the call is reconnecting - when true, shows reconnection view
|
|
109
109
|
var isReconnecting: Bool = false {
|
|
110
110
|
didSet {
|
|
@@ -114,52 +114,52 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
/// Whether screen sharing is active (used for content state tracking)
|
|
119
119
|
var isScreenSharing: Bool = false
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
/// Whether the participant has audio enabled (shown in participant overlay)
|
|
122
122
|
var hasAudio: Bool = true {
|
|
123
123
|
didSet {
|
|
124
124
|
participantOverlayView.hasAudio = hasAudio
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
/// Whether the video track is paused (shown in participant overlay)
|
|
129
129
|
var isTrackPaused: Bool = false {
|
|
130
130
|
didSet {
|
|
131
131
|
participantOverlayView.isTrackPaused = isTrackPaused
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
/// Whether the participant is pinned (shown in participant overlay)
|
|
136
136
|
var isPinned: Bool = false {
|
|
137
137
|
didSet {
|
|
138
138
|
participantOverlayView.isPinned = isPinned
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
/// Whether the participant is currently speaking (shows border highlight)
|
|
143
143
|
var isSpeaking: Bool = false {
|
|
144
144
|
didSet {
|
|
145
145
|
updateSpeakingIndicator()
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
|
|
148
|
+
|
|
149
149
|
/// The connection quality level (0: unknown, 1: poor, 2: good, 3: excellent)
|
|
150
150
|
var connectionQuality: Int = 0 {
|
|
151
151
|
didSet {
|
|
152
152
|
connectionQualityIndicator.connectionQuality = PictureInPictureConnectionQualityIndicator.ConnectionQuality(rawValue: connectionQuality) ?? .unspecified
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
/// Whether the participant overlay is enabled
|
|
157
157
|
var isParticipantOverlayEnabled: Bool = true {
|
|
158
158
|
didSet {
|
|
159
159
|
participantOverlayView.isOverlayEnabled = isParticipantOverlayEnabled
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
/// The publisher which is used to streamline the frames received from the track.
|
|
164
164
|
private let bufferPublisher: PassthroughSubject<CMSampleBuffer, Never> = .init()
|
|
165
165
|
|
|
@@ -191,7 +191,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
191
191
|
didUpdateTrackSize()
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
/// A property that defines if the RTCVideoFrame instances that will be rendered need to be resized
|
|
196
196
|
/// to fid the view's contentSize.
|
|
197
197
|
private var requiresResize = false {
|
|
@@ -208,8 +208,8 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
208
208
|
/// ``renderFrame(_:)``.
|
|
209
209
|
private var skippedFrames = 0
|
|
210
210
|
|
|
211
|
-
///
|
|
212
|
-
private var shouldRenderFrame: Bool { skippedFrames == 0 && trackSize != .zero }
|
|
211
|
+
/// Render only when not skipping and both trackSize and contentSize are non-zero.
|
|
212
|
+
private var shouldRenderFrame: Bool { skippedFrames == 0 && trackSize != .zero && contentSize != .zero }
|
|
213
213
|
|
|
214
214
|
/// A size ratio threshold used to determine if resizing is required.
|
|
215
215
|
/// - Note: It seems that Picture-in-Picture doesn't like rendering frames that are bigger than its
|
|
@@ -218,7 +218,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
218
218
|
|
|
219
219
|
/// A size ratio threshold used to determine if skipping frames is required.
|
|
220
220
|
private let sizeRatioThreshold: CGFloat = 15
|
|
221
|
-
|
|
221
|
+
|
|
222
222
|
/// The avatar view shown when video is disabled
|
|
223
223
|
/// Note: Uses alpha=0 for visibility instead of isHidden to match upstream SwiftUI behavior
|
|
224
224
|
/// and ensure layoutSubviews is always called for proper constraint layout.
|
|
@@ -228,7 +228,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
228
228
|
view.alpha = 0 // Initially invisible (video enabled by default)
|
|
229
229
|
return view
|
|
230
230
|
}()
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
/// The reconnection view shown when connection is being recovered
|
|
233
233
|
private lazy var reconnectionView: PictureInPictureReconnectionView = {
|
|
234
234
|
let view = PictureInPictureReconnectionView()
|
|
@@ -236,22 +236,22 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
236
236
|
view.isHidden = true // Initially hidden (not reconnecting by default)
|
|
237
237
|
return view
|
|
238
238
|
}()
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
|
|
240
|
+
|
|
241
241
|
/// The participant overlay view showing name and mute status
|
|
242
242
|
private lazy var participantOverlayView: PictureInPictureParticipantOverlayView = {
|
|
243
243
|
let view = PictureInPictureParticipantOverlayView()
|
|
244
244
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
245
245
|
return view
|
|
246
246
|
}()
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
/// Connection quality indicator view (bottom-right)
|
|
249
249
|
private lazy var connectionQualityIndicator: PictureInPictureConnectionQualityIndicator = {
|
|
250
250
|
let view = PictureInPictureConnectionQualityIndicator()
|
|
251
251
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
252
252
|
return view
|
|
253
253
|
}()
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
/// Speaking indicator border layer
|
|
256
256
|
private lazy var speakingBorderLayer: CAShapeLayer = {
|
|
257
257
|
let layer = CAShapeLayer()
|
|
@@ -261,7 +261,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
261
261
|
layer.isHidden = true
|
|
262
262
|
return layer
|
|
263
263
|
}()
|
|
264
|
-
|
|
264
|
+
|
|
265
265
|
/// The speaking indicator corner radius (matches upstream)
|
|
266
266
|
private var speakingCornerRadius: CGFloat {
|
|
267
267
|
if #available(iOS 26.0, *) {
|
|
@@ -270,7 +270,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
270
270
|
return 16
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
// MARK: - Lifecycle
|
|
275
275
|
|
|
276
276
|
@available(*, unavailable)
|
|
@@ -301,8 +301,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
301
301
|
|
|
302
302
|
override func layoutSubviews() {
|
|
303
303
|
super.layoutSubviews()
|
|
304
|
+
let previousContentSize = contentSize
|
|
304
305
|
contentSize = frame.size
|
|
305
|
-
|
|
306
|
+
|
|
307
|
+
// Recompute resize params once the real size is known; trackSize alone won't trigger it.
|
|
308
|
+
if contentSize != previousContentSize, contentSize != .zero, trackSize != .zero {
|
|
309
|
+
didUpdateTrackSize()
|
|
310
|
+
}
|
|
311
|
+
|
|
306
312
|
// Update speaking border frame
|
|
307
313
|
CATransaction.begin()
|
|
308
314
|
CATransaction.setDisableActions(true)
|
|
@@ -322,12 +328,12 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
322
328
|
guard let frame = frame else {
|
|
323
329
|
return
|
|
324
330
|
}
|
|
325
|
-
|
|
331
|
+
|
|
326
332
|
// Ignore empty frames
|
|
327
333
|
if frame.width <= 0 || frame.height <= 0 {
|
|
328
334
|
return
|
|
329
335
|
}
|
|
330
|
-
|
|
336
|
+
|
|
331
337
|
// Update the trackSize and re-calculate rendering properties if the size
|
|
332
338
|
// has changed.
|
|
333
339
|
trackSize = .init(width: Int(frame.width), height: Int(frame.height))
|
|
@@ -339,13 +345,13 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
339
345
|
guard shouldRenderFrame else {
|
|
340
346
|
return
|
|
341
347
|
}
|
|
342
|
-
|
|
348
|
+
|
|
343
349
|
if
|
|
344
350
|
let yuvBuffer = bufferTransformer.transformAndResizeIfRequired(frame, targetSize: contentSize)?
|
|
345
|
-
|
|
351
|
+
.buffer as? StreamRTCYUVBuffer,
|
|
346
352
|
let sampleBuffer = yuvBuffer.sampleBuffer {
|
|
347
353
|
bufferPublisher.send(sampleBuffer)
|
|
348
|
-
}
|
|
354
|
+
}
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
// MARK: - Private helpers
|
|
@@ -354,35 +360,35 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
354
360
|
private func setUp() {
|
|
355
361
|
// Add speaking border layer first (behind everything else)
|
|
356
362
|
layer.addSublayer(speakingBorderLayer)
|
|
357
|
-
|
|
363
|
+
|
|
358
364
|
addSubview(contentView)
|
|
359
365
|
addSubview(avatarView)
|
|
360
366
|
addSubview(reconnectionView)
|
|
361
367
|
addSubview(participantOverlayView)
|
|
362
368
|
addSubview(connectionQualityIndicator)
|
|
363
|
-
|
|
369
|
+
|
|
364
370
|
NSLayoutConstraint.activate([
|
|
365
371
|
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
366
372
|
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
367
373
|
contentView.topAnchor.constraint(equalTo: topAnchor),
|
|
368
374
|
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
369
|
-
|
|
375
|
+
|
|
370
376
|
avatarView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
371
377
|
avatarView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
372
378
|
avatarView.topAnchor.constraint(equalTo: topAnchor),
|
|
373
379
|
avatarView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
374
|
-
|
|
380
|
+
|
|
375
381
|
reconnectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
376
382
|
reconnectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
377
383
|
reconnectionView.topAnchor.constraint(equalTo: topAnchor),
|
|
378
384
|
reconnectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
379
|
-
|
|
385
|
+
|
|
380
386
|
// Participant overlay positioned at bottom
|
|
381
387
|
participantOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
382
388
|
participantOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
383
389
|
participantOverlayView.topAnchor.constraint(equalTo: topAnchor),
|
|
384
390
|
participantOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
385
|
-
|
|
391
|
+
|
|
386
392
|
// Connection quality indicator at bottom-right
|
|
387
393
|
connectionQualityIndicator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
388
394
|
connectionQualityIndicator.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
@@ -390,7 +396,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
390
396
|
connectionQualityIndicator.heightAnchor.constraint(equalToConstant: 28)
|
|
391
397
|
])
|
|
392
398
|
}
|
|
393
|
-
|
|
399
|
+
|
|
394
400
|
/// Updates the visibility of overlay views based on current state.
|
|
395
401
|
/// Priority: reconnection view > avatar view > video content
|
|
396
402
|
///
|
|
@@ -416,24 +422,24 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
416
422
|
let shouldShowVideo = isVideoEnabled && track != nil
|
|
417
423
|
let shouldShowAvatar = !shouldShowVideo
|
|
418
424
|
PictureInPictureLogger.log("updateOverlayVisibility: isVideoEnabled=\(isVideoEnabled), track=\(track?.trackId ?? "nil"), shouldShowAvatar=\(shouldShowAvatar)")
|
|
419
|
-
|
|
425
|
+
|
|
420
426
|
// Update avatar visibility - setting isVideoEnabled triggers internal layout
|
|
421
427
|
avatarView.isVideoEnabled = !shouldShowAvatar
|
|
422
428
|
avatarView.alpha = shouldShowAvatar ? 1 : 0
|
|
423
|
-
|
|
429
|
+
|
|
424
430
|
// Force layout when avatar becomes visible to ensure proper sizing
|
|
425
431
|
if shouldShowAvatar {
|
|
426
432
|
PictureInPictureLogger.log("updateOverlayVisibility: showing avatar, forcing layout. participantName=\(participantName ?? "nil"), avatarView.participantName='\(avatarView.participantName ?? "nil")'")
|
|
427
433
|
avatarView.setNeedsLayout()
|
|
428
434
|
avatarView.layoutIfNeeded()
|
|
429
435
|
}
|
|
430
|
-
|
|
436
|
+
|
|
431
437
|
// Participant overlay shows on BOTH video and avatar (matches upstream)
|
|
432
438
|
// Only hide during reconnection
|
|
433
439
|
participantOverlayView.isOverlayEnabled = true
|
|
434
440
|
}
|
|
435
441
|
}
|
|
436
|
-
|
|
442
|
+
|
|
437
443
|
/// Updates the speaking indicator border visibility based on isSpeaking state.
|
|
438
444
|
/// The border is shown when the participant is speaking, on BOTH video and avatar views
|
|
439
445
|
/// (matching upstream behavior). Only hidden during reconnection.
|
|
@@ -441,7 +447,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
441
447
|
let shouldShowBorder = isSpeaking && !isReconnecting
|
|
442
448
|
speakingBorderLayer.isHidden = !shouldShowBorder
|
|
443
449
|
}
|
|
444
|
-
|
|
450
|
+
|
|
445
451
|
/// A method used to process the frame's buffer and enqueue on the rendering view.
|
|
446
452
|
private func process(_ buffer: CMSampleBuffer) {
|
|
447
453
|
guard
|
|
@@ -472,14 +478,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
472
478
|
on window: UIWindow?
|
|
473
479
|
) {
|
|
474
480
|
guard window != nil, let track else { return }
|
|
475
|
-
|
|
481
|
+
|
|
476
482
|
bufferUpdatesCancellable = bufferPublisher
|
|
477
483
|
.receive(on: DispatchQueue.main)
|
|
478
484
|
.sink { [weak self] in self?.process($0) }
|
|
479
|
-
|
|
485
|
+
|
|
480
486
|
track.add(self)
|
|
481
487
|
}
|
|
482
|
-
|
|
488
|
+
|
|
483
489
|
/// A method that stops the frame consumption from the track. Used automatically when the rendering
|
|
484
490
|
/// view move's away from the window or when the track changes.
|
|
485
491
|
private func stopFrameStreaming(for track: RTCVideoTrack?) {
|
|
@@ -504,8 +510,8 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
504
510
|
noOfFramesToSkipAfterRendering = requiresFramesSkipping ? max(Int(max(Int(widthDiffRatio), Int(heightDiffRatio)) / 2), 1) :
|
|
505
511
|
0
|
|
506
512
|
skippedFrames = 0
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
|
|
514
|
+
/// We update the provided windowSizePolicy with the size of the track we received, transformed
|
|
509
515
|
/// to the value that fits.
|
|
510
516
|
pictureInPictureWindowSizePolicy.trackSize = trackSize
|
|
511
517
|
}
|
|
@@ -531,7 +537,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
531
537
|
requiresResize = false
|
|
532
538
|
startFrameStreaming(for: track, on: window)
|
|
533
539
|
}
|
|
534
|
-
|
|
540
|
+
|
|
535
541
|
private func isSameTrackInstance(_ lhs: RTCVideoTrack?, _ rhs: RTCVideoTrack?) -> Bool {
|
|
536
542
|
switch (lhs, rhs) {
|
|
537
543
|
case (nil, nil):
|
|
@@ -542,23 +548,23 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
542
548
|
return false
|
|
543
549
|
}
|
|
544
550
|
}
|
|
545
|
-
|
|
551
|
+
|
|
546
552
|
// MARK: - Content State System
|
|
547
|
-
|
|
553
|
+
|
|
548
554
|
/// Subscribes to the content state manager for reactive updates.
|
|
549
555
|
private func subscribeToContentState() {
|
|
550
556
|
contentStateCancellable?.cancel()
|
|
551
557
|
contentStateCancellable = nil
|
|
552
|
-
|
|
558
|
+
|
|
553
559
|
guard let contentState = contentState else { return }
|
|
554
|
-
|
|
560
|
+
|
|
555
561
|
contentStateCancellable = contentState.contentPublisher
|
|
556
562
|
.receive(on: DispatchQueue.main)
|
|
557
563
|
.sink { [weak self] newContent in
|
|
558
564
|
self?.content = newContent
|
|
559
565
|
}
|
|
560
566
|
}
|
|
561
|
-
|
|
567
|
+
|
|
562
568
|
/// Applies the given content state to update all view components.
|
|
563
569
|
/// This method synchronizes the unified content enum with the individual properties
|
|
564
570
|
/// for backward compatibility while providing a cleaner API.
|
|
@@ -568,7 +574,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
568
574
|
isApplyingContentBatch = false
|
|
569
575
|
updateOverlayVisibility()
|
|
570
576
|
}
|
|
571
|
-
|
|
577
|
+
|
|
572
578
|
switch content {
|
|
573
579
|
case .inactive:
|
|
574
580
|
// Clear everything
|
|
@@ -578,7 +584,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
578
584
|
isVideoEnabled = true
|
|
579
585
|
isReconnecting = false
|
|
580
586
|
isScreenSharing = false
|
|
581
|
-
|
|
587
|
+
|
|
582
588
|
case let .video(newTrack, name, imageURL):
|
|
583
589
|
// Show video content
|
|
584
590
|
track = newTrack
|
|
@@ -587,7 +593,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
587
593
|
isVideoEnabled = true
|
|
588
594
|
isReconnecting = false
|
|
589
595
|
isScreenSharing = false
|
|
590
|
-
|
|
596
|
+
|
|
591
597
|
case let .screenSharing(newTrack, name):
|
|
592
598
|
// Show screen sharing content with indicator
|
|
593
599
|
track = newTrack
|
|
@@ -596,7 +602,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
596
602
|
isVideoEnabled = true
|
|
597
603
|
isReconnecting = false
|
|
598
604
|
isScreenSharing = true
|
|
599
|
-
|
|
605
|
+
|
|
600
606
|
case let .avatar(name, imageURL):
|
|
601
607
|
// Show avatar placeholder (video disabled)
|
|
602
608
|
// Keep existing track for potential quick re-enable
|
|
@@ -605,7 +611,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
605
611
|
isVideoEnabled = false
|
|
606
612
|
isReconnecting = false
|
|
607
613
|
isScreenSharing = false
|
|
608
|
-
|
|
614
|
+
|
|
609
615
|
case .reconnecting:
|
|
610
616
|
// Show reconnection view
|
|
611
617
|
// Keep existing track and participant info for recovery
|
|
@@ -613,7 +619,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
|
|
|
613
619
|
isScreenSharing = false
|
|
614
620
|
}
|
|
615
621
|
}
|
|
616
|
-
|
|
622
|
+
|
|
617
623
|
/// Returns the current content as a `PictureInPictureContent` enum value.
|
|
618
624
|
/// This is useful for reading the current state in a unified way.
|
|
619
625
|
func getCurrentContent() -> PictureInPictureContent {
|
|
@@ -65,17 +65,21 @@ final class StreamRTCYUVBuffer: NSObject, RTCVideoFrameBuffer {
|
|
|
65
65
|
scaleHeight: Int32(targetSize.height)
|
|
66
66
|
)
|
|
67
67
|
return .init(source: resizedSource, conversion: conversion)
|
|
68
|
-
} else if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
} else if let pixelBuffer = source as? RTCCVPixelBuffer {
|
|
69
|
+
do {
|
|
70
|
+
let dequeuedPixelBuffer = try pixelBufferRepository.dequeuePixelBuffer(
|
|
71
|
+
of: targetSize,
|
|
72
|
+
pixelFormat: CVPixelBufferGetPixelFormatType(pixelBuffer.pixelBuffer)
|
|
73
|
+
)
|
|
74
|
+
let count = pixelBuffer.bufferSizeForCroppingAndScaling(toWidth: Int32(targetSize.width), height: Int32(targetSize.height))
|
|
75
|
+
let tempBuffer: UnsafeMutableRawPointer? = malloc(Int(count))
|
|
76
|
+
pixelBuffer.cropAndScale(to: dequeuedPixelBuffer, withTempBuffer: tempBuffer)
|
|
77
|
+
tempBuffer?.deallocate()
|
|
78
|
+
return .init(source: RTCCVPixelBuffer(pixelBuffer: dequeuedPixelBuffer))
|
|
79
|
+
} catch {
|
|
80
|
+
// No pixel buffer available for this size; drop the frame.
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
79
83
|
} else {
|
|
80
84
|
return nil
|
|
81
85
|
}
|
|
@@ -97,9 +101,9 @@ final class StreamRTCYUVBuffer: NSObject, RTCVideoFrameBuffer {
|
|
|
97
101
|
guard let pixelBuffer else {
|
|
98
102
|
return nil
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
|
|
101
105
|
var sampleBuffer: CMSampleBuffer?
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
var timingInfo = CMSampleTimingInfo()
|
|
104
108
|
var formatDescription: CMFormatDescription?
|
|
105
109
|
CMVideoFormatDescriptionCreateForImageBuffer(
|
|
@@ -107,12 +111,12 @@ final class StreamRTCYUVBuffer: NSObject, RTCVideoFrameBuffer {
|
|
|
107
111
|
imageBuffer: pixelBuffer,
|
|
108
112
|
formatDescriptionOut: &formatDescription
|
|
109
113
|
)
|
|
110
|
-
|
|
114
|
+
|
|
111
115
|
guard let formatDescription = formatDescription else {
|
|
112
116
|
// log.error("Cannot create sample buffer formatDescription.")
|
|
113
117
|
return nil
|
|
114
118
|
}
|
|
115
|
-
|
|
119
|
+
|
|
116
120
|
_ = CMSampleBufferCreateReadyWithImageBuffer(
|
|
117
121
|
allocator: kCFAllocatorDefault,
|
|
118
122
|
imageBuffer: pixelBuffer,
|
|
@@ -120,7 +124,7 @@ final class StreamRTCYUVBuffer: NSObject, RTCVideoFrameBuffer {
|
|
|
120
124
|
sampleTiming: &timingInfo,
|
|
121
125
|
sampleBufferOut: &sampleBuffer
|
|
122
126
|
)
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
guard let buffer = sampleBuffer else {
|
|
125
129
|
// log.error("Cannot create sample buffer")
|
|
126
130
|
return nil
|