Handle camera error callbacks promised alongside synchronous open failures
This commit is contained in:
1 parent
a6f41c0256
commit
5e9dad2e9e
2 files changed
+49
-17
No files matched your search
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.camera2.CameraCaptureSession
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraDevice
|
||||
import android.hardware.camera2.CameraManager
|
||||
@@ -79,10 +80,12 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
private var portOwned = false // TX: выставляется до native open, снимается только после close.
|
||||
private val ownedReaders = linkedSetOf<ImageReader>()
|
||||
private val closingCameras = linkedSetOf<CameraDevice>()
|
||||
private val closedCameras = linkedSetOf<CameraDevice>() // Отсекает повтор уже подтверждённого SDK callback.
|
||||
private val ownedSessions = linkedMapOf<CameraCaptureSession, CameraDevice>()
|
||||
private val closingSessions = linkedSetOf<CameraCaptureSession>()
|
||||
private val pendingEncoders = linkedSetOf<CallVideoCodec>()
|
||||
private var openingToken = 0L
|
||||
private var nativeUnavailable = false
|
||||
private val cleanupTask = Runnable {
|
||||
synchronized(this) { cleanupQueued = false }
|
||||
cleanupResources()
|
||||
@@ -283,12 +286,22 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
fun range(value: CallVideoBitrateRange) {
|
||||
if (!handler.post {
|
||||
if (stopped) { LogManager.addLog("WARN", "call", "Bitrate selection after stop id=$id range=$value"); return@post }
|
||||
if (CallVideoNative.range(id, value.minimum * 1000, value.maximum * 1000)) {
|
||||
mutable.value = mutable.value.copy(range = value)
|
||||
ChatApplication.instance.configProvider.saveCallVideoBitrateRange(value)
|
||||
NativeLib.setChatSetting("call_video_bitrate_range_kbps", value.encode())
|
||||
LogManager.addLog("INFO", "call", "Video bitrate selected id=$id range=$value")
|
||||
} else LogManager.addLog("WARN", "call", "Bitrate range rejected id=$id range=$value")
|
||||
try {
|
||||
if (CallVideoNative.range(id, value.minimum * 1000, value.maximum * 1000)) {
|
||||
mutable.value = mutable.value.copy(range = value)
|
||||
try { ChatApplication.instance.configProvider.saveCallVideoBitrateRange(value) } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "call", "video bitrate applied but not saved id=$id range=$value: $e")
|
||||
mutable.value = mutable.value.copy(notice = R.string.call_video_unavailable)
|
||||
}
|
||||
try { NativeLib.setChatSetting("call_video_bitrate_range_kbps", value.encode()) } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "call", "video bitrate default update failed id=$id range=$value: $e")
|
||||
}
|
||||
LogManager.addLog("INFO", "call", "Video bitrate selected id=$id range=$value")
|
||||
} else LogManager.addLog("WARN", "call", "Bitrate range rejected id=$id range=$value")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "call", "video bitrate selection failed id=$id range=$value: $e")
|
||||
mutable.value = mutable.value.copy(notice = R.string.call_video_unavailable)
|
||||
}
|
||||
}) LogManager.addLog("WARN", "call", "Bitrate selection rejected by stopped worker id=$id range=$value")
|
||||
}
|
||||
private fun report(message: String) {
|
||||
@@ -309,10 +322,13 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
}
|
||||
|
||||
private fun tick() {
|
||||
if (stopped) return
|
||||
if (stopped || nativeUnavailable) return
|
||||
try {
|
||||
val s = CallVideoNative.status(id) ?: return
|
||||
check(s[5] == 0L) { "Core rejected video format" }
|
||||
val s = CallVideoNative.status(id) ?: run {
|
||||
nativeUnavailable = true; mutable.value = mutable.value.copy(ready = false)
|
||||
fail("Video native port unavailable"); return
|
||||
}
|
||||
if (s[5] != 0L && active != null) { fail("Core rejected video format generation=${format?.generation}"); return }
|
||||
if (orientation.confirm(SystemClock.elapsedRealtime())) logOrientation()
|
||||
canEncode = s[10] != 0L && s[4] == format?.generation
|
||||
if (s[6] >= 300000 && s[7] >= s[6]) mutable.value = mutable.value.copy(range = CallVideoBitrateRange((s[6] / 1000).toInt(), (s[7] / 1000).toInt()))
|
||||
@@ -499,31 +515,39 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
opening = true; openingToken = token; openedAt = System.nanoTime(); lastCaptureAt = openedAt
|
||||
try { manager.openCamera(settings.camera, object : CameraDevice.StateCallback() {
|
||||
override fun onOpened(device: CameraDevice) {
|
||||
if (device in closedCameras) { LogManager.addLog("WARN", "call", "closed camera opened callback ignored id=$id token=$token"); return }
|
||||
if (openingToken == token) opening = false
|
||||
if (stopped || token != cameraToken) {
|
||||
closingCameras.add(device); closing = true; queueCleanup(0); return
|
||||
}
|
||||
camera = device
|
||||
LogManager.addLog("INFO", "call", "camera opened id=$id device=${device.id} token=$token")
|
||||
createSession(device, nextReader, token)
|
||||
}
|
||||
override fun onDisconnected(device: CameraDevice) { cameraFailure(device, token, "Camera disconnected") }
|
||||
override fun onError(device: CameraDevice, error: Int) { cameraFailure(device, token, "Camera error $error") }
|
||||
override fun onClosed(device: CameraDevice) {
|
||||
if (!closedCameras.add(device)) { LogManager.addLog("DEBUG", "call", "duplicate camera closed ignored id=$id token=$token"); return }
|
||||
sessions.values.filter { it.first === device }.map { it.second }.forEach { it() }
|
||||
ownedSessions.entries.removeAll { it.value === device }
|
||||
closingSessions.removeAll { it !in ownedSessions }
|
||||
if (camera === device) camera = null
|
||||
closingCameras.remove(device)
|
||||
LogManager.addLog("INFO", "call", "camera closed id=$id device=${device.id} token=$token current=$cameraToken")
|
||||
queueCleanup(0)
|
||||
}
|
||||
}, handler) } catch (e: Exception) {
|
||||
if (openingToken == token) opening = false
|
||||
val awaitsCallback = e is CameraAccessException && e.reason in listOf(CameraAccessException.CAMERA_IN_USE,
|
||||
CameraAccessException.CAMERA_DISABLED, CameraAccessException.CAMERA_DISCONNECTED)
|
||||
if (openingToken == token && !awaitsCallback) opening = false
|
||||
LogManager.addLog("ERROR", "call", "camera open failed id=$id token=$token awaitingCallback=$awaitsCallback: $e")
|
||||
queueCleanup(0)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun cameraFailure(device: CameraDevice, token: Long, detail: String) {
|
||||
if (device in closedCameras) { LogManager.addLog("WARN", "call", "closed camera error ignored id=$id token=$token detail=$detail"); return }
|
||||
if (openingToken == token) opening = false
|
||||
closingCameras.add(device)
|
||||
if (!stopped && token == cameraToken) { camera = device; fail(detail) }
|
||||
@@ -673,9 +697,10 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
} catch (e: Exception) { LogManager.addLog("ERROR", "call", "camera reader close failed id=$id; retained: $e") }
|
||||
if (ownedReaders.isEmpty() && sessions.isEmpty() && ownedSessions.isEmpty()) closing = false
|
||||
}
|
||||
if (camera != null && camera !in closingCameras && !opening) closing = false
|
||||
for (owned in pendingEncoders.toList()) releaseEncoder(owned)
|
||||
if (!wanted.enabled) encoder?.let { releaseEncoder(it) }
|
||||
val pending = opening || closing || closingSessions.isNotEmpty() || pendingEncoders.isNotEmpty() ||
|
||||
val pending = opening || closing || closingCameras.isNotEmpty() || closingSessions.isNotEmpty() || pendingEncoders.isNotEmpty() ||
|
||||
(!wanted.enabled && (encoder != null || orientationEnabled))
|
||||
if (stopped && !pending) {
|
||||
if (portOwned) try { CallVideoNative.close(id); portOwned = false } catch (e: Exception) {
|
||||
@@ -702,14 +727,15 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
}
|
||||
|
||||
/** Один ближайший запрос; сильный владелец при stop остаётся также при отказе Handler. */
|
||||
@Synchronized private fun queueCleanup(delay: Long) {
|
||||
if (cleanupComplete) return
|
||||
@Synchronized private fun queueCleanup(delay: Long): Boolean {
|
||||
if (cleanupComplete) return true
|
||||
val due = System.nanoTime() + delay * 1_000_000
|
||||
if (cleanupQueued && cleanupDue <= due) return
|
||||
if (cleanupQueued && cleanupDue <= due) return true
|
||||
handler.removeCallbacks(cleanupTask)
|
||||
cleanupDue = due
|
||||
cleanupQueued = if (delay == 0L) handler.post(cleanupTask) else handler.postDelayed(cleanupTask, delay)
|
||||
if (!cleanupQueued) LogManager.addLog("ERROR", "call", "video cleanup request rejected id=$id delayMs=$delay; owner retained")
|
||||
return cleanupQueued
|
||||
}
|
||||
|
||||
/** Main: запретить producers и подтвердить RX cleanup, затем запросить TX teardown.
|
||||
@@ -718,8 +744,7 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
fun stop(): Boolean {
|
||||
if (cleanupComplete) return true
|
||||
retained.add(this); stopped = true; player.stop()
|
||||
queueCleanup(0)
|
||||
check(cleanupQueued || cleanupComplete) {
|
||||
check(queueCleanup(0)) {
|
||||
LogManager.addLog("ERROR", "call", "video stop cleanup rejected id=$id; stop remains retryable")
|
||||
"Video cleanup worker rejected stop"
|
||||
}
|
||||
|
||||
+8
-1
@@ -7,6 +7,10 @@ import android.util.Range
|
||||
import android.view.Surface
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class CameraAccessException(val reason: Int) : Exception("camera access failure reason=$reason") {
|
||||
companion object { const val CAMERA_IN_USE = 4; const val CAMERA_DISABLED = 1; const val CAMERA_DISCONNECTED = 2 }
|
||||
}
|
||||
|
||||
class HostStreamMap {
|
||||
fun getOutputSizes(format: Int) = arrayOf(Size(640, 480))
|
||||
fun getOutputSizes(type: Class<SurfaceTexture>) = arrayOf(Size(640, 480))
|
||||
@@ -57,8 +61,11 @@ class CameraManager {
|
||||
check("open" !in failures) { "camera open failed" }
|
||||
val device = CameraDevice(id, this, callback, handler).also { it.failures = nextDeviceFailures }
|
||||
opened.add(Open(device, callback, handler))
|
||||
if ("open after" in failures) {
|
||||
handler.post { opened.last().delivered = true; callback.onError(device, 1) }
|
||||
throw CameraAccessException(CameraAccessException.CAMERA_IN_USE)
|
||||
}
|
||||
if (autoOpened) deliverOpened(opened.lastIndex)
|
||||
check("open after" !in failures) { "camera open failed after effect" }
|
||||
}
|
||||
fun deliverOpened(index: Int) { val r = opened[index]; r.handler.post { r.delivered = true; r.callback.onOpened(r.device) } }
|
||||
fun deliverClosed(index: Int) { val r = opened[index]; r.handler.post { r.callback.onClosed(r.device) } }
|
||||
|
||||
Reference in new issue
Block a user