Kotlin拿Android本地视频缩略图

news2025/8/12 7:56:18

        本文主要讨论如下三个问题:

  1. 如何拿到本地视频?
  2. 怎么拿视频缩略图?
  3. 缩略图如何压缩?

1 如何拿到本地视频?

1.1 定义数据结构

        先定义媒体信息数据结构MediaInfo,以及视频信息数据结构VideoInfo。

open class MediaInfo(
    var size: Long = 0L, // 大小
    var width: Float = 0f, // 宽
    var height: Float = 0f, // 高
    var filePath: String = "", // 系统绝对路径
    var fileName: String = "", // 文件名
    var mimeType: String = "", // 媒体类型
)
/**
 * 码率(比特率),单位为 bps,比特率越高,传送的数据速度越快。在压缩视频时指定码率,则可确定压缩后的视频大小。
 * 视频大小(byte) = (duration(ms) / 1000) * (biteRate(bit/s) / 8)
 */
data class VideoInfo(
    var firstFrame: Bitmap? = null, // 视频第一帧图
    var duration: Long = 0L, // 视频长度 ms
    var biteRate: Long = 0L, // 视频码率 bps

    /* --------not necessary, maybe not value---- */
    var lastModified: Long = 0L, // 视频最后更改时间
    var addTime: Long = 0L, // 视频添加时间
    var videoRotation: Int = 0, // 视频方向
    /* --------not necessary, maybe not value---- */

) : MediaInfo()

1.2 ContentResolver查询系统中视频

        这里直接去Android媒体库找到所有视频即可,代码使用了Kotlin协程,参考如下。

import android.provider.MediaStore.Video.Media

/**
 * 获取系统所有视频文件
 */
@SuppressLint("InlinedApi")
suspend fun getSystemVideos(contentResolver: ContentResolver): MutableList<MediaInfo> =
    withContext(Dispatchers.IO) {
        val videoList: MutableList<MediaInfo> = mutableListOf()
        var cursor: Cursor? = null
        try {
            cursor = contentResolver.query(
                Media.EXTERNAL_CONTENT_URI,
                arrayOf(
                    Media._ID,
                    Media.SIZE, // 视频大小
                    Media.WIDTH, // 视频宽
                    Media.HEIGHT, // 视频搞、高
                    Media.DATA, // 视频绝对路径
                    Media.DISPLAY_NAME, // 视频文件名
                    Media.MIME_TYPE, // 媒体类型
                    Media.DURATION, // 视频长度
                    Media.BITRATE, // 视频码率
                    Media.DATE_ADDED, // 视频添加时间
                    Media.DATE_MODIFIED, // 视频最后更改时间
                ), null, null, Media.DATE_ADDED + " DESC", null
            )
            cursor?.moveToFirst()
            if (cursor == null || cursor.isAfterLast) return@withContext videoList
            while (!cursor.isAfterLast) {
                videoList.add(getVideoInfo(contentResolver, cursor))
                // videoList.add(getVideoInfo(cursor.getString(cursor.getColumnIndex(Media.DATA))))
                cursor.moveToNext()
            }
        } finally {
            cursor?.close()
        }
        videoList
    }

注:getVideoInfo获取视频文件信息可参考附件1。

2 怎么拿视频缩略图?

2.1 MediaMetadataRetriever

        可以通过视频系统路径,直接使用getFrameAtTime方法拿到第一帧作为缩略图。

val retriever = MediaMetadataRetriever()
retriever.setDataSource("filePath")
val bitmap: Bitmap? = retriever.frameAtTime

        也可以使用getScaledFrameAtTime,指定缩略图Bitmap尺寸512*512。

retriever.getScaledFrameAtTime(-1, OPTION_CLOSEST_SYNC,512,512)

2.2 ThumbnailUtils

  • 第二个参数kind可取如下值:MICRO_KIND(3)不清晰;FULL_SCREEN_KIND(2)清晰;MINI_KIND(1)较清晰
  • MICRO_KIND:96*96的缩略图
  • MINI_KIND:512*384的缩略图
  • FULL_SCREEN_KIND:完整大小的图片
ThumbnailUtils.createVideoThumbnail("filePath",MediaStore.Video.Thumbnails.FULL_SCREEN_KIND)

        ThumbnailUtils.createVideoThumbnail其实也是使用的MediaMetadataRetriever,如下源码截图:

