From 3a7aab918684a6074d3f404bb6b892f6d5e33ce4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Oct 2026 00:24:28 +0200 Subject: [PATCH] Cancel Android codec verification when the call stops --- .../java/com/utun/chat/data/CallVideoCodec.kt | 24 ++++++++++++------- .../com/utun/chat/data/CallVideoEngine.kt | 14 +++++++---- 2 files changed, 25 insertions(+), 13 deletions(-) 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 e1abee9b..2b209125 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 @@ -177,16 +177,17 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name codec.release() } /** Проверяем фактический режим до публикации; пробные кадры не уходят в звонок. */ - fun prepare() { + fun prepare(keepRunning: () -> Boolean = { true }) { preparing = true try { var input = 0; var output = 0; var key = false val deadline = System.nanoTime() + 5_000_000_000 - while (output < 8 && System.nanoTime() < deadline) { + while (output < 8 && System.nanoTime() < deadline && keepRunning()) { if (input < 8 && submit(null, 1_000_000L + input * 1_000_000L / format.fps, input == 0)) input++ for (packet in drain()) { if (output == 0) key = packet.key; output++ } Thread.sleep(2) } + check(keepRunning()) { "Encoder probe cancelled" } check(input == 8 && output == 8 && key) { "Encoder probe incomplete inputs=$input outputs=$output IDR=$key" } LogManager.addLog("INFO", "call", "video encoder verified name=$name actual=$format bitrate=$bitrate") } finally { preparing = false } @@ -244,12 +245,13 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name return media } /** Encode и decode независимы: ошибка собственного encoder не запрещает приём. */ - fun probe(): Map { + fun probe(keepRunning: () -> Boolean = { true }): Map { val result = mutableMapOf() for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) { val modes = mutableListOf() for (size in listOf(Size(640, 480), Size(1280, 720), Size(1920, 1080))) { for (fps in listOf(30, 50, 60)) { + if (!keepRunning()) return emptyMap() for (entry in candidates(true)) { if (entry.supportedTypes.none { it == type.mime }) continue try { @@ -265,9 +267,10 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name profile = if (type.id == 1) 2 else 1) var verified: CallVideoFormat? = null for (level in (if (type.id == 2) listOf(f.level, maxOf(f.level, 120)) else listOf(f.level)).distinct()) { + if (!keepRunning()) return emptyMap() try { CallVideoCodec(f.copy(level = level), 1_000_000, entry.name).use { encoder -> - encoder.prepare(); verified = encoder.format + encoder.prepare(keepRunning); verified = encoder.format } break } catch (e: Exception) { @@ -288,12 +291,13 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name } return result } - fun probeDecoders(): Map { + fun probeDecoders(keepRunning: () -> Boolean = { true }): Map { val result = mutableMapOf() for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) { val modes = mutableListOf() for (size in listOf(Size(1280, 720), Size(1920, 1080))) { for (fps in listOf(30, 50, 60)) { + if (!keepRunning()) return emptyMap() for (entry in candidates(false)) { if (entry.supportedTypes.none { it == type.mime }) continue try { @@ -301,6 +305,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name if (caps.videoCapabilities?.areSizeAndRateSupported(size.width, size.height, fps.toDouble()) != true) continue var profiles = 0; var pipeline = 0; var maximumLevel = 0 for (profile in listOf(1, 2)) { + if (!keepRunning()) return emptyMap() try { val level = if (type.id == 1) { if (size.width > 1280) { if (fps > 30) 42 else 40 } else { if (fps > 30) 32 else 31 } } else if (size.width > 1280) { if (fps > 30) 123 else 120 } else if (fps > 30 || profile == 2) 120 else 93 @@ -314,7 +319,8 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name val key = checkNotNull(CallVideoNative.probeKey(type.id, profile, w, h, if (fps > 30) 60 else 30)) validateKey(key, f) profilePipeline = maxOf(profilePipeline, - verifyDecode(f, List(8) { Packet(1_000_000L + it * 1_000_000L / fps, true, key) }, entry.name, true)) + verifyDecode(f, List(8) { Packet(1_000_000L + it * 1_000_000L / fps, true, key) }, + entry.name, true, keepRunning)) } profiles = profiles or profile; maximumLevel = maxOf(maximumLevel, level) pipeline = maxOf(pipeline, profilePipeline) @@ -335,7 +341,8 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name } return result } - internal fun verifyDecode(f: CallVideoFormat, packets: List, name: String, paced: Boolean = false): Int { + internal fun verifyDecode(f: CallVideoFormat, packets: List, name: String, paced: Boolean = false, + keepRunning: () -> Boolean = { true }): Int { val type = StreamVideoCodec.fromId(f.codec) val config = type.unpackKey(packets.first().bytes).first val decoder = MediaCodec.createByCodecName(name) @@ -343,7 +350,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name decoder.configure(decoderFormat(f.copy(rotation = 0), decoder, config), null, null, 0); decoder.start() var sent = 0; var decoded = 0; var pipeline = 0 val info = MediaCodec.BufferInfo(); val started = System.nanoTime(); val deadline = started + 2_000_000_000 - while (decoded < packets.size && System.nanoTime() < deadline) { + while (decoded < packets.size && System.nanoTime() < deadline && keepRunning()) { if (sent < packets.size && (!paced || System.nanoTime() - started >= sent * 1_000_000_000L / f.fps)) { val i = decoder.dequeueInputBuffer(0) if (i >= 0) { @@ -360,6 +367,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name decoded++; decoder.releaseOutputBuffer(out, false) } } + check(keepRunning()) { "Decoder probe cancelled" } check(decoded == packets.size) { "Decoder did not return all probe frames $decoded/${packets.size}" } decoder.stop(); return pipeline } finally { decoder.release() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoEngine.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoEngine.kt index 79048b06..508f0844 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoEngine.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoEngine.kt @@ -134,10 +134,11 @@ class CallVideoEngine(private val context: Context, private val id: Long) { } handler.post { try { - val decoders = CallVideoCodec.probeDecoders() + val decoders = CallVideoCodec.probeDecoders { !stopped } + if (stopped) { LogManager.addLog("INFO", "call", "video decoder preparation cancelled id=$id"); return@post } player.decoders(decoders) - encoders = CallVideoCodec.probe() - if (stopped) return@post + encoders = CallVideoCodec.probe { !stopped } + if (stopped) { LogManager.addLog("INFO", "call", "video encoder preparation cancelled id=$id"); return@post } val cameras = CameraLensManager.enumerate(manager).mapNotNull { lens -> val c = manager.getCameraCharacteristics(lens.cameraId) val map = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: run { @@ -184,7 +185,10 @@ class CallVideoEngine(private val context: Context, private val id: Long) { codecModes = encoders.mapValues { entry -> entry.value.modes.map { CallVideoMode(it.size.width, it.size.height, it.fps) } }) report(if (encoders.isEmpty()) "Receive-only video" else "Video ready") tick() - } catch (e: Exception) { fail("Video setup: ${e.message}") } + } catch (e: Exception) { + if (stopped) LogManager.addLog("INFO", "call", "video setup cancelled id=$id: ${e.message}") + else fail("Video setup: ${e.message}") + } } } @@ -286,7 +290,7 @@ class CallVideoEngine(private val context: Context, private val id: Long) { val verified = CallVideoFormat.from(selected).let { it.copy(level = maxOf(it.level, implementation.level)) } check(CallVideoNative.supported(caps, peer, verified.array())) { "Verified encoder level exceeds peer capabilities" } candidate = CallVideoCodec(verified, bitrate, implementation.name) - candidate.prepare() + candidate.prepare { !stopped } check(CallVideoNative.supported(caps, peer, candidate.format.array())) { "Actual encoder format exceeds peer capabilities" } return candidate } catch (e: Exception) {