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 43cd5a1d..92c1244e 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 @@ -20,7 +20,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name private set data class Packet(val pts: Long, val key: Boolean, val bytes: ByteArray) private val type = StreamVideoCodec.fromId(format.codec) - private val resources = VideoCodecResources(MediaCodec.createByCodecName(name)) + private val resources = VideoCodecResources(MediaCodec.createByCodecName(name), name) private val codec = resources.codec private var config = byteArrayOf() private val partial = java.io.ByteArrayOutputStream() @@ -73,36 +73,36 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name val acquired: Long val copied: Long try { - check(input.width >= format.width && input.height >= format.height) - val crop = image?.cropRect - if (crop != null) { - check(crop.left % 2 == 0 && crop.top % 2 == 0) - check((if (rotation % 180 == 0) crop.width() else crop.height()) == format.width - && (if (rotation % 180 == 0) crop.height() else crop.width()) == format.height) { "YUV input geometry mismatch" } - } - acquired = System.nanoTime() - for (plane in 0..2) { - val w = format.width shr (if (plane == 0) 0 else 1) - val h = format.height shr (if (plane == 0) 0 else 1) - val dst = input.planes[plane]; val src = image?.planes?.get(plane) - if (src != null && crop != null) { - val shift = if (plane == 0) 0 else 1 - val source = src.buffer.duplicate().apply { - position(position() + (crop.top shr shift) * src.rowStride + (crop.left shr shift) * src.pixelStride) - }.slice() - check(CallVideoNative.rotatePlane(source, src.rowStride, src.pixelStride, dst.buffer.slice(), dst.rowStride, - dst.pixelStride, crop.width() shr shift, crop.height() shr shift, rotation)) { "YUV plane rotation failed" } - continue + check(input.width >= format.width && input.height >= format.height) + val crop = image?.cropRect + if (crop != null) { + check(crop.left % 2 == 0 && crop.top % 2 == 0) + check((if (rotation % 180 == 0) crop.width() else crop.height()) == format.width + && (if (rotation % 180 == 0) crop.height() else crop.width()) == format.height) { "YUV input geometry mismatch" } } - val row = ByteArray(w) - for (y in 0 until h) { - row.fill(if (plane == 0) 64 else 128.toByte()) - val buffer = dst.buffer.duplicate(); val start = buffer.position() + y * dst.rowStride - if (dst.pixelStride == 1) { buffer.position(start); buffer.put(row) } - else for (x in 0 until w) buffer.put(start + x * dst.pixelStride, row[x]) + acquired = System.nanoTime() + for (plane in 0..2) { + val w = format.width shr (if (plane == 0) 0 else 1) + val h = format.height shr (if (plane == 0) 0 else 1) + val dst = input.planes[plane]; val src = image?.planes?.get(plane) + if (src != null && crop != null) { + val shift = if (plane == 0) 0 else 1 + val source = src.buffer.duplicate().apply { + position(position() + (crop.top shr shift) * src.rowStride + (crop.left shr shift) * src.pixelStride) + }.slice() + check(CallVideoNative.rotatePlane(source, src.rowStride, src.pixelStride, dst.buffer.slice(), dst.rowStride, + dst.pixelStride, crop.width() shr shift, crop.height() shr shift, rotation)) { "YUV plane rotation failed" } + continue + } + val row = ByteArray(w) + for (y in 0 until h) { + row.fill(if (plane == 0) 64 else 128.toByte()) + val buffer = dst.buffer.duplicate(); val start = buffer.position() + y * dst.rowStride + if (dst.pixelStride == 1) { buffer.position(start); buffer.put(row) } + else for (x in 0 until w) buffer.put(start + x * dst.pixelStride, row[x]) + } } - } - copied = System.nanoTime() + copied = System.nanoTime() } catch (e: Exception) { LogManager.addLog("ERROR", "call", "video YUV input failed name=$name pts=$pts rotation=$rotation: $e") try { resources.closeImage(input) } catch (cleanup: Exception) { e.addSuppressed(cleanup) } @@ -399,7 +399,7 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name keepRunning: () -> Boolean = { true }): Int { val type = StreamVideoCodec.fromId(f.codec) val config = type.unpackKey(packets.first().bytes).first - return VideoCodecResources(MediaCodec.createByCodecName(name)).use { resources -> + return VideoCodecResources(MediaCodec.createByCodecName(name), name).use { resources -> val decoder = resources.codec decoder.configure(decoderFormat(f.copy(rotation = 0), decoder, config), null, null, 0); decoder.start() var sent = 0; var decoded = 0; var pipeline = 0 diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoCodecResources.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoCodecResources.kt index 78651404..220d11a8 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoCodecResources.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/VideoCodecResources.kt @@ -10,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap * Ошибка close сохраняет владельца независимо от вызывающего кода, включая неудавшийся конструктор. * Retry использует только Android-ресурсы и не обращается к остановленному native-ядру. */ -internal class VideoCodecResources(val codec: MediaCodec) : AutoCloseable { +internal class VideoCodecResources(val codec: MediaCodec, private val name: String) : AutoCloseable { companion object { private val retained = ConcurrentHashMap.newKeySet() } private val handler = Handler(Looper.getMainLooper()) private val images = linkedSetOf() @@ -29,7 +29,7 @@ internal class VideoCodecResources(val codec: MediaCodec) : AutoCloseable { @Synchronized fun closeImage(image: Image) { if (image !in images) return try { image.close(); images.remove(image) } catch (e: Exception) { - LogManager.addLog("ERROR", "video", "codec input Image close failed name=${codec.name}; retained: $e") + LogManager.addLog("ERROR", "video", "codec input Image close failed name=$name; retained: $e") throw e } } @@ -39,24 +39,24 @@ internal class VideoCodecResources(val codec: MediaCodec) : AutoCloseable { for (image in images.toList()) try { closeImage(image) } catch (e: Exception) { failure = e } if (!released) { if (!stopped) try { codec.stop(); stopped = true } catch (e: Exception) { - LogManager.addLog("WARN", "video", "codec stop failed name=${codec.name}; attempting release: $e") + LogManager.addLog("WARN", "video", "codec stop failed name=$name; attempting release: $e") } try { codec.release(); released = true } catch (e: Exception) { failure = e - LogManager.addLog("ERROR", "video", "codec release failed name=${codec.name}; retained: $e") + LogManager.addLog("ERROR", "video", "codec release failed name=$name; retained: $e") } } if (released && images.isEmpty()) { handler.removeCallbacks(retry); scheduled = false - if (retained.remove(this)) LogManager.addLog("INFO", "video", "codec cleanup confirmed name=${codec.name}") + if (retained.remove(this)) LogManager.addLog("INFO", "video", "codec cleanup confirmed name=$name") return } retained.add(this) if (!scheduled) { scheduled = handler.postDelayed(retry, delay) - if (!scheduled) LogManager.addLog("ERROR", "video", "codec cleanup retry rejected name=${codec.name}; owner retained") - else { LogManager.addLog("DEBUG", "video", "codec cleanup scheduled name=${codec.name} delayMs=$delay"); delay = (delay * 2).coerceAtMost(30_000) } + if (!scheduled) LogManager.addLog("ERROR", "video", "codec cleanup retry rejected name=$name; owner retained") + else { LogManager.addLog("DEBUG", "video", "codec cleanup scheduled name=$name delayMs=$delay"); delay = (delay * 2).coerceAtMost(30_000) } } - throw IllegalStateException("Codec cleanup pending name=${codec.name} released=$released images=${images.size}", failure) + throw IllegalStateException("Codec cleanup pending name=$name released=$released images=${images.size}", failure) } }