expo-realtime-ivs-broadcast 0.1.8 → 0.1.10

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.
@@ -5,7 +5,6 @@ import android.os.Build
5
5
  import android.os.Handler
6
6
  import android.os.Looper
7
7
  import android.util.Log
8
- import android.view.View
9
8
  import android.view.ViewTreeObserver
10
9
  import android.widget.FrameLayout
11
10
  import androidx.annotation.RequiresApi
@@ -37,9 +36,8 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
37
36
 
38
37
  init {
39
38
  Log.i("ExpoIVSStagePreviewView", "Initializing Stage Preview View...")
40
- // Start initialization immediately - don't wait for onAttachedToWindow
41
- // This ensures the preview is ready when the view becomes visible
42
- resolveStageManagerAndStreamWithRetry()
39
+ // Note: We don't start initialization here because isViewAttached is false
40
+ // The initialization will happen in onAttachedToWindow when the view is ready
43
41
  }
44
42
 
45
43
  // Override onLayout to ensure child views are properly sized
@@ -50,12 +48,18 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
50
48
  val childWidth = right - left
51
49
  val childHeight = bottom - top
52
50
 
53
- for (i in 0 until childCount) {
54
- val child = getChildAt(i)
51
+ // Use safe iteration - get count once and check for null children
52
+ val count = childCount
53
+ for (i in 0 until count) {
54
+ val child = getChildAt(i) ?: continue // Skip null children
55
+ try {
55
56
  child.layout(0, 0, childWidth, childHeight)
57
+ } catch (e: Exception) {
58
+ Log.w("ExpoIVSStagePreviewView", "Error laying out child $i: ${e.message}")
59
+ }
56
60
  }
57
61
 
58
- Log.d("ExpoIVSStagePreviewView", "📐 onLayout: ${childWidth}x${childHeight}, children: $childCount")
62
+ Log.d("ExpoIVSStagePreviewView", "📐 onLayout: ${childWidth}x${childHeight}, children: $count")
59
63
  }
60
64
 
61
65
  // Override onMeasure to properly measure children
@@ -69,26 +73,38 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
69
73
  val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
70
74
  val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
71
75
 
72
- for (i in 0 until childCount) {
73
- val child = getChildAt(i)
76
+ // Use safe iteration - get count once and check for null children
77
+ val count = childCount
78
+ for (i in 0 until count) {
79
+ val child = getChildAt(i) ?: continue // Skip null children
80
+ try {
74
81
  child.measure(childWidthSpec, childHeightSpec)
82
+ } catch (e: Exception) {
83
+ Log.w("ExpoIVSStagePreviewView", "Error measuring child $i: ${e.message}")
84
+ }
75
85
  }
76
86
 
77
87
  Log.d("ExpoIVSStagePreviewView", "📐 onMeasure: ${width}x${height}")
78
88
  }
79
89
 
