Android Camera2視頻錄製流程

上一次寫了一篇關於Camera2拍照流程的文章,今天總結一下利用Camera2與MediaRecorder實現視頻錄製的流程。一樣參考了Google官方Samplephp

Camera2實現預覽

咱們先來回顧一下打開相機預覽的流程:java

  1. 經過CameraManager獲取可用的相機設備列表。
  2. 經過CameraManager拿到對應相機的參數
  3. 調用openCamera打開相機。
  4. 在回調中建立CaptureRequestBuilder與CameraCaptureSession。其中,要將咱們的Surface添加到CaptureRequestBuilder中,這裏咱們仍是使用TextureView,經過其SurfaceTexture來建立Surface。
  5. 調用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>
複製代碼

下面是代碼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用法介紹

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。

相關文章
相關標籤/搜索