MediaRecorder + Camera2 视频录制最佳实践
MediaRecorder 是 Android 官方提供的一个用于音视频录制的类,简化了音频和视频的录制过程,如果只是想单纯的录制视频,MediaRecorder是一个不错的选择
这里采用 Camera2 + MediaRecorder 的方式实现视频录制
步骤:
- 获取 CameraManager 实例
- 调用 openCamera,打开摄像头
- 创建 session
- 开启预览
- 开启录制
- 停止录制
一、获取CameraManager 实例
// 1. 先获取 CameraManager
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
通过 getSystemService(Context.Camera_SERVICE) 即可获取到 CameraManager 的实例,CameraManager 是一个负责查询和建立相机连接的系统服务,Camera2 的所有操作都是从先获取 CameraManager 实例开始
二、调用 openCamera,打开摄像头
// 2. 调用 openCamera,打开摄像头
mCameraManager.openCamera(String.valueOf(mCurrentFacing), new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
LogUtil.d(TAG, "onOpened: 摄像头链接成功, facing: " + mCurrentFacing);
// 摄像头成功打开
mCurrentCameraDevice = camera;
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
LogUtil.d(TAG, "onDisconnected: 摄像头链接断开");
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
LogUtil.d(TAG, "onError: 摄像头打开失败, error: " + error);
camera.close();
}
}, null);
使用上一步获取到的 CameraManager 实例,调用 openCamera 方法,即可获取到指定摄像头的 CameraDevice 实例,可以对指定摄像头进行操作
三、创建 session
// 3. 创建session,可以看到这里绑定了两个 surface,一个用于承载预览显示,一个用于视频录制
List<Surface> mOutputs = new ArrayList<>();
mOutputs.add(mPreviewSurface);
mOutputs.add(mRecordSurface);
mCurrentCameraDevice.createCaptureSession(mOutputs, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCurrentCameraCaptureSession = session;
try {
// 4. 开启预览
createPreviewCaptureRequest();
mCurrentCameraCaptureSession.setRepeatingRequest(mCurrentPreviewRequest, null, null);
} catch (CameraAccessException e) {
LogUtil.e(TAG, "onConfigured: ", e);
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, null);
Camera2 比较特殊的点就是,所有的操作行为都是在 session 上完成的,所以 打开摄像头之后,通过 CameraDevice 实例创建指定的 session
值得注意的是,在 createSession 的时候有一个绑定 surface 的操作,这关系到后续能在 session 上执行什么操作,如果需要预览,那需要绑定 承载预览流的 previewSurface,如果需要录制视频,需要绑定 承载视频流的 recordSurface,如果需要拍照,则需要绑定 承载拍照数据的 imageReader
这里我们需要预览和录制,所以绑定了两个surface,一个用于预览,一个用于录制
四、开启预览
// 4. 开启预览
createPreviewCaptureRequest();
mCurrentCameraCaptureSession.setRepeatingRequest(mCurrentPreviewRequest, null, null);
/**
* 用于预览显示的request
*/
private void createPreviewCaptureRequest() {
try {
CaptureRequest.Builder mCurrentPreviewRequestBuilder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mCurrentPreviewRequestBuilder.addTarget(mPreviewSurface);
mCurrentPreviewRequest = mCurrentPreviewRequestBuilder.build();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "createPreviewCaptureRequest: ", e);
}
}
createSession 成功之后,就可以 创建 CaptureRequest,然后通过 session 的 setRepeatingRequest 方法开始呈现预览了
如果只需要呈现预览,到这里已经完成了,接下来开始录制
五、开启录制
private void startRecording(){
isRecording = true;
try {
// 5. 开启录制
// 先准备 mediaRecorder
prepareMediaRecorder();
// 创建录制视频用的 request
createVideoRecordCaptureRequest();
// 先将 PreviewRequest 停掉,因为 PreviewRequest 只绑定了一个previewSurface target,录制的时候还需要绑定 recordSurface target
mCurrentCameraCaptureSession.stopRepeating();
// 开始 recordRequest 请求
mCurrentCameraCaptureSession.setRepeatingRequest(mVideoRecordRequest, null, null);
// 开始录制
mMediaRecorder.start();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "startRecording: ", e);
}
}
private void prepareMediaRecorder(){
try {
// 这里很重要的一个点 mediaRecorder 的复用,不需要每次重新 new 一个新的
if(mMediaRecorder == null){
mMediaRecorder = new MediaRecorder();
}else {
// 如果 mMediaRecorder 存在,只需要重新 reset 就行
mMediaRecorder.reset();
}
// surface 通过 MediaCodec.createPersistentInputSurface() 创建,
// 然后通过 setInputSurface 的方式设置给 MediaRecorder
// 这个 surface 也会在 createSession 时和 previewSurface 一起传入,这样开始录制时就不需要重新创建 session
if(mRecordSurface == null){
mRecordSurface = MediaCodec.createPersistentInputSurface();
}
mMediaRecorder.setInputSurface(mRecordSurface);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// 设置视频文件地址,需要读写权限
mMediaRecorder.setOutputFile(mVideoFilePath);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoFrameRate(25);
mMediaRecorder.setOrientationHint(90);
// prepare 执行之后 才能使用
mMediaRecorder.prepare();
} catch (IOException e) {
LogUtil.e(TAG, "prepareMediaRecorder: ", e);
}
}
/**
* 用于录制视频的request
*/
private void createVideoRecordCaptureRequest(){
try {
CaptureRequest.Builder builder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 用于承载录制视频流
builder.addTarget(mRecordSurface);
// 用于承载预览显示流
builder.addTarget(mPreviewSurface);
mVideoRecordRequest = builder.build();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "createVideoRecordCaptureRequest: ", e);
}
}
对于视频录制,我们用到了 MediaRecorder,先准备好 MediaRecorder,然后调用 start 方法就可以开始录制了,这里有一个点,MediaRecorder 录制所需要的视频流数据从哪里来,如何和 Camera 绑定?
我们可以先创建 surface,和 MediaRecorder 绑定
if(mRecordSurface == null){
mRecordSurface = MediaCodec.createPersistentInputSurface();
}
mMediaRecorder.setInputSurface(mRecordSurface);
还记得上面我们创建 session 的时候绑定了两个 surface,其中一个就是 mRecorderSurface,通过这种方式
-
mRecordSurface 通过 RepeatingRequest ,持续的从底层拿到了视频流数据
-
MediaRecorder 又从 mRecordSurface 中拿到视频流数据合成视频
六、停止录制
private void stopRecording(){
isRecording = false;
// 6. 结束录制
if(mMediaRecorder != null){
// 停止录制
mMediaRecorder.stop();
}
if(mCurrentCameraCaptureSession != null){
try {
// 先将 RecordRequest 请求停掉,因为录制停止之后,只需要一个预览流即可,降低功耗
mCurrentCameraCaptureSession.stopRepeating();
mCurrentCameraCaptureSession.setRepeatingRequest(mCurrentPreviewRequest, null, null);
} catch (CameraAccessException e) {
LogUtil.e(TAG, "stopRecording: ", e);
}
}
}
只需要调用 stop 方法,就可以停止视频录制,这里我们还是做了一个功耗优化,停掉了当前的 repeatingRequset,重新只发送只有一个 previewSurface 的 repeatingRequset,这样可以在预览时减少一路流的消耗,降低功耗
完整代码
public class VideoRecord2Activity extends AppCompatActivity {
private static final String TAG = "VideoRecordActivity";
private Context mContext;
private CameraManager mCameraManager;
private int mFacingFront = -1;
private int mFacingBack = -1;
private int mCurrentFacing = mFacingFront;
private CameraDevice mCurrentCameraDevice;
private CaptureRequest mCurrentPreviewRequest;
private TextureView mTextureView;
private Surface mPreviewSurface;
private CameraCaptureSession mCurrentCameraCaptureSession;
private Button mBtnTakePicture;
private Size mVideoSize = new Size(810,720);
private boolean isRecording = false;
private MediaRecorder mMediaRecorder;
private Surface mRecordSurface;
private CaptureRequest mVideoRecordRequest;
private String mVideoFilePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/video.mp4";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_record);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
this.mContext = this;
initView();
initCamera();
mCurrentFacing = mFacingBack;
}
private void initView() {
mTextureView = findViewById(R.id.view_camera_texture);
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
// 这里解释一下,为什么摄像头打开的时候没有马上打开预览,因为预览画面的展示需要 surface 承载,所以需要等 surface 准备好才行
surface.setDefaultBufferSize(mVideoSize.getWidth(),mVideoSize.getHeight());
// 预览所需的 previewSurface
mPreviewSurface = new Surface(surface);
// 准备视频录制时所需的 recordSurface
prepareMediaRecorder();
startPreview();
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
mPreviewSurface.release();
mPreviewSurface = null;
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
});
mBtnTakePicture = findViewById(R.id.btn_take_picture);
mBtnTakePicture.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtil.d(TAG, "onClick: 点击拍照");
if(isRecording){
stopRecording();
mBtnTakePicture.setText("录制");
}else {
startRecording();
mBtnTakePicture.setText("停止");
}
}
});
}
private void initCamera() {
// 1. 先获取 CameraManager
mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
if (mCameraManager == null) {
return;
}
try {
for (String cameraId : mCameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
mFacingFront = Integer.parseInt(cameraId);
int supportLEVEL = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
LogUtil.d(TAG, "testCamera2: 当前是前摄,supportLEVEL: " + supportLEVEL);
} else if (facing == CameraCharacteristics.LENS_FACING_BACK) {
mFacingBack = Integer.parseInt(cameraId);
int supportLEVEL = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
LogUtil.d(TAG, "testCamera2: 当前是后摄,supportLEVEL: " + supportLEVEL);
}
}
} catch (CameraAccessException e) {
Log.e(TAG, "testCamera2: ", e);
}
}
private void openCamera(){
closeCamera();
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
try {
// 2. 调用 openCamera,打开摄像头
mCameraManager.openCamera(String.valueOf(mCurrentFacing), new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
LogUtil.d(TAG, "onOpened: 摄像头链接成功, facing: " + mCurrentFacing);
// 摄像头成功打开
mCurrentCameraDevice = camera;
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
LogUtil.d(TAG, "onDisconnected: 摄像头链接断开");
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
LogUtil.d(TAG, "onError: 摄像头打开失败, error: " + error);
camera.close();
}
}, null);
} catch (CameraAccessException e) {
Log.e(TAG, "openCamera: ", e);
}
}
private void startPreview(){
if(mCurrentCameraDevice == null){
return;
}
if(mPreviewSurface == null){
return;
}
try {
// 3. 创建session,可以看到这里绑定了两个 surface,一个用于承载预览显示,一个用于视频录制
List<Surface> mOutputs = new ArrayList<>();
mOutputs.add(mPreviewSurface);
mOutputs.add(mRecordSurface);
mCurrentCameraDevice.createCaptureSession(mOutputs, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCurrentCameraCaptureSession = session;
try {
// 4. 开启预览
createPreviewCaptureRequest();
mCurrentCameraCaptureSession.setRepeatingRequest(mCurrentPreviewRequest, null, null);
} catch (CameraAccessException e) {
LogUtil.e(TAG, "onConfigured: ", e);
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, null);
} catch (CameraAccessException e) {
LogUtil.e(TAG, "startPreview: ", e);
}
}
/**
* 用于预览显示的request
*/
private void createPreviewCaptureRequest() {
try {
CaptureRequest.Builder mCurrentPreviewRequestBuilder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mCurrentPreviewRequestBuilder.addTarget(mPreviewSurface);
mCurrentPreviewRequest = mCurrentPreviewRequestBuilder.build();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "createPreviewCaptureRequest: ", e);
}
}
/**
* 用于录制视频的request
*/
private void createVideoRecordCaptureRequest(){
try {
CaptureRequest.Builder builder = mCurrentCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 用于承载录制视频流
builder.addTarget(mRecordSurface);
// 用于承载预览显示流
builder.addTarget(mPreviewSurface);
mVideoRecordRequest = builder.build();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "createVideoRecordCaptureRequest: ", e);
}
}
private void closeCamera(){
if(mCurrentCameraDevice != null){
mCurrentCameraDevice.close();
mCurrentCameraDevice = null;
}
}
@Override
protected void onResume() {
super.onResume();
openCamera();
}
@Override
protected void onPause() {
super.onPause();
closeCamera();
if(mMediaRecorder != null){
mMediaRecorder.release();
mMediaRecorder = null;
}
if(mRecordSurface != null){
mRecordSurface.release();
mRecordSurface = null;
}
}
private void prepareMediaRecorder(){
try {
// 这里很重要的一个点 mediaRecorder 的复用,不需要每次重新 new 一个新的
if(mMediaRecorder == null){
mMediaRecorder = new MediaRecorder();
}else {
// 如果 mMediaRecorder 存在,只需要重新 reset 就行
mMediaRecorder.reset();
}
// surface 通过 MediaCodec.createPersistentInputSurface() 创建,
// 然后通过 setInputSurface 的方式设置给 MediaRecorder
// 这个 surface 也会在 createSession 时和 previewSurface 一起传入,这样开始录制时就不需要重新创建 session
if(mRecordSurface == null){
mRecordSurface = MediaCodec.createPersistentInputSurface();
}
mMediaRecorder.setInputSurface(mRecordSurface);
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
// 设置视频文件地址,需要读写权限
mMediaRecorder.setOutputFile(mVideoFilePath);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoFrameRate(25);
mMediaRecorder.setOrientationHint(90);
// prepare 执行之后 才能使用
mMediaRecorder.prepare();
} catch (IOException e) {
LogUtil.e(TAG, "prepareMediaRecorder: ", e);
}
}
private void startRecording(){
isRecording = true;
try {
// 5. 开启录制
// 先准备 mediaRecorder
prepareMediaRecorder();
// 创建录制视频用的 request
createVideoRecordCaptureRequest();
// 先将 PreviewRequest 停掉,因为 PreviewRequest 只绑定了一个previewSurface target,录制的时候还需要绑定 recordSurface target
mCurrentCameraCaptureSession.stopRepeating();
// 开始 recordRequest 请求
mCurrentCameraCaptureSession.setRepeatingRequest(mVideoRecordRequest, null, null);
// 开始录制
mMediaRecorder.start();
} catch (CameraAccessException e) {
LogUtil.e(TAG, "startRecording: ", e);
}
}
private void stopRecording(){
isRecording = false;
// 6. 结束录制
if(mMediaRecorder != null){
// 停止录制
mMediaRecorder.stop();
}
if(mCurrentCameraCaptureSession != null){
try {
// 先将 RecordRequest 请求停掉,因为录制停止之后,只需要一个预览流即可,降低功耗
mCurrentCameraCaptureSession.stopRepeating();
mCurrentCameraCaptureSession.setRepeatingRequest(mCurrentPreviewRequest, null, null);
} catch (CameraAccessException e) {
LogUtil.e(TAG, "stopRecording: ", e);
}
}
}
}