11. 视频录制进阶:视频尺寸与帧率控制、音频采集与同步、录制文件管理

好,咱们继续往下聊。上一章我们把视频录制的基础流程跑通了,能录,也能存。但说实话,那只是「能跑」的阶段。真正要上生产环境,你会发现一堆坑等着你踩。

这一章,咱们就聊聊进阶的那些事。视频尺寸怎么选?帧率设多少合适?音频怎么同步?录到一半磁盘满了怎么办?

嗯,这些才是实战中真正要面对的问题。

本章核心脉络:从「能录」到「录得好」,再到「录得稳」。

视频录制进阶 视频尺寸与帧率 分辨率选择 帧率适配 码率控制 音频采集与同步 AudioRecord MediaCodec 音画同步 录制文件管理 分段录制 空间管理 文件命名 目标:稳定、流畅、高质量的视频录制 性能与画质平衡 音画精准对齐 存储安全可靠

11.1 视频尺寸与帧率:不是越大越好

很多新手一上来就喜欢设 4K、60fps。觉得参数越高越牛。但实际呢?

我见过一个项目,在低端机上跑 4K 录制,结果手机烫得能煎鸡蛋,录了 3 分钟直接闪退。用户反馈说「录个视频手机差点爆炸」。

所以,尺寸和帧率的选择,本质上是在做一道权衡题。

分辨率怎么选?

Android 支持的摄像头分辨率五花八门。但常用的就这几个:

分辨率 宽高比 适用场景 我个人的建议
1920x1080 (1080p) 16:9 通用录制,社交分享 首选,兼容性最好
3840x2160 (4K) 16:9 专业拍摄,后期裁剪 仅限高端机,注意发热
1280x720 (720p) 16:9 低端机,长时录制 保底方案,稳定优先
640x480 (VGA) 4:3 老设备,特殊需求 尽量别用,画质太差

我的经验:不要直接写死分辨率。应该先通过 CameraCharacteristics 查询设备支持的尺寸列表,然后选一个最接近目标尺寸的。有些设备标称支持 4K,但实际编码器撑不住。

帧率:30fps 还是 60fps?

帧率决定了视频的流畅度。30fps 是行业标准,60fps 适合运动场景。

但这里有个坑:帧率越高,每帧的曝光时间就越短。暗光环境下,60fps 的画面会明显偏暗,噪点也多。

我曾经在一个室内直播项目里,默认设了 60fps。结果用户反馈画面暗得看不清。后来改成 30fps,曝光时间翻倍,画面一下就亮了。

注意:帧率不是想设多少就设多少。必须通过 StreamConfigurationMap.getHighSpeedVideoFpsRanges() 查询设备支持的帧率范围。强行设不支持的帧率,轻则降帧,重则崩溃。

码率控制:画质与文件大小的平衡

码率决定了视频的清晰度。码率太低,画面全是马赛克;码率太高,文件体积爆炸。

我一般用这个公式估算:

// 1080p 30fps 的推荐码率
int bitRate = width * height * fps * 0.07; // 约 8-12 Mbps

// 实际代码
MediaFormat videoFormat = MediaFormat.createVideoFormat(
    MediaFormat.MIME_TYPE_AVC, width, height);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔 1 秒

小技巧:如果录制的是静态场景(比如讲课),码率可以降低 30%。如果是运动场景(比如跑步),码率需要提高 50%。动态调整码率,能省不少存储空间。

11.2 音频采集与同步:音画不同步的噩梦

视频画面再清晰,如果声音对不上,那体验就是灾难级的。

我调试过一个项目,录出来的视频总是声音比画面慢 200ms。排查了两天,最后发现是音频的采样率设成了 44100Hz,但编码器默认用 48000Hz 处理。这种细节,你不踩坑根本想不到。

音频采集:AudioRecord 还是 MediaCodec?

Android 上采集音频,主流方案有两种:

  • AudioRecord:直接采集 PCM 裸数据,灵活度高,但需要自己处理编码。
  • MediaCodec + AudioRecord:采集后直接喂给编码器,省心一些。

我个人习惯用 AudioRecord 采集,然后手动喂给 MediaCodec。原因很简单——我能控制缓冲区的大小,方便做同步。

// 初始化 AudioRecord
int sampleRate = 44100; // 或者 48000
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioRecord audioRecord = new AudioRecord(
    MediaRecorder.AudioSource.MIC,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize
);