2.3 Glide

Glide.with(mContext)
    .load(Uri.fromFile(File("filePath")))
    .into(binding.icon)

        项目中一般会使用图片加载框架如Glide,它内部也是支持加载视频作为图片的,亦是使用的MediaMetadataRetriever。 

2.4 Android媒体库

        直接从Android媒体库中查询缩略图,但是调试时发现未找到(视频为手机本地录制mp4格式) ,下面是查询视频缩略图的方法:

/**
 * 获取视频缩略图:从媒体库中查询——不是很稳定,且有新视频的时候要通知系统重新扫描
 */
suspend fun getVideoThumbnailDefault(
    contentResolver: ContentResolver,
    cursor: Cursor
): Bitmap? = withContext(Dispatchers.IO) {
    // 开发调试时发现,thumbCursor.moveToFirst()为false,也就是说cursor为空,视频缩略图路径未找到
    contentResolver.query(
        Thumbnails.EXTERNAL_CONTENT_URI,
        arrayOf(Thumbnails.DATA, Thumbnails.VIDEO_ID),
        Thumbnails.VIDEO_ID + "=" + cursor.getInt(cursor.getColumnIndex(Media._ID)),
        null,
        null
    )?.let { thumbCursor ->
        if (thumbCursor.moveToFirst()) {
            // 获取视频缩略图路径,并转为bitmap
            // MediaStore.Video.Thumbnails.DATA: 视频缩略图的文件路径
            BitmapFactory.decodeFile(thumbCursor.getString(thumbCursor.getColumnIndex(Thumbnails.DATA)))
        } else null
    }
}

 2.5 小结

  • Android媒体库可以获取缩略图但不稳定;

  • 第三方图片库如Glide以及ThumbnailUtils都是采用MediaMetadataRetriever。

          所以根本上来说,目前有两种方式拿到视频缩略图,Android媒体库或MediaMetadataRetriever,一般来说采用MediaMetadataRetriever方式,参考代码如下:

    /**
     * 获取视频缩略图:从路径中拿取第一帧
     */
    suspend fun getVideoThumbnail(filePath: String): Bitmap? = withContext(Dispatchers.IO) {
        val bitmap: Bitmap?
        val retriever = MediaMetadataRetriever()
        try {
            retriever.setDataSource(filePath)
            // OPTION_CLOSEST_SYNC:在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧
            bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
                retriever.getScaledFrameAtTime(
                    -1, OPTION_CLOSEST_SYNC,
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt(),
                    THUMBNAIL_DEFAULT_COMPRESS_VALUE.toInt()
                )
            } else {
                retriever.frameAtTime?.let { compressVideoThumbnail(it) }
            }
        } finally {
            try {
                retriever.release()
            } catch (e: Exception) {
            }
        }
        bitmap
    }

        为避免应用OOM,需要对缩略图大小进行压缩compressVideoThumbnail,下面看看Bitmap压缩方式。

3 缩略图如何压缩?        

        宽高压缩、缩放法压缩可针对Bitmap操作,而质量压缩和采样率压缩针对于File、Resource操作,下面可主要看看宽高压缩、缩放法压缩。

3.1 宽高压缩

    /**
     * 视频缩略图默认压缩尺寸
     */
    const val THUMBNAIL_DEFAULT_COMPRESS_VALUE = 512f

    /**
     * 压缩视频缩略图
     * @param bitmap 视频缩略图
     */
    fun compressVideoThumbnail(bitmap: Bitmap): Bitmap? {
        val width: Int = bitmap.width
        val height: Int = bitmap.height
        val max: Int = Math.max(width, height)
        if (max > THUMBNAIL_DEFAULT_COMPRESS_VALUE) {
            val scale: Float = THUMBNAIL_DEFAULT_COMPRESS_VALUE / max
            val w = (scale * width).roundToInt()
            val h = (scale * height).roundToInt()
            return compressVideoThumbnail(bitmap, w, h)
        }
        return bitmap
    }

    /**
     * 压缩视频缩略图:宽高压缩
     * 注:如果用户期望的长度和宽度和原图长度宽度相差太多的话,图片会很不清晰。
     * @param bitmap 视频缩略图
     */
    fun compressVideoThumbnail(bitmap: Bitmap, width: Int, height: Int): Bitmap? {
        return Bitmap.createScaledBitmap(bitmap, width, height, true)
    }

