9、Video 录像流程:MediaRecorder集成、VideoCaptureSession配置、Audio/Video同步、录制状态机与错误处理
录像这个功能,说白了就是把摄像头采集到的画面和麦克风采集到的声音,打包成一个视频文件存下来。听起来简单,但里面坑不少。我最早做录像功能时,以为就是调个MediaRecorder.start()就完事了,结果各种音画不同步、文件损坏、录制中途崩溃的问题,搞得我焦头烂额。
今天我们就来拆解一下Android录像的完整流程。我会从MediaRecorder的集成讲起,再到Camera2的VideoCaptureSession配置,最后聊聊音视频同步和状态机管理。嗯,这些都是实战中绕不开的点。
9.1 MediaRecorder集成:从入门到实战
MediaRecorder是Android官方提供的录制工具。它封装了底层的音视频编码、复用、文件写入等逻辑。你想想看,如果没有它,你得自己处理MediaCodec、MediaMuxer,那代码量就大了去了。
我个人习惯用MediaRecorder配合Camera2来做录像。为什么不用Camera1?因为Camera1在API 21之后就被标记为deprecated了,而且它在多摄像头、高帧率场景下支持不好。
先看一个标准的MediaRecorder初始化流程:
MediaRecorder mediaRecorder = new MediaRecorder();
// 设置音频源 - 麦克风
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 设置视频源 - 摄像头
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
// 设置输出格式 - MP4
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 设置输出文件路径
mediaRecorder.setOutputFile(getOutputFilePath());
// 设置视频编码器 - H.264
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// 设置音频编码器 - AAC
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
// 设置视频参数
mediaRecorder.setVideoSize(1920, 1080);
mediaRecorder.setVideoFrameRate(30);
mediaRecorder.setVideoEncodingBitRate(10_000_000); // 10Mbps
// 设置音频参数
mediaRecorder.setAudioSamplingRate(44100);
mediaRecorder.setAudioEncodingBitRate(128_000);
// 准备
mediaRecorder.prepare();
这里有个关键点:MediaRecorder需要从Camera2获取一个Surface作为视频输入源。这个Surface就是Camera2的CaptureSession的输出目标之一。
9.2 VideoCaptureSession配置:Surface的传递
录像时,Camera2的CaptureSession需要同时输出预览画面和录制画面。预览走的是TextureView的Surface,录制走的是MediaRecorder的Surface。
配置流程大概是这样的:
- 创建MediaRecorder并获取它的Surface
- 创建Camera2的CaptureSession,把预览Surface和录制Surface都加进去
- 启动预览的重复请求
- 开始录制时,把录制Surface对应的请求也加进去
看代码:
// 获取MediaRecorder的Surface
Surface recorderSurface = mediaRecorder.getSurface();
// 创建输出配置列表
List<Surface> outputSurfaces = new ArrayList<>();
outputSurfaces.add(previewSurface); // 预览Surface
outputSurfaces.add(recorderSurface); // 录制Surface
// 创建CaptureSession
cameraDevice.createCaptureSession(outputSurfaces,
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
// 配置成功
captureSession = session;
startPreview(); // 启动预览
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// 配置失败 - 这里要处理
Log.e(TAG, "CaptureSession配置失败");
}
}, null);
启动录制时,我们需要创建一个新的CaptureRequest,把录制Surface作为目标:
// 创建录制用的CaptureRequest
CaptureRequest.Builder recordBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
recordBuilder.addTarget(previewSurface);
recordBuilder.addTarget(recorderSurface);
// 设置自动对焦、自动白平衡等
recordBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
recordBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
// 开始录制
captureSession.setRepeatingRequest(recordBuilder.build(),
null, null);
mediaRecorder.start();
9.3 Audio/Video同步:时间戳的艺术
音视频同步,这是录像里最头疼的问题之一。你想想看,如果画面和声音对不上,用户看视频时会非常难受。
MediaRecorder内部其实已经做了基本的音视频同步。它会把音频和视频的时间戳对齐到同一个时间基线上。但为什么有时候还是会出现不同步呢?
原因主要有两个:
- 音频和视频的时钟源不同:音频用AudioRecord的时钟,视频用Camera2的帧时间戳。这两个时钟可能有偏差。
- 处理延迟不一致:音频处理快,视频处理慢,导致累积偏差。
MediaRecorder的解决方案是:在封装成MP4时,它会根据每个音视频帧的PTS(Presentation Time Stamp)来重新排序和同步。只要你的时间戳是单调递增的,它就能处理好。
但如果你用MediaCodec + MediaMuxer自己实现录制,那就得手动处理同步了。我做过一个项目,需要录制高帧率视频,MediaRecorder满足不了需求,只能自己撸码。那时候我用了下面的策略:
// 视频帧回调
MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
// 音频帧回调
MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
// 同步策略:以视频时间戳为基准
long videoPts = videoInfo.presentationTimeUs;
long audioPts = audioInfo.presentationTimeUs;
// 如果音频落后太多,丢弃音频帧
if (audioPts < videoPts - 100_000) { // 落后超过100ms
// 丢弃这帧音频
continue;
}
// 如果音频超前太多,等待
if (audioPts > videoPts + 100_000) { // 超前超过100ms
// 先写入视频帧
muxer.writeSampleData(videoTrack, byteBuf, videoInfo);
}
9.4 录制状态机与错误处理
MediaRecorder有一个明确的状态机。不理解这个状态机,你很容易写出崩溃的代码。
状态转换如下:
Initial -> Idle -> Initialized -> DataSourceConfigured -> Prepared -> Recording -> Released
每个状态能调用的方法不同。比如:
- 在Initialized状态,可以调用setAudioSource/setVideoSource
- 在DataSourceConfigured状态,可以调用setOutputFormat/setEncoder等
- 在Prepared状态,可以调用start()
- 在Recording状态,可以调用stop()或pause()
如果状态不对就调用方法,会抛IllegalStateException。我曾经在录制过程中不小心调了两次start(),结果直接crash了。
所以,我建议用一个状态变量来跟踪MediaRecorder的状态:
private int recorderState = MediaRecorderState.IDLE;
public void startRecording() {
if (recorderState != MediaRecorderState.PREPARED) {
Log.e(TAG, "状态不对,无法开始录制");
return;
}
mediaRecorder.start();
recorderState = MediaRecorderState.RECORDING;
}
public void stopRecording() {
if (recorderState != MediaRecorderState.RECORDING) {
Log.e(TAG, "不在录制状态,无法停止");
return;
}
try {
mediaRecorder.stop();
recorderState = MediaRecorderState.INITIALIZED;
} catch (RuntimeException e) {
// stop()可能抛异常,比如文件写入失败
Log.e(TAG, "停止录制失败", e);
// 重置MediaRecorder
mediaRecorder.reset();
recorderState = MediaRecorderState.IDLE;
}
}
错误处理方面,MediaRecorder提供了OnErrorListener和OnInfoListener:
mediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
@Override
public void onError(MediaRecorder mr, int what, int extra) {
// 录制出错,比如磁盘空间不足、编码器崩溃等
Log.e(TAG, "录制错误: what=" + what + ", extra=" + extra);
// 停止录制并释放资源
releaseRecorder();
// 通知UI层
notifyRecordingError(what);
}
});
mediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
@Override
public void onInfo(MediaRecorder mr, int what, int extra) {
switch (what) {
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
// 达到最大录制时长
stopRecording();
break;
case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
// 达到最大文件大小
stopRecording();
break;
case MediaRecorder.MEDIA_RECORDER_INFO_NEXT_OUTPUT_FILE_STARTED:
// 开始写入下一个文件(分段录制时)
break;
}
}
});
9.5 整体流程图
下面我用一张SVG图来总结录像的整体流程。从MediaRecorder初始化,到CaptureSession配置,再到录制开始和结束,每一步都有对应的状态和错误处理。
这张图把整个录像流程串起来了。从MediaRecorder初始化开始,到配置参数、获取Surface、创建CaptureSession,然后开始录制,最后停止并释放资源。中间任何一个环节出错,都会触发错误处理回调。
嗯,录像这块内容确实不少。但只要你理解了状态机,掌握了Surface的传递方式,再注意一下音视频同步的细节,基本上就能搞定大部分场景了。我在项目中遇到过最奇葩的问题是在某些低端机上,MediaRecorder的prepare()方法会卡住,后来发现是编码器初始化太慢,加了一个超时机制才解决。
好了,这一章就到这里。记住,录像不是简单的start/stop,背后是一整套状态管理和资源协调的工程。
公众号:蓝海资料掘金营,微信deep3321