Own recorded video track inspection until extractor release succeeds

This commit is contained in:
evgeny committed 2026-10-29 13:34:26 +01:00
1 parent 8a1c77793d
commit fca68080bc
3 files changed
+74

No files matched your search

@@ -0,0 +1,56 @@
package com.utun.chat.data
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Handler
import android.os.Looper
import java.util.concurrent.ConcurrentHashMap
/** IO caller проверяет дорожки готового файла внутри use; extractor сохраняется до успешного release.
* Ошибка чтения не маскируется cleanup; повтор close работает также после отказа main-таймера. */
internal class VideoFileTracks : AutoCloseable {
companion object { private val retained = ConcurrentHashMap.newKeySet<VideoFileTracks>() }
private var extractor: MediaExtractor? = MediaExtractor()
private var closing = false
private val main = Handler(Looper.getMainLooper())
private var delayMs = 250L
private var scheduled = false
private val retry = Runnable {
synchronized(this) { scheduled = false }
try { close() } catch (e: Exception) { LogManager.addLog("WARN", "Capture", "Track reader cleanup pending: $e") }
}
@Synchronized fun verify(path: String, expectedMime: String): String {
check(!closing) { "Track reader closing" }
val current = checkNotNull(extractor) { "Track reader closed" }
current.setDataSource(path)
val video = (0 until current.trackCount).map(current::getTrackFormat)
.firstOrNull { it.getString(MediaFormat.KEY_MIME)?.startsWith("video/") == true }
checkNotNull(video) { "Recorded file has no video track" }
val mime = video.getString(MediaFormat.KEY_MIME)
check(mime == expectedMime) { "Recorded codec mismatch expected=$expectedMime actual=$mime" }
val rotation = if (video.containsKey(MediaFormat.KEY_ROTATION)) video.getInteger(MediaFormat.KEY_ROTATION) else 0
val width = video.getInteger(MediaFormat.KEY_WIDTH); val height = video.getInteger(MediaFormat.KEY_HEIGHT)
check(width > 0 && height > 0 && rotation in listOf(0, 90, 180, 270)) { "Invalid recorded geometry ${width}x$height rotation=$rotation" }
return "$mime ${width}x${height} rotation=$rotation"
}
@Synchronized override fun close() {
closing = true
val current = extractor ?: return
try {
current.release(); extractor = null
main.removeCallbacks(retry); scheduled = false; retained.remove(this)
LogManager.addLog("DEBUG", "Capture", "Track reader released owner=${System.identityHashCode(this)}")
} catch (e: Exception) {
retained.add(this)
LogManager.addLog("ERROR", "Capture", "Track reader release failed; owner retained: $e")
if (!scheduled) {
scheduled = main.postDelayed(retry, delayMs)
if (scheduled) delayMs = (delayMs * 2).coerceAtMost(30_000)
else LogManager.addLog("ERROR", "Capture", "Track reader cleanup timer rejected; owner retained")
}
throw e
}
}
}
@@ -1,5 +1,19 @@
package android.media
class MediaExtractor {
companion object { val created = mutableListOf<MediaExtractor>(); var nextFailures = emptySet<String>() }
var failures = nextFailures
var released = false
var formats = listOf(MediaFormat().apply {
values[MediaFormat.KEY_MIME] = "video/avc"; values[MediaFormat.KEY_WIDTH] = 1280; values[MediaFormat.KEY_HEIGHT] = 720
})
init { created.add(this) }
val trackCount: Int get() = formats.size
fun setDataSource(path: String) { check("source" !in failures) { "Track source rejected" } }
fun getTrackFormat(index: Int) = formats[index]
fun release() { check("release" !in failures) { "Track release rejected" }; released = true }
}
import android.view.Surface
/** Только управляемая граница MediaPlayer: callbacks и отказы SDK, без декодирования. */
@@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
/** Минимальный формат для настоящего StreamVideoCodec; данные сохраняются для проверки configure. */
class MediaFormat {
companion object {
const val KEY_MIME = "mime"; const val KEY_WIDTH = "width"; const val KEY_HEIGHT = "height"
const val KEY_ROTATION = "rotation"; const val KEY_MAX_INPUT_SIZE = "max-input-size"
const val KEY_LOW_LATENCY = "low-latency"
const val KEY_COLOR_FORMAT = "color"; const val KEY_PROFILE = "profile"; const val KEY_BIT_RATE = "bitrate"
@@ -18,6 +19,9 @@ class MediaFormat {
fun setInteger(key: String, value: Int) { values[key] = value }
fun setByteBuffer(key: String, value: ByteBuffer) { values[key] = value }
fun getByteBuffer(key: String) = values[key] as? ByteBuffer
fun containsKey(key: String) = values.containsKey(key)
fun getString(key: String) = values[key] as? String
fun getInteger(key: String) = values[key] as Int
}
class MediaCodecInfo(val name: String = "host.decoder") {
val isEncoder = false