一、简介

MediaRecorder 是 Android 提供的一个用于音视频录制的高级类,旨在简化音频和视频的录制过程。它封装了底层的音视频编码器(通常是 MediaCodec)和其他相关组件。

如果你不需要对音视频进行更底层的控制,而只是想要方便地进行录制操作,那么可以选择使用 MediaRecorder。否则可以考虑使用 MediaCodec + MediaMutex

官网文档链接:MediaRecorder

二、常用设置和方法

MediaRecorder 提供了一些默认的配置,但也提供了一些可供调整的参数,以便你更好地适应你的应用需求。

1. 视频设置:

mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);  // 设置视频来源
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);  // 设置视频编码格式
mediaRecorder.setVideoSize(width, height);  // 设置视频分辨率大小
mediaRecorder.setVideoFrameRate(30);  // 设置视频帧率
mediaRecorder.setCaptureRate(30); // 设置视频捕获率
int bitRate = width * height * 8;
mediaRecorder.setVideoEncodingBitRate(bitRate);  // 设置比特率
  • 现在一般都是用Camera2的接口了,这里就指定视频源为 MediaRecorder.VideoSource.SURFACE。(MediaRecorder.VideoSource.CAMERA 是旧的Camera使用的)
  • H264 是常用的视频编码格式。
  • setVideoFrameRate() 方法系统会尽量匹配你设置的帧率,但实际录制的帧率可能会受到硬件和系统限制。
  • 视频比特率 1440*1080分辨率为例上述代码设置的频比特率大约为 11.8 Mbps。

视频比特率

与视频质量和文件大小之间存在紧密的关系。在给定的视频分辨率下,增加比特率可以提高视频质量,但也会增加文件大小;减小比特率则会导致视频质量下降,但文件大小减小。以下是一些建议的视频比特率范围,用于一般应用场景:

  • 低质量视频(360p 分辨率):比特率范围:200 kbps - 500 kbps。
  • 标准质量视频(480p 到 720p 分辨率):比特率范围:500 kbps - 2.5 Mbps。
  • 高质量视频(720p 到 1080p 分辨率):比特率范围:2.5 Mbps - 8 Mbps。
  • 全高清视频(1080p 到 1440p 分辨率):比特率范围:8 Mbps - 16 Mbps。
  • 超高清视频(4K 分辨率及以上):比特率范围:16 Mbps - 50 Mbps 或更高。

2. 音频设置:

如果不需要录制音频,这里也可以不用设置。

mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源从麦克风采集
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式
mediaRecorder.setAudioEncodingBitRate(96000);  // 设置音频编码比特率(一般音乐和语音录制)
mediaRecorder.setAudioSamplingRate(44100);  // 设置音频采样率(CD音质)

音频编码比特率

一般而言,音频编码比特率的典型范围是 16 kbps 到 320 kbps。以下是一些常见的音频比特率设置:

  • 16 kbps:较低质量,适用于语音通话等要求较低的场景。
  • 64 kbps - 128 kbps:一般质量,适用于一般音乐和语音录制。
  • 192 kbps - 320 kbps:较高质量,适用于对音频质量要求较高的音乐录制等场景。

音频采样率

音频采样率表示在一秒钟内对声音信号进行采样的次数,通常以赫兹(Hz)为单位。以下是一些常见的音频采样率:

  • 8 kHz: 适用于电话通话等语音通信,因为人类语音的主要频率范围通常在 300 Hz 到 3.4 kHz 之间。
  • 16 kHz: 常用于语音识别和语音合成等应用,可以更好地捕捉语音的细节。
  • 44.1 kHz: CD音质,适用于音乐录制和播放,因为这是 CD 音频的标准采样率。
  • 48 kHz: 常用于广播、视频制作和音频/视频同步应用,是许多数字音频设备的标准采样率。
  • 96 kHz 或更高: 在专业音频制作领域,高采样率用于捕捉更宽的频率范围,但对于大多数应用,通常不需要使用这么高的采样率。

3. 输出路径、格式等其他设置

mediaRecorder.setMaxDuration(maxDurationMillis);  // 设置最大录制时长
mediaRecorder.setMaxFileSize(maxFileSizeBytes);  // 设置最大文件大小
File outputFile = new File(GALLERY_PATH, "output.mp4");
mediaRecorder.setOutputFile(outputFile);  // 设置输出路径
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式

