13、提取缩略图:利用MediaMetadataRetriever与MediaExtractor配合
做音视频开发的朋友,肯定都遇到过这个需求:给视频文件生成一张封面图。说白了,就是提取缩略图。
我记得刚入行那会儿,产品经理丢过来一个需求——「视频列表要显示封面,不能黑屏」。我当时第一反应就是拿MediaMetadataRetriever硬干。后来发现,有些场景下MediaExtractor配合着用,反而更灵活。今天咱们就把这两种方式掰开揉碎了讲清楚。
13.1 MediaMetadataRetriever:最直接的缩略图方案
Android官方提供的MediaMetadataRetriever,说白了就是为媒体文件元数据检索量身定做的。提取缩略图只是它的功能之一。
用法非常简单,几行代码就能搞定:
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
retriever.setDataSource(context, videoUri);
Bitmap bitmap = retriever.getFrameAtTime(1000000); // 微秒单位,1秒处
// 拿到bitmap,你想怎么用就怎么用
} catch (Exception e) {
e.printStackTrace();
} finally {
retriever.release();
}
这里有个细节,我特别想强调一下。getFrameAtTime()的参数单位是微秒,不是毫秒。1000000微秒才等于1秒。我在项目中见过不止一个同事传了1000进去,结果拿到的帧不对,还跑来问我为什么缩略图是黑的……嗯,单位搞错了。
getFrameAtTime(),它会返回视频第一帧(通常是关键帧)。但不同厂商的ROM实现有差异,我建议还是显式传一个时间点,比如视频总时长的一半,这样更靠谱。
13.2 参数选择:时间点与格式选项
MediaMetadataRetriever提供了几个重载方法,咱们来看看:
| 方法 | 说明 | 适用场景 |
|---|---|---|
| getFrameAtTime(long timeUs) | 获取指定时间点的帧 | 通用场景,最常用 |
| getFrameAtTime(long timeUs, int option) | 带选项的获取方式 | 需要精确控制关键帧匹配时 |
| getScaledFrameAtTime(...) | 获取指定尺寸的缩略图 | 节省内存,避免OOM |
| getEmbeddedPicture() | 获取内嵌的封面图 | MP3专辑封面、部分视频内嵌封面 |
关于那个option参数,我多说两句。它有两个可选值:
OPTION_CLOSEST_SYNC:找最近的关键帧。速度快,但时间精度差一点。OPTION_CLOSEST:找最近的帧(不一定是关键帧)。精度高,但解码开销大。
我个人习惯用OPTION_CLOSEST_SYNC。为什么?因为大部分场景下,缩略图不需要精确到毫秒级,关键帧解码快,用户体验更好。你想想看,用户滑动列表时,每张缩略图都要精确解码,那卡顿感谁受得了?
13.3 MediaExtractor的另类用法:精准定位帧
说到MediaExtractor,大家第一反应是提取音视频数据。但其实它也能帮我们定位到特定帧,然后配合MediaCodec解码出Bitmap。
为什么要绕这么大一圈?我在项目中遇到过一种情况:某些视频文件用MediaMetadataRetriever提取缩略图时,返回的是null或者花屏。后来排查发现,是视频的moov box(元数据)在文件末尾,而MediaMetadataRetriever在流式场景下解析不完整。
这时候MediaExtractor就派上用场了。我们可以手动控制数据源,精确找到目标帧:
MediaExtractor extractor = new MediaExtractor();
try {
extractor.setDataSource(videoPath);
// 找到视频轨道
int videoTrackIndex = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime != null && mime.startsWith("video/")) {
videoTrackIndex = i;
break;
}
}
if (videoTrackIndex < 0) return;
extractor.selectTrack(videoTrackIndex);
// 定位到目标时间点(微秒)
long targetTimeUs = 1000000; // 1秒处
extractor.seekTo(targetTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
// 读取一个样本数据
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
int sampleSize = extractor.readSampleData(buffer, 0);
// 这里拿到的数据是编码后的,需要MediaCodec解码
// 实际项目中建议封装成工具类
} finally {
extractor.release();
}
13.4 两种方案的对比与选择
咱们用一张表来对比一下:
| 对比维度 | MediaMetadataRetriever | MediaExtractor + MediaCodec |
|---|---|---|
| 代码量 | 少,几行搞定 | 多,需要手动管理解码器 |
| 灵活性 | 低,只能获取帧 | 高,可以控制解码流程 |
| 兼容性 | 好,大部分设备没问题 | 依赖设备解码能力 |
| 性能 | 中等,内部有优化 | 可控,可以复用解码器 |
| 适用场景 | 快速提取缩略图 | 需要批量处理、流式场景 |
我个人建议:80%的场景用MediaMetadataRetriever就够了。剩下20%——比如视频文件结构特殊、需要批量生成缩略图、或者要自定义解码参数——再考虑MediaExtractor方案。
13.5 实战中的避坑指南
做缩略图提取,坑其实不少。我把自己踩过的坑列出来,你们遇到类似问题可以少走弯路:
- OOM问题:高清视频的帧可能很大,直接拿Bitmap容易内存溢出。建议用
getScaledFrameAtTime()指定输出尺寸,或者拿到Bitmap后做压缩。 - 耗时操作:提取缩略图是IO密集型操作,千万别在主线程执行。我曾经见过一个同事直接在UI线程调
getFrameAtTime(),结果列表滑动卡成PPT。 - 文件不存在:
setDataSource()如果传了无效路径,会抛异常。记得加try-catch,并且做好降级处理——比如显示一个默认封面。 - 某些MP4文件:有些视频的缩略图时间点恰好是黑帧(比如淡入淡出效果)。我建议取视频总时长的10%~20%位置,避开开头和结尾。
好了,关于缩略图提取的内容就讲到这里。两种方案各有优劣,关键是根据实际场景做选择。记住一点:能用简单方案解决的问题,就别搞复杂了。