Browse Source

Release all preview session outputs when the camera device closes

stream-multiview
evgeny 1 week ago
parent
commit
12ad2bf14b
  1. 10
      doc/stream.txt
  2. 28
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt

10
doc/stream.txt

@ -837,6 +837,9 @@ Android:
Локальный preview получает прямой Camera2 output в TextureView одновременно с encoder Surface.
Собственный источник не открывает viewer/cursor, decoder, AudioTrack и playback controller.
Preview ориентируется по сенсору камеры и текущему положению экрана; ручной поворот трансляции его не вращает.
TextureView уже применяет ориентацию сенсора. UI Matrix компенсирует только -displayDegrees,
размеры view учитывают relative rotation; повторно поворачивать на SENSOR_ORIENTATION нельзя.
DisplayListener обновляет preview и метки кнопок также при 180°, без изменения Configuration.
Верх картинки зрителей отмечается красной гранью на кнопках поворота; метка «Верх» на видео не рисуется.
В обычном режиме preview выбирает поддерживаемый SurfaceTexture размер того же aspect, не более 1280x720;
high-speed использует размер encoder и createHighSpeedRequestList для двух targets с фиксированным FPS range.
@ -844,10 +847,15 @@ Android:
При отказе дополнительного output логируется ERROR, UI показывает ошибку preview, камера возвращается к encoder-only session.
TextureView возвращает false из onSurfaceTextureDestroyed; UI, очередь команд camera thread и camera sessions
держат отдельные ссылки. Surface/texture освобождаются после последнего onClosed/onConfigureFailed и ухода UI.
CameraDevice.onClosed освобождает оставшиеся ссылки всех своих sessions: Android не всегда доставляет
session.onClosed при закрытии всего устройства. Повторный callback не освобождает ссылку второй раз.
Логи direct preview показывают first frame, FPS, возраст frame по SurfaceTexture timestamp и поворот;
для SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME возраст считается через elapsedRealtimeNanos,
при UNKNOWN абсолютный возраст не выдумывается, логируется timestamp; начальная очистка texture с timestamp=0 не считается кадром.
camera session logs показывают generation, outputs, размер preview и сохранённый encoder/origin.
Удалённый viewer применяет video rotation источника. ON_STOP закрывает viewer либо отсоединяет прямой preview;
ON_START создаёт output заново. Камера при закрытии preview продолжает работать; publisher от UI не зависит.
ON_START создаёт output заново. Lifecycle отключает target сразу, независимо от приостановленной Compose recomposition.
Камера при закрытии preview продолжает работать; publisher от UI не зависит.
Видимый player удерживает экран включённым. Preview ограничен оставшимся местом, кнопки управления не перекрывает.
- Source list общий для Android/desktop: stream_sources_json в uasync использует stream_member_load
и stream_member_online. stream_watch_sources наблюдает один выбранный канал; group=0 снимает наблюдение.

28
tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt

@ -44,7 +44,7 @@ class StreamCaptureEngine(
private var activeConfig: StreamCaptureConfig? = null
private var previewTarget: StreamPreviewTarget? = null
private var sessionGeneration = 0L
private var cameraSessions = 0
private val cameraSessions = mutableMapOf<Long, Pair<CameraDevice, (String) -> Unit>>()
private var record: AudioRecord? = null
private var audioThread: Thread? = null
@Volatile private var audioRunning = false
@ -85,6 +85,7 @@ class StreamCaptureEngine(
preview.bind { target ->
if (!handler.post {
if (stopping || stopped || terminalFailure) { target?.release(); return@post }
if (target === previewTarget) { target?.release(); return@post }
val previous = previewTarget; previewTarget = target; previous?.release()
val config = activeConfig; val device = camera
LogManager.addLog("DEBUG", "video", "preview output change id=$id target=${target?.let(System::identityHashCode)} " +
@ -215,6 +216,9 @@ class StreamCaptureEngine(
}
override fun onClosed(device: CameraDevice) {
if (camera === device) camera = null
val pending = cameraSessions.values.filter { it.first === device }.map { it.second }
LogManager.addLog("DEBUG", "video", "camera closed id=$id revision=$token retiringSessions=${pending.size}")
pending.forEach { it("cameraClosed") }
if (releaseDone != null && camera == null && !opening) finishRelease(true)
maybeQuit()
}
@ -241,11 +245,13 @@ class StreamCaptureEngine(
val output = if (withPreview) previewTarget?.retain() else null
var format: StreamPreviewFormat? = null
var completed = false
cameraSessions++
fun complete() {
fun complete(reason: String) {
if (completed) return
completed = true; output?.release(); cameraSessions--; maybeQuit()
completed = true; cameraSessions.remove(serial); output?.release()
LogManager.addLog("DEBUG", "video", "camera session output released id=$id session=$serial reason=$reason remaining=${cameraSessions.size}")
maybeQuit()
}
cameraSessions[serial] = device to ::complete
fun valid() = current(token) && serial == sessionGeneration
fun rejected(detail: String) {
if (!valid()) { LogManager.addLog("DEBUG", "video", "stale camera session failure id=$id session=$serial: $detail"); return }
@ -312,21 +318,25 @@ class StreamCaptureEngine(
} catch (e: Exception) { configured.close(); rejected("camera request: ${e.message}") }
}
override fun onConfigureFailed(session: CameraCaptureSession) {
complete(); rejected("camera session configuration")
complete("configureFailed"); rejected("camera session configuration")
}
override fun onClosed(closed: CameraCaptureSession) {
if (session === closed) session = null
complete()
LogManager.addLog("DEBUG", "video", "camera session closed id=$id session=$serial remaining=$cameraSessions")
complete("sessionClosed")
}
}
LogManager.addLog("DEBUG", "video", "camera session configuring id=$id revision=$token session=$serial outputs=${targets.size}")
if (mode.highSpeed) device.createConstrainedHighSpeedCaptureSession(targets, callback, handler)
else device.createCaptureSession(targets, callback, handler)
} catch (e: Exception) { complete(); rejected("camera session: ${e.message}") }
} catch (e: Exception) { complete("exception"); rejected("camera session: ${e.message}") }
}
private fun maybeQuit() { if (stopped && camera == null && !opening && cameraSessions == 0) thread.quitSafely() }
private fun maybeQuit() {
if (stopped && camera == null && !opening && cameraSessions.isEmpty()) {
LogManager.addLog("INFO", "video", "capture handler released id=$id pendingSessions=0")
thread.quitSafely()
}
}
private fun captureError(token: Long, detail: String) {
if (!current(token)) { LogManager.addLog("DEBUG", "video", "stale capture error revision=$token: $detail"); return }

Loading…
Cancel
Save