最大录制时长和最大文件大小不设置也可以。输出格式设置为常用的 mp4 格式。

4. 开始和停止录制

请注意,之前的设置需要在 prepare() 之前进行,以确保 MediaRecorder 正确初始化。

mediaRecorder.prepare();
mediaRecorder.start();
// ...
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();

5. 代码流程

MediaRecorder 中很多方法都是有一定先后的执行顺序的,错误的执行顺序会导致抛出 IllegalStateException 异常,使用的时候需要额外注意。

下面是一个简单的代码流程示例:

MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 设置视频来源
mediaRecorder.setVideoEncodingBitRate(mPreviewSize.getWidth() * mPreviewSize.getHeight() * 8); // 设置比特率
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源
mediaRecorder.setAudioEncodingBitRate(96000);  // 设置音频编码比特率(一般音乐和语音录制)
mediaRecorder.setAudioSamplingRate(44100);  // 设置音频采样率(CD音质)
mediaRecorder.setCaptureRate(30); // 捕获率
mediaRecorder.setOrientationHint(0);  // 设置录制视频时的预期方向,取值为 0、90、180 或 270

mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
mediaRecorder.setVideoSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());  // 设置视频宽高
mediaRecorder.setVideoFrameRate(30); // 设置帧数
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式

mediaRecorder.setInputSurface(mRecordSurface);
mediaRecorder.setOutputFile(outputFile);
mediaRecorder.prepare();
mediaRecorder.start();  // 开始录制

// 录制...

mediaRecorder.stop();  // 停止录制
mediaRecorder.reset();
mediaRecorder.release();  // 释放

三、应用实例

想要使用 Camera2 + MediaRecorder 做一个相机录制视频的功能,可以按照如下几步完成。

注意: 避免篇幅过长,示例代码会进行一定缩减,完整的代码会在链接中贴出。

1. 实现相机显示View

我们选择使用 SurfaceView 来显示相机的预览画面,它相比 TextureView 性能更优,且比 GLSurfaceView 简单易用。另外我们需要修改 SurfaceView 的宽高,使其和相机预览的分辨率比例一致,这样画面才不会被拉伸。

a. 自定义一个可以设置宽高比例的SurfaceView

onMeasure() 中通过设定的宽高去修改view的宽高。

public class AutoFitSurfaceView extends SurfaceView {
    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
    public AutoFitSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public void setAspectRatio(int width, int height) {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Size cannot be negative.");
        }
        mRatioWidth = width;
        mRatioHeight = height;
        post(() -> requestLayout());
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, height);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }
    }
}

b. 自定义一个能操作相机的CameraSurfaceView

可以将相机的一些基础操作和实现都封装起来,并在CameraSurfaceView中完成相关操作。

完整文件地址: https://github.com/afei-cn/CameraRecorder/blob/main/app/src/main/java/com/afei/camerarecorder/normal/CameraSurfaceView.java

public class CameraSurfaceView extends AutoFitSurfaceView {
    private CameraModule mCameraModule;  // 封装相机的各种操作
    private Size mPreviewSize;

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        getHolder().addCallback(mSurfaceHolderCallback);
        setKeepScreenOn(true); // 设置屏幕常亮
    }

    public void setCameraModule(CameraModule cameraModule) {
        mCameraModule = cameraModule;
        mPreviewSize = mCameraModule.getPreviewSize();
    }

    private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            holder.setFixedSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        }
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            // 将View的大小修改为和相机预览分辨率相同的比例
            setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
            mCameraModule.setPreviewSurface(holder.getSurface());
            mCameraModule.openCamera();
        }
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            mCameraModule.releaseCamera();
        }
    };
}

2. 相机封装类

将打开、配置、关闭相机,以及开始、停止录制等和相机息息相关的操作封装起来,方便日后维护和扩展,即 CameraModule 类。

完整文件地址: https://github.com/afei-cn/CameraRecorder/blob/main/app/src/main/java/com/afei/camerarecorder/camera/CameraModule.java

public class CameraModule {

