Bound video thumbnails and share retriever cleanup ownership
This commit is contained in:
1 parent
d3e83c3c1a
commit
236aeae828
3 files changed
+44
-1
No files matched your search
@@ -1,6 +1,8 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@@ -11,6 +13,7 @@ internal data class VideoInfo(val width: Int, val height: Int, val rotation: Int
|
||||
val displayHeight: Int get() = if (rotation == 90 || rotation == 270) width else height
|
||||
val aspect: Float get() = if (displayHeight > 0) displayWidth.toFloat() / displayHeight else 0f
|
||||
}
|
||||
internal data class VideoFilePreview(val bitmap: Bitmap?, val durationMs: Int)
|
||||
|
||||
/** Один IO caller читает metadata внутри use; после завершения чтения допускается только повтор close.
|
||||
* Неудавшийся release удерживает retriever до SDK ACK, retry не обращается к UI/native core. */
|
||||
@@ -35,6 +38,23 @@ internal class VideoFileMetadata : AutoCloseable {
|
||||
require(width > 0 && height > 0 && rotation in listOf(0, 90, 180, 270)) { "Invalid video geometry ${width}x$height rotation=$rotation" }
|
||||
return VideoInfo(width, height, rotation)
|
||||
}
|
||||
/** IO: thumbnail не больше 640 по каждой стороне; Bitmap принадлежит caller, duration в мс.
|
||||
* API 26 сначала извлекает полный кадр и уменьшает до передачи UI; null bitmap не запрещает воспроизведение. */
|
||||
@Synchronized fun preview(path: String): VideoFilePreview {
|
||||
check(!closing) { "Metadata reader closing" }
|
||||
val current = checkNotNull(retriever) { "Metadata reader closed" }
|
||||
current.setDataSource(path)
|
||||
val duration = current.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toIntOrNull()?.coerceAtLeast(0) ?: 0
|
||||
val raw = if (Build.VERSION.SDK_INT >= 27) current.getScaledFrameAtTime(1_000_000,
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 640, 640) else current.getFrameAtTime(1_000_000)
|
||||
val image = if (raw != null && maxOf(raw.width, raw.height) > 640) {
|
||||
val scale = 640.0 / maxOf(raw.width, raw.height)
|
||||
Bitmap.createScaledBitmap(raw, (raw.width * scale).toInt().coerceAtLeast(1),
|
||||
(raw.height * scale).toInt().coerceAtLeast(1), true).also { if (it !== raw) raw.recycle() }
|
||||
} else raw
|
||||
if (image == null) LogManager.addLog("WARN", "VideoPlayer", "Video preview has no frame owner=${System.identityHashCode(this)}")
|
||||
return VideoFilePreview(image, duration)
|
||||
}
|
||||
@Synchronized override fun close() {
|
||||
closing = true
|
||||
val current = retriever ?: return
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package android.graphics
|
||||
|
||||
/** Только размеры и владение промежуточным кадром, без пикселей/драйвера. */
|
||||
class Bitmap(val width: Int, val height: Int) {
|
||||
var recycled = false
|
||||
fun recycle() { recycled = true }
|
||||
companion object {
|
||||
fun createScaledBitmap(source: Bitmap, width: Int, height: Int, filter: Boolean) = Bitmap(width, height)
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ class MediaPlayer {
|
||||
class MediaMetadataRetriever {
|
||||
companion object {
|
||||
const val METADATA_KEY_VIDEO_WIDTH = 18; const val METADATA_KEY_VIDEO_HEIGHT = 19; const val METADATA_KEY_VIDEO_ROTATION = 24
|
||||
const val METADATA_KEY_DURATION = 9; const val OPTION_CLOSEST_SYNC = 2
|
||||
val created = mutableListOf<MediaMetadataRetriever>()
|
||||
var nextFailures = emptySet<String>()
|
||||
}
|
||||
@@ -55,10 +56,22 @@ class MediaMetadataRetriever {
|
||||
var released = false
|
||||
var releases = 0
|
||||
var width = "1280"; var height = "720"; var rotation = "90"
|
||||
var lastFrame: android.graphics.Bitmap? = null
|
||||
var scaledReads = 0
|
||||
init { created.add(this) }
|
||||
fun setDataSource(path: String) { check("source" !in failures) { "Metadata source failed" } }
|
||||
fun extractMetadata(key: Int): String? = when (key) {
|
||||
METADATA_KEY_VIDEO_WIDTH -> width; METADATA_KEY_VIDEO_HEIGHT -> height; METADATA_KEY_VIDEO_ROTATION -> rotation; else -> null
|
||||
METADATA_KEY_VIDEO_WIDTH -> width; METADATA_KEY_VIDEO_HEIGHT -> height; METADATA_KEY_VIDEO_ROTATION -> rotation
|
||||
METADATA_KEY_DURATION -> "10000"; else -> null
|
||||
}
|
||||
fun getFrameAtTime(time: Long): android.graphics.Bitmap? {
|
||||
check("frame" !in failures)
|
||||
return android.graphics.Bitmap(width.toInt(), height.toInt()).also { lastFrame = it }
|
||||
}
|
||||
fun getScaledFrameAtTime(time: Long, option: Int, maxWidth: Int, maxHeight: Int): android.graphics.Bitmap? {
|
||||
scaledReads++; check("frame" !in failures)
|
||||
val scale = minOf(maxWidth.toDouble() / width.toInt(), maxHeight.toDouble() / height.toInt(), 1.0)
|
||||
return android.graphics.Bitmap((width.toInt() * scale).toInt(), (height.toInt() * scale).toInt()).also { lastFrame = it }
|
||||
}
|
||||
fun release() {
|
||||
releases++; check("release" !in failures) { "Metadata release failed" }; released = true
|
||||
|
||||
Reference in new issue
Block a user