Android 自定义Camera2 系列(二)

该博客主要记录在开发过程中所运用到的Api 和部分技术调用的记录,阅读时间:15分钟+ ,该博客记录内容相对简单,仅以用于开发过程记录。

本文已独家授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发

说明

在我们前一篇文章中提到Camera,在开发中发现很多api 都已经不推荐使用,google给出的替代方案则就是我们今天的主角 Camera2 ,从5.0开始(API Level 21),可以完全控制Android设备相机的新api 。当然如果产品覆盖的还是有4.0版本的Android 用户的话,还是建议 使用Camera。但是在以前的Camera 中,对相机的手动控制都是通过更改系统才能实现,而且api也不友好。Camera2 这时针对这一点进行了管理的分离,Api会更加的友好,分工明确。

文章对应 Demo 地址
google demo 地址
文章参考地址
很感谢该篇文章作者的描述。

效果图


在这里插入图片描述

1. camera2 概念

camera2流程图

相对于camera ,camera2 在Api上将拍照对象进行了独立,camera2采用pipeline的方式,将Camera 设备和 Android 设备连接起来,Android Device通过管道发送CaptureRequest拍照请求给Camera Device,Camera Device通过管道返回CameraMetadata数据给Android Device,这一切都发生在CameraCaptureSession的会话中。

2. camera2 API简要说明

camera2 主要API

CameraCaptureSession:这是一个非常重要的API,当程序需要预览、拍照时,都通过该类的实例创建Session,控制预览的方法setRepeatingRequest();控制拍照的方法为capture()。

CameraDevices:提供一组静态属性信息,描述硬件设备以及设备的可用设置和输出参数。通过getCameraCharacteristics获得。

CameraManager:所有相机设备(CameraDevice)的管理者,要枚举,查询和打开可用的相机设备,用于打开和关闭系统摄像头,就获取CameraManager实例。

CaptureRequest:定义了相机设备捕获单个映像所需的所有捕获参数。该请求还列出了哪些配置的输出表面应该用作此捕获的目标。

CameraDevice:具有用于为给定用例创建请求构建器的工厂方法,针对应用程序正在运行的Android设备进行了优化,描述系统摄像头,类似于早期的Camera。

CameraRequest CameraRequest.Builder:当程序调用setRepeatingRequest()方法进行预览时,或调用capture()方法进行拍照时,都需要传入CameraRequest参数。CameraRequest代表了一次捕获请求,用于描述捕获图片的各种参数设置,程序对照片所做的各种控制,都通过CameraRequest参数进行设置。CameraRequest.Builder则负责生成CameraRequest对象。

CameraCharacteristics:描述摄像头的各种特性,我们可以通过CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法来获取。

CaptureResult:描述拍照完成后的结果。

ImageReader :通过添加 PreviewRequestBuilder.addTarget(mImageReader.getSurface()); 可以在 OnImageAvailableListener 接口中实时获取 yuv 数据。

在这里插入图片描述

3.Camera2接口使用的流程

接口流程图

1.调用openCamera方法后会回调CameraDevice.StateCallback这个方法,在该方法里重写onOpened函数。

2.在onOpened方法中调用createCaptureSession,该方法又回调CameraCaptureSession.StateCallback方法。

3.CameraCaptureSession.StateCallback中重写onConfigured方法,设置setRepeatingRequest方法(也就是开启预览)。

4.setRepeatingRequest又会回调 CameraCaptureSession.CaptureCallback方法。

5.重写CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未经过处理的帧数据了。

4. 自定义Camera2

Camera2与Camera一样也有cameraId的概念,我们通过mCameraManager.getCameraIdList()来获取cameraId列表,然后通过mCameraManager.getCameraCharacteristics(id) 获取每个id对应摄像头的参数。

关于CameraCharacteristics里面的参数,主要用到的有以下几个:

LENS_FACING:前置摄像头(LENS_FACING_FRONT)还是后置摄像头(LENS_FACING_BACK)。

SENSOR_ORIENTATION:摄像头拍照方向。

FLASH_INFO_AVAILABLE:是否支持闪光灯。

CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL:获取当前设备支持的相机特性。

注:事实上,在各个厂商的的Android设备上,Camera2的各种特性并不都是可用的,需要通过characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)方法 来根据返回值来获取支持的级别,具体说来:

INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬件支持,允许手动控制全高清的摄像、支持连拍模式以及其他新特性。

INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支持,这个需要单独查询。

INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有设备都会支持,也就是和过时的Camera API支持的特性是一致的。

利用这个INFO_SUPPORTED_HARDWARE_LEVEL参数,我们可以来判断是使用Camera还是使用Camera

通过上面的 原理的说明,大致流程可能还会有点模糊,我们直接对应上述逻辑开始代码。

4.1 界面布局

<?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"
    android:background="@android:color/black"
    tools:context="cn.tongue.tonguecamera.ui.CameraActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        // 自定义 TextureView
        <cn.tongue.tonguecamera.view.AutoFitTextureView
            android:id="@+id/textureView_g"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </FrameLayout>

    <RelativeLayout
        android:id="@+id/homecamera_bottom_relative2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00ffffff"
        android:layout_alignParentBottom="true">
        // 返回按钮
        <ImageView
            android:id="@+id/iv_back_g"
            android:layout_width="40dp"
            android:layout_height="30dp"
            android:scaleType="centerInside"
            android:layout_marginBottom="20dp"
            android:layout_marginStart="20dp"
            android:layout_centerVertical="true"
            android:background="@drawable/icon_back" />
        // 拍照按钮
        <ImageView
            android:id="@+id/img_camera_g"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:scaleType="centerInside"
            android:layout_marginBottom="20dp"
            android:layout_centerInParent="true"
            android:background="@drawable/camera" />

    </RelativeLayout>

</RelativeLayout>

通过上述布局我们会发现 camera2 中我们并没有继续选择 SurfaceView作为呈现图片的载体,这里选择的TextureView。

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    public void onResume() {
        super.onResume();
        // 启动 HandlerThread ,后台维护一个 handler
        startBackgroundThread();
        // 存在关联则打开相机,没有则绑定事件
        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            // 绑定 载体监听事件
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }

如果是第一次进入 就需要配置 相机相关配置

    /**
     * 打开相机
     *
     * @param width 宽度
     * @param height 长度
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    private void openCamera(int width, int height) {
        // 判断相机权限 6.0 以上的动态权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
            return;
        }
        // 配置 相机的 预览尺寸
        setUpCameraOutputs(width, height);
        // Matrix 转换配置为 mTextureView
        configureTransform(width, height);
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            // 都配置完成后 打开相机 并绑定回调接口
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }

上述代码中,setUpCameraOutputs() 方法主要进行 相机参数的设置(前后摄像头,闪光灯的配置),设置mImageReader 的成像格式及数据流的回调监听事件 OnImageAvailableListener,并且根据硬件数据 查看是否需要交换尺寸以获得相对于传感器的预览尺寸。

    /**
     * CameraDevice 改变状态时候 调用
     */
    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        // 打开事件监听
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            //打开相机时会调用此方法。 我们在这里开始相机预览。
            mCameraOpenCloseLock.release();
            mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }

        // 关闭监听
        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            mCameraOpenCloseLock.release();
            cameraDevice.close();
            mCameraDevice = null;
        }

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

    };

