本文主要讨论如下三个问题:
- 如何拿到本地视频?
- 怎么拿视频缩略图?
- 缩略图如何压缩?
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
}