From 63f393c615e26bc592bc738e9370c481e8bf7fc0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 00:54:55 +0100 Subject: [PATCH] Provide video engine capability, encoder and native port test boundaries --- .../main/kotlin/com/utun/chat/VideoHostApp.kt | 10 ++++ .../com/utun/chat/data/VideoEngineHost.kt | 14 +++++ .../kotlin/com/utun/chat/data/VideoHost.kt | 55 ++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/VideoHostApp.kt create mode 100644 tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoEngineHost.kt diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/VideoHostApp.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/VideoHostApp.kt new file mode 100644 index 00000000..276aba5b --- /dev/null +++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/VideoHostApp.kt @@ -0,0 +1,10 @@ +package com.utun.chat + +import android.content.Context +import com.utun.chat.data.VideoHostConfig + +object ChatApplication { val instance = VideoHostApplication() } +class VideoHostApplication { + val configProvider = VideoHostConfig() +} +object R { object string { const val call_video_unavailable = 1; const val call_video_mode_restored = 2 } } diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoEngineHost.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoEngineHost.kt new file mode 100644 index 00000000..1d272569 --- /dev/null +++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoEngineHost.kt @@ -0,0 +1,14 @@ +package com.utun.chat.data + +import android.hardware.camera2.CameraManager + +/** Границы сохранённых настроек и списка объективов; реальные Camera2 callbacks остаются у engine. */ +class VideoHostConfig { + var failures = emptySet() + fun getCallVideoCaptureSettings(): CallVideoSettings { check("settings load" !in failures); return CallVideoSettings() } + fun getCallVideoBitrateRange(): CallVideoBitrateRange { check("range load" !in failures); return CallVideoBitrateRange() } + fun saveCallVideoCaptureSettings(settings: CallVideoSettings) { check("settings save" !in failures) } + fun saveCallVideoBitrateRange(range: CallVideoBitrateRange) { check("range save" !in failures) } +} +class VideoHostLens(val cameraId: String = "0", val label: String = "host", val mm: String = "26mm", val eqFocal: Float = 26f) +object CameraLensManager { fun enumerate(manager: CameraManager) = listOf(VideoHostLens()) } diff --git a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt index 06d56d57..b3580157 100644 --- a/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt +++ b/tools/chatgui-android/video-tests/src/main/kotlin/com/utun/chat/data/VideoHost.kt @@ -2,6 +2,8 @@ package com.utun.chat.data import android.media.MediaCodec import android.media.MediaFormat +import android.media.Image +import android.util.Size import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @@ -18,11 +20,42 @@ object LogManager { } /** Граница аппаратной пробы; сам RX и разбор uAV2 KEY компилируются из приложения. */ -internal class CallVideoCodec { companion object { +internal class CallVideoCodec(val format: CallVideoFormat, var bitrate: Int, val name: String) : AutoCloseable { + data class Packet(val pts: Long, val key: Boolean, val bytes: ByteArray) + var closed = false + private set + var failures = nextFailures + val pendingFrames get() = packets.size + val inputWaitUs = 0L; val copyUs = 0L; val queueUs = 0L + private val packets = ConcurrentLinkedQueue() + init { check("create" !in failures); created.add(this) } + fun prepare(keepRunning: () -> Boolean = { true }) { check(keepRunning() && "prepare" !in failures) } + fun submit(image: Image, pts: Long, key: Boolean, turn: Int): Boolean { + check("submit" !in failures); packets.add(Packet(pts, key, byteArrayOf(1))); return true + } + fun drain(): List { check("drain" !in failures); return buildList { while (true) add(packets.poll() ?: break) } } + fun finish() = drain() + fun setBitrate(value: Int): Boolean { if ("bitrate" in failures) return false; bitrate = value; return true } + override fun close() { check("close" !in failures); closed = true; check("close after" !in failures) } + companion object { + val created = CopyOnWriteArrayList() + @Volatile var nextFailures = emptySet() + @Volatile var onProbe: () -> Unit = {} + data class EncoderMode(val size: Size = Size(640, 480), val name: String = "host.encoder", val profile: Int = 1, + val level: Int = 31, val fps: Int = 30) + data class Encoder(val modes: List = listOf(EncoderMode())) { + val sizes get() = modes.map { it.size } + val fps get() = 30; val pixelsPerSecond get() = 640 * 480 * 30; val profile get() = 1; val level get() = 31 + } data class DecoderMode(val name: String = "host.decoder", val pipeline: Int = 0) { fun supports(format: CallVideoFormat) = format.codec == 1 } - data class Decoder(val modes: List = listOf(DecoderMode())) + data class Decoder(val modes: List = listOf(DecoderMode())) { + val size get() = Size(640, 480); val fps get() = 30; val pixelsPerSecond get() = 640 * 480 * 30 + val profiles get() = 1; val level get() = 31 + } + fun probe(keepRunning: () -> Boolean): Map { onProbe(); return if (keepRunning()) mapOf(1 to Encoder()) else emptyMap() } + fun probeDecoders(keepRunning: () -> Boolean): Map = if (keepRunning()) mapOf(1 to Decoder()) else emptyMap() fun validateKey(bytes: ByteArray, format: CallVideoFormat) { StreamVideoCodec.fromId(format.codec).unpackKey(bytes) } fun decoderFormat(format: CallVideoFormat, decoder: MediaCodec, config: ByteArray): MediaFormat = StreamVideoCodec.fromId(format.codec).decoderFormat(format.width, format.height, format.rotation, config) @@ -30,6 +63,7 @@ internal class CallVideoCodec { companion object { object NativeLib { fun callAudioClock(id: Long, now: Long, out: LongArray): Boolean { out[5] = 1; return true } + fun setChatSetting(name: String, value: String) {} } /** Native playout/encoded FIFO: только управляемые границы, без загрузки JNI/декодирования/сетевого ядра. */ @@ -46,9 +80,15 @@ internal object CallVideoNative { @Volatile var onPull: () -> Unit = {} val destroys = CopyOnWriteArrayList() val keyRequests = AtomicLong() + val ports = ConcurrentHashMap.newKeySet() + val portOperations = CopyOnWriteArrayList() + @Volatile var configured = 0L + @Volatile var onOpen: () -> Unit = {} + @Volatile var onClose: () -> Unit = {} fun reset() { frames.clear(); live.clear(); rejected.clear(); accepted.clear(); presented.clear(); destroys.clear() failures = emptySet(); onCreate = {}; onPull = {}; keyRequests.set(0) + ports.clear(); portOperations.clear(); configured = 0; onOpen = {}; onClose = {} } private fun access(operation: String) { check(operation !in failures) { "native $operation failed" } } fun playoutCreate(id: Long): Long { @@ -73,4 +113,15 @@ internal object CallVideoNative { fun decoded(id: Long, generation: Long, success: Boolean) { access("decoded"); (if (success) accepted else rejected).add(generation) } + fun rotatableCaps(bytes: ByteArray): ByteArray? { access("caps"); return bytes.copyOf() } + fun open(id: Long, caps: ByteArray): Boolean { onOpen(); access("open"); ports.add(id); portOperations.add("open:$id"); return true } + fun close(id: Long) { onClose(); access("close"); ports.remove(id); portOperations.add("close:$id") } + fun range(id: Long, minimum: Int, maximum: Int): Boolean { access("range"); return "range reject" !in failures } + fun peer(id: Long): ByteArray? = if (id in ports) ByteArray(64) else null + fun status(id: Long): LongArray? = if (id in ports) longArrayOf(1, 0, 0, 1_000_000, configured, 0, 300_000, 4_000_000, 0, 0, 1, 1) else null + fun choose(local: ByteArray, peer: ByteArray, desired: LongArray): LongArray? = desired.copyOf() + fun supported(local: ByteArray, peer: ByteArray, desired: LongArray): Boolean = true + fun format(id: Long, format: LongArray, bitrate: Int): Boolean { access("format"); configured = format[0]; return true } + fun applied(id: Long, generation: Long, request: Long, bitrate: Int) { access("applied") } + fun send(id: Long, metadata: LongArray, bytes: ByteArray): Int { access("send"); return 1 } }