3.2 缩放法压缩

    /**
     * 视频缩略图默认压缩比例
     */
    private const val THUMBNAIL_DEFAULT_SCALE_VALUE = 0.5f

    /**
     * 压缩视频缩略图:缩放法压缩
     * 注:长度和宽度没有变,内存缩小4倍(宽高各缩小一半)
     */
    fun compressVideoThumbnailMatrix(bitmap: Bitmap): Bitmap? {
        val matrix = Matrix()
        matrix.setScale(THUMBNAIL_DEFAULT_SCALE_VALUE, THUMBNAIL_DEFAULT_SCALE_VALUE)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    }

3.3 采样率压缩

    /**
     * 压缩视频缩略图:采样率压缩
     * @param filePath 视频缩略图路径
     */
    fun compressVideoThumbnail(filePath: String, width: Int, height: Int): Bitmap? {
        val options = BitmapFactory.Options()
        if (width > 0 && height > 0) { // 当取到控件的宽高时就按控件的比例取缩略图
            options.inJustDecodeBounds = true // 不生成Bitmap对象,而仅仅是读取该图片的尺寸和类型信息
            val hRatio = ceil(options.outHeight.div(height.toDouble())) // 大于1:图片高度>手机屏幕高度
            val wRatio = ceil(options.outWidth.div(width.toDouble())) // 大于1:图片宽度>手机屏幕宽度
            options.inSampleSize = if (hRatio > wRatio) height else width
            options.inJustDecodeBounds = false
        }
        return BitmapFactory.decodeFile(filePath, options)
    }

4 附件

【附1:获取视频文件信息】

/**
 * 获取视频文件信息
 * 注:暂未包括videoRotation
 */
@SuppressLint("InlinedApi")
suspend fun getVideoInfo(contentResolver: ContentResolver, cursor: Cursor): VideoInfo =
    withContext(Dispatchers.IO) {
        val videoInfo = VideoInfo()
        videoInfo.size = cursor.getLong(cursor.getColumnIndex(Media.SIZE))
        videoInfo.width = cursor.getFloat(cursor.getColumnIndex(Media.WIDTH))
        videoInfo.height = cursor.getFloat(cursor.getColumnIndex(Media.HEIGHT))
        videoInfo.filePath = cursor.getString(cursor.getColumnIndex(Media.DATA))
        videoInfo.fileName = cursor.getString(cursor.getColumnIndex(Media.DISPLAY_NAME))
        videoInfo.mimeType = cursor.getString(cursor.getColumnIndex(Media.MIME_TYPE))
        videoInfo.firstFrame = getVideoThumbnailDefault(contentResolver, cursor)
            ?: getVideoThumbnail(cursor.getString(cursor.getColumnIndex(Media.DATA)))
        videoInfo.duration = cursor.getLong(cursor.getColumnIndex(Media.DURATION))
        videoInfo.biteRate = cursor.getLong(cursor.getColumnIndex(Media.BITRATE))
        if (videoInfo.biteRate == 0L || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            videoInfo.biteRate = (8 * videoInfo.size / (videoInfo.duration / 1000f)).toLong()
        }
        videoInfo.addTime = cursor.getLong(cursor.getColumnIndex(Media.DATE_ADDED))
        videoInfo.lastModified = cursor.getLong(cursor.getColumnIndex(Media.DATE_MODIFIED))
        videoInfo
    }

