package com.vgmlr.varnish
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.key.Keyer
import coil.request.Options
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.security.MessageDigest
import java.util.Locale
import okio.Path.Companion.toOkioPath
data class VideoThumb(val path: String, val modified: Long)
object VarnishThumbs {
private const val DIR = "thumbs"
private const val PX = 720
private const val AT = 0.1
private const val QUALITY = 85
fun get(context: Context, path: String, modified: Long): File? {
val f = file(context, path)
if (f.isFile && f.lastModified() / 1000 == modified / 1000) return f
val dir = f.parentFile ?: return null
val bmp = frame(path) ?: return null
dir.mkdirs()
val tmp = File(dir, "${f.name}.${System.nanoTime()}.tmp")
val ok = runCatching {
FileOutputStream(tmp).use { bmp.compress(Bitmap.CompressFormat.JPEG, QUALITY, it) }
}.getOrDefault(false)
bmp.recycle()
tmp.setLastModified(modified) // stale check
if (!ok || !tmp.renameTo(f)) {
tmp.delete()
return null
}
return f
}
fun drop(context: Context, path: String) {
file(context, path).delete()
}
fun move(context: Context, from: String, to: String) {
val src = file(context, from)
if (src.isFile) src.renameTo(file(context, to))
}
private fun file(context: Context, path: String): File =
File(File(context.cacheDir, DIR), "${hash(path)}.jpg")
private fun hash(s: String): String =
MessageDigest.getInstance("SHA-1").digest(s.toByteArray())
.joinToString("") { "%02x".format(Locale.ROOT, it) }
private fun frame(path: String): Bitmap? = runCatching {
MediaMetadataRetriever().use { r ->
r.setDataSource(path)
val ms = r.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLongOrNull() ?: 0L
r.getScaledFrameAtTime(
(ms * 1000 * AT).toLong(),
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
PX,
PX
)
}
}.getOrNull()
}
class VideoThumbFetcher(
private val data: VideoThumb,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
val f = VarnishThumbs.get(options.context, data.path, data.modified)
?: throw IOException("No frame: ${data.path}")
return SourceResult(
source = ImageSource(f.toOkioPath()),
mimeType = "image/jpeg",
dataSource = DataSource.DISK
)
}
class Factory : Fetcher.Factory<VideoThumb> {
override fun create(data: VideoThumb, options: Options, imageLoader: ImageLoader): Fetcher =
VideoThumbFetcher(data, options)
}
}
class VideoThumbKeyer : Keyer<VideoThumb> {
override fun key(data: VideoThumb, options: Options): String = "${data.path}:${data.modified}"
}