以下是基于 Camera2 API + MediaCodec + MediaMuxer 实现摄像头画面编码到 MP4 文件的全链路代码及 Mermaid 流程图,结合最佳实践和关键注意事项。
📽️ 全流程代码实现(Activity 级)
1. 权限声明与初始化
xml
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2. Activity 核心代码
scss
public class CameraRecorderActivity extends AppCompatActivity {
private CameraDevice cameraDevice;
private MediaCodec mediaCodec;
private MediaMuxer mediaMuxer;
private Surface encoderSurface;
private int videoTrackIndex = -1;
private boolean isMuxerStarted = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
// 1. 初始化 MediaCodec 和 MediaMuxer
initMediaCodec(1280, 720); // 分辨率 720p
// 2. 检查相机权限(代码略)
}
// 初始化编码器与混合器
private void initMediaCodec(int width, int height) {
try {
// 配置 MediaCodec
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000); // 4 Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // Surface输入
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoderSurface = mediaCodec.createInputSurface(); // 获取编码器输入Surface
mediaCodec.start();
// 初始化 MediaMuxer
String outputPath = getExternalFilesDir(null) + "/video.mp4";
mediaMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 设置异步回调
mediaCodec.setCallback(new MediaCodecCallback());
} catch (IOException e) {
e.printStackTrace();
}
}
// 3. 启动 Camera2 会话
private void startCameraSession() {
CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
try {
String cameraId = manager.getCameraIdList()[0];
manager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera;
// 创建预览 + 编码的 Surface 列表
List<Surface> surfaces = new ArrayList<>();
surfaces.add(previewSurface); // 预览Surface(TextureView)
surfaces.add(encoderSurface); // 编码器Surface
// 创建捕获会话
cameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
// 开始连续预览请求
try {
CaptureRequest request = session.getDevice().createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW);
request.addTarget(previewSurface);
request.addTarget(encoderSurface); // 同时输出到编码器
session.setRepeatingRequest(request.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}, null);
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
// 4. MediaCodec 异步回调处理编码数据
private class MediaCodecCallback extends MediaCodec.Callback {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {} // Surface模式无需实现
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
ByteBuffer buffer = codec.getOutputBuffer(index);
if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
// 忽略编解码配置数据
codec.releaseOutputBuffer(index, false);
return;
}
if (buffer != null && info.size > 0) {
buffer.position(info.offset);
buffer.limit(info.offset + info.size);
if (isMuxerStarted) {
mediaMuxer.writeSampleData(videoTrackIndex, buffer, info);
}
}
codec.releaseOutputBuffer(index, false);
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// 编码器格式变化时启动 Muxer
videoTrackIndex = mediaMuxer.addTrack(format);
mediaMuxer.start();
isMuxerStarted = true;
}
}
// 5. 释放资源
private void releaseResources() {
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if (mediaMuxer != null) {
if (isMuxerStarted) {
mediaMuxer.stop();
}
mediaMuxer.release();
}
}
@Override
protected void onDestroy() {
releaseResources();
super.onDestroy();
}
}
📊 Mermaid 流程图
flowchart TD
A[启动Activity] --> B[初始化MediaCodec]
B --> C[创建编码器输入Surface]
C --> D[启动Camera2会话]
D -->|"绑定预览Surface + 编码Surface"| E[摄像头数据流]
E -->|"YUV帧→编码器"| F[MediaCodec编码H.264]
F --> G{编码输出回调}
G -->|"INFO_OUTPUT_FORMAT_CHANGED"| H[启动MediaMuxer]
G -->|"有效数据帧"| I[写入MP4文件]
H --> I
I --> J[释放缓冲区]
J --> G
K[停止录制] --> L[释放Camera/Codec/Muxer]
⚠️ 关键技术与注意事项
-
颜色格式匹配
- Camera2 默认输出
YUV_420_888
,需通过COLOR_FormatSurface
直接输入编码器,避免手动转换。
- Camera2 默认输出
-
MediaMuxer 启动时机
- 必须在
onOutputFormatChanged()
回调中启动 Muxer,否则写入失败。
- 必须在
-
时间戳同步
- Camera2 自动生成帧时间戳(
System.nanoTime()
),无需手动计算。
- Camera2 自动生成帧时间戳(
-
资源释放顺序
scsscameraDevice.close() → mediaCodec.stop() → mediaMuxer.stop() → mediaCodec.release() → mediaMuxer.release()
反向操作会导致崩溃或文件损坏。
-
低端设备优化
- 降低分辨率(如 720p→480p)或使用
BITRATE_MODE_CQ
(质量优先模式)。
- 降低分辨率(如 720p→480p)或使用
💡 扩展场景方案
- 音视频同步录制 :
添加AudioRecord
+MediaCodec
音频编码,在 Muxer 中同步写入音轨。 - 屏幕录制 :
替换 Camera2 为MediaProjection
,通过VirtualDisplay
绑定到encoderSurface
。 - 实时直播 :
将MediaMuxer
替换为 RTMP 封包库(如libRtmp
),直接推送编码数据。
完整可运行项目参考:Google Grafika 示例 或 AudioVideoRecordingSample。