【附2:通过视频路径直接获取文件信息】

    /**
     * 获取视频文件信息
     * 注:暂未包括lastModified、addTime
     *
     * @param path 视频文件的路径
     * @return VideoInfo 视频文件信息
     */
    suspend fun getVideoInfo(path: String?): VideoInfo = withContext(Dispatchers.IO) {
        val videoInfo = VideoInfo()
        if (!path.isNullOrEmpty()) {
            val media = MediaMetadataRetriever()
            try {
                media.setDataSource(path)
                videoInfo.size =
                    File(path).let { if (FileUtils.isFileExists(it)) it.length() else 0 }
                videoInfo.width = media.extractMetadata(METADATA_KEY_VIDEO_WIDTH)?.toFloat() ?: 0f
                videoInfo.height = media.extractMetadata(METADATA_KEY_VIDEO_HEIGHT)?.toFloat() ?: 0f
                videoInfo.filePath = path
                videoInfo.fileName = path.split(File.separator).let {
                    if (it.isNotEmpty()) it[it.size - 1] else ""
                }
                videoInfo.mimeType = media.extractMetadata(METADATA_KEY_MIMETYPE) ?: ""
                videoInfo.firstFrame = media.frameAtTime?.let { compressVideoThumbnail(it) }
                videoInfo.duration = media.extractMetadata(METADATA_KEY_DURATION)?.toLong() ?: 0
                videoInfo.biteRate = media.extractMetadata(METADATA_KEY_BITRATE)?.toLong() ?: 0
                videoInfo.videoRotation =
                    media.extractMetadata(METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
            } finally {
                media.release()
            }
        }
        videoInfo
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/33535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我参加NVIDIA Sky Hackathon 训练文件的路径设置

各变量的作用 KEY 对应的是 NVIDIA ngc 的那个网站上面生成的那个 keyGPU 的索引&#xff0c; 这个一般不需要修改&#xff0c; 因为大家只有一块 GPU用户实验目录&#xff0c; 这个文件夹用于存放后续过程产生的一系列的文件数据下载目录&#xff0c; 存放数据 本地工程目录&a…

Java并发编程实战读书笔记二

第五章 基础构建模块 5.1 同步容器类 5.1.1 同步容器类的问题 如下&#xff0c;如果list含有10个元素&#xff0c;线程A调用getLast的同时线程B调用deleteLast&#xff0c;那么getLast可能会报ArrayIndexOutOfBoundsException 改为如下方式能确保size和get一致 Vector迭代也…

【795. 区间子数组个数】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一个整数数组 nums 和两个整数&#xff1a;left 及 right 。找出 nums 中连续、非空且其中最大元素在范围 [left, right] 内的子数组&#xff0c;并返回满足条件的子数组的个数。 生成的测试用例…

微信小程序| 用小程序复刻微信Wechat

&#x1f4cc;个人主页&#xff1a;个人主页 ​&#x1f9c0; 推荐专栏&#xff1a;小程序开发成神之路 --【这是一个为想要入门和进阶小程序开发专门开启的精品专栏&#xff01;从个人到商业的全套开发教程&#xff0c;实打实的干货分享&#xff0c;确定不来看看&#xff1f; …

新的趋势:From Big to Small and Wide data

新的趋势&#xff1a;From Big to Small and Wide data 所以&#xff0c;在这个时候&#xff0c;作为率先提出要做 MySQL 开源 HTAP 数据库的 StoneDB&#xff0c;想要稍微冷静一下。 不是说我们不做 HTAP 了&#xff0c;而是有了一个新的思路。这个思路&#xff0c;也同样来…

【模型训练】YOLOv7车辆三类别检测

YOLOv7车辆三类别检测 1、车辆三类别检测模型训练2、模型评估3、模型和数据集下载网盘链接1、本项目采用YOLOv7算法实现对车辆三类别检测,在几千多张车辆三类别数据集中训练得到,我们训练了YOLOv7、,所有指标都是在同一个验证集上得到; 2、目标类别数:3;类别名:car、bus…

【蓝桥杯选拔赛真题29】python堆砖块 青少年组蓝桥杯python 选拔赛STEMA比赛真题解析

目录 python堆砖块 一、题目要求 1、提示信息 1、编程实现 2、输入输出

WindowsServer域控的安装与卸载

搭建域服务器 1.安装域控 打开服务器管理器, 点击右上角的管理, 选择添加角色和功能 一直点击下一步,直到选择服务器角色处, 勾选Active Directory域服务器 一直下一步&#xff0c;然后点击安装 安装完毕后将此服务器提升为域控制器 自行设置DSRM的密码, 后面一直点击下一步直…

【优化调度】遗传算法求解公交车调度排班优化问题【含Matlab源码 2212期】

⛄ 一、 遗传算法简介 1 引言 公交排班问题是城市公交调度的核心内容,是公交调度人员、司乘人员进行工作以及公交车辆正常运行的基本依据。行车时刻表是按照线路的当前客流量情况,确定发车频率,提供线路车辆的首、末车时间。它是公交企业对社会的承诺,决定着为乘客服务的水平,…

2023-2028年中国花炮行业市场供需与投资预测分析报告

本报告由锐观咨询重磅推出&#xff0c;对中国花炮行业的发展现状、竞争格局及市场供需形势进行了具体分析&#xff0c;并从行业的政策环境、经济环境、社会环境及技术环境等方面分析行业面临的机遇及挑战。还重点分析了重点企业的经营现状及发展格局&#xff0c;并对未来几年行…

【Java 设计模式】简单工厂模式 静态工厂模式

简单工厂模式 & 静态工厂模式1 简单工厂模式1.1 角色1.2 点咖啡案例1.2.1 类图1.2.2 实现1.3 优点1.4 缺点2 静态工厂模式2.1 代码变动2.2 优点1 简单工厂模式 简单工厂模式并不属于 23 种设计模式。 1.1 角色 抽象产品&#xff1a;定义产品的规范&#xff0c;描述产品的…

相控阵天线(七):常规平面阵列分布(矩形阵列、三角栅格、六边形阵列和圆形阵列)

目录简介矩形栅格平面阵列三角栅格平面阵列六边形阵列圆形平面阵列空心平面阵列简介 常见的平面阵有一些基本类型&#xff0c;按照栅格形式可以进行以下划分&#xff1a;矩形栅格、三角形栅格、同心圆环和椭圆环栅格等&#xff1b;按照边界形式可以进行以下划分&#xff1a;矩…

React Native Webview 中input type=file accept=“image/*“ 无法调起相机问题排查及解决

最近在写一个react native 项目&#xff0c;其中react-native-webview库一些使用着实遇到了不少问题&#xff0c;耗时比较长&#xff0c;现在和大家分享一下。 图片上传时选择拍照是很常见的功能&#xff0c;写的h5项目一直调用正常。使用方式大概如下&#xff1a; <input…

【数据结构】—— 双链表的增删改查

❤️一名热爱Java的大一学生&#xff0c;希望与各位大佬共同学习进步❤️ &#x1f9d1;个人主页&#xff1a;周小末天天开心 各位大佬的点赞&#x1f44d; 收藏⭐ 关注✅&#xff0c;是本人学习的最大动力 感谢&#xff01; &#x1f4d5;该篇文章收录专栏—数据结构 目录 双…

艾美捷小鼠肿瘤坏死因子α-ELISpot试剂盒使用指南

ELISpot Plus for enumeration of cells secreting TNF-α This kit is ideal for users who want a convenient and sensitive assay. The assay is designed for the enumeration of cells secreting mouse TNF-α. The kit includes ELISpot plates pre-coated with monocl…

[附源码]计算机毕业设计JAVA面试刷题系统

[附源码]计算机毕业设计JAVA面试刷题系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis M…

【C++】--模拟实现vector

文章目录Constructors(构造函数)myvector()myvector(int n, const T& val T())myvector(InputIterator first, InputIterator last)拷贝构造交换函数myvector< T >& operator(myvector< T > v)迭代器扩容reserveresize插入和删除push_backpop_backinserte…

CDH启用kerberos 高可用运维实战

一、背景说明 在前的文章中介绍过《CDH集成的kerberos迁移实战》,由此也考虑到kerberos单节点可能引发的线上事故&#xff0c;所有考虑到把线上kerberos服务启用高可用。 二、环境介绍 系统版本 CentOS Linux release 7.6.1810 (Core) CM版本 Kerberos版本 三、实操…

桌面应用开发有哪些主流框架?

受益于开源技术的发展&#xff0c;以及响应快速开发的实际业务需求&#xff0c;跨平台开发不仅限于移动端跨平台&#xff0c;桌面端虽然在市场应用方面场景不像移动端那么丰富&#xff0c;但也有市场的需求。 相对于个人开发者而言&#xff0c;跨平台框架的使用&#xff0c;主要…

零基础学习下载FL Studio2023水果编曲软件

FL Studio工具常称水果编曲软件&#xff0c;是一款功能强大的编曲软件&#xff0c;集编曲&#xff0c;录音&#xff0c;剪辑&#xff0c;混音于一身&#xff0c;简单易上手&#xff0c;灵活性高&#xff0c;强大到突破想象。FL Studio&#xff0c;当前版本 FL Studio21&#xf…