@stream-io/video-react-native-sdk 1.37.1-beta.0 → 1.38.1

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 (84) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +0 -81
  3. package/dist/commonjs/hooks/index.js +0 -11
  4. package/dist/commonjs/hooks/index.js.map +1 -1
  5. package/dist/commonjs/modules/call-manager/CallManager.js +13 -0
  6. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  7. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js +37 -0
  8. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  9. package/dist/commonjs/providers/StreamCall/index.js +2 -1
  10. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  11. package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
  12. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  13. package/dist/commonjs/utils/internal/registerSDKGlobals.js +16 -0
  14. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  15. package/dist/commonjs/utils/push/internal/ios.js +5 -0
  16. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  17. package/dist/commonjs/version.js +1 -1
  18. package/dist/commonjs/version.js.map +1 -1
  19. package/dist/module/hooks/index.js +0 -1
  20. package/dist/module/hooks/index.js.map +1 -1
  21. package/dist/module/modules/call-manager/CallManager.js +13 -0
  22. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  23. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js +30 -0
  24. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  25. package/dist/module/providers/StreamCall/index.js +2 -1
  26. package/dist/module/providers/StreamCall/index.js.map +1 -1
  27. package/dist/module/utils/internal/callingx/callingx.js +2 -2
  28. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  29. package/dist/module/utils/internal/registerSDKGlobals.js +17 -1
  30. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  31. package/dist/module/utils/push/internal/ios.js +5 -0
  32. package/dist/module/utils/push/internal/ios.js.map +1 -1
  33. package/dist/module/version.js +1 -1
  34. package/dist/module/version.js.map +1 -1
  35. package/dist/typescript/hooks/index.d.ts +0 -1
  36. package/dist/typescript/hooks/index.d.ts.map +1 -1
  37. package/dist/typescript/modules/call-manager/CallManager.d.ts +8 -1
  38. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  39. package/dist/typescript/modules/call-manager/types.d.ts +6 -0
  40. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  41. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts +5 -0
  42. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -0
  43. package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
  44. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  45. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  46. package/dist/typescript/version.d.ts +1 -1
  47. package/dist/typescript/version.d.ts.map +1 -1
  48. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +3 -1
  49. package/ios/PictureInPicture/StreamBufferTransformer.swift +13 -4
  50. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +79 -71
  51. package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +20 -16
  52. package/ios/StreamInCallManager.swift +256 -81
  53. package/ios/StreamVideoReactNative-Bridging-Header.h +0 -2
  54. package/ios/StreamVideoReactNative.m +0 -81
  55. package/package.json +11 -11
  56. package/src/hooks/index.ts +0 -1
  57. package/src/modules/call-manager/CallManager.ts +25 -1
  58. package/src/modules/call-manager/types.ts +7 -0
  59. package/src/providers/StreamCall/AudioInterruptionTracer.tsx +51 -0
  60. package/src/providers/StreamCall/index.tsx +2 -0
  61. package/src/utils/internal/callingx/callingx.ts +2 -2
  62. package/src/utils/internal/registerSDKGlobals.ts +23 -1
  63. package/src/utils/push/internal/ios.ts +5 -0
  64. package/src/version.ts +1 -1
  65. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +0 -436
  66. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +0 -17
  67. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +0 -36
  68. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +0 -60
  69. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +0 -31
  70. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +0 -329
  71. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +0 -472
  72. package/dist/commonjs/hooks/useLoopbackRecording.js +0 -243
  73. package/dist/commonjs/hooks/useLoopbackRecording.js.map +0 -1
  74. package/dist/module/hooks/useLoopbackRecording.js +0 -238
  75. package/dist/module/hooks/useLoopbackRecording.js.map +0 -1
  76. package/dist/typescript/hooks/useLoopbackRecording.d.ts +0 -85
  77. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +0 -1
  78. package/ios/TracksRecorder/AudioPipeline.swift +0 -270
  79. package/ios/TracksRecorder/PipelineHost.swift +0 -56
  80. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +0 -154
  81. package/ios/TracksRecorder/RecorderVideoSink.swift +0 -137
  82. package/ios/TracksRecorder/TracksRecorderManager.swift +0 -327
  83. package/ios/TracksRecorder/VideoPipeline.swift +0 -297
  84. 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
- ? CGAffineTransform(scaleX: -1, y: 1)
72
- : .identity
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
- /// We render frames every time the stepper/counter value is 0 and have a valid trackSize.
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
- .buffer as? StreamRTCYUVBuffer,
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,26 @@ 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
- avatarView.layoutIfNeeded()
434
+ if bounds.width > 0, bounds.height > 0 {
435
+ avatarView.layoutIfNeeded()
436
+ }
429
437
  }
430
-
438
+
431
439
  // Participant overlay shows on BOTH video and avatar (matches upstream)
432
440
  // Only hide during reconnection
433
441
  participantOverlayView.isOverlayEnabled = true
434
442
  }
435
443
  }
