11. 视频录制进阶:视频尺寸与帧率控制、音频采集与同步、录制文件管理
好,咱们继续往下聊。上一章我们把视频录制的基础流程跑通了,能录,也能存。但说实话,那只是「能跑」的阶段。真正要上生产环境,你会发现一堆坑等着你踩。
这一章,咱们就聊聊进阶的那些事。视频尺寸怎么选?帧率设多少合适?音频怎么同步?录到一半磁盘满了怎么办?
嗯,这些才是实战中真正要面对的问题。
本章核心脉络:从「能录」到「录得好」,再到「录得稳」。
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。两者必须基于同一个时钟源。
我一般这样做:
- 录制开始时,记录
System.nanoTime()作为基准时间。 - 视频帧的时间戳 = (当前时间 - 基准时间) / 1000(微秒)。
- 音频帧的时间戳 = 已采集的音频样本数 / 采样率 * 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();
}
}
总结一下:视频录制进阶,说白了就是跟细节较劲。分辨率选对了,帧率调好了,音频同步了,文件管理稳了,你的录制功能才算真正能打。别嫌麻烦,这些坑我替你们踩过了,照着做就行。