Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin

发布于:2025-08-12 ⋅ 阅读:(22) ⋅ 点赞:(0)

Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin

    <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 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")

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
    }
}


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)
        }
    }
}


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)
    }
}


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
    }
}

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)"
    }
}

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)
        }
    }
}

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

Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(七)-CSDN博客文章浏览阅读589次,点赞4次,收藏6次。本文在之前的基础上,进一步优化了Android应用中Coil 3.2.0版本加载缩略图和正式图的实现。主要改进点在于,当正式图加载完成后,主动删除缓存中的缩略图,以节省内存资源。文章提供了相关的Kotlin代码示例,并指出尽管配置了磁盘缓存路径,但实际运行时缓存文件为空,表明磁盘缓存未生效。作者建议将缩略图和正图的内存缓存合并为单一缓存系统,以提升性能。此外,文章还列出了所需的权限声明和Coil库的依赖项,包括对GIF、视频和SVG格式的支持。更多细节可参考CSDN博客链接。 https://blog.csdn.net/zhangphil/article/details/147983753