Keep codec cleanup diagnostics independent of released SDK handles

This commit is contained in:
evgeny committed 2026-10-29 13:34:24 +01:00
1 parent b519536237
commit d9959e5176
2 files changed
+38 -38

No files matched your search

@@ -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
@@ -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<VideoCodecResources>() }
private val handler = Handler(Looper.getMainLooper())
private val images = linkedSetOf<Image>()
@@ -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)
}
}