    private Activity mActivity;
    private CameraManager mCameraManager; // 相机管理者
    private CameraCharacteristics mCameraCharacteristics; // 相机属性
    private CameraDevice mCameraDevice; // 相机对象
    private CameraCaptureSession mCameraSession; // 相机事务
    private Surface mPreviewSurface;  // 预览的Surface
    private CaptureRequest.Builder mRequestBuilder;
    private Handler mCameraHandler;
    private HandlerThread mCameraThread;
    private int mDisplayRotation;  // 用于设置视频方向
    private CameraConfig mCameraConfig;
    private Size mPreviewSize;
    
    /* 录制相关*/
    private Surface mRecordSurface;
    private MediaRecorder mMediaRecorder;
    private boolean mIsRecording;  // 是否正在录制
    
    private CameraDevice.StateCallback mCameraOpenCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCameraDevice = camera;
            createVideoSession();  // 相机打开后开始创建session
        }
        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {}
        @Override
        public void onError(@NonNull CameraDevice camera, int error) {}
    };

    public void openCamera() {
        String cameraId = mCameraConfig.getCameraId();
        startBackgroundThread();
        try {
            mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
            initDisplayRotation(mCameraCharacteristics);
            mCameraManager.openCamera(cameraId, mCameraOpenCallback, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    private void startBackgroundThread() {
        mCameraThread = new HandlerThread("CameraBackground");
        mCameraThread.start();
        mCameraHandler = new Handler(mCameraThread.getLooper());
    }
    
    private void initDisplayRotation(CameraCharacteristics cameraCharacteristics) {
        int displayRotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
        switch (displayRotation) {
            case Surface.ROTATION_0:
                displayRotation = 90;
                break;
            case Surface.ROTATION_90:
                displayRotation = 0;
                break;
            case Surface.ROTATION_180:
                displayRotation = 270;
                break;
            case Surface.ROTATION_270:
                displayRotation = 180;
                break;
        }
        int sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
        mDisplayRotation = (displayRotation + sensorOrientation + 270) % 360;
    }
    
    private void createVideoSession() {
        try {
            mRecordSurface = MediaCodec.createPersistentInputSurface();
            mMediaRecorder = createRecorder();
            ArrayList<Surface> sessionSurfaces = new ArrayList<>();
            sessionSurfaces.add(mPreviewSurface);
            sessionSurfaces.add(mRecordSurface);
            createPreviewRequest();
            mCameraDevice.createCaptureSession(sessionSurfaces, mSessionCreateCallback, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    private void createPreviewRequest() {
        CaptureRequest.Builder builder;
        try {
            builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
        } catch (CameraAccessException e) {
            e.printStackTrace();
            return;
        }
        builder.addTarget(mPreviewSurface);
        builder.addTarget(mRecordSurface);
        builder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
        builder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO);  // 设置聚焦
        builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO); // 设置白平衡
        builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); // 设置曝光
        mRequestBuilder = builder;
    }
    
    private CameraCaptureSession.StateCallback mSessionCreateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            mCameraSession = session;
            startPreview();  // 开始预览
        }
        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {}
    };
    
    public void startPreview() {
        try {
            CaptureRequest captureRequest = mRequestBuilder.build();
            mCameraSession.setRepeatingRequest(captureRequest, null, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    private MediaRecorder createRecorder() {
        MediaRecorder mediaRecorder = new MediaRecorder();
        try {
            File tmpFile = configRecorder(mediaRecorder);
            if (tmpFile != null) tmpFile.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return mediaRecorder;
    }
    
    private File configRecorder(@NonNull MediaRecorder mediaRecorder) throws IOException {
        mediaRecorder.reset();
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 设置视频来源
        mediaRecorder.setVideoEncodingBitRate(mPreviewSize.getWidth() * mPreviewSize.getHeight() * 8); // 设置比特率
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频来源
        mediaRecorder.setAudioEncodingBitRate(96000);  // 设置音频编码比特率(一般音乐和语音录制)
        mediaRecorder.setAudioSamplingRate(44100);  // 设置音频采样率(CD音质)
        mediaRecorder.setCaptureRate(30); // 捕获率
        mediaRecorder.setOrientationHint(mDisplayRotation);  // 设置录制视频时的预期方向,取值为 0、90、180 或 270
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置输出格式
        mediaRecorder.setVideoSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());  // 设置视频宽高
        mediaRecorder.setVideoFrameRate(30); // 设置帧数
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置视频编码格式
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置音频编码格式
        mediaRecorder.setInputSurface(mRecordSurface);
        File outputFile = getOutputFile();
        mediaRecorder.setOutputFile(outputFile);
        mediaRecorder.prepare();
        mIsRecording = false;
        return outputFile;
    }
    
    private File getOutputFile() {
        File saveDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "CameraRecorder");
        saveDirectory.mkdirs();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        String fileName = simpleDateFormat.format(new Date(System.currentTimeMillis())) + ".mp4";
        File outputFile = new File(saveDirectory, fileName);
        return outputFile;
    }
    
    public void startRecorder() {
        try {
            configRecorder(mMediaRecorder);
            mMediaRecorder.start();
            mIsRecording = true;
        } catch (IOException e) {
            Log.e(TAG, "startRecorder failed! " + e.getMessage());
        }
    }
    
    public void stopRecorder() {
        if (mMediaRecorder != null && mIsRecording) {
            mMediaRecorder.stop();
            mIsRecording = false;
        }
    }
    
    private void releaseRecorder() {
        stopRecorder();
        mMediaRecorder.reset();
        mMediaRecorder.release();
        mMediaRecorder = null;
    }
    
    public void releaseCamera() {
        releaseRecorder();  // 如果正在录制,则先停止录制
        stopPreview();
        closeCameraSession();
        closeCameraDevice();
        stopBackgroundThread();
    }
    
    public void stopPreview() {
        try {
            mCameraSession.stopRepeating();
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    private void closeCameraSession() {
        if (mCameraSession != null) {
            mCameraSession.close();
            mCameraSession = null;
        }
    }
    
    private void closeCameraDevice() {
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }
    
    private void stopBackgroundThread() {
        if (mCameraThread != null) {
            mCameraThread.quitSafely();
            try {
                mCameraThread.join();
                mCameraThread = null;
                mCameraHandler = null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

通过该类,我们就可以比较方便的去开始和停止录制了,重点关注 MediaRecorder 类相关代码即可。

相机开发步骤不是本文重点,包含拍照等功能也移除掉了,感兴趣的也可以参考其他文章去修改。如:
自定义Camera系列之:SurfaceView + Camera2

3. 布局文件

基本包含一个 CameraSurfaceView 和一个录制按钮,一个停止录制按钮,其他功能可以再根据需要扩展。

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.afei.camerarecorder.ui.CameraSurfaceView
        android:id="@+id/camera_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/start_recorder_iv"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="10dp"
        android:src="@mipmap/shutter_btn_video"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <ImageView
        android:id="@+id/stop_recorder_iv"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="10dp"
        android:src="@mipmap/shutter_btn_video_stop"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4. CameraActivity调用代码

逻辑比较简单,监听相应的点击事件,去进行开始录制和停止录制的操作即可。

public class CameraActivity extends AppCompatActivity implements View.OnClickListener {
    private ActivityCameraBinding mBinding;
    private CameraModule mCameraModule;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityCameraBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());
        init();
    }
    
    private void init() {
        // 这是一个提前创建好的配置文件,包含摄像头id,分辨率大小等信息
        CameraConfig config = CameraConfig.sCameraConfig;
        mCameraModule = new CameraModule(this, config);
        mBinding.cameraView.setCameraModule(mCameraModule);
        mBinding.startRecorderIv.setOnClickListener(this::onClick);
        mBinding.stopRecorderIv.setOnClickListener(this::onClick);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.start_recorder_iv) {
            startRecorder();
        } else if (id == R.id.stop_recorder_iv) {
            stopRecorder();
        } else {
            Log.w(TAG, "unknown view id = " + id);
        }
    }

    private void startRecorder() {
        mCameraModule.startRecorder();
        mBinding.startRecorderIv.setVisibility(View.GONE);
        mBinding.stopRecorderIv.setVisibility(View.VISIBLE);
    }

    private void stopRecorder() {
        mCameraModule.stopRecorder();
        mBinding.startRecorderIv.setVisibility(View.VISIBLE);
        mBinding.stopRecorderIv.setVisibility(View.GONE);
    }
}

5. 项目地址

完整且可运行的代码地址如下:

https://github.com/afei-cn/CameraRecorder

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