Android Camera2视频录制流程

8,291 阅读7分钟

上一次写了一篇关于Camera2拍照流程的文章,今天总结一下利用Camera2与MediaRecorder实现视频录制的流程。同样参考了Google官方Sample

Camera2实现预览

我们先来回顾一下打开相机预览的流程:

  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>

下面是代码

@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。