android使用camera2的Api 实现拍照录视频的功能

  • 背景:在视频录制的同时,对视频帧数据进行人脸识别检测,需要用到预览返回的nv21格式数据,
    android.hardware.Camera中的Camera.PreviewCallback接口是不支持同时录制视频和预览回调获取nv21数据的。
    查了查android.hardware.camera2倒是支持的,CaptureRequest.Builder分别添加预览的surface,以及mediaRecorder的surface,即可达到效果。

应用camera2需要注意,camera2 仅支持android 5.0( LOLLIPOP 21 )及以上的版本,如果要兼容之前的,可能得写分支了。

github连接

常用类解释
CameraManager ,用来检测,描绘,连接相机的相机管理类, 类似于WindowManager,ActivityManager的系统级服务
获取方式也一致,如下:

Context.getSystemService(Context.CAMERA_SERVICE);

CameraCharacteristics,描述相机设备的属性类,可以通过它获取到相机对一些功能的支持情况。
demo中用到的一些属性

//获取到相机支持的各种输出size
StreamConfigurationMap map = characteristics.get(
                   CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
map.getOutputSizes(ImageFormat.JPEG) //照片输出尺寸
map.getOutputSizes(SurfaceTexture.class)//预览支持尺寸
map.getOutputSizes(MediaRecorder.class)//录制的视频支持尺寸
characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);//是否支持闪光灯
characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);//是否支持自动对焦
characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//硬件传感器角度

CameraDevice,相机类
获取方式:CameraManager打开指定相机成功后的回调方法里。

CameraManager.openCamera(cameraId, deviceCallback, backgroudHandler);

CaptureRequest.Builder,capture请求builder类,用来生成capture请求。
获取方式:

/**
     * templateType 类别 如下几个
     * TEMPLATE_PREVIEW 预览
     * TEMPLATE_RECORD 录制视频
     * TEMPLATE_STILL_CAPTURE 拍照
     * TEMPLATE_VIDEO_SNAPSHOT //没用到 igonre
     * TEMPLATE_MANUAL  //手动,貌似需要硬件是full级别才能全支持,没详细了解
**/
CaptureRequest.Builder builder = CameraDevice.createCaptureRequest(@RequestTemplate int templateType);

demo主要用到以下几个

//1. 自动聚焦相关:
CaptureRequest.Builder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//2.自动曝光相关         
CaptureRequest.Builder.set(CaptureRequest.CONTROL_AE_MODE,
                    CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//3.预览放大缩小
CaptureRequest.Builder.set(CaptureRequest.SCALER_CROP_REGION, region);
//4.自动控制模式
CaptureRequest.Builder.set(CaptureRequest.CONTROL_MODE,CameraMetadata.CONTROL_MODE_AUTO);
//5.手动触发对焦
CaptureRequest.Builder.set(CaptureRequest.CONTROL_AF_TRIGGER,CameraMetadata.CONTROL_AF_TRIGGER_START);
//6.手动触发曝光
CaptureRequest.Builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);

CameraCaptureSession,相机capture事务,获取或者处理相机图像类,
当session创建成功后,会一直处于激活状态,直到该相机创建了新的session,或者相机关闭。
因为createCaptureSession是较耗时的操作,大概需要几百毫秒的时间,所以是异步实现。

获取方式 :相机调用createCaptureSession方法,在CameraCaptureSession.StateCallback回调中获取
code如下:

/**
* 创建session请求
**/
        CameraDevice.createCaptureSession(List outputs,
            @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler
        );

/**
*回调onConfigured中获取到session
**/
private CameraCaptureSession.StateCallback callback= new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            previewSession = session;
            //.....  
        }
    };

ImageReader, 图像处理类,直接获取到渲染到surface对象上的图像数据
拍照,预览数据的获取
获取方式:

1. 拍照相关
//初始化
 ImageReader  jpegReader = ImageReader.newInstance(width, height,
                    ImageFormat.JPEG, 1); //返回格式是JPEG格式
            jpegReader.setOnImageAvailableListener(
                    jpegImageLister, backgroudHandler);
//获取到数据
ImageReader.OnImageAvailableListener jpegImageLister = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            //照片处理流程
            Image image = reader.acquireLatestImage();

        }
    };
2. 预览相关
ImageReader nv21Reader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2);
nv21Reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                //nv21格式处理相关
                Image image = reader.acquireLatestImage();
                if (image != null) {
                    image.close();
                }
            }
        }, backgroudHandler);

创建一个预览的session示例

private void createCameraPreviewSession() throws CameraAccessException {

        SurfaceTexture texture = textureView.getSurfaceTexture();
        assert texture != null;
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        Surface surface = new Surface(texture);
        mPreviewRequestBuilder
                = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);
        // capture到的image数据会输出到 surface, jpegReader.getSurface() 这两个指定的surfaces中
        cameraDevice.createCaptureSession(Arrays.asList(surface, jpegReader.getSurface()),
                previewStateCallback, null
        );
    }

预览返回数据转NV21代码

/**
     * 根据image对象,转成人脸引擎需要的nv21格式数组
     *
     * @param image
     * @param colorFormat
     * @return
     */
    private byte[] getDataFromImage(Image image, int colorFormat) {

        long startTime = System.currentTimeMillis();
        if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) {
            throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21");
        }
        if (!isImageFormatSupported(image)) {
            throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat());
        }
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        int width = crop.width();
        int height = crop.height();
        Image.Plane[] planes = image.getPlanes();
        byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
        byte[] rowData = new byte[planes[0].getRowStride()];

        int channelOffset = 0;
        int outputStride = 1;
        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    if (colorFormat == COLOR_FormatI420) {
                        channelOffset = width * height;
                        outputStride = 1;
                    } else if (colorFormat == COLOR_FormatNV21) {
                        channelOffset = width * height + 1;
                        outputStride = 2;
                    }
                    break;
                case 2:
                    if (colorFormat == COLOR_FormatI420) {
                        channelOffset = (int) (width * height * 1.25);
                        outputStride = 1;
                    } else if (colorFormat == COLOR_FormatNV21) {
                        channelOffset = width * height;
                        outputStride = 2;
                    }
                    break;
            }
            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();
            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(data, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        data[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
//            if (VERBOSE)
//            Log.v(TAG, "Finished reading data from plane " + i);
        }
        return data;
    }

效果图(本想做gif,但录屏的同时会相机也在录制视频,报start failed: -38异常,就没搞)


1.jpg
2.jpg
4.jpg

代码几个问题

  1. 相机屏幕适配只做了竖屏
    2.在小米4,红米note上录屏的时预览画面会压扁,没解决

参考:
Camera2Basic
这个官方demo有时因为设备支持原因,导致拍照点击无响应,再点就好了。可以跳过ae,af这些回调直接进行拍照。
Camera2Video
这个官方demo连续拍视频的时候会异常,在重拍前调用session的abortCaptures,stopRepeating方法可以避免异常。
CameraView
UI相关的东西参考的这里,封装的很好,代码思路清晰,耦合度低。

你可能感兴趣的:(android使用camera2的Api 实现拍照录视频的功能)