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 bitmapWidth: Int
|
||||||
val bitmapHeight: Int
|
val bitmapHeight: Int
|
||||||
if (width / height > svgWidth / svgHeight) {
|
if (width / height.toFloat() > svgWidth / svgHeight) {
|
||||||
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
|
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
|
||||||
bitmapHeight = height
|
bitmapHeight = height
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -63,7 +63,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
|
||||||
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
TiffBitmapFactory.decodeFileDescriptor(fd, options)
|
||||||
val imageWidth = options.outWidth
|
val imageWidth = options.outWidth
|
||||||
val imageHeight = options.outHeight
|
val imageHeight = options.outHeight
|
||||||
if (imageHeight > height || imageWidth > width) {
|
if (imageWidth > width || imageHeight > height) {
|
||||||
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
|
while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
|
||||||
sampleSize *= 2
|
sampleSize *= 2
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package deckers.thibault.aves.decoder
|
package deckers.thibault.aves.decoder
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.Priority
|
import com.bumptech.glide.Priority
|
||||||
import com.bumptech.glide.Registry
|
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.load.model.MultiModelLoaderFactory
|
||||||
import com.bumptech.glide.module.LibraryGlideModule
|
import com.bumptech.glide.module.LibraryGlideModule
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.MemoryUtils
|
||||||
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -24,6 +29,8 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
class VideoThumbnailGlideModule : LibraryGlideModule() {
|
class VideoThumbnailGlideModule : LibraryGlideModule() {
|
||||||
|
@ -36,7 +43,7 @@ class VideoThumbnail(val context: Context, val uri: Uri)
|
||||||
|
|
||||||
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
||||||
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<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
|
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)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||||
|
@ -68,10 +75,62 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
||||||
if (durationMillis != null) {
|
if (durationMillis != null) {
|
||||||
timeMillis = if (durationMillis < 15000) 0 else 15000
|
timeMillis = if (durationMillis < 15000) 0 else 15000
|
||||||
}
|
}
|
||||||
val frame = if (timeMillis != null) {
|
val timeMicros = if (timeMillis != null) timeMillis * 1000 else -1
|
||||||
retriever.getFrameAtTime(timeMillis * 1000)
|
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 {
|
} 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)
|
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
|
// already cleaned up in loadData and ByteArrayInputStream will be GC'd
|
||||||
override fun cleanup() {}
|
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 getDataClass(): Class<InputStream> = InputStream::class.java
|
||||||
|
|
||||||
override fun getDataSource(): DataSource = DataSource.LOCAL
|
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