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
44 changes: 44 additions & 0 deletions apps/simple-camera/__tests__/visioncamera.session.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,50 @@ describe('VisionCamera - Session', () => {
await session.stop()
})

it('starts again after all connections were removed', async () => {
const device = factory.getDefaultCamera('back')
assert.exists(device, 'no back camera')

const session = await VisionCamera.createCameraSession(false)
const photoOutput = VisionCamera.createPhotoOutput({
targetResolution: CommonResolutions.HD_4_3,
containerFormat: 'jpeg',
quality: 0.8,
qualityPrioritization: 'balanced',
})
const connection = {
input: device,
outputs: [{ output: photoOutput, mirrorMode: 'auto' as const }],
constraints: [],
}

let started = deferred()
const startSub = session.addOnStartedListener(() => {
started.resolve()
})
const errorSub = session.addOnErrorListener((error) => {
started.reject(error)
})

try {
await session.configure([connection])
await session.start()
await withTimeout(started.promise, 10_000, 'session start')

started = deferred()
await session.configure([])
await session.stop()

await session.configure([connection])
await session.start()
await withTimeout(started.promise, 10_000, 'session restart')
} finally {
await session.stop()
startSub.remove()
errorSub.remove()
}
})

it('supports a multi-cam session when the platform allows it', async (context) => {
if (!VisionCamera.supportsMultiCamSessions) {
return context.skip('multi-cam session: not supported on this platform')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class HybridCameraSession(
ActiveCameraSession.LifecycleListener {
private val context: ReactApplicationContext
get() = NitroModules.applicationContext ?: throw Error("No Context!")
private val lifecycleOwner = CustomLifecycle(context)
private var lifecycleOwner: CustomLifecycle? = null
private var isActive = false
private val uiScope = CoroutineScope(Dispatchers.Main)

override val isRunning: Boolean
Expand Down Expand Up @@ -66,6 +67,9 @@ class HybridCameraSession(
cameraProvider.unbindAll()
activeSession?.close()
activeSession = null
// Only `destroy()` releases the owner from CameraX and the `ReactApplicationContext`
lifecycleOwner?.destroy()
lifecycleOwner = null

// 2. Build up Camera depending on connections count
when (connections.size) {
Expand All @@ -87,7 +91,9 @@ class HybridCameraSession(
connection.onSessionConfigSelected(hybridConfig)
}

val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraInfo.cameraSelector, config.sessionConfig)
val owner = CustomLifecycle(context, isActive)
lifecycleOwner = owner
val camera = cameraProvider.bindToLifecycle(owner, cameraInfo.cameraSelector, config.sessionConfig)
// Notify outputs that their use-cases are now attached to the Camera
config.preparedUseCases.forEach { it.notifyAttached() }
activeSession = ActiveCameraSessionSingle(camera, this)
Expand All @@ -100,6 +106,8 @@ class HybridCameraSession(
// TODO: In Multi-Cam we cannot use the CameraX `SessionConfig` API, so we cannot use `ConstraintsResolver`!
// This effectively means that no special features (FPS, HDR, Stabilization, ...) are supported in multi-cam... :(
// Implement this once CameraX supports it - feature request: https://issuetracker.google.com/issues/470629644
val owner = CustomLifecycle(context, isActive)
lifecycleOwner = owner
val allPreparedUseCases = mutableListOf<NativeCameraOutput.PreparedUseCase>()
val configs =
connections.map { connection ->
Expand All @@ -117,7 +125,7 @@ class HybridCameraSession(
ConcurrentCamera.SingleCameraConfig(
cameraInfo.cameraSelector,
useCaseGroup.build(),
lifecycleOwner,
owner,
)
}
Log.i(TAG, "Binding configs: ${configs.joinToString { it.useCaseGroup.useCases.joinToString { it.name } }}")
Expand All @@ -137,21 +145,24 @@ class HybridCameraSession(

override fun start(): Promise<Unit> {
return Promise.async(uiScope) {
lifecycleOwner.setActive(true)
isActive = true
lifecycleOwner?.setActive(true)
}
}

override fun stop(): Promise<Unit> {
return Promise.async(uiScope) {
lifecycleOwner.setActive(false)
isActive = false
lifecycleOwner?.setActive(false)
}
}

override fun dispose() {
super.dispose()
Log.i(TAG, "Destroying CameraSession...")
Promise.async(uiScope) {
lifecycleOwner.destroy()
lifecycleOwner?.destroy()
lifecycleOwner = null
activeSession?.close()
cameraProvider.unbindAll()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
*/
class CustomLifecycle(
private val context: ReactApplicationContext,
initialIsActive: Boolean,
) : LifecycleEventListener,
LifecycleOwner {
companion object {
Expand All @@ -29,7 +30,7 @@ class CustomLifecycle(
private val lifecycleRegistry = LifecycleRegistry(this)
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

@Volatile private var isActive: Boolean = false
@Volatile private var isActive: Boolean = initialIsActive

@Volatile private var isDestroyed = false

Expand Down