diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt index f8ea0c81..703da05f 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamCaptureEngine.kt @@ -119,7 +119,7 @@ class StreamCaptureEngine( if (previous != null && config.copy(videoBitrate = previous.videoBitrate) == previous) { try { encoder!!.setParameters(Bundle().apply { putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, config.bitrate) }) - lastGood = config; ready(config, false) + lastGood = config; activeConfig = config; ready(config, false) LogManager.addLog("INFO", "video", "bitrate changed id=$id revision=$generation ${previous.bitrate}->${config.bitrate}") return@post } catch (e: Exception) { @@ -256,7 +256,8 @@ class StreamCaptureEngine( } try { if (output != null) { - val map = manager.getCameraCharacteristics(mode.camera).get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + val characteristics = manager.getCameraCharacteristics(mode.camera) + val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: error("Missing preview stream map") val size = if (mode.highSpeed) android.util.Size(mode.width, mode.height) else map.getOutputSizes(SurfaceTexture::class.java).orEmpty().filter { @@ -264,11 +265,11 @@ class StreamCaptureEngine( && it.width.toLong() * mode.height == it.height.toLong() * mode.width }.maxByOrNull { it.width * it.height } ?: error("No compatible camera preview size") output.texture.setDefaultBufferSize(size.width, size.height) - format = StreamPreviewFormat(size.width, size.height, cameraPose(mode.camera)) + format = StreamPreviewFormat(size.width, size.height, cameraPose(mode.camera), + characteristics.get(CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE) == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME) preview.update(format) } else if (withPreview) preview.update(preview.state.value.format) - val targets = listOfNotNull(input, output?.surface) - check(targets.isNotEmpty()) { "Missing encoder surface" } + 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 } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamPreview.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamPreview.kt index 2328fd94..a6dc0fb9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamPreview.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/StreamPreview.kt @@ -10,7 +10,7 @@ data class StreamPreviewCamera(val sensor: Int, val front: Boolean) { fun rotation(displayDegrees: Int): Int = (sensor + (if (front) displayDegrees else -displayDegrees) + 360) % 360 } -data class StreamPreviewFormat(val width: Int, val height: Int, val camera: StreamPreviewCamera) { +data class StreamPreviewFormat(val width: Int, val height: Int, val camera: StreamPreviewCamera, val realtimeTimestamp: Boolean = false) { fun aspect(displayDegrees: Int): Float = if (camera.rotation(displayDegrees) % 180 == 0) width.toFloat() / height else height.toFloat() / width /** Углы TextureView после поворота; размеры view уже учитывают поворот сенсора. */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt index 8db4d7ff..0f15955b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt @@ -6,6 +6,7 @@ import android.content.pm.PackageManager import android.content.Context import android.graphics.Matrix import android.graphics.SurfaceTexture +import android.os.SystemClock import android.view.Surface import android.view.TextureView import androidx.activity.compose.rememberLauncherForActivityResult @@ -389,13 +390,16 @@ private class CameraPreviewView(context: Context, private val preview: StreamPre return false // Camera session удерживает свою ссылку до onClosed/onConfigureFailed. } override fun onSurfaceTextureUpdated(texture: SurfaceTexture) { + val timestamp = texture.timestamp + if (timestamp == 0L) return // Очистка TextureView до первого кадра камеры. frames++ val now = System.nanoTime() if (frames == 1L || now - reportNs >= 1_000_000_000) { val fps = (frames - reportFrames) * 1_000_000_000.0 / (now - reportNs).coerceAtLeast(1) + val age = if (format?.realtimeTimestamp == true) (SystemClock.elapsedRealtimeNanos() - timestamp) / 1000 else null LogManager.addLog(if (frames == 1L) "INFO" else "DEBUG", "video", "direct preview frame " + "target=${target?.let(System::identityHashCode)} frames=$frames fps=${"%.1f".format(fps)} " + - "ageUs=${(now - texture.timestamp) / 1000} timestampNs=${texture.timestamp} " + + "ageUs=$age timestampNs=$timestamp realtime=${format?.realtimeTimestamp} " + "rotation=${format?.camera?.rotation(displayDegrees)}") reportNs = now; reportFrames = frames }