相机录像流程:MediaRecorder与Camera2的协作之道

0 阅读14分钟

引言:录个视频,比想象中复杂得多

"录个视频有什么难的?"——这句话,每个第一次在Android上实现录像功能的开发者大概都说过,然后花了一整天踩坑。

MediaRecorder的调用顺序稍有错乱就会抛IllegalStateException;忘记添加录像Surface到CaptureSession,画面一片黑;音视频不同步导致口型对不上;录到一半触发温度降频……

本文系统梳理Camera2录像的两条实现路径:MediaRecorder高层封装(快速上手)和MediaCodec+MediaMuxer手动控制(灵活精细),帮你把每个坑都提前踩掉。

一、两种录像方案的选择

Android提供两种录像方案,各有适用场景:

对比项MediaRecorderMediaCodec + 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

正确顺序如下:

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配置

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才能同时输出到两个目标。

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启动录像

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();
    }
}

下图展示了完整的录像链路:

05-01-mediarecorder-flow.png

值得注意的是,预览和录像数据同时从HAL3输出,分别流入各自的Surface,互不干扰——这也是为什么录像画面质量和预览画面质量可以不同(录像用更高分辨率,预览用较低分辨率)。

2.5 停止录像

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) 开始支持暂停/恢复录制,且续录的视频和暂停前是同一个文件,无需手动合并:

// 暂停录制
@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 码率与分辨率的选择

不同录像场景的推荐参数:

场景分辨率帧率视频码率音频码率
日常记录1080p30fps8 Mbps128 kbps
高质量4K30fps50 Mbps256 kbps
慢动作1080p120fps40 Mbps
会议录制720p30fps4 Mbps64 kbps
直播推流720p30fps1-2 Mbps64 kbps
// 动态调整码率(根据分辨率自动计算)
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 查询设备支持的录像能力

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支持两种防抖方式:

// 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 架构设计

下图展示了手动录像方案的完整架构:

05-02-manual-recording.png

视频和音频分别走独立的编码路径,最终由MediaMuxer合并成MP4文件。关键是音视频PTS时间戳对齐,否则会出现音画不同步。

4.2 视频编码器配置

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 音频编码器配置

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)对齐:

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读取位置(需要手动计算),两者必须使用同一时间基准:

// 视频帧的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()实现:

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倍慢放:

// ExoPlayer 设置慢放速度
PlaybackParameters slowParams = new PlaybackParameters(0.25f); // 0.25倍速
player.setPlaybackParameters(slowParams);

5.2 延时摄影(Time-lapse)

延时摄影的核心是定时拍摄 + 快速播放。Camera2实现方式:

// 使用 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。

// 检测存储写入速度是否够用
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甚至更低):

// 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运行时权限:

adb shell dumpsys package com.yourapp | grep RECORD_AUDIO

问题2:视频画面旋转90°

检查setOrientationHint()是否正确设置:

# 用 mediainfo 查看视频元数据
adb shell mediainfo /sdcard/test.mp4 | grep Rotation

问题3:录像文件损坏(无法播放)

通常是stop()前没有发送EOS信号,或者直接kill了进程。确保调用mMediaRecorder.stop()。对于MediaCodec方案,需要发送BUFFER_FLAG_END_OF_STREAM并等待Muxer正确关闭:

// 正确结束录像
mVideoEncoder.signalEndOfInputStream(); // 通知编码器结束
// 等待 BUFFER_FLAG_END_OF_STREAM 从 dequeueOutputBuffer 返回
// 然后停止 Muxer
mMuxer.stop();
mMuxer.release();

问题4:音视频不同步

使用ffprobe检查音视频时间戳:

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进一步完善了支持:

// 检查是否支持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支持同时打开多个摄像头并录像(如前后摄同时录):

// 查询是否支持并发摄像头
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
Set<Set<String>> concurrentCameraIds = manager.getConcurrentCameraIds();
// 如果包含前摄和后摄ID,则支持并发录像

总结

本文系统梳理了Camera2录像的完整技术栈:

  1. 方案选择:MediaRecorder适合常规录像(配置简单),MediaCodec+MediaMuxer适合需要精细控制的场景(直播/特效)。

  2. MediaRecorder严格调用顺序setSource → setFormat → setEncoder → setOutput → prepare → getSurface → createCaptureSession → start。顺序错误必然抛异常。

  3. Camera2录像Session配置:预览Surface和录像Surface必须同时传入createCaptureSession,TEMPLATE_RECORD比TEMPLATE_PREVIEW有更稳定的AE策略。

  4. MediaCodec音视频同步:视频PTS取传感器时间戳,音频PTS按采样数计算。两者必须对齐到同一时间基准,差值超过200ms需排查。

  5. 性能优化三要素:存储写入速度(4K需≥10MB/s)、Thermal监控降级、合理的I帧间隔设置(直播0.5s,录像1-2s)。

下一篇我们将深入Android相机系统的底层,解析Camera HAL3接口——相机驱动工程师如何通过HAL3实现硬件抽象,Request/Result机制如何工作,以及Buffer管理的精妙之处。

参考资料