// 读取音频数据
byte[] audioData = new byte[bufferSize];
int bytesRead = audioRecord.read(audioData, 0, audioData.length);

避坑指南:我曾经遇到过某些设备上 getMinBufferSize 返回的值偏小,导致音频采集卡顿。后来我手动把 buffer 大小乘以 2,问题就解决了。嗯,Android 碎片化就是这么烦人。

音画同步:时间戳是关键

音画同步的核心,就是给每一帧视频和每一帧音频打上正确的时间戳。

视频的时间戳,用 presentationTimeUs。音频的时间戳,用 ptsUs。两者必须基于同一个时钟源。

我一般这样做:

  1. 录制开始时,记录 System.nanoTime() 作为基准时间。
  2. 视频帧的时间戳 = (当前时间 - 基准时间) / 1000(微秒)。
  3. 音频帧的时间戳 = 已采集的音频样本数 / 采样率 * 1000000(微秒)。
// 视频帧时间戳
long videoTimestamp = (System.nanoTime() - startTime) / 1000;

// 音频帧时间戳
long audioTimestamp = totalAudioSamples * 1000000 / sampleRate;

// 喂给 MediaCodec
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.presentationTimeUs = videoTimestamp; // 或 audioTimestamp
info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
info.size = outputBuffer.remaining();

核心原则:视频和音频的时间戳必须单调递增,且不能有大幅跳变。如果某一帧的时间戳比上一帧还小,编码器会直接丢弃它。

11.3 录制文件管理:别让用户丢了素材

录了半天,结果文件损坏了,或者存储空间满了没提示——这种体验,用户会骂娘的。

文件管理这块,看似简单,其实细节很多。

分段录制:防止单文件过大

长时间录制,单个文件太大,既不利于上传,也不利于编辑。我一般会做分段录制:

  • 每 10 分钟切一个文件。
  • 或者每个文件达到 500MB 时自动切换。
// 分段录制逻辑
private static final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
private static final long MAX_DURATION_MS = 10 * 60 * 1000;  // 10分钟

// 在 MediaCodec 的 output 循环中检查
if (currentFileSize >= MAX_FILE_SIZE || 
    (System.currentTimeMillis() - segmentStartTime) >= MAX_DURATION_MS) {
    // 停止当前 MediaMuxer,创建新的
    stopMuxer();
    startNewMuxer();
    segmentStartTime = System.currentTimeMillis();
    currentFileSize = 0;
}

我的习惯:分段时,文件名加上序号和时间戳。比如 video_20250101_143000_01.mp4。这样即使文件乱了,也能按时间排序找回来。

空间管理:提前预警

录制到一半,磁盘满了,那这段视频就废了。所以必须在录制前和录制中检查剩余空间。

// 检查剩余空间
File storageDir = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
long freeBytes = storageDir.getFreeSpace();

// 低于 200MB 时警告
if (freeBytes < 200 * 1024 * 1024) {
    // 弹窗提示用户清理空间
    showWarningDialog("存储空间不足,请清理后再录制");
}

// 低于 50MB 时强制停止
if (freeBytes < 50 * 1024 * 1024) {
    stopRecording();
    showErrorDialog("存储空间已满,录制已停止");
}

曾经踩过的坑:有一次我忘了检查 getFreeSpace()getUsableSpace() 的区别。前者返回的是名义上的剩余空间,后者才是应用真正能用的。结果在某个定制 ROM 上,明明显示还有 1GB,但写入时却报空间不足。后来统一改用 getUsableSpace(),再没出过问题。

文件命名与清理

录制文件多了,管理起来也麻烦。我一般会做这几件事:

  • 文件名包含日期、时间、序号,方便检索。
  • 录制完成后,生成一个同名的 .json 文件,记录元数据(分辨率、帧率、时长等)。
  • 定期清理 7 天前的临时文件。
// 文件命名
String fileName = String.format(
    "VID_%s_%03d.mp4",
    new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(new Date()),
    segmentIndex
);

// 清理旧文件
File[] files = storageDir.listFiles();
long now = System.currentTimeMillis();
for (File file : files) {
    if (file.lastModified() < now - 7 * 24 * 60 * 60 * 1000L) {
        file.delete();
    }
}

总结一下:视频录制进阶,说白了就是跟细节较劲。分辨率选对了,帧率调好了,音频同步了,文件管理稳了,你的录制功能才算真正能打。别嫌麻烦,这些坑我替你们踩过了,照着做就行。


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