×

Android Camera2 使用总结

96
_小河马
2017.07.10 09:35* 字数 1918
最近在做自定义相机相关的项目,网上查了资料都是有关android.hardware.Camera的资料,开始使用的才发现这个类已经废弃了。Android 5.0(21)之后android.hardware.Camera就被废弃了,取而代之的是全新的android.hardware.Camera2Android 5.0对拍照API进行了全新的设计,新增了全新设计的Camera v2 API,这些API不仅大幅提高了Android系统拍照的功能,还能支持RAW照片输出,甚至允许程序调整相机的对焦模式、曝光模式、快门等。
demo 地址https://github.com/Hemumu/WallpaperDemo
Camera2主要的类说明
  • CameraManager:摄像头管理器。这是一个全新的系统管理器,专门用于检测系统摄像头、打开系统摄像头。除此之外,调用CameraManagergetCameraCharacteristics(String)方法即可获取指定摄像头的相关特性。
  • CameraCharacteristics:摄像头特性。该对象通过CameraManager来获取,用于描述特定摄像头所支持的各种特性。
  • CameraDevice:代表系统摄像头。该类的功能类似于早期的Camera类。
  • CameraCaptureSession:这是一个非常重要的API,当程序需要预览、拍照时,都需要先通过该类的实例创建Session。而且不管预览还是拍照,也都是由该对象的方法进行控制的,其中控制预览的方法为setRepeatingRequest();控制拍照的方法为capture()
  • CameraRequestCameraRequest.Builder:当程序调用setRepeatingRequest()方法进行预览时,或调用capture()方法进行拍照时,都需要传入CameraRequest参数。CameraRequest代表了一次捕获请求,用于描述捕获图片的各种参数设置,比如对焦模式、曝光模式……总之,程序需要对照片所做的各种控制,都通过CameraRequest参数进行设置。CameraRequest.Builder则负责生成CameraRequest对象。
开启相机预览

开启相机请一定添加相关的相机权限,判断6.0以后添加动态权限的获取。如果相机预览出现黑屏多半就是因为没有相机权限而导致的

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

首页我们要设置相机相关的参数

CameraManager manager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE);
        try {
            //获取可用摄像头列表
            for (String cameraId : manager.getCameraIdList()) {
                //获取相机的相关参数
                CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
                // 不使用前置摄像头。
                Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    continue;
                }
                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                if (map == null) {
                    continue;
                }
                // 检查闪光灯是否支持。
                Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
                mFlashSupported = available == null ? false : available;
                mCameraId = cameraId;
                Log.e(TAG," 相机可用 ");
                return;
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (NullPointerException e) {
            //不支持Camera2API
        }
    }

通过getSystemService(Context.CAMERA_SERVICE);拿到了CameraManager 返回当前可用的相机列表,在这里你可以选择使用前置还是后置摄像头。CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);可以拿到当前相机的相关参数,在这里你可以进行相关的参数检查,例如检查闪光灯是否支持等。在这里我们拿到当前相机的cameraId后面使用。

拿到cameraId我们就可以调用CameraManageropenCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)打开相机了

CameraManager manager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE);
        try {
            //打开相机预览
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
添加 CameraDevice.StateCallback 监听

我们需要对相机状态就行监听,以便在相机状态发生改变的时候做相应的操作。openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)CameraDevice.StateCallback就是对相机的状态改变的Callback

 private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            //创建CameraPreviewSession
            createCameraPreviewSession();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            cameraDevice.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            cameraDevice.close();
            mCameraDevice = null;
        }

    };



创建 CameraCaptureSession

onOpened()中我们可以拿到CameraDevice对象,在相机打开后需要创建CameraCaptureSession
CameraCaptureSession是什么呢?由于Camera2是一套全新的API,所以它引用了管道的概念将安卓设备和摄像头之间联通起来,系统向摄像头发送 Capture 请求,而摄像头会返回 CameraMetadata。这一切建立在一个叫作 CameraCaptureSession的会话中。如下图:

2086682-e68d187e1240bfc5.png

图片来自http://wiki.jikexueyuan.com/project/android-actual-combat-skills/android-hardware-camera2-operating-guide.html

这里我们需要预览相机的内容就需要创建CameraCaptureSession向相机发送Capture请求预览相机内容

/**
     * 为相机预览创建新的CameraCaptureSession
     */
    private void createCameraPreviewSession() {


        try {
            //设置了一个具有输出Surface的CaptureRequest.Builder。
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(mSurfaceHolder.getSurface());
            //创建一个CameraCaptureSession来进行相机预览。
            mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceHolder.getSurface()),
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                            // 相机已经关闭
                            if (null == mCameraDevice) {
                                return;
                            }
                            // 会话准备好后,我们开始显示预览
                            mCaptureSession = cameraCaptureSession;
                            try {
                                // 自动对焦应
                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                                // 闪光灯
                                setAutoFlash(mPreviewRequestBuilder);
                                // 开启相机预览并添加事件
                                mPreviewRequest = mPreviewRequestBuilder.build();
                                //发送请求
                                mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                        null, mBackgroundHandler);
                                Log.e(TAG," 开启相机预览并添加事件");
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onConfigureFailed(
                                @NonNull CameraCaptureSession cameraCaptureSession) {
                            Log.e(TAG," onConfigureFailed 开启预览失败");
                        }
                    }, null);
        } catch (CameraAccessException e) {
            Log.e(TAG," CameraAccessException 开启预览失败");
            e.printStackTrace();
        }
    }

