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;

关键点:时间缩放不是简单地丢弃帧。如果直接丢帧,画面会跳帧,看起来不流畅。更好的做法是用MediaCodecreleaseOutputBuffer控制显示时机,或者用OpenGL做帧插值。

21.6 慢动作视频的完整流程

我把整个流程画了张图,方便你理解:

慢动作视频完整流程 1. 硬件能力检测 查询高速帧率范围 获取支持的分辨率 2. 配置高速会话 createHighSpeedSession 设置FPS Range 3. 高速录制 Burst请求列表 MediaCodec编码 4. 存储视频文件 MP4容器 保留原始帧率信息 5. 慢动作播放 时间戳缩放 帧率转换/插值 6. 渲染显示 SurfaceView/TextureView 同步音频(如有) 关键:高速录制时帧率是播放时的N倍,播放时通过时间缩放实现慢动作效果 录制: 120fps 播放: 30fps 慢放: 4x

21.7 避坑指南

做慢动作功能,我踩过的坑不少,挑几个典型的说说:

坑1:音频不同步

我曾经做过一个慢动作录制功能,视频慢放了4倍,但音频还是正常速度播放,结果声音和画面完全对不上。解决方案是:慢动作视频要么不录音频,要么在播放时对音频做时间拉伸(用SoundPool或AudioTrack的变速播放)。

坑2:预览卡顿

高速录制时,预览也要跟着走。但预览Surface的帧率如果跟不上录制帧率,就会出现预览卡顿但录制正常的情况。我的做法是:预览用单独的线程,降低预览帧率(比如30fps),录制保持高速。两者通过不同的Surface分开处理。

坑3:内存暴涨

高速录制时,每一帧的数据量都很大。如果编码器处理不过来,帧就会在内存里堆积。我遇到过内存涨到200MB以上导致OOM的情况。后来加了帧率监控,如果编码器队列超过一定数量,就主动丢帧。

21.8 性能优化建议

最后给几个实用的优化建议:

  • 使用SurfaceView而不是TextureView:SurfaceView有独立的合成层,性能更好,延迟更低
  • 控制编码器输入队列:不要无限制地往编码器塞数据,用dequeueInputBuffer的超时机制来控制
  • 考虑硬件编码器限制:有些芯片的编码器不支持120fps的实时编码,需要降级到60fps
  • 录制时关闭不必要的处理:比如美颜、滤镜等,这些会增加处理延迟,影响帧率稳定性

我的习惯:在开发阶段,我会在录制的每一帧上打时间戳,然后统计实际帧率。如果实际帧率达不到目标值,就降低分辨率或者帧率。这个监控逻辑在生产环境也可以保留,用于异常上报。

慢动作视频这块,说白了就是「硬件能力 + 软件配置 + 编码优化」三者的平衡。硬件决定了上限,软件配置决定了能不能达到这个上限,编码优化决定了最终效果。把这三点吃透了,慢动作功能基本就稳了。

公众号:蓝海资料掘金营,微信 deep3321