@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -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/push/internal/ios.js +5 -0
  14. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  15. package/dist/commonjs/version.js +1 -1
  16. package/dist/commonjs/version.js.map +1 -1
  17. package/dist/module/hooks/index.js +0 -1
  18. package/dist/module/hooks/index.js.map +1 -1
  19. package/dist/module/modules/call-manager/CallManager.js +13 -0
  20. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  21. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js +30 -0
  22. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  23. package/dist/module/providers/StreamCall/index.js +2 -1
  24. package/dist/module/providers/StreamCall/index.js.map +1 -1
  25. package/dist/module/utils/internal/callingx/callingx.js +2 -2
  26. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  27. package/dist/module/utils/push/internal/ios.js +5 -0
  28. package/dist/module/utils/push/internal/ios.js.map +1 -1
  29. package/dist/module/version.js +1 -1
  30. package/dist/module/version.js.map +1 -1
  31. package/dist/typescript/hooks/index.d.ts +0 -1
  32. package/dist/typescript/hooks/index.d.ts.map +1 -1
  33. package/dist/typescript/modules/call-manager/CallManager.d.ts +8 -1
  34. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  35. package/dist/typescript/modules/call-manager/types.d.ts +6 -0
  36. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  37. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts +5 -0
  38. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -0
  39. package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
  40. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  41. package/dist/typescript/version.d.ts +1 -1
  42. package/dist/typescript/version.d.ts.map +1 -1
  43. package/ios/PictureInPicture/StreamBufferTransformer.swift +13 -4
  44. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +76 -70
  45. package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +20 -16
  46. package/ios/StreamInCallManager.swift +237 -81
  47. package/ios/StreamVideoReactNative-Bridging-Header.h +0 -2
  48. package/ios/StreamVideoReactNative.m +0 -81
  49. package/package.json +11 -11
  50. package/src/hooks/index.ts +0 -1
  51. package/src/modules/call-manager/CallManager.ts +25 -1
  52. package/src/modules/call-manager/types.ts +7 -0
  53. package/src/providers/StreamCall/AudioInterruptionTracer.tsx +51 -0
  54. package/src/providers/StreamCall/index.tsx +2 -0
  55. package/src/utils/internal/callingx/callingx.ts +2 -2
  56. package/src/utils/push/internal/ios.ts +5 -0
  57. package/src/version.ts +1 -1
  58. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +0 -436
  59. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +0 -17
  60. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +0 -36
  61. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +0 -60
  62. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +0 -31
  63. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +0 -329
  64. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +0 -472
  65. package/dist/commonjs/hooks/useLoopbackRecording.js +0 -243
  66. package/dist/commonjs/hooks/useLoopbackRecording.js.map +0 -1
  67. package/dist/module/hooks/useLoopbackRecording.js +0 -238
  68. package/dist/module/hooks/useLoopbackRecording.js.map +0 -1
  69. package/dist/typescript/hooks/useLoopbackRecording.d.ts +0 -85
  70. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +0 -1
  71. package/ios/TracksRecorder/AudioPipeline.swift +0 -270
  72. package/ios/TracksRecorder/PipelineHost.swift +0 -56
  73. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +0 -154
  74. package/ios/TracksRecorder/RecorderVideoSink.swift +0 -137
  75. package/ios/TracksRecorder/TracksRecorderManager.swift +0 -327
  76. package/ios/TracksRecorder/VideoPipeline.swift +0 -297
  77. 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,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
- /// We update the provided windowSizePolicy with the size of the track we received, transformed
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
- 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