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。

相关文章
相关标签/搜索