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();
}
注意:MediaExtractor拿到的样本数据是编码后的,不能直接转成Bitmap。你需要配合MediaCodec进行解码。如果只是简单提取缩略图,我建议优先用MediaMetadataRetriever。MediaExtractor方案适合那些需要精细控制解码流程的场景。

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%位置,避开开头和结尾。
核心总结:MediaMetadataRetriever是提取缩略图的首选方案,简单高效。MediaExtractor作为补充方案,在特殊场景下能解决MediaMetadataRetriever搞不定的问题。实际开发中,建议封装一个缩略图提取工具类,内部做策略切换,对外暴露统一的接口。
缩略图提取方案决策流程 需要提取缩略图 视频结构是否标准? (moov box位置正常) MediaMetadata Retriever MediaExtractor + MediaCodec 操作步骤 1. setDataSource() 2. getFrameAtTime() 3. 获取Bitmap 4. release()释放 操作步骤 1. 选择视频轨道 2. seekTo()定位 3. 读取样本数据 4. MediaCodec解码 推荐优先使用 MediaMetadataRetriever

好了,关于缩略图提取的内容就讲到这里。两种方案各有优劣,关键是根据实际场景做选择。记住一点:能用简单方案解决的问题,就别搞复杂了。