Cancel Android codec verification when the call stops

This commit is contained in:
evgeny committed 2026-10-20 00:24:28 +02:00
1 parent 521dce5528
commit 3a7aab9186
2 files changed
+25 -13

No files matched your search

@@ -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<Int, Encoder> {
fun probe(keepRunning: () -> Boolean = { true }): Map<Int, Encoder> {
val result = mutableMapOf<Int, Encoder>()
for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) {
val modes = mutableListOf<EncoderMode>()
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<Int, Decoder> {
fun probeDecoders(keepRunning: () -> Boolean = { true }): Map<Int, Decoder> {
val result = mutableMapOf<Int, Decoder>()
for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) {
val modes = mutableListOf<DecoderMode>()
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<Packet>, name: String, paced: Boolean = false): Int {
internal fun verifyDecode(f: CallVideoFormat, packets: List<Packet>, 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() }
@@ -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) {