引言:录个视频,比想象中复杂得多
"录个视频有什么难的?"------这句话,每个第一次在Android上实现录像功能的开发者大概都说过,然后花了一整天踩坑。
MediaRecorder的调用顺序稍有错乱就会抛IllegalStateException;忘记添加录像Surface到CaptureSession,画面一片黑;音视频不同步导致口型对不上;录到一半触发温度降频......
本文系统梳理Camera2录像的两条实现路径:MediaRecorder高层封装 (快速上手)和MediaCodec+MediaMuxer手动控制(灵活精细),帮你把每个坑都提前踩掉。
一、两种录像方案的选择
Android提供两种录像方案,各有适用场景:
| 对比项 | MediaRecorder | MediaCodec + MediaMuxer |
|---|---|---|
| 使用复杂度 | 低(10行代码起步) | 高(需要手动管理Buffer) |
| 灵活性 | 低(参数有限) | 高(完全控制编码参数) |
| 暂停/续录 | Android 7.0+ 支持 | 自行实现(更简单) |
| 实时帧处理 | 不支持 | 支持(可修改每一帧) |
| 自定义编码格式 | 不支持 | 支持(自定义Codec) |
| 适用场景 | 常规录像App | 短视频滤镜、直播推流、特殊格式 |
如果你只是做一个普通的相机录像功能,用MediaRecorder就够了。如果你需要录像中叠加水印、实时变速、或者推RTMP直播流,选MediaCodec+MediaMuxer。
二、MediaRecorder录像:Camera2集成
2.1 MediaRecorder严格的调用顺序
MediaRecorder有一个让无数开发者踩过的坑:方法调用顺序必须严格遵守 ,否则抛IllegalStateException,错误信息通常是setXxx called in an invalid state。
正确顺序如下:
scss
new MediaRecorder()
→ setAudioSource()
→ setVideoSource()
→ setOutputFormat() ← 必须在 setAudioEncoder/setVideoEncoder 之前
→ setVideoSize()
→ setVideoEncoder()
→ setAudioEncoder()
→ setVideoEncodingBitRate()
→ setVideoFrameRate()
→ setOutputFile()
→ prepare() ← 之后 getSurface() 才可用
→ start()
[录像中...]
→ pause() / resume()
→ stop()
→ reset() / release()
关键约束 :getSurface()必须在prepare()之后调用,且必须在createCaptureSession()之前获取,因为Surface需要提前传入Session配置。
2.2 完整的MediaRecorder配置
java
private MediaRecorder mMediaRecorder;
private File mVideoFile;
private void setupMediaRecorder() throws IOException {
mMediaRecorder = new MediaRecorder(this);
// 1. 设置音频源(麦克风)
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 2. 设置视频源(Surface,Camera2专用模式)
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
// 3. 设置输出格式(必须在 setEncoder 之前)
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 4. 视频参数
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setVideoEncodingBitRate(10_000_000); // 10 Mbps
mMediaRecorder.setVideoFrameRate(30);
// I帧间隔:1秒一个关键帧(对于直播应设置更短,如0.5s)
// 注意:这里的单位是秒
// 实际上 MediaRecorder 没有直接设置 I帧间隔的API
// 需要通过 MediaCodec 手动控制
// 5. 音频参数
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioEncodingBitRate(128_000); // 128 kbps
mMediaRecorder.setAudioSamplingRate(44100);
// 6. 输出文件(Android 10+ 推荐写入 MediaStore,此处先写临时文件)
mVideoFile = createVideoFile();
mMediaRecorder.setOutputFile(mVideoFile.getAbsolutePath());
// 7. 视频方向(处理传感器旋转,确保视频朝向正确)
int rotation = getWindowManager().getDefaultDisplay().getRotation();
int sensorOrientation = mCameraCharacteristics.get(
CameraCharacteristics.SENSOR_ORIENTATION);
mMediaRecorder.setOrientationHint(getVideoOrientation(sensorOrientation, rotation));
// 8. 准备(之后才能调用 getSurface())
mMediaRecorder.prepare();
}
// 根据传感器方向和设备旋转计算视频方向
private int getVideoOrientation(int sensorOrientation, int deviceRotation) {
int[] ORIENTATIONS = {90, 0, 270, 180};
int deviceDegrees = ORIENTATIONS[deviceRotation];
// 后置摄像头
return (sensorOrientation + deviceDegrees) % 360;
}
2.3 录像专用的CaptureSession
Camera2录像的关键:预览Surface和录像Surface必须同时加入同一个CaptureSession,Camera才能同时输出到两个目标。
java
private void createVideoSession() {
try {
// prepare() 之后才能 getSurface()
Surface recorderSurface = mMediaRecorder.getSurface();
SurfaceTexture texture = mTextureView.getSurfaceTexture();
texture.setDefaultBufferSize(mPreviewSize.getWidth(),
mPreviewSize.getHeight());
Surface previewSurface = new Surface(texture);
// 同时传入预览Surface和录像Surface
mCameraDevice.createCaptureSession(
Arrays.asList(previewSurface, recorderSurface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCaptureSession = session;
updatePreview(); // 先启动预览
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Log.e(TAG, "Session配置失败");
}
},
mBackgroundHandler
);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
2.4 使用TEMPLATE_RECORD启动录像
java
private void startRecordingVideo() {
try {
// 使用 TEMPLATE_RECORD 模板(针对录像场景优化的3A策略)
// vs TEMPLATE_PREVIEW:TEMPLATE_RECORD 减少闪烁,AE算法更保守
CaptureRequest.Builder mRecorderRequestBuilder = mCameraDevice
.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
// 同时输出到预览和录像两个 Surface
Surface previewSurface = new Surface(mTextureView.getSurfaceTexture());
mRecorderRequestBuilder.addTarget(previewSurface);
mRecorderRequestBuilder.addTarget(mMediaRecorder.getSurface());
// 设置连续对焦模式(录像推荐使用视频对焦模式)
mRecorderRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
// 可选:开启视频防抖
mRecorderRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
// 固定帧率(避免录像帧率波动导致卡顿)
mRecorderRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<>(30, 30));
// 开始录像循环请求
mCaptureSession.setRepeatingRequest(
mRecorderRequestBuilder.build(), null, mBackgroundHandler);
// 启动 MediaRecorder
mMediaRecorder.start();
mIsRecording = true;
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
下图展示了完整的录像链路:

值得注意的是,预览和录像数据同时从HAL3输出,分别流入各自的Surface,互不干扰------这也是为什么录像画面质量和预览画面质量可以不同(录像用更高分辨率,预览用较低分辨率)。
2.5 停止录像
java
private void stopRecordingVideo() {
mIsRecording = false;
// 停止 MediaRecorder(顺序很重要!必须先 stop 再 reset)
try {
mMediaRecorder.stop();
} catch (RuntimeException e) {
// 如果录像时间太短(<1秒),stop()会抛异常
// 此时文件可能是空的或损坏的,需要删除
Log.e(TAG, "录像时间过短,文件可能已损坏", e);
mVideoFile.delete();
}
mMediaRecorder.reset();
// 恢复到普通预览请求
updatePreview();
// 将临时文件写入 MediaStore
saveVideoToMediaStore(mVideoFile);
}
private void saveVideoToMediaStore(File videoFile) {
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DISPLAY_NAME,
"VID_" + System.currentTimeMillis() + ".mp4");
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
values.put(MediaStore.Video.Media.RELATIVE_PATH,
Environment.DIRECTORY_MOVIES + "/MyCamera");
values.put(MediaStore.Video.Media.DURATION,
getVideoDuration(videoFile)); // 毫秒
ContentResolver resolver = getContentResolver();
Uri uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream os = resolver.openOutputStream(uri)) {
Files.copy(videoFile.toPath(), os);
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(uri, values, null, null);
} catch (IOException e) {
Log.e(TAG, "保存视频失败", e);
}
}
}
2.6 暂停与续录(Android 7.0+)
Android 7.0 (API 24) 开始支持暂停/恢复录制,且续录的视频和暂停前是同一个文件,无需手动合并:
java
// 暂停录制
@RequiresApi(api = Build.VERSION_CODES.N)
private void pauseRecording() {
if (mIsRecording) {
mMediaRecorder.pause();
mIsPaused = true;
}
}
// 恢复录制
@RequiresApi(api = Build.VERSION_CODES.N)
private void resumeRecording() {
if (mIsPaused) {
mMediaRecorder.resume();
mIsPaused = false;
}
}
注意:暂停期间Camera仍在输出帧(预览不中断),但MediaRecorder不记录这段时间,所以恢复后视频中不会有跳帧的感觉------MediaRecorder内部会处理时间戳连续性问题。
三、录像参数调优
3.1 码率与分辨率的选择
不同录像场景的推荐参数:
| 场景 | 分辨率 | 帧率 | 视频码率 | 音频码率 |
|---|---|---|---|---|
| 日常记录 | 1080p | 30fps | 8 Mbps | 128 kbps |
| 高质量 | 4K | 30fps | 50 Mbps | 256 kbps |
| 慢动作 | 1080p | 120fps | 40 Mbps | --- |
| 会议录制 | 720p | 30fps | 4 Mbps | 64 kbps |
| 直播推流 | 720p | 30fps | 1-2 Mbps | 64 kbps |
java
// 动态调整码率(根据分辨率自动计算)
private int calculateBitRate(Size videoSize) {
// 经验公式:像素数 × 帧率 × 系数(高质量0.15,普通0.1)
long pixels = (long) videoSize.getWidth() * videoSize.getHeight();
return (int) (pixels * 30 * 0.12); // ~8Mbps for 1080p30
}
3.2 查询设备支持的录像能力
java
private void queryVideoCapabilities() {
CamcorderProfile profile;
// 查询是否支持4K
if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_2160P)) {
profile = CamcorderProfile.get(CamcorderProfile.QUALITY_2160P);
Log.d(TAG, "支持4K录像,默认码率:" + profile.videoBitRate + " bps");
}
// 查询是否支持高帧率(慢动作)
if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_HIGH_SPEED_1080P)) {
profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH_SPEED_1080P);
Log.d(TAG, "支持1080p高帧率,最大帧率:" + profile.videoFrameRate);
}
// 通过 Camera2 API 精确查询高速帧率范围
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Range<Integer>[] highSpeedFpsRanges = map.getHighSpeedVideoFpsRanges();
if (highSpeedFpsRanges != null) {
for (Range<Integer> range : highSpeedFpsRanges) {
Log.d(TAG, "支持高速帧率范围:" + range);
}
}
}
3.3 视频防抖配置
Camera2支持两种防抖方式:
java
// 1. 电子防抖 (EIS, Electronic Image Stabilization)
// 在设备方向传感器辅助下,裁切画面边缘来补偿抖动
// 代价:轻微裁切视野(通常~10%)
mRecorderRequestBuilder.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE_ON);
// 2. 光学防抖 (OIS, Optical Image Stabilization)
// 移动镜头组来补偿抖动,不裁切画面
// 只有支持OIS的镜头才有此选项
mRecorderRequestBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE,
CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE_ON);
// 查询设备支持哪种防抖
int[] oisModes = mCameraCharacteristics.get(
CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION);
boolean supportsOIS = oisModes != null && oisModes.length > 0;
四、手动录像:MediaCodec + MediaMuxer
当你需要对编码过程进行精细控制时(比如实时加滤镜、自定义码率控制、推流),MediaCodec + MediaMuxer是更好的选择。
4.1 架构设计
下图展示了手动录像方案的完整架构:

