21、视频缩略图与封面:关键帧提取、自定义封面生成、九宫格缩略图
做视频处理,缩略图这事儿看着简单,其实坑不少。
我记得刚接手一个短视频项目时,产品说“就取个封面嘛,多简单”。结果上线后用户反馈:封面黑屏、封面模糊、封面跟视频内容完全不搭……嗯,那段时间我几乎天天跟MediaMetadataRetriever打交道。
今天咱们就把缩略图和封面这块彻底聊透。关键帧提取、自定义封面、九宫格缩略图,一个一个来。
21.1 关键帧提取:别让用户看到黑屏
关键帧,说白了就是视频里那些“完整的画面”。视频压缩时,为了省空间,很多帧只记录变化部分(P帧、B帧),只有关键帧(I帧)才存完整图像。
你想想看,如果取缩略图时刚好抽到个B帧,那画面大概率是花的、模糊的,甚至黑的。我早期就踩过这个坑——取出来的缩略图用户一看:“这什么玩意儿?”
Android里提取关键帧,最常用的就是MediaMetadataRetriever。我个人习惯这样用:
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, videoUri);
// 获取关键帧(推荐)
Bitmap keyFrame = retriever.getFrameAtTime(
timeUs,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
);
// 参数说明:
// timeUs:你想取的时间点,单位微秒
// OPTION_CLOSEST_SYNC:取最近的关键帧
// OPTION_CLOSEST:取最近的帧(可能不是关键帧)
// OPTION_NEXT_SYNC:取下一个关键帧
// OPTION_PREVIOUS_SYNC:取上一个关键帧
为什么会这样?因为很多视频开头有黑场过渡,或者APP启动logo,取第0秒的关键帧大概率是黑的。我见过不少APP的封面就是黑的,十有八九是直接取了第0帧。
21.2 自定义封面生成:让用户自己选
自动取关键帧虽然方便,但用户不一定满意。比如你取了个风景镜头,用户想展示的是人物特写。所以,给用户“自定义封面”的能力,是成熟APP的标配。
实现思路其实不复杂:
- 提供时间轴选择:让用户拖动进度条,预览每一帧
- 实时预览:用户滑动时,快速显示对应帧的画面
- 确认选择:用户满意后,保存该帧为封面
这里有个性能问题:用户每滑动一次就去解码一帧,卡顿是必然的。我建议的做法是:
// 预取关键帧列表(比如每隔1秒取一帧)
List<Bitmap> thumbnails = new ArrayList<>();
long interval = 1000000; // 1秒 = 1000000微秒
long duration = Long.parseLong(
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
)
) * 1000; // 转为微秒
for (long pos = 0; pos < duration; pos += interval) {
Bitmap frame = retriever.getFrameAtTime(
pos,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
);
thumbnails.add(frame);
}
// 用户滑动时,直接从列表取,不用重新解码
// 注意:列表不要太大,否则内存扛不住
另外,用户选好封面后,我建议把封面保存到本地,同时生成一个缩略图缓存。这样下次打开时不用重新解码,用户体验会好很多。
21.3 九宫格缩略图:一图胜千言
九宫格缩略图,就是把视频的多个关键画面拼成一张图。常见于视频列表、课程封面等场景。用户一看就知道视频大概讲了什么。
实现步骤:
- 从视频中均匀抽取9帧(或自定义数量)
- 将每帧缩放到统一大小
- 按网格排列,合成一张大图
代码实现:
public Bitmap createGridThumbnail(String videoPath, int cols, int rows) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(videoPath);
long duration = Long.parseLong(
retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION
)
) * 1000;
int totalFrames = cols * rows;
long interval = duration / (totalFrames + 1);
int cellWidth = 200;
int cellHeight = 150;
Bitmap result = Bitmap.createBitmap(
cellWidth * cols,
cellHeight * rows,
Bitmap.Config.ARGB_8888
);
Canvas canvas = new Canvas(result);
int index = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
long pos = interval * (index + 1);
Bitmap frame = retriever.getFrameAtTime(
pos,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
);
Bitmap scaled = Bitmap.createScaledBitmap(
frame, cellWidth, cellHeight, true
);
canvas.drawBitmap(scaled, c * cellWidth, r * cellHeight, null);
scaled.recycle();
frame.recycle();
index++;
}
}
retriever.release();
return result;
}
21.4 知识体系总览
下面这张图,把缩略图相关的技术点串起来了。你可以对照着看,哪个环节还没掌握:
21.5 性能与内存:不得不说的痛
做缩略图,最怕的就是OOM。我总结了几条铁律:
| 场景 | 风险 | 解决方案 |
|---|---|---|
| 取单帧缩略图 | 低 | 直接使用,注意及时recycle |
| 取多帧(九宫格) | 中 | 控制图片尺寸,用完后立即释放 |
| 预取全部帧 | 高 | 改为按需加载,最多缓存10-20帧 |
| 大分辨率视频 | 极高 | 先缩放到合理尺寸再解码 |
嗯,缩略图这块其实不难,但细节决定体验。关键帧选对了,封面就好看了;封面好看了,用户点击率就上去了。就这么简单。
公众号:蓝海资料掘金营,微信deep3321