Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin
XML
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:padding="1px">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="200px"
android:background="@android:color/darker_gray"
android:scaleType="centerCrop" />
</LinearLayout>
implementation("io.coil-kt.coil3:coil:3.3.0")
implementation("io.coil-kt.coil3:coil-core:3.3.0")
Kotlin
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "fly/MainActivity"
const val SPAN_COUNT = 4
const val VIDEO = 1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rv = findViewById<RecyclerView>(R.id.rv)
val layoutManager = GridLayoutManager(this, SPAN_COUNT)
layoutManager.orientation = GridLayoutManager.VERTICAL
rv.layoutManager = layoutManager
val adapter = MyAdapter(this)
rv.adapter = adapter
rv.layoutManager = layoutManager
val ctx = this
lifecycleScope.launch(Dispatchers.IO) {
val videoList = readAllVideo(ctx)
Log.d(TAG, "readAllVideo size=${videoList.size}")
val lists = arrayListOf<MyData>()
lists.addAll(videoList)
lifecycleScope.launch(Dispatchers.Main) {
adapter.dataChanged(lists)
}
}
}
private fun readAllVideo(ctx: Context): ArrayList<MyData> {
val videos = ArrayList<MyData>()
//读取视频Video
val cursor = ctx.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
null,
null,
null,
null
)
while (cursor!!.moveToNext()) {
//路径
val path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA))
val id = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)
val videoUri: Uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(id))
//名称
//val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME))
//大小
//val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE))
videos.add(MyData(videoUri, path, VIDEO))
}
cursor.close()
return videos
}
}
Kotlin
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.toBitmap
class MyAdapter : RecyclerView.Adapter<MyAdapter.VideoHolder> {
companion object {
const val TAG = "fly/MyAdapter"
}
private var mCtx: Context? = null
private var mItems = ArrayList<MyData>()
constructor(ctx: Context) : super() {
mCtx = ctx
}
fun dataChanged(items: ArrayList<MyData>) {
this.mItems = items
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoHolder {
val v = LayoutInflater.from(mCtx).inflate(R.layout.image_layout, null)
return VideoHolder(v)
}
override fun onBindViewHolder(holder: VideoHolder, position: Int) {
loadVideoCover(mItems[position], holder.image)
}
override fun getItemCount(): Int {
return mItems.size
}
class VideoHolder : RecyclerView.ViewHolder {
var image: ImageView? = null
constructor(itemView: View) : super(itemView) {
image = itemView.findViewById<ImageView>(R.id.image)
}
}
private fun loadVideoCover(data: MyData, image: ImageView?) {
val imageMemoryCacheKey = MemoryCache.Key(data.toString())
val imageMemoryCache = MyCoilManager.Companion.INSTANCE.getImageLoader(mCtx!!).memoryCache?.get(imageMemoryCacheKey)
if (imageMemoryCache != null) {
Log.d(TAG, "命中内存缓存 $data")
image?.setImageBitmap(imageMemoryCache.image.toBitmap())
} else {
//placeholder
image?.setImageResource(android.R.drawable.ic_menu_gallery)
val imageReq = ImageRequest.Builder(mCtx!!)
.data(data)
.memoryCacheKey(imageMemoryCacheKey)
.memoryCachePolicy(CachePolicy.WRITE_ONLY)
.size(400)
.listener(object : ImageRequest.Listener {
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
image?.setImageBitmap(result.image.toBitmap())
}
override fun onError(request: ImageRequest, result: ErrorResult) {
Log.e(TAG, "onError ${request.data}")
image?.setImageResource(android.R.drawable.stat_notify_error)
}
}).build()
MyCoilManager.Companion.INSTANCE.getImageLoader(mCtx!!).enqueue(imageReq)
}
}
}
Kotlin
import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
class MyApp : Application(), SingletonImageLoader.Factory {
companion object {
const val TAG = "fly/MyApp"
}
override fun newImageLoader(context: PlatformContext): ImageLoader {
return MyCoilManager.Companion.INSTANCE.getImageLoader(this)
}
}
Kotlin
import android.content.Context
import android.os.Environment
import android.util.Log
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.imageDecoderEnabled
import coil3.memory.MemoryCache
import coil3.request.CachePolicy
import java.io.File
class MyCoilManager {
companion object {
const val TAG = "fly/MyCoilManager"
val INSTANCE by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { MyCoilManager() }
}
private var mImageLoader: ImageLoader? = null
private constructor() {
Log.d(TAG, "constructor")
}
fun getImageLoader(ctx: Context): ImageLoader {
if (mImageLoader != null) {
return mImageLoader!!
}
Log.d(TAG, "初始化ImageLoader")
//初始化加载器。
mImageLoader = ImageLoader.Builder(ctx)
.imageDecoderEnabled(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache(initMemoryCache())
.diskCachePolicy(CachePolicy.ENABLED)
.diskCache(initDiskCache())
.components {
add(MyVideoFetcher.Factory(ctx))
}.build()
return mImageLoader!!
}
private fun initMemoryCache(): MemoryCache {
//内存缓存。
val memoryCache = MemoryCache.Builder()
.maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
.build()
return memoryCache
}
private fun initDiskCache(): DiskCache {
//磁盘缓存。
val diskCacheFolder = Environment.getExternalStorageDirectory()
val diskCacheName = "fly_disk_cache"
val cacheFolder = File(diskCacheFolder, diskCacheName)
if (cacheFolder.exists()) {
Log.d(TAG, "${cacheFolder.absolutePath} exists")
} else {
if (cacheFolder.mkdir()) {
Log.d(TAG, "${cacheFolder.absolutePath} create OK")
} else {
Log.e(TAG, "${cacheFolder.absolutePath} create fail")
}
}
val diskCache = DiskCache.Builder()
.maxSizeBytes(1024 * 1024 * 1024 * 2L) //2GB
.directory(cacheFolder)
.build()
Log.d(TAG, "cache folder = ${diskCache.directory.toFile().absolutePath}")
return diskCache
}
}
Kotlin
import android.net.Uri
import android.text.TextUtils
open class MyData {
var uri: Uri? = null
var path: String? = null
var lastModified = 0L
var width = 0
var height = 0
var position = -1
var type = -1 //-1未知。1,普通图。2,视频。
constructor(uri: Uri?, path: String?, type: Int = -1) {
this.uri = uri
this.path = path
this.type = type
}
override fun equals(other: Any?): Boolean {
return TextUtils.equals(this.toString(), other.toString())
}
override fun toString(): String {
return "MyData(uri=$uri, path=$path, lastModified=$lastModified, width=$width, height=$height, position=$position, type=$type)"
}
}
Kotlin
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DataSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.request.Options
class MyVideoFetcher(private val ctx: Context, private val item: MyData, private val options: Options) : Fetcher {
companion object {
const val TAG = "fly/MyVideoFetcher"
}
override suspend fun fetch(): FetchResult {
var bitmap: Bitmap? = VideoUtil.readBmpDiskCache(MyCoilManager.INSTANCE.getImageLoader(ctx), item)
if (bitmap == null) {
val t1 = System.currentTimeMillis()
bitmap = VideoUtil.getBmpBySysMMR(item)
val t2 = System.currentTimeMillis()
Log.d(TAG, "耗时 MMR: ${t2 - t1} ms")
if (bitmap != null) {
VideoUtil.writeBmpDiskCache(MyCoilManager.INSTANCE.getImageLoader(ctx), bitmap, item)
}
}
return ImageFetchResult(
bitmap?.asImage()!!,
true,
dataSource = DataSource.DISK
)
}
class Factory(private val ctx: Context) : Fetcher.Factory<MyData> {
override fun create(
item: MyData,
options: Options,
imageLoader: ImageLoader,
): Fetcher {
return MyVideoFetcher(ctx, item, options)
}
}
}
Kotlin
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.util.Log
import coil3.ImageLoader
import java.io.BufferedOutputStream
import java.io.FileOutputStream
object VideoUtil {
const val TAG = "fly/VideoUtil"
fun readBmpDiskCache(il: ImageLoader?, item: MyData?): Bitmap? {
var bitmap: Bitmap? = null
val snapShot = il?.diskCache?.openSnapshot(item.toString())
if (snapShot != null) {
Log.d(TAG, "命中Disk缓存 $item")
val source = ImageDecoder.createSource(snapShot.data.toFile())
try {
bitmap = ImageDecoder.decodeBitmap(source)
} catch (e: Exception) {
Log.e(TAG, "读Disk缓存异常 $e $item")
}
}
snapShot?.close()
return bitmap
}
fun writeBmpDiskCache(il: ImageLoader?, bitmap: Bitmap?, item: MyData?): Any? {
var bool = false
if (bitmap != null) {
val editor = il?.diskCache?.openEditor(item.toString())
var bos: BufferedOutputStream? = null
try {
bos = FileOutputStream(editor?.data?.toFile()).buffered(1024 * 32)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
bos.flush()
bos.close()
editor?.commit()
Log.d(TAG, "Bitmap写入Disk缓存 $item")
bool = true
} catch (e: Exception) {
Log.e(TAG, "Bitmap写Disk磁盘异常 $e")
} finally {
try {
bos?.close()
} catch (e: Exception) {
Log.e(TAG, "$e $item")
}
}
}
return bool
}
fun getBmpBySysMMR(item: MyData?): Bitmap? {
var bitmap: Bitmap? = null
var sysRetriever: android.media.MediaMetadataRetriever? = null
try {
sysRetriever = android.media.MediaMetadataRetriever()
sysRetriever.setDataSource(item?.path)
bitmap = sysRetriever.frameAtTime
} catch (e: Exception) {
Log.e(TAG, "${e.message} $item")
} finally {
try {
sysRetriever?.release()
sysRetriever?.close()
} catch (e: Exception) {
Log.e(TAG, "release ${e.message} $item")
}
}
return bitmap
}
}
Android MediaMetadataRetriever取视频封面,Kotlin(1)-CSDN博客文章浏览阅读801次,点赞17次,收藏11次。该Android项目实现了一个视频缩略图展示功能,主要包含以下内容:1)声明了读写存储权限;2)使用RecyclerView以9列网格布局展示视频;3)通过MediaMetadataRetriever获取视频首帧作为缩略图;4)采用协程处理耗时操作,避免阻塞主线程。项目包含MainActivity、MyAdapter和MyData三个核心类,分别负责UI初始化、数据适配和数据封装。遇到视频损坏或0字节文件时,会显示错误图标并记录日志。整体实现了高效读取设备视频并生成缩略图展示的功能。https://blog.csdn.net/zhangphil/article/details/150023739Android快速视频解码抽帧FFmpegMediaMetadataRetriever,Kotlin(2)-CSDN博客文章浏览阅读294次。本文介绍了两种Android视频封面提取方案对比:1)原生MediaMetadataRetriever速度较慢;2)第三方FFmpegMediaMetadataRetriever(FFMMR)实现快速抽帧。详细说明了FFMMR的集成方法(添加依赖和权限),并提供了完整的Kotlin实现代码,包括视频列表读取、缓存管理、协程异步处理等核心功能。通过LruCache缓存缩略图提升性能,记录处理耗时和失败情况。相比前文介绍的原生方案,本文重点突出了FFMMR在解码效率和性能上的优势,为需要快速获取视频帧的场景提供
https://blog.csdn.net/zhangphil/article/details/150061648