diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CameraRecordScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CameraRecordScreen.kt index 1fd005d3..18e268f6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CameraRecordScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/CameraRecordScreen.kt @@ -283,13 +283,18 @@ fun CameraRecordScreen(initialMode: CaptureMode, onSend: (String, CaptureMode) - val ready = provider; val selected = lens if (ready != null && selected != null) { if (mode == CaptureMode.PHOTO) { - resolutions = CameraLensManager.photoResolutions(ready, selected.cameraId) - resolution = photoSettings.resolveResolution(resolutions) - if (photoSettings.width > 0 && (resolution?.width != photoSettings.width || resolution?.height != photoSettings.height)) - LogManager.addLog("WARN", "Capture", "Saved photo resolution unavailable camera=${selected.cameraId} " + - "saved=${photoSettings.width}x${photoSettings.height} selected=${resolution?.label}") - LogManager.addLog("INFO", "Capture", "Photo settings restored camera=${selected.cameraId} " + - "resolution=${resolution?.label} jpegQuality=$jpegQuality") + try { + resolutions = CameraLensManager.photoResolutions(ready, selected.cameraId) + resolution = photoSettings.resolveResolution(resolutions) + if (photoSettings.width > 0 && (resolution?.width != photoSettings.width || resolution?.height != photoSettings.height)) + LogManager.addLog("WARN", "Capture", "Saved photo resolution unavailable camera=${selected.cameraId} " + + "saved=${photoSettings.width}x${photoSettings.height} selected=${resolution?.label}") + LogManager.addLog("INFO", "Capture", "Photo settings restored camera=${selected.cameraId} " + + "resolution=${resolution?.label} jpegQuality=$jpegQuality") + } catch (e: Exception) { + resolutions = emptyList(); resolution = null; error = true + LogManager.addLog("ERROR", "Capture", "Photo capabilities failed camera=${selected.cameraId}: $e") + } return@LaunchedEffect } try { @@ -404,7 +409,11 @@ fun CameraRecordScreen(initialMode: CaptureMode, onSend: (String, CaptureMode) - val pending = videoCapture.output.prepareRecording(context, FileOutputOptions.Builder(file).build()).withAudioEnabled() attemptedStart = true recording = pending.start(main) { event -> - if (event is VideoRecordEvent.Finalize) resources.completed(file) + if (event is VideoRecordEvent.Finalize) { + if (event.hasError()) LogManager.addLog(if (closed || output != file) "DEBUG" else "ERROR", "Capture", + "Video finalize code=${event.error} discarded=${closed || output != file}: ${event.cause}") + resources.completed(file) + } if (closed || output != file) { if (event is VideoRecordEvent.Finalize) resources.discard(file) } else when (event) { @@ -426,7 +435,6 @@ fun CameraRecordScreen(initialMode: CaptureMode, onSend: (String, CaptureMode) - recording = null if (event.hasError()) { resources.discard(file); output = null; pendingSend = false; phase = CapturePhase.IDLE; error = true - LogManager.addLog("ERROR", "Capture", "Video finalize failed code=${event.error}: ${event.cause?.message}") } else { phase = CapturePhase.FINALIZING scope.launch { @@ -486,7 +494,10 @@ fun CameraRecordScreen(initialMode: CaptureMode, onSend: (String, CaptureMode) - val request = selected?.cameraControl?.enableTorch(next) request?.addListener({ try { request.get(); if (!closed && camera === selected) torch = next } - catch (e: Exception) { error = true; LogManager.addLog("ERROR", "Capture", "Torch failed: $e") } + catch (e: Exception) { + if (!closed && camera === selected) error = true + LogManager.addLog("ERROR", "Capture", "Torch failed: $e") + } }, main) } catch (e: Exception) { error = true; LogManager.addLog("ERROR", "Capture", "Torch request failed: $e") } }) { Icon(if (torch) Icons.Default.FlashOn else Icons.Default.FlashOff, stringResource(R.string.camera_light)) } diff --git a/tools/chatgui-android/doc/ARCHITECTURE.md b/tools/chatgui-android/doc/ARCHITECTURE.md index c8a633a1..bfe23b08 100644 --- a/tools/chatgui-android/doc/ARCHITECTURE.md +++ b/tools/chatgui-android/doc/ARCHITECTURE.md @@ -266,6 +266,17 @@ feed/pull workers и маршрутом Android Telecom; DSP и сетевой Вызовы lifecycle DSP выполняются после остановки его внешних feed/pull callers. Контракты PCM/PTS: `src/call/call_audio.h`, `src/radio/radio_audio.h`, `jni_bridge/android_jni_bridge.h`. +CameraRecordScreen сохраняет CameraCaptureResources до захвата и снимает только свои +use cases. Cache фото живёт до OnImageSaved/onError, видео — до Finalize; discard/выход +UI сначала запрашивает закрытие записи и ждёт writer перед удалением файла. +Ошибки Recording.close, unbind, отключения ориентации и удаления независимы и сохраняются +в process registry с retry. Поздний callback не восстанавливает UI и не удаляет переданный sender файл. +Pause/resume подтверждаются событиями CameraX; разрешения перечитываются при ON_RESUME. +VideoFileTracks проверяет MIME/геометрию готового MP4; MediaExtractor остаётся до release ACK, +ошибка чтения сохраняется при дополнительной ошибке cleanup. Контракты CameraX: +[Recording.close](https://developer.android.com/reference/androidx/camera/video/Recording#close()), +[PendingRecording.start](https://developer.android.com/reference/androidx/camera/video/PendingRecording#start(java.util.concurrent.Executor,androidx.core.util.Consumer)). + Локальный `VideoPlayerScreen` сохраняет пассивный VideoFilePlayer до start. UI отражает результаты MediaPlayer start/pause/seek, ошибка команды завершает playback и оставляет доступным закрытие экрана. Пользовательская пауза сохраняется при пересоздании Surface.