视频和音频分别走独立的编码路径,最终由MediaMuxer合并成MP4文件。关键是音视频PTS时间戳对齐,否则会出现音画不同步。
4.2 视频编码器配置
java
private static final String MIME_TYPE_VIDEO = "video/avc"; // H.264
private static final String MIME_TYPE_AUDIO = "audio/mp4a-latm"; // AAC
private static final int FRAME_RATE = 30;
private static final int I_FRAME_INTERVAL = 1; // 1秒一个关键帧
private MediaCodec mVideoEncoder;
private Surface mInputSurface; // 相机数据输入Surface
private void setupVideoEncoder(int width, int height) throws IOException {
MediaFormat videoFormat = MediaFormat.createVideoFormat(
MIME_TYPE_VIDEO, width, height);
// 基本参数
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, calculateBitRate(width, height));
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
// 颜色格式:Surface输入模式固定使用这个值
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 码率控制模式(Android 8.0+)
// BITRATE_MODE_VBR: 可变码率,质量更好
// BITRATE_MODE_CBR: 固定码率,适合直播
videoFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
// Profile/Level(影响兼容性)
videoFormat.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
mVideoEncoder = MediaCodec.createEncoderByType(MIME_TYPE_VIDEO);
mVideoEncoder.configure(videoFormat, null, null,
MediaCodec.CONFIGURE_FLAG_ENCODE);
// 创建输入Surface(相机数据将直接渲染到这里,零拷贝)
mInputSurface = mVideoEncoder.createInputSurface();
mVideoEncoder.start();
}
4.3 音频编码器配置
java
private MediaCodec mAudioEncoder;
private AudioRecord mAudioRecord;
private static final int SAMPLE_RATE = 44100;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private void setupAudioEncoder() throws IOException {
MediaFormat audioFormat = MediaFormat.createAudioFormat(
MIME_TYPE_AUDIO, SAMPLE_RATE, 1); // 1 channel = mono
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128_000);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
mAudioEncoder = MediaCodec.createEncoderByType(MIME_TYPE_AUDIO);
mAudioEncoder.configure(audioFormat, null, null,
MediaCodec.CONFIGURE_FLAG_ENCODE);
mAudioEncoder.start();
// 配置 AudioRecord 用于麦克风录音
int minBufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
mAudioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT,
minBufferSize * 2);
}
4.4 MediaMuxer与音视频同步
MediaMuxer负责将编码后的视频流和音频流封装成MP4文件,关键是保证音视频的PTS(Presentation Time Stamp)对齐:
java
private MediaMuxer mMuxer;
private int mVideoTrackIndex = -1;
private int mAudioTrackIndex = -1;
private boolean mMuxerStarted = false;
private void setupMuxer(String outputPath) throws IOException {
mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
// 视频编码线程:将编码后的数据写入Muxer
private void drainVideoEncoder() {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (true) {
int outputBufferId = mVideoEncoder.dequeueOutputBuffer(bufferInfo, 10_000);
if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 格式已确定,注册视频轨道
MediaFormat newFormat = mVideoEncoder.getOutputFormat();
mVideoTrackIndex = mMuxer.addTrack(newFormat);
checkAndStartMuxer(); // 等待音视频轨道都注册完毕
} else if (outputBufferId >= 0) {
ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(outputBufferId);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// 跳过编解码器配置数据(SPS/PPS),已在 outputFormat 中包含
bufferInfo.size = 0;
}
if (bufferInfo.size > 0 && mMuxerStarted) {
// 将编码数据写入Muxer
encodedData.position(bufferInfo.offset);
encodedData.limit(bufferInfo.offset + bufferInfo.size);
mMuxer.writeSampleData(mVideoTrackIndex, encodedData, bufferInfo);
}
mVideoEncoder.releaseOutputBuffer(outputBufferId, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break; // 录像结束
}
}
}
}
// 等待音频和视频轨道都就绪后才启动Muxer
private synchronized void checkAndStartMuxer() {
if (mVideoTrackIndex >= 0 && mAudioTrackIndex >= 0 && !mMuxerStarted) {
mMuxer.start();
mMuxerStarted = true;
}
}
4.5 音视频PTS同步核心
这是手动录像中最容易出错的地方。视频PTS来自传感器时间戳(纳秒),音频PTS来自AudioRecord读取位置(需要手动计算),两者必须使用同一时间基准:
java
// 视频帧的PTS:来自onCaptureCompleted的sensor timestamp(纳秒 → 微秒)
@Override
public void onCaptureCompleted(CameraCaptureSession session,
CaptureRequest request,
TotalCaptureResult result) {
long sensorTimestampNs = result.get(CaptureResult.SENSOR_TIMESTAMP);
// 记录第一帧时间作为基准
if (mStartTimeUs == 0) {
mStartTimeUs = sensorTimestampNs / 1000;
}
long ptsUs = sensorTimestampNs / 1000 - mStartTimeUs;
// 将PTS传递给视频编码线程(通过Queue等方式)
mVideoPtsQueue.offer(ptsUs);
}
// 音频帧的PTS:基于采样数计算(更精确!)
private long mAudioPtsUs = 0;
private long mTotalAudioFrames = 0;
private void encodeAudioFrame(short[] pcmBuffer, int readSize) {
// 基于采样数计算精确PTS(比System.nanoTime()更稳定)
long audioPts = mTotalAudioFrames * 1_000_000L / SAMPLE_RATE;
mTotalAudioFrames += readSize / 2; // 16-bit单声道,每个采样2字节
// 将PCM数据送入音频编码器
int inputBufferId = mAudioEncoder.dequeueInputBuffer(10_000);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = mAudioEncoder.getInputBuffer(inputBufferId);
inputBuffer.clear();
for (short sample : pcmBuffer) {
inputBuffer.putShort(sample);
}
mAudioEncoder.queueInputBuffer(inputBufferId, 0,
readSize * 2, audioPts, 0);
}
}
音视频同步原则:以音频时钟为主(因为人耳对音频不同步更敏感),视频追音频:
- 视频帧PTS < 当前音频PTS:正常写入Muxer
- 视频帧PTS > 当前音频PTS超过1帧:等待音频追上
- 音视频差值 > 200ms:说明同步出了问题,需要检查PTS计算逻辑
五、特殊录像场景
5.1 慢动作录像(高帧率)
慢动作录像的本质是高帧率采集 + 低帧率播放 。Camera2通过createConstrainedHighSpeedCaptureSession()实现:
java
private void startSlowMotionRecording() throws CameraAccessException {
// 高速录像必须使用专用Session类型
mCameraDevice.createConstrainedHighSpeedCaptureSession(
Arrays.asList(mHighSpeedSurface, mMediaRecorder.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
// 创建高速Burst请求
CaptureRequest.Builder builder =
mCameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_RECORD);
builder.addTarget(mHighSpeedSurface);
builder.addTarget(mMediaRecorder.getSurface());
// 设置高帧率范围(如 [120, 120] 固定120fps)
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<>(120, 120));
// 高速Session必须使用 createHighSpeedRequestList
List<CaptureRequest> highSpeedRequests =
((CameraConstrainedHighSpeedCaptureSession) session)
.createHighSpeedRequestList(builder.build());
session.setRepeatingBurst(highSpeedRequests,
null, mBackgroundHandler);
mMediaRecorder.start();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {}
},
mBackgroundHandler);
}
慢动作视频的播放:120fps录制的视频,设置播放速度为1/4即可实现4倍慢放:
java
// ExoPlayer 设置慢放速度
PlaybackParameters slowParams = new PlaybackParameters(0.25f); // 0.25倍速
player.setPlaybackParameters(slowParams);
5.2 延时摄影(Time-lapse)
延时摄影的核心是定时拍摄 + 快速播放。Camera2实现方式:
java
// 使用 ImageReader 定时拍摄
private Handler mTimeLapseHandler = new Handler(Looper.getMainLooper());
private int mFrameCount = 0;
private static final long INTERVAL_MS = 1000; // 每1秒拍一帧
private void startTimeLapse() {
mTimeLapseHandler.postDelayed(mTimeLapseRunnable, INTERVAL_MS);
}
private final Runnable mTimeLapseRunnable = new Runnable() {
@Override
public void run() {
// 拍摄一帧(使用 TEMPLATE_STILL_CAPTURE 获得最高质量)
captureTimeLapseFrame();
mFrameCount++;
// 继续下一帧
if (mIsRecording) {
mTimeLapseHandler.postDelayed(this, INTERVAL_MS);
}
}
};
六、性能优化与常见问题
6.1 存储速度要求
4K/60fps录像对存储速度要求极高:
- 4K30fps H.264 ~50Mbps = 6.25 MB/s
- 4K60fps H.265 ~80Mbps = 10 MB/s
内置存储(UFS 3.1)写入速度通常 200MB/s+,没问题。但如果写入外部SD卡,要确认SD卡的写入速度:Class 10 / UHS-I U3 (≥30MB/s) 才能流畅录制4K。
java
// 检测存储写入速度是否够用
private boolean checkStorageSpeed(long requiredBytesPerSec) {
File testFile = new File(getCacheDir(), "speed_test.tmp");
byte[] testData = new byte[1024 * 1024]; // 1MB测试数据
long start = System.nanoTime();
try (FileOutputStream fos = new FileOutputStream(testFile)) {
fos.write(testData);
fos.getFD().sync(); // 等待数据真正写入磁盘
} catch (IOException e) {
return false;
}
long elapsed = System.nanoTime() - start;
long bytesPerSec = (long) (1e9 / elapsed * testData.length);
testFile.delete();
Log.d(TAG, String.format("存储写入速度:%.1f MB/s", bytesPerSec / 1e6));
return bytesPerSec >= requiredBytesPerSec;
}
6.2 温度控制与Thermal降频
长时间4K录像会导致手机发热,触发Thermal降频,表现为录像帧率下降(从60fps自动降到30fps甚至更低):
java
// Android 11+ 监听热状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
mThermalListener = status -> {
switch (status) {
case PowerManager.THERMAL_STATUS_NONE:
case PowerManager.THERMAL_STATUS_LIGHT:
// 正常,维持当前录像质量
break;
case PowerManager.THERMAL_STATUS_MODERATE:
// 温度偏高,考虑降低码率
adjustBitRateForThermal(0.8f); // 降低20%
break;
case PowerManager.THERMAL_STATUS_SEVERE:
// 高温,必须降质量
adjustBitRateForThermal(0.5f); // 降低50%
break;
case PowerManager.THERMAL_STATUS_CRITICAL:
// 临界温度,建议停止录像并提示用户
showThermalWarning();
break;
}
};
powerManager.addThermalStatusListener(mainExecutor, mThermalListener);
}
6.3 录像常见问题排查
问题1:视频无声音
检查是否申请了RECORD_AUDIO运行时权限:
bash
adb shell dumpsys package com.yourapp | grep RECORD_AUDIO
问题2:视频画面旋转90°
检查setOrientationHint()是否正确设置:
bash
# 用 mediainfo 查看视频元数据
adb shell mediainfo /sdcard/test.mp4 | grep Rotation
问题3:录像文件损坏(无法播放)
通常是stop()前没有发送EOS信号,或者直接kill了进程。确保调用mMediaRecorder.stop()。对于MediaCodec方案,需要发送BUFFER_FLAG_END_OF_STREAM并等待Muxer正确关闭:
java
// 正确结束录像
mVideoEncoder.signalEndOfInputStream(); // 通知编码器结束
// 等待 BUFFER_FLAG_END_OF_STREAM 从 dequeueOutputBuffer 返回
// 然后停止 Muxer
mMuxer.stop();
mMuxer.release();
问题4:音视频不同步
使用ffprobe检查音视频时间戳:
bash
adb shell ffprobe -v error -show_streams output.mp4 2>&1 | grep -E "start_time|nb_frames"
# 正常的音视频起始时间差应该 < 100ms
七、Android 15录像新特性
7.1 10-bit HDR视频录制
Android 13+开始支持10-bit HDR视频录制,Android 15进一步完善了支持:
java
// 检查是否支持10-bit HDR录制
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// 检查是否支持 HEVC HDR 10-bit
boolean supports10bitHDR = false;
int[] availableCapabilities = mCameraCharacteristics.get(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
for (int cap : availableCapabilities) {
if (cap == CameraCharacteristics
.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT) {
supports10bitHDR = true;
break;
}
}
if (supports10bitHDR) {
// 配置10-bit HDR录像格式
mRecorderRequestBuilder.set(CaptureRequest.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP,
DynamicRangeProfiles.HLG10); // HLG10 or HDR10/HDR10+
}
7.2 多路并发录像
Android 15支持同时打开多个摄像头并录像(如前后摄同时录):
java
// 查询是否支持并发摄像头
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
Set<Set<String>> concurrentCameraIds = manager.getConcurrentCameraIds();
// 如果包含前摄和后摄ID,则支持并发录像
总结
本文系统梳理了Camera2录像的完整技术栈:
-
方案选择:MediaRecorder适合常规录像(配置简单),MediaCodec+MediaMuxer适合需要精细控制的场景(直播/特效)。
-
MediaRecorder严格调用顺序 :
setSource → setFormat → setEncoder → setOutput → prepare → getSurface → createCaptureSession → start。顺序错误必然抛异常。 -
Camera2录像Session配置:预览Surface和录像Surface必须同时传入createCaptureSession,TEMPLATE_RECORD比TEMPLATE_PREVIEW有更稳定的AE策略。
-
MediaCodec音视频同步:视频PTS取传感器时间戳,音频PTS按采样数计算。两者必须对齐到同一时间基准,差值超过200ms需排查。
-
性能优化三要素:存储写入速度(4K需≥10MB/s)、Thermal监控降级、合理的I帧间隔设置(直播0.5s,录像1-2s)。
下一篇我们将深入Android相机系统的底层,解析Camera HAL3接口------相机驱动工程师如何通过HAL3实现硬件抽象,Request/Result机制如何工作,以及Buffer管理的精妙之处。