Own recorded video track inspection until extractor release succeeds
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user