expo-realtime-ivs-broadcast 0.2.2 → 0.2.4

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.
@@ -103,9 +103,17 @@ class ExpoIVSRemoteStreamView: ExpoView {
103
103
  print("✅ [REMOTE VIEW] Manager commanded me to render URN: \(deviceUrn)")
104
104
 
105
105
  // Notify the stage manager that a stream started rendering (for PiP)
106
- // Use a small delay to ensure the view is fully set up
107
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
108
- self?.stageManager?.notifyRemoteStreamRendered()
106
+ // Use a longer delay to ensure the view hierarchy is fully set up
107
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
108
+ guard let self = self else { return }
109
+ print("✅ [REMOTE VIEW] Notifying PiP system - view in window: \(self.window != nil), isRenderingVideo: \(self.isRenderingVideo)")
110
+ self.stageManager?.notifyRemoteStreamRendered()
111
+ }
112
+
113
+ // Also notify immediately for faster setup if view is already ready
114
+ if self.window != nil {
115
+ print("✅ [REMOTE VIEW] View already in window, notifying PiP immediately")
116
+ self.stageManager?.notifyRemoteStreamRendered()
109
117
  }
110
118
  } catch {
111
119
  print("❌ [REMOTE VIEW] Failed to create preview for URN \(deviceUrn): \(error)")
@@ -304,6 +304,8 @@ class IVSStageManager: NSObject, IVSStageStreamDelegate, IVSStageStrategy, IVSSt
304
304
  private var currentPiPDevice: IVSImageDevice?
305
305
  // Store the local preview view for broadcaster PiP
306
306
  private weak var localPreviewView: UIView?
307
+ // Track whether we have a valid visible source view (not just device.previewView())
308
+ private var pipHasValidSourceView: Bool = false
307
309
 
308
310
  @available(iOS 15.0, *)
309
311
  private var pipController: IVSPictureInPictureController {
@@ -1026,19 +1028,22 @@ class IVSStageManager: NSObject, IVSStageStreamDelegate, IVSStageStrategy, IVSSt
1026
1028
  currentPiPDevice = nil
1027
1029
  currentPiPSourceDeviceUrn = nil
1028
1030
  pipTargetView = nil
1031
+ pipHasValidSourceView = false
1029
1032
  }
1030
1033
 
1031
1034
  /// Attach frame callback to an IVS Image Device
1032
1035
  @available(iOS 15.0, *)
1033
1036
  private func attachToDevice(_ device: IVSImageDevice, sourceView: UIView? = nil) {
1034
- // If we are already attached to this device, do nothing
1035
- if currentPiPDevice === device {
1037
+ // If we are already attached to this device AND have a valid source view, do nothing
1038
+ if currentPiPDevice === device && pipHasValidSourceView {
1039
+ print("🖼️ [PiP] Already attached to device with valid source view, skipping")
1036
1040
  return
1037
1041
  }
1038
1042
 
1039
1043
  // If attached to another device, detach first
1040
- if let oldDevice = currentPiPDevice {
1044
+ if let oldDevice = currentPiPDevice, oldDevice !== device {
1041
1045
  oldDevice.setOnFrameCallback(nil)
1046
+ print("🖼️ [PiP] Detached from previous device")
1042
1047
  }
1043
1048
 
1044
1049
  currentPiPDevice = device
@@ -1051,36 +1056,44 @@ class IVSStageManager: NSObject, IVSStageStreamDelegate, IVSStageStrategy, IVSSt
1051
1056
  if let view = sourceView {
1052
1057
  pipController.setupWithSourceView(view)
1053
1058
  pipTargetView = view
1059
+ pipHasValidSourceView = true
1060
+ print("🖼️ [PiP] Set up with provided source view (VALID)")
1054
1061
  } else {
1055
- // Try to get a preview view from the device
1056
- do {
1057
- let previewView = try device.previewView()
1058
- pipController.setupWithSourceView(previewView)
1059
- pipTargetView = previewView
1060
- } catch {
1061
- print("🖼️ [PiP] Warning: Could not get preview view from device: \(error)")
1062
- // Try to find a remote view that's rendering this device
1063
- if let remoteView = remoteViews.compactMap({ $0.value }).first(where: { $0.currentRenderedDeviceUrn == device.descriptor().urn }) {
1064
- // Always set up with the remote view container - it's the visible video view
1065
- pipController.setupWithSourceView(remoteView as UIView)
1066
- // Use the inner preview view if available, otherwise use the container
1067
- pipTargetView = remoteView.previewViewForPiP ?? remoteView
1068
- print("🖼️ [PiP] Set up with remote view container")
1069
- } else {
1070
- // Last resort: try to find ANY registered remote view that's rendering video
1071
- if let anyRemoteView = remoteViews.compactMap({ $0.value }).first(where: { $0.isRenderingVideo }) {
1072
- pipController.setupWithSourceView(anyRemoteView as UIView)
1073
- pipTargetView = anyRemoteView.previewViewForPiP ?? anyRemoteView
1074
- print("🖼️ [PiP] Set up with fallback remote view")
1075
- } else {
1076
- print("🖼️ [PiP] ERROR: No source view found for PiP - controller will not be initialized!")
1077
- }
1062
+ // For remote streams, device.previewView() returns an internal view that's not visible
1063
+ // We should prefer finding a registered remote view first
1064
+ if let remoteView = remoteViews.compactMap({ $0.value }).first(where: { $0.currentRenderedDeviceUrn == device.descriptor().urn }) {
1065
+ pipController.setupWithSourceView(remoteView as UIView)
1066
+ pipTargetView = remoteView.previewViewForPiP ?? remoteView
1067
+ pipHasValidSourceView = true
1068
+ print("🖼️ [PiP] Set up with matching remote view container (VALID)")
1069
+ } else if let anyRenderingView = remoteViews.compactMap({ $0.value }).first(where: { $0.isRenderingVideo }) {
1070
+ pipController.setupWithSourceView(anyRenderingView as UIView)
1071
+ pipTargetView = anyRenderingView.previewViewForPiP ?? anyRenderingView
1072
+ pipHasValidSourceView = true
1073
+ print("🖼️ [PiP] Set up with fallback rendering view (VALID)")
1074
+ } else if let anyView = remoteViews.compactMap({ $0.value }).first {
1075
+ pipController.setupWithSourceView(anyView as UIView)
1076
+ pipTargetView = anyView
1077
+ pipHasValidSourceView = true
1078
+ print("🖼️ [PiP] Set up with any available view (VALID)")
1079
+ } else {
1080
+ // Last resort: try device.previewView() - but mark as NOT valid for remote streams
1081
+ // This allows frame capture to start, but we'll re-setup when a remote view becomes available
1082
+ do {
1083
+ let previewView = try device.previewView()
1084
+ pipController.setupWithSourceView(previewView)
1085
+ pipTargetView = previewView
1086
+ // Mark as NOT valid - we need to re-setup when remote view is available
1087
+ pipHasValidSourceView = false
1088
+ print("🖼️ [PiP] Set up with device preview view (NOT VALID - will re-setup when remote view available)")
1089
+ } catch {
1090
+ print("🖼️ [PiP] ERROR: Could not get any preview view: \(error)")
1091
+ pipHasValidSourceView = false
1078
1092
  }
1079
1093
  }
1080
1094
  }
1081
1095
 
1082
- // Set frame callback to receive CVPixelBuffers
1083
- // Using a dedicated queue for better performance
1096
+ // Set frame callback to receive CVPixelBuffers (only if not already set on this device)
1084
1097
  let frameQueue = DispatchQueue(label: "com.ivs.pip.frameCallback", qos: .userInteractive)
1085
1098
  device.setOnFrameCallbackQueue(frameQueue, includePixelBuffer: true) { [weak self] frame in
1086
1099
  guard let self = self else { return }
@@ -1118,30 +1131,55 @@ class IVSStageManager: NSObject, IVSStageStreamDelegate, IVSStageStrategy, IVSSt
1118
1131
  }
1119
1132
 
1120
1133
  if let stream = candidateStream, let imageDevice = stream.device as? IVSImageDevice {
1121
- // Found a valid video stream, attach to it
1122
- if currentPiPSourceDeviceUrn != imageDevice.descriptor().urn {
1123
- print("🖼️ [PiP] Found new remote video stream: \(imageDevice.descriptor().urn)")
1134
+ // Found a valid video stream
1135
+ // Re-attach if: 1) different device, OR 2) same device but we don't have a valid source view yet
1136
+ let needsSetup = currentPiPSourceDeviceUrn != imageDevice.descriptor().urn || !pipHasValidSourceView
1137
+
1138
+ if needsSetup {
1139
+ let isNewDevice = currentPiPSourceDeviceUrn != imageDevice.descriptor().urn
1140
+ let reason = isNewDevice ? "new device" : "need valid source view"
1141
+ print("🖼️ [PiP] Setting up PiP for remote video stream (\(reason)): \(imageDevice.descriptor().urn)")
1142
+ print("🖼️ [PiP] pipHasValidSourceView: \(pipHasValidSourceView)")
1143
+
1144
+ // Debug: Log all registered remote views and their URNs
1145
+ print("🖼️ [PiP] Registered remote views: \(remoteViews.count)")
1146
+ for (index, viewWrapper) in remoteViews.enumerated() {
1147
+ if let view = viewWrapper.value {
1148
+ print("🖼️ [PiP] View \(index): URN=\(view.currentRenderedDeviceUrn ?? "nil"), isRendering=\(view.isRenderingVideo)")
1149
+ }
1150
+ }
1124
1151
 
1125
1152
  // Try to find the remote view that's rendering this stream
1126
1153
  if let remoteView = remoteViews.compactMap({ $0.value }).first(where: { $0.currentRenderedDeviceUrn == imageDevice.descriptor().urn }) {
1127
- // Use the remote view (container) as the source view for Video Call API
1128
- // Cast to UIView explicitly since ExpoIVSRemoteStreamView extends ExpoView -> UIView
1129
1154
  candidateSourceView = remoteView as UIView
1130
1155
  print("🖼️ [PiP] Found matching remote view for source")
1131
1156
  } else {
1132
- // Fallback: use any remote view that's rendering video
1157
+ // Fallback: Use ANY remote view that's rendering video
1133
1158
  if let anyRenderingView = remoteViews.compactMap({ $0.value }).first(where: { $0.isRenderingVideo }) {
1134
1159
  candidateSourceView = anyRenderingView as UIView
1135
- print("🖼️ [PiP] Using fallback remote view for source (URN mismatch)")
1160
+ print("🖼️ [PiP] Using fallback remote view (URN didn't match but view is rendering)")
1161
+ } else if let anyView = remoteViews.compactMap({ $0.value }).first {
1162
+ // Last resort: use any registered view
1163
+ candidateSourceView = anyView as UIView
1164
+ print("🖼️ [PiP] Using any available remote view as last resort")
1165
+ } else {
1166
+ print("🖼️ [PiP] No remote views available yet - will retry when view registers")
1136
1167
  }
1137
1168
  }
1138
1169
 
1139
1170
  attachToDevice(imageDevice, sourceView: candidateSourceView)
1140
1171
  }
1141
1172
  } else {
1142
- // No candidate stream found
1173
+ // No candidate stream found - log why
1143
1174
  if currentPiPDevice == nil {
1144
1175
  print("🖼️ [PiP] No active remote video stream found yet")
1176
+ print("🖼️ [PiP] Total participants: \(participants.count)")
1177
+ for p in participants {
1178
+ print("🖼️ [PiP] Participant \(p.info.participantId ?? "nil"): \(p.streams.count) streams")
1179
+ for s in p.streams {
1180
+ print("🖼️ [PiP] Stream type: \(s.device.descriptor().type.rawValue), URN: \(s.device.descriptor().urn)")
1181
+ }
1182
+ }
1145
1183
  }
1146
1184
  }
1147
1185
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-realtime-ivs-broadcast",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",