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
|
-
//
|
|
41
|
-
//
|
|
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
|
-
|
|
54
|
-
|
|
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: $
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
//
|
|
344
|
-
|
|
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
|
-
//
|
|
364
|
-
|
|
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
|
|