1. MediaRecorder.prepare方法深度解析
在Android多媒体开发中,MediaRecorder是处理音视频录制的核心类,而prepare()方法则是整个录制流程中的关键环节。这个看似简单的方法调用背后,隐藏着复杂的系统级交互和状态管理机制。
1.1 prepare方法的核心作用
prepare()方法主要完成三个关键任务:
- 参数校验:检查setAudioSource/setVideoSource等设置的参数是否合法
- 资源分配:为音频编码器、视频编码器分配系统资源
- 管道建立:创建音视频数据从采集到编码的完整通路
在实际项目中,我们遇到过因忽略prepare调用导致的典型问题:
- 直接调用start()会抛出IllegalStateException
- 未prepare就设置参数导致配置不生效
- 跨线程调用prepare引发的线程安全问题
重要提示:prepare()必须在配置完所有参数后调用,且与start()必须在同一线程执行
1.2 典型调用时序分析
标准调用流程应该遵循以下顺序:
java复制MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setOutputFile(outputFile.getAbsolutePath());
try {
recorder.prepare(); // 关键节点
recorder.start();
} catch (IOException e) {
Log.e(TAG, "prepare failed", e);
}
2. 底层调用流程剖析
2.1 Java层到Native层的调用链
prepare()的调用会经过以下关键路径:
- MediaRecorder.java (Java层)
- android_media_MediaRecorder.cpp (JNI层)
- MediaRecorderClient.cpp (Native层)
- StagefrightRecorder.cpp (实际实现)
在这个过程中,系统会创建多个关键对象:
- AudioSource/VideoSource:负责原始数据采集
- MediaWriter:处理数据写入
- 编码器实例:进行音视频编码
2.2 状态机转换机制
MediaRecorder内部维护着严格的状态机,prepare()会触发从INITIALIZED到PREPARED的状态转换。常见的状态包括:
- INITIALIZED:对象已创建
- DATA_SOURCE_CONFIGURED:参数已设置
- PREPARED:准备完成
- RECORDING:正在录制
- ERROR:发生错误
状态转换图如下:
| 当前状态 | 允许操作 | 新状态 |
|---|---|---|
| INITIALIZED | setXXX() | DATA_SOURCE_CONFIGURED |
| DATA_SOURCE_CONFIGURED | prepare() | PREPARED |
| PREPARED | start() | RECORDING |
2.3 资源分配细节
在prepare阶段会进行的关键资源操作:
- 音频子系统:
- 打开音频设备(/dev/snd/*)
- 设置采样率、通道数等参数
- 创建环形缓冲区
- 视频子系统:
- 连接Camera服务
- 配置图像尺寸和帧率
- 初始化预览surface
- 编码器:
- 通过MediaCodec创建编码实例
- 配置编码参数(比特率、GOP等)
- 分配输入/输出缓冲区
3. 实战问题与解决方案
3.1 典型异常处理
案例1:prepare耗时过长
现象:在低端设备上prepare()可能阻塞数秒
解决方案:
java复制// 在子线程执行prepare
final Handler handler = new Handler(Looper.getMainLooper());
new Thread(() -> {
try {
recorder.prepare();
handler.post(() -> {
recorder.start();
});
} catch (IOException e) {
handler.post(() -> {
// 错误处理
});
}
}).start();
案例2:文件权限问题
现象:prepare()抛出IOException
排查步骤:
- 检查输出文件路径是否有效
- 确认应用有存储权限(READ_EXTERNAL_STORAGE)
- 对于Android 10+,需要使用MediaStore API
3.2 性能优化技巧
- 预初始化技巧:
java复制// 应用启动时预先创建实例
MediaRecorder preWarmedRecorder = new MediaRecorder();
preWarmedRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// ...其他基础配置
// 实际使用时重置并快速准备
preWarmedRecorder.reset();
preWarmedRecorder.setOutputFile(newFile);
preWarmedRecorder.prepare(); // 这次会更快
- 参数调优建议:
- 音频:采样率44100Hz,比特率96-128kbps
- 视频:关键帧间隔2秒,比特率根据分辨率调整
- 使用setProfile()快速应用预设配置
4. 高级应用场景
4.1 自定义数据源集成
通过继承MediaRecorder实现自定义数据源:
- 创建自定义AudioSource
java复制public class CustomAudioSource implements AudioRecord.OnRecordPositionUpdateListener {
private AudioRecord audioRecord;
public void configure() {
audioRecord = new AudioRecord(...);
audioRecord.setRecordPositionUpdateListener(this);
}
@Override
public void onPeriodicNotification(AudioRecord recorder) {
// 提供音频数据
}
}
- 通过JNI将数据注入MediaRecorder
4.2 低延迟录制方案
针对直播等场景的优化方案:
- 使用AudioRecord直接采集
- 配置编码器为低延迟模式
java复制MediaFormat format = MediaFormat.createAudioFormat(...);
format.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
- 减小缓冲区大小(但会增加CPU负载)
4.3 多路录制实现
同时录制多个音视频流的方案:
- 主录制流:使用MediaRecorder
- 辅助音频流:通过AudioRecord获取
- 视频截图:通过ImageReader获取
- 使用MediaMuxer混合多路流
关键同步机制:
- 使用PresentationTimeUs保持音视频同步
- 通过SystemClock.elapsedRealtimeNanos()打时间戳
5. 调试与问题排查
5.1 常用调试命令
通过adb获取MediaRecorder状态:
bash复制adb shell dumpsys media.recorder
监控底层资源使用:
bash复制adb shell top -m 10 -d 1 -t
adb shell cat /proc/meminfo
5.2 日志分析技巧
关键日志标签:
- MediaRecorder:基础操作日志
- AudioRecord:音频采集日志
- StagefrightRecorder:核心实现日志
- MediaCodec:编码器相关日志
启用详细日志:
java复制// 在应用启动时设置
System.setProperty("android.media.recorder.debug", "true");
5.3 常见错误代码
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| -38 (ERROR_IO) | 文件IO错误 | 检查存储权限和路径 |
| -32 (ERROR_MALFORMED) | 参数不合法 | 验证音视频参数组合 |
| -100 (ERROR_CANNOT_CONNECT) | 服务连接失败 | 检查mediaserver进程状态 |
6. 兼容性处理
6.1 厂商定制ROM问题
常见厂商差异:
- 华为EMUI:
- 需要额外调用setAudioEncodingBitRate()
- 部分机型不支持高采样率录制
- 小米MIUI:
- 需要申请特殊权限:Manifest.permission.MIUI_RECORD_AUDIO
- 锁屏后可能被限制
- 三星OneUI:
- 视频录制时自动应用场景优化
- 可通过setParameters("cam-mode=1")禁用
6.2 Android版本适配要点
各版本关键变化:
- Android 5.0+: 要求动态权限申请
- Android 8.0: 后台服务限制
- Android 10: 分区存储影响
- Android 12: 精确的麦克风权限
版本兼容代码示例:
java复制if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 使用MediaStore API
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DISPLAY_NAME, "recording.mp4");
Uri uri = context.getContentResolver().insert(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
recorder.setOutputFile(context.getContentResolver().openFileDescriptor(uri, "w"));
} else {
// 传统文件路径方式
recorder.setOutputFile(Environment.getExternalStorageDirectory() + "/recording.mp4");
}
7. 替代方案对比
7.1 Camera2 + MediaCodec方案
与MediaRecorder对比:
-
优点:
- 更精细的控制
- 支持更多高级功能(如手动对焦)
- 更好的性能表现
-
缺点:
- 实现复杂度高
- 需要处理更多底层细节
典型实现结构:
java复制// 创建CameraCaptureSession时配置Surface
Surface encoderSurface = mediaCodec.createInputSurface();
captureSession.setRepeatingRequest(
captureRequestBuilder.build(),
cameraCaptureCallback,
backgroundHandler);
7.2 FFmpeg方案
使用libffmpeg录制的特点:
- 跨平台一致性
- 支持更多格式
- 但性能开销较大
集成示例:
java复制// 通过JNI调用
FFmpegMediaRecorder recorder = new FFmpegMediaRecorder();
recorder.setAudioSource(FFmpegMediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(FFmpegMediaRecorder.OutputFormat.MPEG4);
// ...其他配置
recorder.prepare();
8. 最佳实践总结
经过多个项目的实践验证,我们总结出以下可靠方案:
- 基础录制配置:
java复制public void setupDefaultRecorder() {
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
recorder.setVideoSize(1280, 720);
recorder.setVideoFrameRate(30);
recorder.setVideoEncodingBitRate(5000000);
recorder.setAudioSamplingRate(44100);
recorder.setAudioEncodingBitRate(128000);
}
- 生命周期管理要点:
- 在Activity/Fragment的onPause()中必须释放资源
- 使用WeakReference防止内存泄漏
- 处理配置变更(如屏幕旋转)
- 异常恢复策略:
java复制private void safeStartRecording() {
try {
if (recorder != null) {
recorder.start();
}
} catch (IllegalStateException e) {
Log.w(TAG, "start failed, trying reset", e);
resetRecorder();
setupRecorder();
startRecording();
}
}
在实现一个视频会议功能时,我们发现prepare()的调用时机对首次帧延迟影响很大。通过预初始化+延迟prepare的方案,成功将首帧时间从1200ms降低到400ms。关键是在用户进入会议时就完成基础配置,等到点击录制时才执行prepare和start。
