expo-realtime-ivs-broadcast 0.2.0 → 0.2.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.
@@ -224,10 +224,16 @@ class ExpoIVSStagePreviewView: ExpoView {
224
224
  }
225
225
 
226
226
  /// Register this view with the stage manager for PiP (Picture-in-Picture) support
227
+ /// Only registers when the view is actually in a window hierarchy
227
228
  private func registerForPiP() {
228
- // Use self as the source view since this is the container that shows the video
229
- stageManager?.registerLocalPreviewView(self)
230
- print("ExpoIVSStagePreviewView: Registered for PiP support")
229
+ // Only register if we're in a window - PiP requires a visible source view
230
+ if window != nil {
231
+ stageManager?.registerLocalPreviewView(self)
232
+ print("ExpoIVSStagePreviewView: Registered for PiP support (view is in window)")
233
+ } else {
234
+ print("ExpoIVSStagePreviewView: Deferring PiP registration until view is in window")
235
+ // Will be registered in didMoveToWindow
236
+ }
231
237
  }
232
238
 
233
239
  override func layoutSubviews() {
@@ -244,12 +250,22 @@ class ExpoIVSStagePreviewView: ExpoView {
244
250
 
245
251
  override func didMoveToWindow() {
246
252
  super.didMoveToWindow()
247
- // When the view is added to a window, try to attach stream if not already done
248
- if window != nil && customPreviewLayer == nil && ivsImagePreviewView == nil {
249
- print("ExpoIVSStagePreviewView: didMoveToWindow - attempting to attach stream")
250
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
251
- self?.attachStream()
253
+ if window != nil {
254
+ // When the view is added to a window, try to attach stream if not already done
255
+ if customPreviewLayer == nil && ivsImagePreviewView == nil {
256
+ print("ExpoIVSStagePreviewView: didMoveToWindow - attempting to attach stream")
257
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
258
+ self?.attachStream()
259
+ }
260
+ } else {
261
+ // Stream already attached, but now we can register for PiP since we're in a window
262
+ print("ExpoIVSStagePreviewView: didMoveToWindow - registering for PiP now that view is in window")
263
+ stageManager?.registerLocalPreviewView(self)
252
264
  }
265
+ } else {
266
+ // View removed from window - unregister from PiP
267
+ print("ExpoIVSStagePreviewView: didMoveToWindow - view removed from window, unregistering PiP")
268
+ stageManager?.registerLocalPreviewView(nil)
253
269
  }
254
270
  }
255
271
  }
@@ -302,6 +302,9 @@ public class IVSPictureInPictureController: NSObject {
302
302
  private var viewFrameCapture: ViewFrameCapture?
303
303
  private weak var captureTargetView: UIView?
304
304
 
305
+ // Pending source view (when setup is deferred because view is not in window)
306
+ private weak var pendingSourceView: UIView?
307
+
305
308
  // Placeholder frame generator for broadcaster PiP (when camera is unavailable in background)
306
309
  private var placeholderTimer: Timer?
307
310
  private var placeholderPixelBuffer: CVPixelBuffer?
@@ -441,6 +444,57 @@ public class IVSPictureInPictureController: NSObject {
441
444
  return
442
445
  }
443
446
 
447
+ // IMPORTANT: Check if the source view is in a window hierarchy
448
+ // If not, defer setup until it is
449
+ if sourceView.window == nil {
450
+ print("🖼️ [PiP] Warning: Source view not in window hierarchy yet. Deferring setup...")
451
+ self.pendingSourceView = sourceView
452
+
453
+ // Observe when the view gets added to window
454
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self, weak sourceView] in
455
+ guard let self = self, let view = sourceView else { return }
456
+ if view.window != nil {
457
+ print("🖼️ [PiP] Source view now in window, completing setup")
458
+ self.pendingSourceView = nil
459
+ self.completeSetupWithSourceView(view, pipVideoCallVC: pipVideoCallVC)
460
+ } else {
461
+ // Retry with increasing delays
462
+ self.retrySetupWithSourceView(view, pipVideoCallVC: pipVideoCallVC, attempt: 1)
463
+ }
464
+ }
465
+ return
466
+ }
467
+
468
+ completeSetupWithSourceView(sourceView, pipVideoCallVC: pipVideoCallVC)
469
+ }
470
+
471
+ /// Retry setup with exponential backoff
472
+ private func retrySetupWithSourceView(_ sourceView: UIView, pipVideoCallVC: AVPictureInPictureVideoCallViewController, attempt: Int) {
473
+ let maxAttempts = 10
474
+ let delay = min(0.1 * pow(1.5, Double(attempt)), 2.0) // Max 2 second delay
475
+
476
+ guard attempt < maxAttempts else {
477
+ print("🖼️ [PiP] Warning: Source view still not in window after \(maxAttempts) attempts. Setting up anyway...")
478
+ completeSetupWithSourceView(sourceView, pipVideoCallVC: pipVideoCallVC)
479
+ return
480
+ }
481
+
482
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak sourceView] in
483
+ guard let self = self, let view = sourceView else { return }
484
+
485
+ if view.window != nil {
486
+ print("🖼️ [PiP] Source view now in window (attempt \(attempt + 1)), completing setup")
487
+ self.pendingSourceView = nil
488
+ self.completeSetupWithSourceView(view, pipVideoCallVC: pipVideoCallVC)
489
+ } else {
490
+ print("🖼️ [PiP] Source view still not in window (attempt \(attempt + 1)), retrying...")
491
+ self.retrySetupWithSourceView(view, pipVideoCallVC: pipVideoCallVC, attempt: attempt + 1)
492
+ }
493
+ }
494
+ }
495
+
496
+ /// Actually complete the PiP setup once we have a valid source view
497
+ private func completeSetupWithSourceView(_ sourceView: UIView, pipVideoCallVC: AVPictureInPictureVideoCallViewController) {
444
498
  self.activeSourceView = sourceView
445
499
  self.pipContainerView = sourceView
446
500
 
@@ -459,15 +513,43 @@ public class IVSPictureInPictureController: NSObject {
459
513
 
460
514
  self.pipController = controller
461
515
 
462
- // Pre-warm with placeholder frames so PiP is possible immediately
463
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
464
- for _ in 0..<3 {
465
- self?.enqueuePlaceholderFrame()
466
- }
516
+ // Pre-warm with more placeholder frames and longer delay to ensure PiP is possible
517
+ print("🖼️ [PiP] Pre-warming display layer with placeholder frames...")
518
+ aggressivePreWarm { [weak self, weak controller] in
519
+ guard let self = self, let controller = controller else { return }
520
+ print("🖼️ [PiP] Setup complete with source view: \(sourceView)")
521
+ print("🖼️ [PiP] Source view in window: \(sourceView.window != nil)")
522
+ print("🖼️ [PiP] isPictureInPicturePossible: \(controller.isPictureInPicturePossible)")
523
+ }
524
+ }
525
+
526
+ /// Aggressive pre-warming to ensure display layer is ready
527
+ private func aggressivePreWarm(completion: @escaping () -> Void) {
528
+ // Enqueue initial batch of frames immediately
529
+ for _ in 0..<5 {
530
+ enqueuePlaceholderFrame()
467
531
  }
468
532
 
469
- print("🖼️ [PiP] Setup complete with source view: \(sourceView)")
470
- print("🖼️ [PiP] isPictureInPicturePossible: \(controller.isPictureInPicturePossible)")
533
+ // Then enqueue more frames over time to ensure the layer is warm
534
+ var framesEnqueued = 5
535
+ let timer = Timer.scheduledTimer(withTimeInterval: 0.033, repeats: true) { [weak self] timer in
536
+ guard let self = self else {
537
+ timer.invalidate()
538
+ return
539
+ }
540
+
541
+ self.enqueuePlaceholderFrame()
542
+ framesEnqueued += 1
543
+
544
+ // After 15 frames (~0.5s), check if PiP is possible
545
+ if framesEnqueued >= 15 {
546
+ timer.invalidate()
547
+ DispatchQueue.main.async {
548
+ completion()
549
+ }
550
+ }
551
+ }
552
+ RunLoop.main.add(timer, forMode: .common)
471
553
  }
472
554
 
473
555
  /// Disable PiP
@@ -535,23 +617,67 @@ public class IVSPictureInPictureController: NSObject {
535
617
  }
536
618
 
537
619
  /// Pre-warm the display layer with placeholder frames to make PiP possible
620
+ /// Uses aggressive pre-warming with retry logic
538
621
  private func preWarmWithPlaceholder(completion: @escaping (Bool) -> Void) {
539
- guard let pixelBuffer = placeholderPixelBuffer else {
622
+ guard placeholderPixelBuffer != nil else {
623
+ print("🖼️ [PiP] No placeholder buffer available")
540
624
  completion(false)
541
625
  return
542
626
  }
543
627
 
544
- // Enqueue several placeholder frames to prime the display layer
545
- for _ in 0..<5 {
546
- enqueuePlaceholderFrame()
628
+ // Check if source view is in window - if not, that's likely the issue
629
+ if let sourceView = activeSourceView {
630
+ print("🖼️ [PiP] Pre-warm: Source view in window: \(sourceView.window != nil)")
631
+ if sourceView.window == nil {
632
+ print("🖼️ [PiP] Warning: Source view not in window during pre-warm!")
633
+ }
547
634
  }
548
635
 
549
- // Wait a moment for frames to be processed, then check if PiP is possible
550
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
551
- let isPossible = self?.pipController?.isPictureInPicturePossible ?? false
552
- print("🖼️ [PiP] Pre-warm complete, isPictureInPicturePossible: \(isPossible)")
553
- completion(isPossible)
636
+ // Use aggressive pre-warming with polling
637
+ var attemptCount = 0
638
+ let maxAttempts = 5
639
+
640
+ func tryPreWarm() {
641
+ attemptCount += 1
642
+
643
+ // Enqueue a batch of frames
644
+ for _ in 0..<10 {
645
+ enqueuePlaceholderFrame()
646
+ }
647
+
648
+ // Use increasing delays for each attempt
649
+ let delay = 0.15 * Double(attemptCount)
650
+
651
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
652
+ guard let self = self else {
653
+ completion(false)
654
+ return
655
+ }
656
+
657
+ let isPossible = self.pipController?.isPictureInPicturePossible ?? false
658
+ print("🖼️ [PiP] Pre-warm attempt \(attemptCount)/\(maxAttempts), isPictureInPicturePossible: \(isPossible)")
659
+
660
+ if isPossible {
661
+ completion(true)
662
+ } else if attemptCount < maxAttempts {
663
+ // Try again with more frames
664
+ tryPreWarm()
665
+ } else {
666
+ // Final attempt - log diagnostic info
667
+ print("🖼️ [PiP] Pre-warm failed after \(maxAttempts) attempts")
668
+ if let sourceView = self.activeSourceView {
669
+ print("🖼️ [PiP] - Source view in window: \(sourceView.window != nil)")
670
+ print("🖼️ [PiP] - Source view frame: \(sourceView.frame)")
671
+ print("🖼️ [PiP] - Source view hidden: \(sourceView.isHidden)")
672
+ }
673
+ print("🖼️ [PiP] - Display layer status: \(self.sampleBufferDisplayLayer?.status.rawValue ?? -1)")
674
+ print("🖼️ [PiP] - Frames enqueued: \(self.enqueuedFrameCount)")
675
+ completion(false)
676
+ }
677
+ }
554
678
  }
679
+
680
+ tryPreWarm()
555
681
  }
556
682
 
557
683
  /// Stop PiP manually
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-realtime-ivs-broadcast",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "An Expo module for real-time broadcasting using Amazon IVS.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",