improved memory usage and check for video thumbnail decoding
This commit is contained in:
parent
99cd9faec7
commit
a3dcf41786
3 changed files with 82 additions and 7 deletions
|
@ -61,7 +61,7 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
|
|||
|
||||
val bitmapWidth: Int
|
||||
val bitmapHeight: Int
|
||||
if (width / height > svgWidth / svgHeight) {
|
||||
if (width / height.toFloat() > svgWidth / svgHeight) {
|
||||
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
|
||||
bitmapHeight = height
|
||||
} else {
|
||||
|
|
|
@ -63,7 +63,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
|
|||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||
val imageWidth = options.outWidth
|
||||
val imageHeight = options.outHeight
|
||||
if (imageHeight > height || imageWidth > width) {
|
||||
if (imageWidth > width || imageHeight > height) {
|
||||
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
|
||||
sampleSize *= 2
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package deckers.thibault.aves.decoder
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Priority
|
||||
import com.bumptech.glide.Registry
|
||||
|
@ -16,7 +19,9 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
|||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import com.bumptech.glide.module.LibraryGlideModule
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -24,6 +29,8 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@GlideModule
|
||||
class VideoThumbnailGlideModule : LibraryGlideModule() {
|
||||
|
@ -36,7 +43,7 @@ class VideoThumbnail(val context: Context, val uri: Uri)
|
|||
|
||||
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
||||
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
|
||||
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model, width, height))
|
||||
}
|
||||
|
||||
override fun handles(model: VideoThumbnail): Boolean = true
|
||||
|
@ -48,7 +55,7 @@ internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
|||
}
|
||||
}
|
||||
|
||||
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> {
|
||||
internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher<InputStream> {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||
|
@ -68,10 +75,62 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
|||
if (durationMillis != null) {
|
||||
timeMillis = if (durationMillis < 15000) 0 else 15000
|
||||
}
|
||||
val frame = if (timeMillis != null) {
|
||||
retriever.getFrameAtTime(timeMillis * 1000)
|
||||
val timeMicros = if (timeMillis != null) timeMillis * 1000 else -1
|
||||
val option = MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||
|
||||
var videoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull()
|
||||
var videoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull()
|
||||
if (videoWidth == null || videoHeight == null) {
|
||||
throw Exception("failed to get video dimensions")
|
||||
}
|
||||
|
||||
var dstWidth = 0
|
||||
var dstHeight = 0
|
||||
if (width > 0 && height > 0) {
|
||||
val rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
|
||||
if (rotationDegrees != null) {
|
||||
val isRotated = rotationDegrees % 180 == 90
|
||||
if (isRotated) {
|
||||
val temp = videoWidth
|
||||
videoWidth = videoHeight
|
||||
videoHeight = temp
|
||||
}
|
||||
|
||||
// cover fit
|
||||
val videoAspectRatio = videoWidth / videoHeight
|
||||
if (videoWidth > width || videoHeight > height) {
|
||||
if (width / height.toFloat() > videoAspectRatio) {
|
||||
dstHeight = ceil(videoHeight * width / videoWidth).toInt()
|
||||
dstWidth = (dstHeight * videoAspectRatio).roundToInt()
|
||||
} else {
|
||||
dstWidth = ceil(videoWidth * height / videoHeight).toInt()
|
||||
dstHeight = (dstWidth / videoAspectRatio).roundToInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the returned frame is already rotated according to the video metadata
|
||||
val frame = if (dstWidth > 0 && dstHeight > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
val targetBitmapSizeBytes: Long = FORMAT_BYTE_SIZE.toLong() * dstWidth * dstHeight
|
||||
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
|
||||
throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the scaled frame at $dstWidth x $dstHeight")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight, getBitmapParams())
|
||||
} else {
|
||||
retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight)
|
||||
}
|
||||
} else {
|
||||
retriever.frameAtTime
|
||||
val targetBitmapSizeBytes: Long = (FORMAT_BYTE_SIZE.toLong() * videoWidth * videoHeight).toLong()
|
||||
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
|
||||
throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the full frame at $videoWidth x $videoHeight")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
retriever.getFrameAtTime(timeMicros, option, getBitmapParams())
|
||||
} else {
|
||||
retriever.getFrameAtTime(timeMicros, option)
|
||||
}
|
||||
}
|
||||
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
|
||||
}
|
||||
|
@ -91,6 +150,17 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
|
||||
// for wide-gamut and HDR content which does not require alpha blending
|
||||
setPreferredConfig(Bitmap.Config.RGBA_1010102)
|
||||
} else {
|
||||
setPreferredConfig(Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
}
|
||||
|
||||
// already cleaned up in loadData and ByteArrayInputStream will be GC'd
|
||||
override fun cleanup() {}
|
||||
|
||||
|
@ -100,4 +170,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
|||
override fun getDataClass(): Class<InputStream> = InputStream::class.java
|
||||
|
||||
override fun getDataSource(): DataSource = DataSource.LOCAL
|
||||
|
||||
companion object {
|
||||
// same for either `ARGB_8888` or `RGBA_1010102`
|
||||
private const val FORMAT_BYTE_SIZE = BitmapUtils.ARGB_8888_BYTE_SIZE
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue