|
|
|
@ -1,6 +1,10 @@ |
|
|
|
package com.utun.chat.data |
|
|
|
package com.utun.chat.data |
|
|
|
|
|
|
|
|
|
|
|
import android.app.Instrumentation |
|
|
|
import android.app.Instrumentation |
|
|
|
|
|
|
|
import android.graphics.ImageFormat |
|
|
|
|
|
|
|
import android.media.Image |
|
|
|
|
|
|
|
import android.media.ImageReader |
|
|
|
|
|
|
|
import android.media.ImageWriter |
|
|
|
import android.os.Bundle |
|
|
|
import android.os.Bundle |
|
|
|
|
|
|
|
|
|
|
|
/** Запускается только явно через adb am instrument; камера и реальные звонки не затрагиваются. */ |
|
|
|
/** Запускается только явно через adb am instrument; камера и реальные звонки не затрагиваются. */ |
|
|
|
@ -26,9 +30,7 @@ class CallVideoProbe : Instrumentation() { |
|
|
|
check(codecs.isNotEmpty() && decoders.isNotEmpty()) { "No verified encoders or decoders" } |
|
|
|
check(codecs.isNotEmpty() && decoders.isNotEmpty()) { "No verified encoders or decoders" } |
|
|
|
for ((codec, entry) in codecs) { |
|
|
|
for ((codec, entry) in codecs) { |
|
|
|
val f = CallVideoFormat(1, 640, 480, codec = codec, level = entry.level, profile = entry.profile) |
|
|
|
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)) { |
|
|
|
verifyRateChanges(f, entry.name) |
|
|
|
CallVideoCodec(f, rate, entry.name).use { it.prepare() } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
CallVideoCodec(f, 1_000_000, entry.name).use { encoder -> |
|
|
|
CallVideoCodec(f, 1_000_000, entry.name).use { encoder -> |
|
|
|
encoder.prepare(); var key: ByteArray? = null; var input = 0 |
|
|
|
encoder.prepare(); var key: ByteArray? = null; var input = 0 |
|
|
|
val deadline = System.nanoTime() + 3_000_000_000 |
|
|
|
val deadline = System.nanoTime() + 3_000_000_000 |
|
|
|
@ -45,4 +47,57 @@ class CallVideoProbe : Instrumentation() { |
|
|
|
finish(1, Bundle().apply { putString("error", e.toString()) }) |
|
|
|
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 |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|