24、截图与GIF生成:视频帧截图、连续帧合成GIF动图
做播放器做到这个阶段,大家应该都有感觉了——解码、渲染、同步这些核心流程跑通之后,剩下的就是怎么把视频“玩出花”来。今天聊的截图和GIF生成,就是两个非常实用的功能。
我个人习惯把这两个功能放在一起讲,因为它们的底层逻辑是相通的:都是从视频流里“捞”帧数据。只不过截图是捞一帧,GIF是捞一堆帧再拼起来。
24.1 视频帧截图:从Surface到Bitmap
先说说截图。这个需求很常见,比如用户看到某个精彩画面,想保存下来发朋友圈。那问题来了——视频画面是在Surface上渲染的,我们怎么把它变成Bitmap?
其实思路很简单:从解码器输出的帧里,把数据拷贝出来。具体怎么做?我给大家梳理了三种主流方案。
方案一:MediaMetadataRetriever(最简单)
如果你不需要实时截图,比如用户点了个按钮“截取当前时间点的画面”,那直接用系统API就行。
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(videoPath);
Bitmap bitmap = retriever.getFrameAtTime(timeUs,
MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
retriever.release();
嗯,这里要注意:OPTION_CLOSEST_SYNC 取的是最近的关键帧,不是精确时间点。如果你要精确到毫秒级,用 OPTION_CLOSEST,但性能会差一些。
方案二:从SurfaceView/TextureView截图
如果你用的是TextureView,那就方便了。它有个 getBitmap() 方法,可以直接拿到当前渲染的画面。
Bitmap bitmap = textureView.getBitmap();
一行代码搞定。但有个坑——这个方法拿到的Bitmap是经过SurfaceFlinger合成的,分辨率可能不是你想要的。我曾经遇到过一个问题:视频是4K的,但TextureView显示区域只有1080p,截出来的图就是1080p的。
方案三:从解码器OutputBuffer拷贝(最灵活)
这是我最推荐的方式,也是专业播放器常用的方案。原理很简单:在解码器输出帧时,把数据从Buffer里拷贝出来,转成Bitmap。
// 在解码循环中
int outputIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs);
if (outputIndex >= 0) {
MediaCodec.BufferInfo info = bufferInfo;
ByteBuffer outputBuffer = codec.getOutputBuffer(outputIndex);
// 关键:把YUV数据转成Bitmap
Bitmap bitmap = yuvToBitmap(outputBuffer, width, height);
// 释放buffer
codec.releaseOutputBuffer(outputIndex, true);
}
这里有个技术难点:YUV转RGB。解码器输出的通常是YUV420格式,而Bitmap需要的是ARGB_8888。转换算法不难,但要注意性能——毕竟你是在解码线程里操作。
核心要点:
- 用RenderScript或libyuv做颜色空间转换,比纯Java快3-5倍
- 截图时最好暂停解码,避免画面撕裂
- 保存Bitmap用PNG格式,质量高且支持透明
24.2 连续帧合成GIF:从序列到动图
截图讲完了,咱们聊聊GIF。说白了,GIF就是把连续的多帧画面拼成一个动图文件。我刚开始做这个功能时,以为很简单——不就是把一堆Bitmap塞进去吗?结果踩了不少坑。
GIF文件结构速览
先简单了解一下GIF的格式。它其实挺古老的,1987年就有了。结构大致是这样的:
GIF文件头 (6字节) → 逻辑屏幕描述块 → 全局颜色表 →
图像块1 (局部颜色表 + 图像数据) →
图像块2 → ... →
图像块N → 结尾标记
每个图像块就是一帧。GIF支持透明色、帧间延迟、循环次数等参数。说白了,你只要按这个格式把数据填进去,就能生成一个可播放的GIF。
方案一:使用Android内置库(不推荐)
Android SDK里其实有个 android.graphics.Movie 类,可以解析GIF。但注意——它只能读,不能写。所以生成GIF还得靠第三方库或自己写。
方案二:使用开源库(推荐)
我个人习惯用 android-gif-encoder 这个库,轻量且稳定。用法很简单:
GifEncoder encoder = new GifEncoder();
encoder.start(outputPath);
encoder.setDelay(100); // 每帧延迟100ms,相当于10fps
encoder.setRepeat(0); // 0表示无限循环
for (Bitmap frame : frameList) {
encoder.addFrame(frame);
}
encoder.finish();
你看,核心就三步:开始、加帧、结束。但这里有个性能问题——如果帧数太多,内存会爆。比如你要生成一个10秒的GIF,每秒10帧,那就是100帧。每帧如果是1080p的Bitmap,光内存就得几百MB。
- 先缩放到合适尺寸,比如480x270,GIF不需要太高分辨率
- 用RGB_565格式,比ARGB_8888省一半内存
- 控制帧率,5-10fps就够用了,再高文件体积会爆炸
方案三:自己实现GIF编码器(进阶)
如果你对性能有极致要求,或者想完全掌控编码过程,可以自己写一个。核心算法是LZW压缩——这是GIF标准里规定的压缩方式。
// 伪代码:LZW压缩核心
public byte[] lzwCompress(int[] pixels) {
// 1. 初始化字典,包含所有单像素颜色
// 2. 遍历像素,查找最长匹配
// 3. 输出编码,更新字典
// 4. 当字典满时,清空并重新开始
}
说实话,自己实现LZW挺折腾的。我当年为了搞懂它,啃了好几天的GIF规范文档。如果你不是对底层特别感兴趣,直接用现成库就好。
24.3 实战:截图+GIF合成完整流程
好了,理论讲完了,咱们来个实战。假设你要做一个功能:用户长按屏幕,开始录制GIF;松手后,把这段时间内的帧合成GIF。
流程是这样的:
- 用户长按时,开启一个帧缓存队列
- 每解码一帧,把帧数据(缩略图)加入队列
- 用户松手时,停止缓存
- 把队列里的帧传给GIF编码器
- 保存GIF文件
这里有个关键点:帧缓存队列要控制大小。我曾经遇到过一个问题:用户长按了30秒,结果队列里存了300帧,内存直接OOM了。
- 限制最大录制时长,比如5秒
- 降低缓存帧的分辨率,比如缩放到320x180
- 使用LRU缓存,超出限制时丢弃最早的帧
代码实现大致如下:
public class GifRecorder {
private static final int MAX_FRAMES = 50;
private static final int TARGET_WIDTH = 320;
private static final int TARGET_HEIGHT = 180;
private Queue<Bitmap> frameQueue = new LinkedList<>();
private boolean isRecording = false;
public void startRecording() {
isRecording = true;
frameQueue.clear();
}
public void addFrame(Bitmap originalFrame) {
if (!isRecording) return;
// 缩放到目标尺寸
Bitmap scaled = Bitmap.createScaledBitmap(
originalFrame, TARGET_WIDTH, TARGET_HEIGHT, true);
frameQueue.offer(scaled);
// 控制队列大小
if (frameQueue.size() > MAX_FRAMES) {
frameQueue.poll().recycle(); // 回收最旧的帧
}
}
public void stopRecording(String outputPath) {
isRecording = false;
GifEncoder encoder = new GifEncoder();
encoder.start(outputPath);
encoder.setDelay(100);
encoder.setRepeat(0);
for (Bitmap frame : frameQueue) {
encoder.addFrame(frame);
frame.recycle(); // 用完就回收
}
encoder.finish();
frameQueue.clear();
}
}
你看,代码其实不复杂。核心就是:控制内存、控制分辨率、控制帧率。做到这三点,GIF生成功能就稳了。
24.4 性能优化与避坑指南
最后,我把自己踩过的坑总结一下,希望能帮大家少走弯路。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 截图颜色偏绿 | YUV转RGB时颜色空间不对 | 使用正确的转换矩阵,推荐BT.709 |
| GIF文件太大 | 帧分辨率高、颜色数多 | 缩小尺寸、降低颜色深度(256色足够) |
| GIF播放卡顿 | 帧间延迟设置不合理 | 根据帧率计算延迟:delay = 1000 / fps |
| 内存溢出 | 缓存了太多原始帧 | 使用缩略图、限制队列大小 |
嗯,说到颜色偏绿这个问题,我印象特别深。有一次做直播截图功能,截出来的图全是绿的。查了半天才发现是YUV的采样格式搞错了——解码器输出的是NV12,但我按YUV420P去解析,颜色当然不对。
所以大家在做截图时,一定要先确认解码器输出的颜色格式。可以通过 MediaCodec.getOutputFormat() 拿到 KEY_COLOR_FORMAT 字段,常见的值是 COLOR_FormatYUV420SemiPlanar(NV12)或 COLOR_FormatYUV420Planar(I420)。
总结一下今天的内容:
- 截图有三种方案:MediaMetadataRetriever、TextureView.getBitmap()、解码器Buffer拷贝
- GIF生成的核心是控制内存和分辨率
- 颜色格式转换是截图最容易出错的地方
- 帧缓存队列要限制大小,防止OOM
好了,截图和GIF生成就聊到这儿。这两个功能在实际项目中非常实用,希望大家能动手实现一下。遇到问题别慌,先检查颜色格式,再检查内存管理——大部分坑都出在这两个地方。