上一次写了一篇关于Camera2拍照流程的文章,今天总结一下利用Camera2与MediaRecorder实现视频录制的流程。同样参考了Google官方Sample。
Camera2实现预览
我们先来回顾一下打开相机预览的流程:
- 通过CameraManager获取可用的相机设备列表。
- 通过CameraManager拿到对应相机的参数
- 调用openCamera打开相机。
- 在回调中创建CaptureRequestBuilder与CameraCaptureSession。其中,要将我们的Surface添加到CaptureRequestBuilder中,这里我们还是使用TextureView,通过其SurfaceTexture来创建Surface。
- 调用CameraCaptureSession的setRepeatingRequest来开启预览。
<?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>
下面是代码
@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有时间会再分析一下他们的源码。
总体来说预览还是比较简单的,与拍照时预览没什么区别。下面开始视频录制的逻辑。
首先我们要先了解一下MediaRecorder的用法,
MediaRecorder用法介绍
MediaRecorder是Android Frameworl提供给开发者的一套用于音频或视频录制的API。我们可以通过它来录制音频或者视频。当然录制视频的时候就需要Camera来配合了,下面我们来看下怎么来配置一个可以录制视频的MediaRecorder。
音频与视频的来源
setAudioSource(int audio_source)
在MediaRecorder里面有一个内部类AudioSource,里面定义了一些静态常量来表示各个音频的来源,我们这里用AudioSource.MIC(麦克风)
setVideoSource(int video_source)
同样的在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。