13、MediaMuxer实战:视频变速(Speed Change)——实现视频的快放或慢放
视频变速,说白了就是让视频播放速度变快或变慢。你刷短视频时看到的「2倍速播放」、「0.5倍慢动作」,底层原理就是今天我们要聊的东西。
我个人习惯把视频变速分成两类:一类是「播放器层面的变速」,只改变播放速率,不生成新文件;另一类是「编辑层面的变速」,要重新编码生成一个变速后的视频文件。咱们今天讲的是后者——用 MediaMuxer 配合 MediaCodec,真正把变速后的视频写出去。
变速的核心原理
视频由一帧一帧的画面组成,帧与帧之间有固定的时间间隔。比如 30fps 的视频,每帧间隔约 33ms。变速的本质,就是改变这个时间间隔的「消费速度」。
举个例子:
- 2倍速快放:原本 10 秒的视频,只取其中 5 秒的帧,播放速度翻倍。
- 0.5倍速慢放:原本 10 秒的视频,把每一帧重复一次,变成 20 秒。
嗯,这里要注意——慢放时如果只是简单重复帧,画面会显得卡顿。真正专业的做法是「帧插值」,但那是另一门学问了。咱们今天先搞定基础的帧重复方案。
核心公式:变速后的时长 = 原始时长 / 速度因子
速度因子 > 1 为快放,速度因子 < 1 为慢放。
整体流程设计
用 MediaMuxer 做视频变速,流程其实不复杂。我画了一张图帮你理清思路:
你看,整个流程就是「解封装 → 解码 → 帧处理 → 编码 → 封装」。其中「帧处理」这一步,就是变速的核心所在。
代码实现:快放与慢放
咱们直接上代码。我先把变速的核心逻辑封装成一个方法:
/**
* 视频变速处理
* @param inputPath 原始视频路径
* @param outputPath 输出视频路径
* @param speed 速度因子(2.0=2倍速,0.5=0.5倍速)
*/
public void changeVideoSpeed(String inputPath, String outputPath, float speed) {
// 1. 初始化 MediaExtractor
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath);
// 2. 找到视频轨道
int videoTrackIndex = -1;
MediaFormat format = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat trackFormat = extractor.getTrackFormat(i);
String mime = trackFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = i;
format = trackFormat;
break;
}
}
extractor.selectTrack(videoTrackIndex);
// 3. 配置解码器
MediaCodec decoder = MediaCodec.createDecoderByType(
format.getString(MediaFormat.KEY_MIME));
decoder.configure(format, null, null, 0);
decoder.start();
// 4. 配置编码器(输出格式与输入保持一致)
MediaCodec encoder = MediaCodec.createEncoderByType(
format.getString(MediaFormat.KEY_MIME));
MediaFormat outputFormat = MediaFormat.createVideoFormat(
format.getString(MediaFormat.KEY_MIME),
format.getInteger(MediaFormat.KEY_WIDTH),
format.getInteger(MediaFormat.KEY_HEIGHT));
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE,
format.getInteger(MediaFormat.KEY_BIT_RATE));
outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE,
(int)(format.getInteger(MediaFormat.KEY_FRAME_RATE) * speed));
outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
// 5. 初始化 MediaMuxer
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int muxerTrackIndex = muxer.addTrack(outputFormat);
muxer.start();
// 6. 帧处理循环(核心变速逻辑)
MediaCodec.BufferInfo decodeInfo = new MediaCodec.BufferInfo();
MediaCodec.BufferInfo encodeInfo = new MediaCodec.BufferInfo();
boolean inputDone = false;
boolean outputDone = false;
long frameCount = 0;
// 用于慢放的帧缓存队列
Queue<Integer> frameQueue = new LinkedList<>();
while (!outputDone) {
// ... 解码、帧处理、编码的完整循环
// 这里省略了标准的 MediaCodec 循环代码
// 重点看下面的帧选择逻辑
}
// 7. 释放资源
decoder.stop();
decoder.release();
encoder.stop();
encoder.release();
muxer.stop();
muxer.release();
extractor.release();
}
代码框架搭好了,咱们重点看帧处理那块。我单独拎出来讲:
帧选择策略:快放时怎么丢帧
快放 2 倍速,意味着每 2 帧里只取 1 帧。我习惯用一个计数器来控制:
// 快放帧选择逻辑
long inputFrameIndex = 0;
while (解码器还有数据) {
if (inputFrameIndex % (int)speed == 0) {
// 这一帧保留,送入编码器
encoder.queueInputBuffer(...);
}
// 否则直接丢弃
inputFrameIndex++;
}
举个例子,speed=2.0 时,保留第 0、2、4、6...帧,丢弃第 1、3、5、7...帧。这样视频时长就缩短了一半。
小技巧:快放时帧率也要跟着调整。比如原始 30fps,2 倍速后输出帧率设为 15fps,这样播放器才能正确播放。否则你虽然丢了帧,但播放器还是按 30fps 播,画面会「跳着走」。
帧选择策略:慢放时怎么补帧
慢放 0.5 倍速,每帧要重复一次。我最初做这个功能时,直接简单重复帧,结果画面有明显的「卡顿感」。后来发现,关键是要保持时间戳的连续性。
// 慢放帧重复逻辑
long originalPts = 解码得到的原始时间戳;
long repeatedPts = originalPts; // 第一份
// 送入编码器
encoder.queueInputBuffer(data, 0, size, repeatedPts, flags);
// 第二份,时间戳增加一帧的间隔
long frameDuration = 1000000 / originalFrameRate; // 微秒
repeatedPts = originalPts + frameDuration;
encoder.queueInputBuffer(data, 0, size, repeatedPts, flags);
为什么要手动加时间戳?因为编码器不会自动帮你算。你重复送同一帧数据,如果不改时间戳,两帧的时间戳一样,播放器会认为它们是同一时刻的帧,直接跳过第二帧。
我曾经踩过的坑:慢放时如果只是简单复制帧数据而不调整 PTS,最终视频时长不变,等于没做慢放。后来排查了半天才发现是时间戳没递增。记住:每一帧送入编码器时,PTS 必须严格递增。
完整的速度因子对照表
| 速度因子 | 效果 | 帧处理策略 | 输出帧率 |
|---|---|---|---|
| 0.25 | 4倍慢放 | 每帧重复 4 次 | 原始帧率 / 4 |
| 0.5 | 2倍慢放 | 每帧重复 2 次 | 原始帧率 / 2 |
| 1.0 | 正常速度 | 不处理 | 原始帧率 |
| 1.5 | 1.5倍快放 | 每 3 帧取 2 帧 | 原始帧率 * 1.5 |
| 2.0 | 2倍快放 | 每 2 帧取 1 帧 | 原始帧率 * 2 |
| 4.0 | 4倍快放 | 每 4 帧取 1 帧 | 原始帧率 * 4 |
你想想看,如果速度因子是 1.5,怎么丢帧?我一般用「累积误差法」:维护一个累积值,每帧加上速度因子,累积值超过 1 就保留这一帧,然后减 1。这样能保证帧分布均匀,不会出现连续丢两帧的情况。
音频怎么办?
嗯,这里要坦白说——视频变速如果不同步处理音频,最终出来的视频会「音画不同步」。音频变速比视频复杂得多,因为音频不能简单丢帧或重复帧,否则声音会变调。
我建议的做法是:
- 快放:音频用 Sonic 或 SoundTouch 库做变速不变调处理
- 慢放:同样用音频处理库,保持音调不变
不过音频变速涉及音频重采样、时域压扩等概念,咱们这个章节先聚焦视频部分。音频的处理,我会在后面的章节单独讲。
重要提醒:如果你只是做「播放器变速」,完全不需要重新编码。直接用 MediaPlayer 的 setPlaybackParams 方法就行。只有当你需要「导出变速后的视频文件」时,才需要用 MediaMuxer 走完整流程。
性能优化建议
我在项目中遇到过一个问题:4K 视频做 4 倍快放时,解码速度跟不上编码速度,导致内存暴涨。后来怎么解决的?
- 控制解码帧数:不要一次性解码所有帧,用生产者-消费者模式,解码一帧处理一帧。
- 使用 Surface 输入:如果只是简单变速,可以用 MediaCodec 的 Surface 模式,减少内存拷贝。
- 异步处理:解码和编码跑在不同的线程,用 BufferQueue 做缓冲。
说白了,视频变速这个功能,代码量不大,但坑不少。尤其是时间戳的处理,稍不注意就会出问题。我建议你写完代码后,用 MediaInfo 工具检查输出视频的帧率和时长,确保符合预期。
好了,这一章的内容就到这里。变速的核心逻辑你已经掌握了,剩下的就是动手实践。记住:快放丢帧,慢放补帧,时间戳一定要递增。
公众号:蓝海资料掘金营,微信deep3321