Provide video engine capability, encoder and native port test boundaries

This commit is contained in:
evgeny committed 2026-10-29 13:34:24 +01:00
1 parent 3a4e1c6a7c
commit 63f393c615
3 files changed
+77 -2

No files matched your search

@@ -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 } }
@@ -0,0 +1,14 @@
package com.utun.chat.data
import android.hardware.camera2.CameraManager
/** Границы сохранённых настроек и списка объективов; реальные Camera2 callbacks остаются у engine. */
class VideoHostConfig {
var failures = emptySet<String>()
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()) }
@@ -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<Packet>()
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<Packet> { 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<CallVideoCodec>()
@Volatile var nextFailures = emptySet<String>()
@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<EncoderMode> = 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<DecoderMode> = listOf(DecoderMode()))
data class Decoder(val modes: List<DecoderMode> = 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<Int, Encoder> { onProbe(); return if (keepRunning()) mapOf(1 to Encoder()) else emptyMap() }
fun probeDecoders(keepRunning: () -> Boolean): Map<Int, Decoder> = 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<Long>()
val keyRequests = AtomicLong()
val ports = ConcurrentHashMap.newKeySet<Long>()
val portOperations = CopyOnWriteArrayList<String>()
@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 }
}