From 607952b709d0cf932f33fcea858c66aacd26bd4e Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 01:14:27 +0200 Subject: [PATCH] Verify hardware rate changes on continuous noisy video --- .../java/com/utun/chat/data/CallVideoProbe.kt | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoProbe.kt b/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoProbe.kt index 0c7a7fdc..de6aa676 100644 --- a/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoProbe.kt +++ b/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoProbe.kt @@ -1,6 +1,10 @@ package com.utun.chat.data import android.app.Instrumentation +import android.graphics.ImageFormat +import android.media.Image +import android.media.ImageReader +import android.media.ImageWriter import android.os.Bundle /** Запускается только явно через adb am instrument; камера и реальные звонки не затрагиваются. */ @@ -26,9 +30,7 @@ class CallVideoProbe : Instrumentation() { check(codecs.isNotEmpty() && decoders.isNotEmpty()) { "No verified encoders or decoders" } for ((codec, entry) in codecs) { val f = CallVideoFormat(1, 640, 480, codec = codec, level = entry.level, profile = entry.profile) - for (rate in listOf(4_000_000, 300_000, 4_000_000)) { - CallVideoCodec(f, rate, entry.name).use { it.prepare() } - } + verifyRateChanges(f, entry.name) CallVideoCodec(f, 1_000_000, entry.name).use { encoder -> encoder.prepare(); var key: ByteArray? = null; var input = 0 val deadline = System.nanoTime() + 3_000_000_000 @@ -45,4 +47,57 @@ class CallVideoProbe : Instrumentation() { finish(1, Bundle().apply { putString("error", e.toString()) }) } } + + /** Шум требует заданную полосу: на однотонных пробах нельзя обнаружить проигнорированный bitrate. */ + private fun verifyRateChanges(format: CallVideoFormat, name: String) { + ImageReader.newInstance(format.width, format.height, ImageFormat.YUV_420_888, 2).use { reader -> + ImageWriter.newInstance(reader.surface, 1).use { writer -> + writer.dequeueInputImage().use { image -> + CallVideoCodec(format, 4_000_000, name).use { encoder -> + encoder.prepare(); var random = 1 + for ((phase, rate) in listOf(4_000_000, 300_000, 4_000_000).withIndex()) { + check(encoder.setBitrate(rate)) { "In-place bitrate unsupported: $name" } + var input = 0; var output = 0; var bytes = 0L; var prepared = false; var key = false + val base = 10_000_000L + phase * 4_000_000L + val deadline = System.nanoTime() + 15_000_000_000L + while (output < 120 && System.nanoTime() < deadline) { + if (input < 120) { + if (!prepared) { random = fillNoise(image, random); prepared = true } + if (encoder.submit(image, base + input * 1_000_000L / 30, input % 30 == 0)) { + input++; prepared = false + } + } + for (packet in encoder.drain()) { + check(packet.pts == base + output * 1_000_000L / 30) { "Lost/reordered rate-change frame" } + if (output == 0) key = packet.key + if (output >= 60) bytes += packet.bytes.size + output++ + } + Thread.sleep(2) + } + val measured = bytes * 8 / 2 + val result = "name=$name phase=$phase target=$rate measured=$measured input=$input output=$output key=$key" + android.util.Log.i("CallVideoProbe", "rate check $result") + check(input == 120 && output == 120 && key) { "Incomplete rate check: $result" } + check(measured > rate / 2 && measured < rate.toLong() * 3 / 2) { "Bitrate change ineffective: $result" } + } + } + } + } + } + } + + private fun fillNoise(image: Image, seed: Int): Int { + var random = seed + for ((index, plane) in image.planes.withIndex()) { + val width = image.width shr (if (index == 0) 0 else 1) + val height = image.height shr (if (index == 0) 0 else 1) + val buffer = plane.buffer.duplicate(); val start = buffer.position() + for (y in 0 until height) for (x in 0 until width) { + random = random * 1664525 + 1013904223 + buffer.put(start + y * plane.rowStride + x * plane.pixelStride, if (index == 0) (random ushr 24).toByte() else 128.toByte()) + } + } + return random + } }