上一次寫了一篇關於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。