21. 慢动作视频:高速帧率配置、慢动作视频录制、播放时的时间缩放
慢动作视频,说白了就是「拍得快,放得慢」。你想想看,正常视频一秒拍30帧,播放也是30帧。但如果我们一秒拍120帧,播放时还是按30帧来放,那时间就被拉长了4倍——这就是慢动作的本质。
我在项目中做过不少慢动作相关的功能,从早期的HTC手机到后来的旗舰机型,踩过的坑还真不少。今天我就把这块的经验好好梳理一下。
21.1 高速帧率的硬件限制
首先得认清一个现实:不是所有摄像头都支持高速帧率。我遇到过产品经理拿着iPhone说「人家能拍240fps,我们为什么不行?」——嗯,这真不是软件能解决的。
高速帧率对硬件的要求:
- 传感器读取速度:CMOS传感器需要足够快的读出速度,否则帧率上不去
- ISP处理能力:每秒要处理的数据量暴增,ISP得扛得住
- 带宽限制:MIPI接口的带宽是固定的,高帧率往往意味着降低分辨率
- 内存带宽:写入速度要跟上,否则丢帧
核心规律:帧率每翻一倍,数据量就翻一倍。120fps是30fps的4倍数据量,240fps就是8倍。所以高帧率通常伴随着分辨率下降——比如1080p@120fps,或者720p@240fps。
21.2 Camera2 API中的帧率配置
在Android上做慢动作,绕不开Camera2 API。我个人习惯用StreamConfigurationMap来查询设备支持的能力。
// 查询支持的帧率范围
StreamConfigurationMap configMap = cameraCharacteristics
.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// 针对特定输出格式和尺寸,查询支持的帧率范围
Range<Integer>[] fpsRanges = configMap.getHighSpeedVideoFpsRanges();
for (Range<Integer> range : fpsRanges) {
Log.d(TAG, "支持的高速帧率范围: " + range);
}
// 或者查询特定分辨率的帧率
Size[] highSpeedSizes = configMap.getHighSpeedVideoSizes();
for (Size size : highSpeedSizes) {
Range<Integer>[] fpsForSize = configMap
.getHighSpeedVideoFpsRangesFor(size);
Log.d(TAG, size + " 支持的帧率: " + Arrays.toString(fpsForSize));
}
这里有个坑——高速帧率必须使用特定的CaptureRequest模板。我曾经直接用了TEMPLATE_PREVIEW去配120fps,结果画面卡成PPT。后来才发现要用TEMPLATE_RECORD或者专门的TEMPLATE_VIDEO_SNAPSHOT。
注意:高速帧率模式下,必须使用CameraDevice.createConstrainedHighSpeedCaptureSession()来创建CaptureSession,而不是普通的createCaptureSession()。否则会抛出IllegalArgumentException。
21.3 高速帧率的录制配置
配置高速录制时,有几个关键参数要设置对:
// 创建高速录制专用的CaptureRequest
CaptureRequest.Builder requestBuilder = cameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
// 设置目标帧率
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<>(120, 120));
// 设置高速视频模式
requestBuilder.set(CaptureRequest.CONTROL_MODE,
CaptureRequest.CONTROL_MODE_AUTO);
// 对于高速录制,建议关闭自动曝光锁定
requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false);
// 创建高速会话
List<Surface> surfaces = new ArrayList<>();
surfaces.add(previewSurface);
surfaces.add(recorderSurface);
cameraDevice.createConstrainedHighSpeedCaptureSession(
surfaces,
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
// 高速录制需要创建Burst请求列表
List<CaptureRequest> burstRequests =
session.createHighSpeedRequestList(requestBuilder.build());
session.setRepeatingBurst(burstRequests, callback, handler);
}
},
handler
);
为什么用setRepeatingBurst而不是setRepeatingRequest?因为高速模式下,底层需要把多个请求打包成burst来保证帧率稳定。我刚开始没注意这个,结果录出来的视频帧率忽高忽低,慢放时一卡一卡的。
21.4 MediaCodec编码器的配置
录制的另一头是编码器。高速帧率意味着编码器要处理更多帧,配置上也有讲究:
| 参数 | 普通录制 | 高速录制 |
|---|---|---|
| 帧率 | 30fps | 120fps / 240fps |
| 比特率 | 10-20 Mbps | 30-60 Mbps(需要更高) |
| I帧间隔 | 1秒(30帧) | 1秒(120帧) |
| 编码器模式 | CBR | CBR 或 VBR |
MediaFormat format = MediaFormat.createVideoFormat(
MIME_TYPE, width, height);
// 关键:设置帧率为目标高速帧率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 120);
format.setInteger(MediaFormat.KEY_BIT_RATE, 40_000_000); // 40Mbps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
// 高速录制建议使用更高profile
format.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
format.setInteger(MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.AVCLevel4_2);
经验之谈:比特率设置太低,高速画面会出现大量马赛克。我一般按「帧率/30 * 10Mbps」来估算基础比特率,120fps就是40Mbps左右。如果画面运动剧烈,还得再加50%。
21.5 慢动作视频的播放时间缩放
视频录好了,怎么播放?这里有两种思路:
- 实时缩放:播放器直接按30fps播放120fps的视频,相当于4倍慢放
- 预处理缩放:用MediaCodec解码后,通过时间戳映射来调整播放速度
我个人更推荐第二种,因为灵活性更高。你可以实现「部分慢动作」——比如视频前3秒正常速度,中间5秒慢动作,后面恢复正常。
// 使用MediaCodec进行时间缩放的核心思路
// 1. 解码时记录每个帧的原始时间戳
// 2. 根据目标播放速度,重新计算时间戳
// 3. 用新时间戳渲染
// 假设原始帧率120fps,目标播放帧率30fps
// 那么每4帧中只取1帧显示,或者用插值生成中间帧
// 简单的时间缩放实现
long originalTimestamp = bufferInfo.presentationTimeUs;
long scaledTimestamp;
if (isSlowMotion) {
// 4倍慢放:时间戳放大4倍
scaledTimestamp = originalTimestamp * 4;
} else {
scaledTimestamp = originalTimestamp;
}
// 设置新的时间戳
bufferInfo.presentationTimeUs = scaledTimestamp;
关键点:时间缩放不是简单地丢弃帧。如果直接丢帧,画面会跳帧,看起来不流畅。更好的做法是用MediaCodec的releaseOutputBuffer控制显示时机,或者用OpenGL做帧插值。
21.6 慢动作视频的完整流程
我把整个流程画了张图,方便你理解:
21.7 避坑指南
做慢动作功能,我踩过的坑不少,挑几个典型的说说:
坑1:音频不同步
我曾经做过一个慢动作录制功能,视频慢放了4倍,但音频还是正常速度播放,结果声音和画面完全对不上。解决方案是:慢动作视频要么不录音频,要么在播放时对音频做时间拉伸(用SoundPool或AudioTrack的变速播放)。
坑2:预览卡顿
高速录制时,预览也要跟着走。但预览Surface的帧率如果跟不上录制帧率,就会出现预览卡顿但录制正常的情况。我的做法是:预览用单独的线程,降低预览帧率(比如30fps),录制保持高速。两者通过不同的Surface分开处理。
坑3:内存暴涨
高速录制时,每一帧的数据量都很大。如果编码器处理不过来,帧就会在内存里堆积。我遇到过内存涨到200MB以上导致OOM的情况。后来加了帧率监控,如果编码器队列超过一定数量,就主动丢帧。
21.8 性能优化建议
最后给几个实用的优化建议:
- 使用SurfaceView而不是TextureView:SurfaceView有独立的合成层,性能更好,延迟更低
- 控制编码器输入队列:不要无限制地往编码器塞数据,用
dequeueInputBuffer的超时机制来控制 - 考虑硬件编码器限制:有些芯片的编码器不支持120fps的实时编码,需要降级到60fps
- 录制时关闭不必要的处理:比如美颜、滤镜等,这些会增加处理延迟,影响帧率稳定性
我的习惯:在开发阶段,我会在录制的每一帧上打时间戳,然后统计实际帧率。如果实际帧率达不到目标值,就降低分辨率或者帧率。这个监控逻辑在生产环境也可以保留,用于异常上报。
慢动作视频这块,说白了就是「硬件能力 + 软件配置 + 编码优化」三者的平衡。硬件决定了上限,软件配置决定了能不能达到这个上限,编码优化决定了最终效果。把这三点吃透了,慢动作功能基本就稳了。