首先我们创建了一个CaptureRequest 上面说过了我们需要跟相机通信只有通过CameraCaptureSession。而要和CameraCaptureSession通信就是发送请求。这里我们相当于在创建请求的一些参数。
createCaptureRequest(int); 也有很多参数可选。这里我们发送的是CameraDevice.TEMPLATE_PREVIEW也就是告诉相机我们只需要预览。更多参数如下

详细信息可以参考官网的API文档。

调用创建方法createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler) 第一参数就是我们需要输出到的Surface列表,这里我们可以输出到一个SurfaceView中或者TextureView中。第二参数是对创建过程的一个回调方法,当onConfigured回调的时候说明CameraCaptureSession创建成功了。现在我们可以向CameraCaptureSession发送前面创建的好的预览相机请求了。调用mCaptureSession.setRepeatingRequest(mPreviewRequest,null, mBackgroundHandler); 这样我们就开启的相机的预览,在刚才添加的输出Surface对应的控件中我们可以看到摄像头的预览内容了。

拍照

当我们需要拍照并且得到相应的照片数据的时候和开启相机预览相同的操作,我们只需要向CameraCaptureSession发送我们创建好的请求就行,就像我们请求网络数据一样,封装好参数直接告诉CameraCaptureSession需要做什么由它去和相机建立通信并执行相应的操作。

对焦
   /**
     * 将焦点锁定为静态图像捕获的第一步。(对焦)
     */
    private void lockFocus() {
        try {
            // 相机对焦
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_START);
            // 修改状态
            mState = STATE_WAITING_LOCK;
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
                    mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

CameraCaptureSession发送对焦请求,并且对对焦是否成功进行监听,在mCaptureCallback中对回调进行处理

 /**
     * 处理与JPEG捕获有关的事件
     */
    private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback() {

        //处理
        private void process(CaptureResult result) {
            switch (mState) {
                case STATE_PREVIEW: {
                    //预览状态
                    break;
                }

                case STATE_WAITING_LOCK: {
                    //等待对焦
                    Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
                    if (afState == null) {
                        captureStillPicture();
                    } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
                            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
                        // CONTROL_AE_STATE can be null on some devices
                        Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                        if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                            mState = STATE_PICTURE_TAKEN;
                            //对焦完成 
                            captureStillPicture();  
                        } else {
                            runPrecaptureSequence();
                        }
                    }
                    break;
                }
                
                
                }
            }
        }
拍摄图片

对焦完成后我们就可以向CameraCaptureSession发送请求可以拍照了

/**
     * 
     * 拍摄静态图片。
     */
    private void captureStillPicture() {
        try {
            if ( null == mCameraDevice) {
                return;
            }
            // 这是用来拍摄照片的CaptureRequest.Builder。
            final CaptureRequest.Builder captureBuilder =
                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureBuilder.addTarget(mImageReader.getSurface());

            // 使用相同的AE和AF模式作为预览。
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            setAutoFlash(captureBuilder);
            // 方向
            int rotation = this.getWindowManager().getDefaultDisplay().getRotation();
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));

            CameraCaptureSession.CaptureCallback CaptureCallback
                    = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                               @NonNull CaptureRequest request,
                                               @NonNull TotalCaptureResult result) {
                    showToast("Saved: " + mFile);
                    Log.d(TAG, mFile.toString());
                    unlockFocus();
                }
            };
            //停止连续取景
            mCaptureSession.stopRepeating();
            //捕获图片
            mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

相信看到这里代码已经不复杂了,组装好我们的请求然后用CameraCaptureSession发送这个请求就可以了。这里需要注意的是我们怎么拿到图片数据呢? 这里要说回在创建CameraCaptureSession时参数不是有一个输出的Surface列表么,在列表中添加一个ImageReaderSurface用户获取图片数据

mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),null).....

ImageReader中对图片获取就行监听

mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);

private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {

        @Override
        public void onImageAvailable(ImageReader reader) {
            //当图片可得到的时候获取图片并保存
            mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
        }

    };

调用reader.acquireNextImage()我们就可以拿到当前的图片数据了。拍完后我们需要解锁焦点让相机回到预览状态,同样的我们发送请求就可以了

   /**
     * 解锁焦点
     */
    private void unlockFocus() {
        try {
            // 重置自动对焦
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
            setAutoFlash(mPreviewRequestBuilder);
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
                    mBackgroundHandler);
            // 将相机恢复正常的预览状态。
            mState = STATE_PREVIEW;
            // 打开连续取景模式
            mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback,
                    mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

效果
GIF.gif

之前看了鸿神推送的Android 仿火萤视频桌面 神奇的LiveWallPaper 一文中提到了用相机来做壁纸也就是透明屏幕,项目地址https://github.com/songixan/Wallpaper 查看源码发现还是用的旧的CameraAPI,所以我在Demo中用Camera2API做了透明屏幕,有兴趣的可以去看下。 ** PS:后来在同事的小米2S(5.1.1)中测试发现出错了,初步猜测是分辨率的原因,目前正在解决中。有问题大家可以私信我 谢谢~**

到此Camera2的学习就结束了,同样的如果你想用相机就行拍摄视频也是如此,用CameraCaptureSession发送相应的请求了就可以了,大家有兴趣可以做一做视频的拍摄。现在做微信的小10秒小视频拍摄也很简单了,思路很简单当用户按下拍摄按钮用CameraCaptureSession发送拍摄视频的请求,松开手指的时候相机恢复到预览状态。so easy~ 大家有兴趣可以尝试下。
拍摄视频的Demohttps://github.com/googlesamples/android-Camera2Video
Tinks android.hardware.camera2 使用指南
日记本
Web note ad 1