diff --git a/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoHevcProbe.kt b/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoHevcProbe.kt index 6e0b76ac..2e0d197c 100644 --- a/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoHevcProbe.kt +++ b/tools/chatgui-android/app/src/debug/java/com/utun/chat/data/CallVideoHevcProbe.kt @@ -45,10 +45,11 @@ object CallVideoHevcProbe { check(files.isNotEmpty()) { "No remote KEY files in $directory" } for (file in files) { val bytes = file.readBytes(); val inspected = checkNotNull(CallVideoNative.inspect(2, bytes)) - val format = CallVideoFormat(1, inspected[0].toInt(), inspected[1].toInt(), codec = 2, + val fps = Regex("-fps(30|50|60)-").find(file.name)?.groupValues?.get(1)?.toInt() ?: 30 + val format = CallVideoFormat(1, inspected[0].toInt(), inspected[1].toInt(), fps = fps, codec = 2, level = inspected[2].toInt(), profile = inspected[3].toInt()) val implementation = checkNotNull(decoders.modes.firstOrNull { it.supports(format) }) - CallVideoCodec.verifyDecode(format, List(FRAMES) { CallVideoCodec.Packet(1_000_000L + it * 33333, true, bytes) }, + CallVideoCodec.verifyDecode(format, List(FRAMES) { CallVideoCodec.Packet(1_000_000L + it * 1_000_000L / fps, true, bytes) }, implementation.name) report("remote PASSED file=${file.name} format=$format decoder=${implementation.name}") } @@ -63,7 +64,7 @@ object CallVideoHevcProbe { report("production encoders=$modes decoders=$receiving") for (mode in modes) for (portrait in listOf(false, true)) { val format = CallVideoFormat(1, if (portrait) mode.size.height else mode.size.width, - if (portrait) mode.size.width else mode.size.height, codec = 2, level = mode.level, profile = mode.profile) + if (portrait) mode.size.width else mode.size.height, fps = mode.fps, codec = 2, level = mode.level, profile = mode.profile) val implementation = checkNotNull(receiving.modes.firstOrNull { it.supports(format) }) CallVideoCodec(format, 1_000_000, mode.name).use { codec -> codec.prepare() @@ -74,13 +75,13 @@ object CallVideoHevcProbe { val base = 2_000_000L + phase * 1_000_000L val deadline = System.nanoTime() + 5_000_000_000L while (packets.size < FRAMES && System.nanoTime() < deadline) { - if (input < FRAMES && codec.submit(null, base + input * 33333, input == 0)) ++input + if (input < FRAMES && codec.submit(null, base + input * 1_000_000L / format.fps, input == 0)) ++input packets += codec.drain(); Thread.sleep(2) } check(packets.size == FRAMES && packets.first().key) { "Incomplete production encode" } CallVideoCodec.verifyDecode(codec.format, packets, implementation.name) report("production PASSED format=${codec.format} rate=$rate frames=${packets.size} decoder=${implementation.name}") - File(directory, "production-${format.width}x${format.height}-$phase.key").writeBytes(packets.first().bytes) + File(directory, "production-${format.width}x${format.height}-fps${format.fps}-$phase.key").writeBytes(packets.first().bytes) } } } diff --git a/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallVideoCadenceTest.kt b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallVideoCadenceTest.kt new file mode 100644 index 00000000..b53cc9fc --- /dev/null +++ b/tools/chatgui-android/app/src/test/java/com/utun/chat/data/CallVideoCadenceTest.kt @@ -0,0 +1,31 @@ +package com.utun.chat.data + +import org.junit.Assert.* +import org.junit.Test + +class CallVideoCadenceTest { + @Test fun fasterCameraPreservesSelectedOutputRate() { + for (source in listOf(50, 60, 100, 330)) for (fps in listOf(30, 50, 60).filter { it <= source }) { + val cadence = CallVideoCadence(fps) + val accepted = (0 until source * 10).count { cadence.accept(1_000_000L + it * 1_000_000L / source) } + assertTrue("source=$source output=$fps accepted=$accepted", kotlin.math.abs(accepted - fps * 10) <= 1) + } + } + @Test fun jitterAndPauseDoNotCauseHalvingOrBurst() { + for (fps in listOf(30, 50, 60)) { + val cadence = CallVideoCadence(fps) + repeat(300) { assertTrue(cadence.accept(1_000_000L + it * 1_000_000L / fps + if (it % 2 == 0) 700 else -700)) } + assertTrue(cadence.accept(20_000_000)); assertFalse(cadence.accept(20_000_001)) + assertFalse(cadence.accept(20_000_000)); assertFalse(cadence.accept(Long.MAX_VALUE)) + } + } + @Test fun advertisedModesKeepSizeAndRateTogether() { + val low = CallVideoMode(1280, 720, 60); val high = CallVideoMode(1920, 1080, 30) + val state = CallVideoState(cameras = listOf(CallVideoCamera("c", "camera", listOf(low, high), 90, false)), + codecModes = mapOf(1 to listOf(high), 2 to listOf(low))) + assertEquals(listOf(low, high), state.modes("c", 0)) + assertEquals(listOf(high), state.modes("c", 1)) + assertEquals(listOf(low), state.modes("c", 2)) + assertFalse(CallVideoMode(1920, 1080, 60) in state.modes("c", 0)) + } +}