80
90
  private fun resolveStageManagerAndStreamWithRetry() {
91
+ // Guard: Don't proceed if view is not attached
92
+ if (!isViewAttached) {
93
+ Log.w("ExpoIVSStagePreviewView", "⚠️ View not attached, skipping resolveStageManagerAndStreamWithRetry")
94
+ return
95
+ }
96
+
81
97
  Log.d("ExpoIVSStagePreviewView", "Attempting to resolve StageManager (attempt ${retryCount + 1})...")
82
98
 
83
99
  val manager = IVSStageManager.instance
84
100
 
85
101
  if (manager == null) {
86
- if (retryCount < maxRetries) {
102
+ if (retryCount < maxRetries && isViewAttached) {
87
103
  retryCount++
88
104
  Log.w("ExpoIVSStagePreviewView", "IVSStageManager not ready, retrying in ${retryDelayMs}ms...")
89
105
  mainHandler.postDelayed({ resolveStageManagerAndStreamWithRetry() }, retryDelayMs)
90
106
  } else {
91
- Log.e("ExpoIVSStagePreviewView", "IVSStageManager singleton is null after $maxRetries attempts.")
107
+ Log.e("ExpoIVSStagePreviewView", "IVSStageManager singleton is null after $maxRetries attempts or view detached.")
92
108
  }
93
109
  return
94
110
  }
@@ -100,15 +116,21 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
100
116
  }
101
117
 
102
118
  private fun attachStreamWithRetry() {
119
+ // Guard: Don't attach if view is not attached to window
120
+ if (!isViewAttached) {
121
+ Log.w("ExpoIVSStagePreviewView", "⚠️ View not attached to window, skipping attachStreamWithRetry")
122
+ return
123
+ }
124
+
103
125
  val cameraDevice = this.stageManager?.getLocalCameraDevice()
104
126
 
105
127
  if (cameraDevice == null) {
106
- if (retryCount < maxRetries) {
128
+ if (retryCount < maxRetries && isViewAttached) {
107
129
  retryCount++
108
130
  Log.w("ExpoIVSStagePreviewView", "Camera device not ready, retrying in ${retryDelayMs}ms...")
109
131
  mainHandler.postDelayed({ attachStreamWithRetry() }, retryDelayMs)
110
132
  } else {
111
- Log.e("ExpoIVSStagePreviewView", "Camera device not available after $maxRetries attempts.")
133
+ Log.e("ExpoIVSStagePreviewView", "Camera device not available after $maxRetries attempts or view detached.")
112
134
  }
113
135
  return
114
136
  }
@@ -128,16 +150,19 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
128
150
  return
129
151
  }
130
152
 
131
- // Cleanup existing preview
132
- if (ivsImagePreviewView != null) {
153
+ // Cleanup existing preview - clear references first, then remove view
154
+ val oldPreview = ivsImagePreviewView
155
+ if (oldPreview != null) {
133
156
  Log.d("ExpoIVSStagePreviewView", "Cleaning up existing preview")
134
- try {
135
- removeView(ivsImagePreviewView)
136
- } catch (e: Exception) {
137
- Log.w("ExpoIVSStagePreviewView", "Error removing old preview: ${e.message}")
138
- }
139
157
  ivsImagePreviewView = null
140
158
  currentPreviewDeviceUrn = null
159
+ if (oldPreview.parent == this) {
160
+ try {
161
+ removeView(oldPreview)
162
+ } catch (e: Exception) {
163
+ Log.w("ExpoIVSStagePreviewView", "Error removing old preview: ${e.message}")
164
+ }
165
+ }
141
166
  }
142
167
 
143
168
  try {
@@ -159,18 +184,24 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
159
184
  }
160
185
 
161
186
  if (newPreview == null) {
162
- if (retryCount < maxRetries) {
187
+ if (retryCount < maxRetries && isViewAttached) {
163
188
  retryCount++
164
189
  Log.w("ExpoIVSStagePreviewView", "Preview view is null, retrying in ${retryDelayMs}ms (attempt $retryCount)...")
165
190
  mainHandler.postDelayed({ attachStreamWithRetry() }, retryDelayMs)
166
191
  } else {
167
- Log.e("ExpoIVSStagePreviewView", "Failed to get preview view from camera after $maxRetries attempts")
192
+ Log.e("ExpoIVSStagePreviewView", "Failed to get preview view from camera after $maxRetries attempts or view detached")
168
193
  }
169
194
  return
170
195
  }
171
196
 
172
197
  Log.i("ExpoIVSStagePreviewView", "📷 Got preview view: ${newPreview.javaClass.simpleName}, hashCode: ${newPreview.hashCode()}")
173
198
 
199
+ // Double-check we're still attached before modifying view hierarchy
200
+ if (!isViewAttached) {
201
+ Log.w("ExpoIVSStagePreviewView", "⚠️ View detached while getting preview, aborting")
202
+ return
203
+ }
204
+
174
205
  // IMPORTANT: Check if preview is attached to another parent and detach it first
175
206
  val existingParent = newPreview.parent
176
207
  if (existingParent != null && existingParent != this) {
@@ -187,6 +218,12 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
187
218
  return
188
219
  }
189
220
 
221
+ // Final check before adding view
222
+ if (!isViewAttached) {
223
+ Log.w("ExpoIVSStagePreviewView", "⚠️ View detached before addView, aborting")
224
+ return
225
+ }
226
+
190
227
  newPreview.layoutParams = FrameLayout.LayoutParams(
191
228
  FrameLayout.LayoutParams.MATCH_PARENT,
192
229
  FrameLayout.LayoutParams.MATCH_PARENT
@@ -210,15 +247,24 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
210
247
  viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
211
248
  override fun onGlobalLayout() {
212
249
  viewTreeObserver.removeOnGlobalLayoutListener(this)
250
+ // Guard: check if still attached and preview exists
251
+ if (!isViewAttached) {
252
+ Log.w("ExpoIVSStagePreviewView", "📐 Deferred layout skipped - view detached")
253
+ return
254
+ }
213
255
  val preview = ivsImagePreviewView ?: return
214
256
  if (this@ExpoIVSStagePreviewView.width > 0 && this@ExpoIVSStagePreviewView.height > 0) {
215
257
  val w = this@ExpoIVSStagePreviewView.width
216
258
  val h = this@ExpoIVSStagePreviewView.height
217
259
  val wSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY)
218
260
  val hSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
261
+ try {
219
262
  preview.measure(wSpec, hSpec)
220
263
  preview.layout(0, 0, w, h)
221
264
  Log.i("ExpoIVSStagePreviewView", "📐 Deferred layout: preview now ${preview.measuredWidth}x${preview.measuredHeight}")
265
+ } catch (e: Exception) {
266
+ Log.w("ExpoIVSStagePreviewView", "📐 Deferred layout error: ${e.message}")
267
+ }
222
268
  }
223
269
  }
224
270
  })
@@ -256,37 +302,74 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
256
302
  return
257
303
  }
258
304
 
259
- // Clear the current preview first
305
+ // Don't refresh if view is no longer attached
306
+ if (!isViewAttached) {
307
+ Log.w("ExpoIVSStagePreviewView", "🔄 Skipping refresh - view not attached")
308
+ return
309
+ }
310
+
311
+ // Capture and clear references BEFORE removing view
260
312
  val oldPreview = ivsImagePreviewView
261
- if (oldPreview != null) {
313
+ ivsImagePreviewView = null
314
+ currentPreviewDeviceUrn = null
315
+
316
+ // Now remove the old preview if it exists
317
+ if (oldPreview != null && oldPreview.parent == this) {
262
318
  Log.i("ExpoIVSStagePreviewView", "🔄 Removing old preview view")
263
319
  try {
264
320
  removeView(oldPreview)
265
321
  } catch (e: Exception) {
266
322
  Log.w("ExpoIVSStagePreviewView", "Error removing old preview view: ${e.message}")
267
323
  }
268
- ivsImagePreviewView = null
269
324
  }
270
325
 
271
- // IMPORTANT: Clear the URN so attachStreamWithRetry will fetch a fresh preview
272
- currentPreviewDeviceUrn = null
273
-
274
326
  retryCount = 0
275
327
  // Add a delay to allow the new camera stream to initialize
276
328
  mainHandler.postDelayed({
329
+ // Double-check we're still attached before trying to attach stream
330
+ if (isViewAttached) {
277
331
  Log.i("ExpoIVSStagePreviewView", "🔄 Now attaching new preview after delay")
278
332
  attachStreamWithRetry()
333
+ } else {
334
+ Log.w("ExpoIVSStagePreviewView", "🔄 Skipping attach - view no longer attached")
335
+ }
279
336
  }, 200)
280
337
  }
281
338
 
282
339
  private fun applyProps() {
283
- setMirror(this.mirror)
340
+ applyMirror()
284
341
  setScaleMode(this.scaleMode)
285
342
  }
286
343
 
344
+ /**
345
+ * Apply mirroring based on camera position and user preference.
346
+ *
347
+ * Front camera: By default, Android shows a "selfie" view (mirrored like a mirror).
348
+ * We apply setMirrored(true) to show the "true" view (as others see you).
349
+ * The user's mirror prop can override this behavior.
350
+ *
351
+ * Back camera: No automatic mirroring applied, only user's mirror prop if set.
352
+ */
353
+ private fun applyMirror() {
354
+ val isFrontCamera = stageManager?.isFrontCameraActive() ?: false
355
+
356
+ // For front camera: apply mirror to counter the natural selfie mirroring
357
+ // This makes it so moving left shows you moving left (true view)
358
+ // If user sets mirror=true on a front camera, it will show selfie view
359
+ // For back camera: just use the mirror prop as-is
360
+ val shouldMirror = if (isFrontCamera) {
361
+ !this.mirror // Invert: default (false) becomes true to counter selfie view
362
+ } else {
363
+ this.mirror // Back camera: use mirror prop directly
364
+ }
365
+
366
+ Log.d("ExpoIVSStagePreviewView", "📷 applyMirror: isFrontCamera=$isFrontCamera, mirrorProp=${this.mirror}, shouldMirror=$shouldMirror")
367
+ (ivsImagePreviewView as? ImagePreviewView)?.setMirrored(shouldMirror)
368
+ }
369
+
287
370
  fun setMirror(mirror: Boolean) {
288
371
  this.mirror = mirror
289
- (ivsImagePreviewView as? ImagePreviewView)?.setMirrored(this.mirror)
372
+ applyMirror()
290
373
  }
291
374
 
292
375
  fun setScaleMode(mode: String) {
@@ -337,31 +420,31 @@ class ExpoIVSStagePreviewView(context: Context, appContext: AppContext) : ExpoVi
337
420
  Log.d("ExpoIVSStagePreviewView", "View detached from window")
338
421
  isViewAttached = false
339
422
 
340
- // Remove pending callbacks
423
+ // Remove pending callbacks FIRST to prevent any async operations
341
424
  mainHandler.removeCallbacksAndMessages(null)
342
425
 
343
- // Remove the preview view from this parent so it can be reused
344
- // Do this in a post to avoid conflicts with the current render cycle
426
+ // Unregister from manager BEFORE touching views
427
+ stageManager?.unregisterPreviewView(this)
428
+
429
+ // Capture reference before clearing
345
430
  val preview = ivsImagePreviewView
346
- if (preview != null) {
347
- mainHandler.post {
348
- try {
349
- if (preview.parent == this) {
350
- removeView(preview)
351
- Log.d("ExpoIVSStagePreviewView", "Preview removed in post")
352
- }
353
- } catch (e: Exception) {
354
- Log.w("ExpoIVSStagePreviewView", "Error removing preview in post: ${e.message}")
355
- }
356
- }
357
- }
358
431
 
359
- // Clear references
432
+ // Clear references BEFORE removing view to prevent race conditions
433
+ // This ensures no other code can access the preview during removal
360
434
  ivsImagePreviewView = null
361
435
  currentPreviewDeviceUrn = null
362
436
 
363
- // Unregister from manager
364
- stageManager?.unregisterPreviewView(this)
437
+ // Remove the preview view synchronously if it exists and is our child
438
+ // We do this AFTER clearing references but BEFORE calling super
439
+ // to ensure the view hierarchy is consistent
440
+ if (preview != null && preview.parent == this) {
441
+ try {
442
+ removeView(preview)
443
+ Log.d("ExpoIVSStagePreviewView", "Preview removed synchronously on detach")
444
+ } catch (e: Exception) {
445
+ Log.w("ExpoIVSStagePreviewView", "Error removing preview on detach: ${e.message}")
446
+ }
447
+ }
365
448
 
366
449
  super.onDetachedFromWindow()
367
450
  }
@@ -77,6 +77,10 @@ class IVSStageManager(private val context: Context) : Stage.Strategy, StageRende
77
77
  return localCamera
78
78
  }
79
79
 
80
+ fun isFrontCameraActive(): Boolean {
81
+ return localCamera?.descriptor?.position == Device.Descriptor.Position.FRONT
82
+ }
83
+
80
84
  fun initializeLocalStreams() {
81
85
  discoverDevices()
82
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-realtime-ivs-broadcast",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",