从回调方法中,我们 通过 CameraCaptureSession 开始 预览图像,createCameraPreviewSession() 则是创建一个 相机预览。

    /**
     * 创建一个新的相机预览
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void createCameraPreviewSession() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            //将默认缓冲区的大小配置为相机预览的大小。
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            Surface surface = new Surface(texture);
            //  Camera2都是通过创建请求会话的方式进行调用的
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            // 使用Surface设置CaptureRequest.Builder
            mPreviewRequestBuilder.addTarget(surface);
            // 方法创建CaptureSession。
            mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.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,
                                        mCaptureCallback, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onConfigureFailed(
                                @NonNull CameraCaptureSession cameraCaptureSession) {
                        }
                    }, null
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

createCaptureRequest()方法里参数templateType代表了请求类型,请求类型一共分为六种,分别为:
TEMPLATE_PREVIEW:创建预览的请求
TEMPLATE_STILL_CAPTURE:创建一个适合于静态图像捕获的请求,图像质量优先于帧速率。
TEMPLATE_RECORD:创建视频录制的请求
TEMPLATE_VIDEO_SNAPSHOT:创建视视频录制时截屏的请求
TEMPLATE_ZERO_SHUTTER_LAG:创建一个适用于零快门延迟的请求。在不影响预览帧率的情况下最大化图像质量。
TEMPLATE_MANUAL:创建一个基本捕获请求,这种请求中所有的自动控制都是禁用的(自动曝光,自动白平衡、自动焦点)。

createCaptureSession()方法一共包含三个参数:
1.List outputs:我们需要输出到的Surface列表。
2.CameraCaptureSession.StateCallback callback:会话状态相关回调。
3.Handler handler:callback可以有多个(来自不同线程),这个handler用来区别那个callback应该被回调,一般写当前线程的Handler即可。

关于CameraCaptureSession.StateCallback里的回调方法
1.onConfigured(@NonNull CameraCaptureSession session); 摄像头完成配置,可以处理Capture请求了。
2.onConfigureFailed(@NonNull CameraCaptureSession session); 摄像头配置失败
3.onReady(@NonNull CameraCaptureSession session); 摄像头处于就绪状态,当前没有请求需要处理。

onActive(@NonNull CameraCaptureSession session); 摄像头正在处理请求。
onClosed(@NonNull CameraCaptureSession session); 会话被关闭
onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface); Surface准备就绪
理解了这些东西,创建预览请求就十分简单了。

设置预览界面尺寸信息,Surface就把它与CaptureRequestBuilder对象关联,然后就是设置会话开始捕获画面。最后的回调CameraCaptureSession.CaptureCallback就给我们设置预览完成的逻辑处理

/**
     * ImageReader 的回调对象。 静止图像已准备好保存。
     */
    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.e(TAG, "onImageAvailable:-------------------");
            Image image = reader.acquireLatestImage();
            //我们可以将这帧数据转成字节数组,类似于Camera1的PreviewCallback回调的预览帧数据
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] data = new byte[buffer.remaining()];
            buffer.get(data);
            image.close();
//            mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
        }

    };

最后这里的方法 就是同 camera 中的 实时帧数据,这里我们可以在这里获取 静止图片的 帧数据,进行一些 图层 水印的处理。google 源码中 一直维系了一个mBackgroundHandler ,我们可以在这里发送 子线程任务reade.acquireNextImage方法获取 静止图片信息,将图片保存到本地文件夹中,基本流程完成后,我们只需要看一下如何触发 拍照即可。

    /**
     * 锁定焦点设置
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void lockFocus() {
        try {
            // 相机锁定的方法 (设置相机对焦)
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                    CameraMetadata.CONTROL_AF_TRIGGER_START);
            // mCaptureCallback 等待锁定  //修改状态
            mState = STATE_WAITING_LOCK;
            //发送对焦请求
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
                    mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

通过点击事件,调用相机锁定 ,设置 mCaptureSession.capture,

    /**
     * 处理与jpg文件捕捉的事件监听(预览)
     */
    private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback() {

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        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) {
                        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;
                }
                case STATE_WAITING_PRECAPTURE: {
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null ||
                            aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
                            aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
                        mState = STATE_WAITING_NON_PRECAPTURE;
                    }
                    break;
                }
                case STATE_WAITING_NON_PRECAPTURE: {
                    Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                    if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
                        mState = STATE_PICTURE_TAKEN;
                        captureStillPicture();
                    }
                    break;
                }
                default:
                    break;
            }
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                        @NonNull CaptureRequest request,
                                        @NonNull CaptureResult partialResult) {
            process(partialResult);
        }

        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                       @NonNull CaptureRequest request,
                                       @NonNull TotalCaptureResult result) {
            process(result);
        }

    };
    /**
     * 拍摄静止图片。 当我们得到响应时,应该调用此方法
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void captureStillPicture() {
        try {
            if (null == activity || null == mCameraDevice) {
                return;
            }
            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 = 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) {
                    Log.e(TAG, mFile.toString());
                    unlockFocus();
                }
            };

            mCaptureSession.stopRepeating();
            mCaptureSession.abortCaptures();
            mCaptureSession.capture(captureBuilder.build(), CaptureCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

此篇文章主要记录 camera2 自定义相机的简单流程,代码基本都来自于 Google 提供的代码,当然 如果有 Kotlin 的朋友,也可以在文章 开头进入Google github 地址查看Kotlin 版本。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269