From 7be4b8884cd814a46a15e490e95e8b455f52ad5a Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 23:36:58 +0200 Subject: [PATCH] Verify production HEVC modes and cross-platform reception --- .../com/utun/chat/data/CallVideoHevcProbe.kt | 38 ++++++++++++++++++- .../java/com/utun/chat/data/CallVideoCodec.kt | 17 ++++----- .../chatgui/tests/test_call_video_encoder.cpp | 2 +- 3 files changed, 45 insertions(+), 12 deletions(-) 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 7d46e39b..b7be4bf3 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 @@ -25,7 +25,9 @@ object CallVideoHevcProbe { } @JvmStatic fun main(args: Array) { - require(args.size in 1..2 && (args.size == 1 || args[1] == "parameters")) { "Expected output directory [parameters]" } + require(args.size in 1..2 && (args.size == 1 || args[1] in listOf("parameters", "call"))) { + "Expected output directory [parameters|call]" + } check(Build.VERSION.SDK_INT >= 29) { "Hardware codec enumeration requires API 29" } val directory = File(args[0]); check(directory.isDirectory || directory.mkdirs()) val journal = File(directory, "results.log") @@ -37,7 +39,39 @@ object CallVideoHevcProbe { !it.isEncoder && it.isHardwareAccelerated && "video/hevc" in it.supportedTypes } report("start encoder=${encoder.name} decoder=${decoder.name} frames=$FRAMES") - if (args.size == 2) { + if (args.getOrNull(1) == "call") { + val encoders = CallVideoCodec.probe() + val decoders = CallVideoCodec.probeDecoders() + val modes = checkNotNull(encoders[2]) { "No verified HEVC encoder" }.modes + val receiving = checkNotNull(decoders[2]) { "No verified HEVC decoder" } + 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) + val implementation = checkNotNull(receiving.modes.firstOrNull { it.supports(format) }) + CallVideoCodec(format, 1_000_000, mode.name).use { codec -> + codec.prepare() + check(codec.format == format) { "Production mode differs from probe: ${codec.format}/$format" } + for ((phase, rate) in listOf(1_000_000, 4_000_000, 300_000, 4_000_000).withIndex()) { + check(codec.setBitrate(rate)) { "Bitrate update refused $rate" } + val packets = mutableListOf(); var input = 0 + 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 + 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) + } + } + } + report("production finished modes=${modes.size}") + return + } + if (args.getOrNull(1) == "parameters") { check(Build.VERSION.SDK_INT >= 31) { "Vendor parameter enumeration requires API 31" } val codec = MediaCodec.createByCodecName(encoder.name) try { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoCodec.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoCodec.kt index fba5abc9..9e9637f5 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoCodec.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoCodec.kt @@ -43,7 +43,8 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name else MediaCodecInfo.CodecProfileLevel.AVCProfileConstrainedBaseline } else MediaCodecInfo.CodecProfileLevel.HEVCProfileMain) - setInteger(MediaFormat.KEY_LEVEL, platformLevel(format.codec, format.level, format.profile)) + // Запрос Main tier одинаков в probe/live; Qualcomm может выдать High, фактический режим проверяет prepare(). + setInteger(MediaFormat.KEY_LEVEL, platformLevel(format.codec, format.level)) setInteger(MediaFormat.KEY_BIT_RATE, bitrate); setInteger(MediaFormat.KEY_FRAME_RATE, format.fps) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) @@ -192,14 +193,11 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name } companion object { - fun platformLevel(codec: Int, level: Int, profile: Int = 1): Int = if (codec == 1) when (level) { + fun platformLevel(codec: Int, level: Int): Int = if (codec == 1) when (level) { 30 -> MediaCodecInfo.CodecProfileLevel.AVCLevel3 31 -> MediaCodecInfo.CodecProfileLevel.AVCLevel31 40 -> MediaCodecInfo.CodecProfileLevel.AVCLevel4 else -> error("Unsupported AVC level $level") - } else if (profile == 2) { - check(level == 120) { "Unsupported HEVC High tier level $level" } - MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel4 } else when (level) { 90 -> MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel3 93 -> MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel31 @@ -291,7 +289,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name if (caps.videoCapabilities?.areSizeAndRateSupported(size.width, size.height, 30.0) != true) continue var profiles = 0; var pipeline = 0; var maximumLevel = 0 for (profile in listOf(1, 2)) { - try { + try { val level = if (type.id == 1) { if (size.width > 1280) 40 else 31 } else if (size.width > 1280 || profile == 2) 120 else 93 var profilePipeline = 0 @@ -306,9 +304,10 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name } profiles = profiles or profile; maximumLevel = maxOf(maximumLevel, level) pipeline = maxOf(pipeline, profilePipeline) - } catch (e: Exception) { - LogManager.addLog("WARN", "call", "video decoder profile probe rejected name=${entry.name} codec=${type.id} profile=$profile size=$size: ${e.message}") - } + } catch (e: Exception) { + LogManager.addLog("WARN", "call", "video decoder profile probe rejected name=${entry.name} " + + "codec=${type.id} profile=$profile size=$size: ${e.message}") + } } check(profiles != 0) { "No verified decoder profiles" } modes += DecoderMode(size, entry.name, profiles, maximumLevel, pipeline) diff --git a/tools/chatgui/tests/test_call_video_encoder.cpp b/tools/chatgui/tests/test_call_video_encoder.cpp index 0baec2dc..0302f2fa 100644 --- a/tools/chatgui/tests/test_call_video_encoder.cpp +++ b/tools/chatgui/tests/test_call_video_encoder.cpp @@ -128,7 +128,7 @@ private slots: } void independentDecoders() { auto caps = CallVideoDecoder::capabilities(); - QCOMPARE(caps.decode[0].profiles, uint8_t(3)); QCOMPARE(caps.decode[1].profiles, uint8_t(1)); + QCOMPARE(caps.decode[0].profiles, uint8_t(3)); QCOMPARE(caps.decode[1].profiles, uint8_t(3)); for (const auto& limit : caps.decode) { QCOMPARE(limit.width, uint16_t(1920)); QCOMPARE(limit.height, uint16_t(1920)); QCOMPARE(limit.pixels_per_second, uint32_t(1920 * 1080 * 30));