Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,104 @@ describe('VisionCamera - NativePreviewView', () => {
}
})

it('re-attaches a preview output to a new NativePreviewView that replaces the old one while running', async () => {
const session = await VisionCamera.createCameraSession(false)
const previewOutput = VisionCamera.createPreviewOutput()
await session.configure([
{
input: backDevice,
outputs: [{ output: previewOutput, mirrorMode: 'auto' }],
constraints: [],
},
])

const firstRef = deferred<PreviewView>()
const secondRef = deferred<PreviewView>()
const firstLayout = deferred<Layout>()
const secondLayout = deferred<Layout>()
const firstPreviewStarted = deferred()
const secondPreviewStarted = deferred()
const errorSub = session.addOnErrorListener((error) => {
firstRef.reject(error)
secondRef.reject(error)
firstLayout.reject(error)
secondLayout.reject(error)
firstPreviewStarted.reject(error)
secondPreviewStarted.reject(error)
})

try {
const { rerender } = await render(
<NativePreviewView
key="first"
style={StyleSheet.absoluteFill}
previewOutput={previewOutput}
hybridRef={callback((preview: PreviewView) => {
firstRef.resolve(preview)
})}
onLayout={(event) => {
firstLayout.resolve(toLayout(event))
}}
onPreviewStarted={callback(firstPreviewStarted.resolve)}
/>,
)

const firstPreview = await withTimeout(
firstRef.promise,
10_000,
'first NativePreviewView hybridRef',
)
const firstPreviewLayout = await withTimeout(
firstLayout.promise,
10_000,
'first NativePreviewView onLayout',
)

await session.start()
await withTimeout(
firstPreviewStarted.promise,
15_000,
'first NativePreviewView onPreviewStarted',
)
expectPreviewGeometry(firstPreview, firstPreviewLayout)

await rerender(
<NativePreviewView
key="second"
style={StyleSheet.absoluteFill}
previewOutput={previewOutput}
hybridRef={callback((preview: PreviewView) => {
secondRef.resolve(preview)
})}
onLayout={(event) => {
secondLayout.resolve(toLayout(event))
}}
onPreviewStarted={callback(secondPreviewStarted.resolve)}
/>,
)

const secondPreview = await withTimeout(
secondRef.promise,
10_000,
'second NativePreviewView hybridRef',
)
const secondPreviewLayout = await withTimeout(
secondLayout.promise,
10_000,
'second NativePreviewView onLayout',
)
await withTimeout(
secondPreviewStarted.promise,
15_000,
'second NativePreviewView onPreviewStarted',
)
expectPreviewGeometry(secondPreview, secondPreviewLayout)
} finally {
errorSub.remove()
await session.stop()
}
})

it('starts a replacement preview and stops the removed preview while running', async () => {
const session = await VisionCamera.createCameraSession(false)
const firstPreviewOutput = VisionCamera.createPreviewOutput()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ class HybridPreviewView(
) : HybridPreviewViewSpec(),
View.OnTouchListener,
Observer<PreviewView.StreamState> {
private val previewView = PreviewView(context)
// released on drop; JS may hold this object long after unmount
private var previewViewOrNull: PreviewView? = PreviewView(context)
private val previewView: PreviewView
get() = previewViewOrNull ?: throw Error("PreviewView has already been dropped from the React tree!")
private val uiScope = CoroutineScope(Dispatchers.Main.immediate)
override val view: View
get() = previewView
Expand Down Expand Up @@ -67,6 +70,7 @@ class HybridPreviewView(
field = newImplementationMode
uiScope.launch {
// Update ImplementationMode and reconnect output
val previewView = previewViewOrNull ?: return@launch
previewView.implementationMode = newImplementationMode.toImplementationMode()
previewOutput?.let { previewOutput -> connectPreviewOutput(previewOutput) }
}
Expand All @@ -80,6 +84,7 @@ class HybridPreviewView(
field = newResizeMode
uiScope.launch {
// Update scaleType
val previewView = previewViewOrNull ?: return@launch
previewView.scaleType = newResizeMode.toScaleType()
}
}
Expand All @@ -100,9 +105,30 @@ class HybridPreviewView(
pixelRatio = context.resources.displayMetrics.density
}

override fun onDropView() {
super.onDropView()
uiScope.launch {
releasePreviewView()
}
}

override fun dispose() {
super.dispose()
uiScope.launch {
releasePreviewView()
}
}

/**
* Detaches the `PreviewView` from its Preview Output and releases it.
* Safe to call from both `onDropView()` and `dispose()`.
*/
@UiThread
private fun releasePreviewView() {
val previewView = previewViewOrNull ?: return
previewView.previewStreamState.removeObserver(this)
previewOutput?.let { previewOutput -> disconnectPreviewOutput(previewOutput) }
previewViewOrNull = null
}

override fun createMeteringPoint(
Expand Down Expand Up @@ -168,15 +194,19 @@ class HybridPreviewView(
private fun connectPreviewOutput(previewOutput: HybridCameraPreviewOutputSpec) {
// 1. Downcast
val previewOutput = previewOutput as? NativePreviewOutput ?: throw Error("PreviewOutput is not of type `NativePreviewOutput`!")
// 2. Set surfaceProvider on UI Thread
// 2. Skip if the PreviewView was already dropped
val previewView = previewViewOrNull ?: return
// 3. Set surfaceProvider on UI Thread
previewOutput.setSurfaceProvider(previewView.surfaceProvider)
}

@UiThread
private fun disconnectPreviewOutput(previewOutput: HybridCameraPreviewOutputSpec) {
// 1. Downcast
val previewOutput = previewOutput as? NativePreviewOutput ?: throw Error("PreviewOutput is not of type `NativePreviewOutput`!")
// 2. Remove surfaceProvider on UI Thread
// 2. Skip if the PreviewView was already dropped
val previewView = previewViewOrNull ?: return
// 3. Remove surfaceProvider on UI Thread
previewOutput.removeSurfaceProvider(previewView.surfaceProvider)
}

Expand All @@ -201,7 +231,7 @@ class HybridPreviewView(
// since it's accessible from the UI-Thread only.
private fun updateCameraSpaceToViewPixelsMatrix() {
val matrix =
previewView.sensorToViewTransform
previewViewOrNull?.sensorToViewTransform
?: return
this.cameraSpaceToViewPixelsMatrix = matrix
}
Expand Down