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

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

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

相关推荐
恋猫de小郭12 分钟前
Flutter 的 build_runner 已经今非昔比,看看 build_runner 2.13 有什么特别?
android·前端·flutter
xiangpanf11 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx14 小时前
安卓线程相关
android
消失的旧时光-194314 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon15 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon15 小时前
VSYNC 信号完整流程2
android
dalancon15 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138416 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android17 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才17 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android