Preserve capture camera and session ownership through late callbacks

This commit is contained in:
evgeny committed 2026-10-29 13:34:24 +01:00
1 parent fd1e3a7f77
commit 5e9f7e01e0
1 file changed
+43 -25
@@ -255,34 +255,48 @@ class StreamCaptureEngine(
try {
manager.openCamera(mode.camera, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
if (openingGeneration == token) { opening = false; camera = device }
if (!current(token)) { device.close(); return }
if (device in closedCameras) { LogManager.addLog("WARN", "video", "closed capture camera callback ignored id=$id token=$token"); return }
if (openingGeneration == token) opening = false
if (!current(token)) { closingCameras.add(device); queueCleanup(0); return }
camera = device
createSession(device, config, token)
}
override fun onClosed(device: CameraDevice) {
if (!closedCameras.add(device)) { LogManager.videoTrace("duplicate capture camera close id=$id token=$token"); return }
if (camera === device) camera = null
closingCameras.remove(device)
val pending = cameraSessions.values.filter { it.first === device }.map { it.second }
LogManager.videoTrace("camera closed id=$id generation=$token retiringSessions=${pending.size}")
pending.forEach { it("cameraClosed") }
if (releaseDone != null && camera == null && !opening) finishRelease(true)
maybeQuit()
}
override fun onDisconnected(device: CameraDevice) {
if (openingGeneration == token) { opening = false; camera = device }
device.close(); captureError(token, "camera disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
if (openingGeneration == token) { opening = false; camera = device }
device.close(); captureError(token, "camera error=$error")
ownedSessions.entries.removeAll { it.value === device }; closingSessions.removeAll { it !in ownedSessions }
queueCleanup(0)
}
override fun onDisconnected(device: CameraDevice) { cameraFailure(device, token, "camera disconnected") }
override fun onError(device: CameraDevice, error: Int) { cameraFailure(device, token, "camera error=$error") }
}, handler)
} catch (e: Exception) { opening = false; throw e }
handler.postDelayed({ if (current(token) && (waitingFirst || confirming)) captureError(token, "keyframe/capture rate validation timeout") }, 8000)
} catch (e: Exception) {
val awaitsCallback = e is CameraAccessException && e.reason in listOf(CameraAccessException.CAMERA_IN_USE,
CameraAccessException.CAMERA_DISABLED, CameraAccessException.CAMERA_DISCONNECTED)
if (!awaitsCallback && openingGeneration == token) opening = false
LogManager.addLog("ERROR", "video", "capture camera open failed id=$id token=$token awaitingCallback=$awaitsCallback: $e")
throw e
}
check(handler.postDelayed({ if (current(token) && (waitingFirst || confirming))
captureError(token, "keyframe/capture rate validation timeout") }, 8000)) { "Capture validation timer rejected" }
} catch (e: Exception) { captureError(token, "video setup: ${e.message}") }
}
private fun current(token: Long) = token == generation && !stopping && !stopped && !terminalFailure && releaseDone == null
private fun cameraFailure(device: CameraDevice, token: Long, detail: String) {
if (device in closedCameras) { LogManager.addLog("WARN", "video", "closed capture camera error ignored id=$id token=$token: $detail"); return }
if (openingGeneration == token) opening = false
closingCameras.add(device)
if (current(token)) { camera = device; captureError(token, detail) }
else LogManager.addLog("WARN", "video", "stale capture camera error id=$id token=$token/$generation: $detail")
queueCleanup(0)
}
/** Меняется только набор Camera2 outputs; encoder, audio и origin не перезапускаются. */
private fun createSession(device: CameraDevice, config: StreamCaptureConfig, token: Long, withPreview: Boolean = true) {
val mode = config.mode
@@ -294,7 +308,7 @@ class StreamCaptureEngine(
if (completed) return
completed = true; cameraSessions.remove(serial); output?.release()
LogManager.videoTrace("camera session output released id=$id session=$serial reason=$reason remaining=${cameraSessions.size}")
maybeQuit()
if (releaseDone != null || stopping) queueCleanup(0)
}
cameraSessions[serial] = device to ::complete
fun valid() = current(token) && serial == sessionGeneration
@@ -305,6 +319,7 @@ class StreamCaptureEngine(
preview.update(format, failed = true)
createSession(device, config, token, withPreview = false)
}
var submitted = false
try {
if (output != null) {
val characteristics = manager.getCameraCharacteristics(mode.camera)
@@ -323,7 +338,9 @@ class StreamCaptureEngine(
val targets = listOfNotNull(checkNotNull(input) { "Missing encoder surface" }, output?.surface)
val callback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(configured: CameraCaptureSession) {
if (!valid()) { configured.close(); return }
if (completed) { LogManager.addLog("WARN", "video", "closed capture session callback ignored id=$id serial=$serial"); return }
ownedSessions[configured] = device
if (!valid()) { closingSessions.add(configured); queueCleanup(0); return }
session = configured
try {
val request = device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
@@ -360,26 +377,27 @@ class StreamCaptureEngine(
LogManager.addLog("INFO", "video", "capture started id=$id generation=$token session=$serial camera=${mode.camera} ${mode.label} " +
"range=${mode.minimumFps}..${mode.fps} highSpeed=${mode.highSpeed} encoder=${encoder?.name} " +
"preview=${output != null} previewSize=${format?.let { "${it.width}x${it.height}" }} origin=$originNs")
} catch (e: Exception) { configured.close(); rejected("camera request: ${e.message}") }
} catch (e: Exception) { closingSessions.add(configured); queueCleanup(0); rejected("camera request: ${e.message}") }
}
override fun onConfigureFailed(session: CameraCaptureSession) {
complete("configureFailed"); rejected("camera session configuration")
}
override fun onClosed(closed: CameraCaptureSession) {
ownedSessions.remove(closed); closingSessions.remove(closed)
if (session === closed) session = null
complete("sessionClosed")
}
}
LogManager.videoTrace("camera session configuring id=$id generation=$token session=$serial outputs=${targets.size}")
submitted = true
if (mode.highSpeed) device.createConstrainedHighSpeedCaptureSession(targets, callback, handler)
else device.createCaptureSession(targets, callback, handler)
} catch (e: Exception) { complete("exception"); rejected("camera session: ${e.message}") }
}
private fun maybeQuit() {
if (stopped && camera == null && !opening && cameraSessions.isEmpty()) {
LogManager.addLog("INFO", "video", "capture handler released id=$id pendingSessions=0")
thread.quitSafely()
} catch (e: Exception) {
if (!submitted) { complete("exception before submit"); rejected("camera session: ${e.message}") }
else {
LogManager.addLog("ERROR", "video", "capture session submit failed id=$id serial=$serial; outputs retained until device closes: $e")
captureError(token, "camera session: ${e.message}")
}
}
}
@@ -390,7 +408,7 @@ class StreamCaptureEngine(
if (previous == null || reverting) { failed(detail); return }
waitingFirst = true; reverting = true
LogManager.addLog("WARN", "video", "restoring camera=${previous.mode.camera} ${previous.mode.label} orientation=${previous.orientation} id=$id")
releaseVideo { closed -> if (closed) configure(previous) else failed("camera close timed out during restore") }
releaseVideo { configure(previous) }
}
private fun encoded(bytes: ByteArray, info: MediaCodec.BufferInfo, config: StreamCaptureConfig, pose: StreamPreviewCamera, token: Long) {