上一次写了一篇关于Camera2拍照流程的文章,今天总结一下利用Camera2与MediaRecorder实现视频录制的流程。一样参考了Google官方Sample。php
咱们先来回顾一下打开相机预览的流程:java
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".camera2demo.Camera2VideoActivity">
<TextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" />
<Button android:id="@+id/capture_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="20dp" android:text="开始" />
</RelativeLayout>
复制代码
下面是代码android
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera2_video);
ButterKnife.bind(this);
Point point = new Point();
getWindowManager().getDefaultDisplay().getSize(point);
screenWidth = point.x;
screenHeight = point.y;
}
@Override
protected void onResume() {
super.onResume();
startBackgroundThread();
if (textureView.isAvailable()) {
openCamera(textureView.getWidth(), textureView.getHeight());
} else {
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
});
}
}
@SuppressLint("MissingPermission")
private void openCamera(int width, int height) {
try {
CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
String[] cameraIdList = cameraManager.getCameraIdList();
String cameraId = cameraIdList[0];
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
assert map != null;
//获取可用的录制视频的尺寸
Size[] videoSizes = map.getOutputSizes(MediaRecorder.class);
mVideoSize = videoSizes[0];
//获取可用的用于渲染图像的尺寸
Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);
mPreviewSize = previewSizes[0];
//为TextureView的尺寸设置合适的宽高
setPreviewSize(mPreviewSize);
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera;
startPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
}
}, null);
} catch (Exception e) {
e.printStackTrace();
}
}
private void startPreviewSession() {
try {
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
//给SurfaceTexture设置缓冲区的大小,这里就是咱们预览的尺寸
surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(surfaceTexture);
mPreviewRequestBuilder.addTarget(surface);
cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mPreviewSession = session;
updatePreview();
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private void updatePreview() {
try {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
//开始预览
mPreviewSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
protected void onPause() {
super.onPause();
stopBackgroundThread();
closeCamera();
}
private void closeCamera() {
if (mPreviewSession != null) {
mPreviewSession.close();
mPreviewSession = null;
}
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
}
private void stopBackgroundThread() {
if (backgroundThread != null) {
backgroundThread.quitSafely();
backgroundThread = null;
backgroundHandler = null;
}
}
private void startBackgroundThread() {
backgroundThread = new HandlerThread("recorderThread");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
@OnClick(R.id.capture_button)
public void onViewClicked() {
}
private void setPreviewSize(Size previewSize) {
//这里为何要这样计算呢,由于经过StreamConfigurationMap获取到的输出尺寸都是以长边为宽,短边为高的,与竖屏状况下咱们认为的宽高恰好相反,因此竖屏状况下,应该讲尺寸反过来设置给TextureView,这样预览的图像才不会变形。若是是横屏状况下就不须要反转了,可是咱们这里的Activity老是竖屏的,没有考虑横屏状况。
int width = screenWidth;
int height = (int) ((float) screenWidth / (float) previewSize.getHeight() * previewSize.getWidth());
ViewGroup.LayoutParams layoutParams = textureView.getLayoutParams();
if (layoutParams == null) {
layoutParams = new RelativeLayout.LayoutParams(width, height);
} else {
if (layoutParams.width == width && layoutParams.height == height) {
return;
}
layoutParams.width = width;
layoutParams.height = height;
}
textureView.setLayoutParams(layoutParams);
}
复制代码
咱们使用HandlerThread来开启一个后台线程,而后经过它的getLooper来建立一个子线程的Handler,后面咱们利用这个Handler来执行一些异步的操做,关于Handler与HandlerThread有时间会再分析一下他们的源码。git
整体来讲预览仍是比较简单的,与拍照时预览没什么区别。下面开始视频录制的逻辑。github
首先咱们要先了解一下MediaRecorder的用法,session
MediaRecorder是Android Frameworl提供给开发者的一套用于音频或视频录制的API。咱们能够经过它来录制音频或者视频。固然录制视频的时候就须要Camera来配合了,下面咱们来看下怎么来配置一个能够录制视频的MediaRecorder。异步
setAudioSource(int audio_source)ide
在MediaRecorder里面有一个内部类AudioSource,里面定义了一些静态常量来表示各个音频的来源,咱们这里用AudioSource.MIC(麦克风)oop
setVideoSource(int video_source)post
一样的在MediaRecorder中有一个VideoSource的内部类,它只有三个静态常量,DEFAULT、CAMERA、SURFACE。CAMERA是与Camera搭配使用的,它须要给MediaRecord经过setCamera(Camera camera)传一个Camera过来,这里咱们用Camera2,因此须要用SURFACE做为视频源,还记得咱们上一篇总结的,Camera是经过CaptureRequest和CameraCaptureSession来将图像数据发送到一些咱们设置的目标Surface中,因此这里咱们用VideoSource.SURFACE。后面咱们就能够经过MediaRecorder的getSurface()方法来拿到它的Surface。
这两个方法都须要在setOutputFormat以前调用,若是在以后调用就会抛IllegalStateException异常。
setOutputFormat(int output_format)
设置录制过程当中输出文件的格式,它须要在setAudioSource()/setVideoSource()以后调用,在prepare()以前调用,同时须要在设置录制参数和解码器以前调用。一样MediaRecorder中的内部类OutputFormat定义了一些静态常量来表示媒体格式。当用H.263视频解码器和AMR音频解码器时,推荐使用3GP格式,对用OutputFormat.THREE_GPP。
setOutputFile(String path)
在setOutputFormat()以后,prepare()以前调用
setVideoSize(int width, int height)
设置录制视频的宽高
setVideoEncodingBitRate(int bitRate)
setVideoFrameRate(int rate)
注意:在某些自动帧率的设备上,这个设置将做为最大帧率而不是一个固定的帧率,实际的帧率会随着光照条件变化而变化。
setAudioEncoder(int audio_encoder)
设置录制的音频编码器,若是没有设置,则输出文件中将不会包含音轨,在setOutputFormat以后prepare以前调用此方法。下面是全部的音频编码器的值
public final class AudioEncoder {
/* Do not change these values without updating their counterparts * in include/media/mediarecorder.h! */
private AudioEncoder() {}
public static final int DEFAULT = 0;
/** AMR (Narrowband) audio codec */
public static final int AMR_NB = 1;
/** AMR (Wideband) audio codec */
public static final int AMR_WB = 2;
/** AAC Low Complexity (AAC-LC) audio codec */
public static final int AAC = 3;
/** High Efficiency AAC (HE-AAC) audio codec */
public static final int HE_AAC = 4;
/** Enhanced Low Delay AAC (AAC-ELD) audio codec */
public static final int AAC_ELD = 5;
/** Ogg Vorbis audio codec */
public static final int VORBIS = 6;
}
复制代码
setVideoEncoder(int video_encoder)
设置录制的视频编码器,若是不设置,输出文件将不包含视频轨道,在setOutputFormat以后prepare以前调用此方法。下面是全部的视频解码器。
public final class VideoEncoder {
/* Do not change these values without updating their counterparts * in include/media/mediarecorder.h! */
private VideoEncoder() {}
public static final int DEFAULT = 0;
public static final int H263 = 1;
public static final int H264 = 2;
public static final int MPEG_4_SP = 3;
public static final int VP8 = 4;
public static final int HEVC = 5;
}
复制代码
setOrientationHint(int degrees)
设置输出文件回放时的方向,在prepare()方法以前调用,它并不会再录制过程当中除法原始视频帧的旋转,可是若是输出格式为OutputFormat.THREE_GPP或者OutputFormat.MPEG_4时,会在输出文件中添加一个包含了旋转角度信息的矩阵,这样播放器能够选择正确的方向来播放,一些播放器播放时可能会忽略这个矩阵。
参数支持0,90,180,270。这里咱们的手机是竖屏的,因此咱们将它设置为90,不然视频播放时是横着的。
基本上经常使用的设置都在这里了,下面咱们正式开始录制。
由于咱们只有一个按钮来控制开始录制跟中止录制,因此咱们用一个boolean值来记录当前的状态。
private boolean isRecording = false;
@OnClick(R.id.capture_button)
public void onViewClicked() {
if (isRecording) {
//中止录制
stopRecord();
//中止录制时的预览
stopPreview();
//开启新的预览回话
startPreviewSession();
//改变按钮状态
captureButton.setText("开始");
isRecording = false;
return;
}
startRecord();
}
private MediaRecorder mediaRecorder;
private void startRecord() {
//狗仔MediaRecorder
setupMediaRecorder();
//中止预览
stopPreview();
try {
//建立一个类型为CameraDevice.TEMPLATE_RECORD的CaptureRequest.Builder
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
//添加预览的Surface
SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface previewSurface = new Surface(surfaceTexture);
mPreviewRequestBuilder.addTarget(previewSurface);
//添加MediaRecorder的Surface
Surface recorderSurface = mediaRecorder.getSurface();
mPreviewRequestBuilder.addTarget(recorderSurface);
//建立新的CameraCaptureSession
cameraDevice.createCaptureSession(Arrays.asList(previewSurface, recorderSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mPreviewSession = session;
//从新开始预览
updatePreview();
//开始录制
mediaRecorder.start();
//改变按钮状态
captureButton.post(new Runnable() {
@Override
public void run() {
isRecording = true;
captureButton.setText("中止");
}
});
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, backgroundHandler);
} catch (Exception exception) {
}
}
private void stopPreview() {
if (mPreviewSession != null) {
mPreviewSession.close();
mPreviewSession = null;
}
}
//构造MediaRecorder,在上面都说过对应的方法了,这里就不注释了
private void setupMediaRecorder() {
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setOutputFile(MediaPathUtil.getMediaPath(MediaPathUtil.TYPE_VIDEO).getPath());
mediaRecorder.setVideoEncodingBitRate(100000000);
mediaRecorder.setVideoFrameRate(30);
mediaRecorder.setVideoSize(mVideoSize.getWidth(),mVideoSize.getHeight());
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setOrientationHint(90);
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}
private void stopRecord() {
if (mediaRecorder != null) {
mediaRecorder.stop();
mediaRecorder.reset();
}
}
复制代码
在Activity onPause方法中调用MediaRecorder.release来释放。
其实视频录制的过程仍是比较清晰的,首先,预览跟拍照没什么区别,就是录制的时候构建一个MediaRecorder,而后从新建立CaptureRequest与CameraCaptureSession,而后将MediaRecorder的Surface传进去,这样当CameraCaptureSession建立好以后图像数据就会渲染后MediaRecorder的Surface中去,而后调用MediaRecorder的start()方法开始录制。最后中止录制的时候调用MediaRecorder的stop()方法中止录制,并从新建立预览的CaptureRequest和CameraCaptureSession从新开启预览。更多细节能够参考https://github.com/googlesamples/android-Camera2Video。