436
-
444
+
437
445
  /// Updates the speaking indicator border visibility based on isSpeaking state.
438
446
  /// The border is shown when the participant is speaking, on BOTH video and avatar views
439
447
  /// (matching upstream behavior). Only hidden during reconnection.
@@ -441,7 +449,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
441
449
  let shouldShowBorder = isSpeaking && !isReconnecting
442
450
  speakingBorderLayer.isHidden = !shouldShowBorder
443
451
  }
444
-
452
+
445
453
  /// A method used to process the frame's buffer and enqueue on the rendering view.
446
454
  private func process(_ buffer: CMSampleBuffer) {
447
455
  guard
@@ -472,14 +480,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
472
480
  on window: UIWindow?
473
481
  ) {
474
482
  guard window != nil, let track else { return }
475
-
483
+
476
484
  bufferUpdatesCancellable = bufferPublisher
477
485
  .receive(on: DispatchQueue.main)
478
486
  .sink { [weak self] in self?.process($0) }
479
-
487
+
480
488
  track.add(self)
481
489
  }
482
-
490
+
483
491
  /// A method that stops the frame consumption from the track. Used automatically when the rendering
484
492
  /// view move's away from the window or when the track changes.
485
493
  private func stopFrameStreaming(for track: RTCVideoTrack?) {
@@ -504,8 +512,8 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
504
512
  noOfFramesToSkipAfterRendering = requiresFramesSkipping ? max(Int(max(Int(widthDiffRatio), Int(heightDiffRatio)) / 2), 1) :
505
513
  0
506
514
  skippedFrames = 0
507
-
508
- /// We update the provided windowSizePolicy with the size of the track we received, transformed
515
+
516
+ /// We update the provided windowSizePolicy with the size of the track we received, transformed
509
517
  /// to the value that fits.
510
518
  pictureInPictureWindowSizePolicy.trackSize = trackSize
511
519
  }
@@ -531,7 +539,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
531
539
  requiresResize = false
532
540
  startFrameStreaming(for: track, on: window)
533
541
  }
534
-
542
+
535
543
  private func isSameTrackInstance(_ lhs: RTCVideoTrack?, _ rhs: RTCVideoTrack?) -> Bool {
536
544
  switch (lhs, rhs) {
537
545
  case (nil, nil):
@@ -542,23 +550,23 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
542
550
  return false
543
551
  }
544
552
  }
545
-
553
+
546
554
  // MARK: - Content State System
547
-
555
+
548
556
  /// Subscribes to the content state manager for reactive updates.
549
557
  private func subscribeToContentState() {
550
558
  contentStateCancellable?.cancel()
551
559
  contentStateCancellable = nil
552
-
560
+
553
561
  guard let contentState = contentState else { return }
554
-
562
+
555
563
  contentStateCancellable = contentState.contentPublisher
556
564
  .receive(on: DispatchQueue.main)
557
565
  .sink { [weak self] newContent in
558
566
  self?.content = newContent
559
567
  }
560
568
  }
561
-
569
+
562
570
  /// Applies the given content state to update all view components.
563
571
  /// This method synchronizes the unified content enum with the individual properties
564
572
  /// for backward compatibility while providing a cleaner API.
@@ -568,7 +576,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
568
576
  isApplyingContentBatch = false
569
577
  updateOverlayVisibility()
570
578
  }
571
-
579
+
572
580
  switch content {
573
581
  case .inactive:
574
582
  // Clear everything
@@ -578,7 +586,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
578
586
  isVideoEnabled = true
579
587
  isReconnecting = false
580
588
  isScreenSharing = false
581
-
589
+
582
590
  case let .video(newTrack, name, imageURL):
583
591
  // Show video content
584
592
  track = newTrack
@@ -587,7 +595,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
587
595
  isVideoEnabled = true
588
596
  isReconnecting = false
589
597
  isScreenSharing = false
590
-
598
+
591
599
  case let .screenSharing(newTrack, name):
592
600
  // Show screen sharing content with indicator
593
601
  track = newTrack
@@ -596,7 +604,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
596
604
  isVideoEnabled = true
597
605
  isReconnecting = false
598
606
  isScreenSharing = true
599
-
607
+
600
608
  case let .avatar(name, imageURL):
601
609
  // Show avatar placeholder (video disabled)
602
610
  // Keep existing track for potential quick re-enable
@@ -605,7 +613,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
605
613
  isVideoEnabled = false
606
614
  isReconnecting = false
607
615
  isScreenSharing = false
608
-
616
+
609
617
  case .reconnecting:
610
618
  // Show reconnection view
611
619
  // Keep existing track and participant info for recovery
@@ -613,7 +621,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
613
621
  isScreenSharing = false
614
622
  }
615
623
  }
616
-
624
+
617
625
  /// Returns the current content as a `PictureInPictureContent` enum value.
618
626
  /// This is useful for reading the current state in a unified way.
619
627
  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
- let pixelBuffer = source as? RTCCVPixelBuffer,
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))
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