Android Camera开发序列:Camera2 API 的简单描述和调用(预览、拍照、录像)

Android 5.1 以后,添加了Camera2 的API,能够满足更多操纵camera的场景。当然,相对应camera1的调用,也变的复杂一点。

一、涉及到的关键类

CameraManager -------------- 获取连接的camera情况,执行打开摄像头的操作;

CameraDevice -------------- 当前连接的摄像头对象;

CaptureRequest -------------- camera数据的请求,比如预览、拍照、录像等 ;

CaptureSession -------------- 发送请求后,就建立了一个会话,可以在当前建立的会话上切换各种请求,不需要的时候可以执行关闭;

二、代码实现

下面代码是基于Google提供的demo https://github.com/googlesamples/android-Camera2Basic

后面自己个人又建了个独立的分支,代码都是基于Google Demo 来的 https://github.com/yorkZJC/AndroidCamera2Sample

Camera2BaseFragment.java

2.1 这里采用的是TextureView来进行显示,在onResume()的时候,进行判断,如果当前TextureView 可用了,则执行打开摄像头的操作,否则等待TextureView available,第一次打开的是,TextureView还没创建完成,所以会在TextureView available回调中执行打开camera的操作。

 @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();

        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }
 private final TextureView.SurfaceTextureListener mSurfaceTextureListener
            = new TextureView.SurfaceTextureListener() {

        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
            openCamera( width, height);
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
            configureTransform(width, height);
        }

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
            return true;
        }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture texture) {
        }
    };

2.2 接下来看下openCamera()的实现

这边完成了camera信息的获取的配置,并调用CameraManager 的openCamera打开摄像头,camera打开状态在CameraDevice.StateCallback中进行回调.

   private void openCamera(int width, int height) {
        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission();
            return;
        }
        setUpCameraOutputs(width, height);
        configureTransform(width, height);
        Activity activity = getActivity();
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock back 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);
        }
    }

2.3 在camera打开的回调中,可以获取到当前的camera对应的CameraDevice,在onOpened()中执行打开预览的操作。

 private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            // This method is called when the camera is opened.  We start camera preview here.
            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;
            Activity activity = getActivity();
            if (null != activity) {
                activity.finish();
            }
        }
    };

2.4 下来就是打开预览的过程,主要做了下面几件事情:

1、预览图像显示在哪里,这就需要绑定surface,这里可以进行多个surface的绑定,如果是上层需要拿到预览数据,则可以设置ImageReader的surface进去;

2、发送预览请求;

3、建立预览会话;

完成这几步,我们就可以看到预览图像了。

  private void createCameraPreviewSession() {
        try {
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;

            // We configure the size of default buffer to be the size of camera preview we want.
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

            // This is the output Surface we need to start preview.
            Surface surface = new Surface(texture);

            // We set up a CaptureRequest.Builder with the output Surface.
            mPreviewRequestBuilder
                    = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

            mPreviewRequestBuilder.addTarget(surface);

            //request builder可以设置多个target,如果需要拿到实时的预览数据,则把imageReader 的surface 也设进去
//            mPreviewRequestBuilder.addTarget(mImageReader.getSurface());

            // Here, we create a CameraCaptureSession for camera preview.
            mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {

                            // The camera is already closed
                            if (null == mCameraDevice) {
                                return;
                            }

                            // When the session is ready, we start displaying the preview.
                            mPreviewCaptureSession = cameraCaptureSession;
                            try {
                                // Auto focus should be continuous for camera preview.
                                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                                // Flash is automatically enabled when necessary.
                                setAutoFlash(mPreviewRequestBuilder);

                                // Finally, we start displaying the camera preview.
                                mPreviewRequest = mPreviewRequestBuilder.build();
//                                mPreviewCaptureSession.setRepeatingRequest(mPreviewRequest,
//                                        mCaptureCallback, mBackgroundHandler);
                                mPreviewCaptureSession.setRepeatingRequest(mPreviewRequest,
                                        null, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

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

2.5 拍照

Camera2 的API,拍照时通过ImageReader返回jpeg数据给上层,交由上层进行保存;

如下面代码所示:

1、首先需要初始化一个JPEG类型的ImageReader,用来接收底层数据回调;

2、设置CameraDevice.TEMPLATE_STILL_CAPTURE 类型的请求,请求拍照;请求成功后,我们需要恢复正常的预览类型请求;

3、在ImageReader回调中将接收到的jpeg数据进行保存;

 /**
  * 初始化一个jpeg类型的imageReader
  **/
 private void initJpegImageReader(int width, int height) {

         StreamConfigurationMap map = mCameraCharacteristics.get(
                 CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
         if (map == null) {
             return;
         }

        Size largest = Collections.max(
                  Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                  new CompareSizesByArea());

        mJpegCpatureWidth = largest.getWidth();
        mJpegCaptureHeight = largest.getHeight();

        mJpegImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                  ImageFormat.JPEG, 2);
        mJpegImageReader.setOnImageAvailableListener(mJpegImageAvailableListener, mBackgroundHandler);
}

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

        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.v(TAG, "--- mJpegImageAvailableListener();reader: " + reader);
            Image image = reader.acquireLatestImage();
            if(image == null){
                return;
            }

            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);

            //将接收到的数据交由独立的线程进行文件的保存操作
            mBackgroundHandler.post(new ImageSaver(bytes,mJpegCpatureWidth,mJpegCaptureHeight, generateJpegFile(),mCaptureListener));
            image.close();
        }
   };

private void captureStillPicture() {

        try {
            if (null == mCameraDevice || mCapturing || mPreviewSession == null) {

                return;
            }

            mCapturing = true;

            // This is the CaptureRequest.Builder that we use to take a picture.
            mPreviewBuilder =
//设置拍照请求                    
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            mPreviewBuilder.addTarget(mJpegImageReader.getSurface());

            // Use the same AE and AF modes as the preview.
//            mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
//                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//            setAutoFlash(captureBuilder);

            // Orientation
            int rotation = 0;//activity.getWindowManager().getDefaultDisplay().getRotation();
            mPreviewBuilder.set(CaptureRequest.JPEG_ORIENTATION, 0);

            CameraCaptureSession.CaptureCallback captureCallback
                    = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                               @NonNull CaptureRequest request,
                                               @NonNull TotalCaptureResult result) {

                      //拍照请求成功后,恢复正常的预览模式
                      startPreview();

                    mCapturing = false;
                }
            };
            mPreviewSession.stopRepeating();
            mPreviewSession.abortCaptures();
            mPreviewSession.capture(mPreviewBuilder.build(),captureCallback , mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

2.6 录像

Android API文档(https://developer.android.google.cn/reference/android/hardware/camera2/package-summary?hl=en)描述有下面这么一段话,我们可以看到MediaRecorder 的surface也是可以作为target Surface进行数据的请求的。那就很简单了,录像编码需要数据来源,而这个source就是通过MediaRecorder.getsurface,然后把该surface设置为target surface,那么MediaRecorder就可以拿到Camera数据了。

下面看下具体的代码实现:

private void startRecordingVideo() {
        if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
            return;
        }
        try {
            closePreviewSession();
            setUpMediaRecorder();
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
            List<Surface> surfaces = new ArrayList<>();

            // Set up Surface for the camera preview
            Surface previewSurface = new Surface(texture);
            surfaces.add(previewSurface);
            mPreviewBuilder.addTarget(previewSurface);

            // Set up Surface for the MediaRecorder
            Surface recorderSurface = mMediaRecorder.getSurface();
            surfaces.add(recorderSurface);
            mPreviewBuilder.addTarget(recorderSurface);

            // Start a capture session
            // Once the session starts, we can update the UI and start recording
            mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {

                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    mPreviewSession = cameraCaptureSession;
                    updatePreview();
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            // UI
                            mButtonVideo.setText(R.string.stop);
                            mIsRecordingVideo = true;

                            // Start recording
                            mMediaRecorder.start();
                        }
                    });
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    Activity activity = getActivity();
                    if (null != activity) {
                        Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
                    }
                }
            }, mBackgroundHandler);
        } catch (CameraAccessException | IOException e) {
            e.printStackTrace();
        }
    }

三、写在后面

看了上面简单的代码流程,我们应该有这样简单的概念。对Camera的操作,无非就是获取到硬件设备相关属性,比如当前挂载了哪些摄像头,摄像头支持哪些分辨率等属性,这个我们需要用到CameraManager来获取;

获取到Camera相关属性后,那么就需要对硬件设备进行操作,操作就是打开Camera,获取预览数据这些了,通过CameraManager,我们能打开对应Id的camera,然后获取到该id对应的Camera设备实例,这个就是CameraDevice了;

那么接下来就是怎么怎么把Camera数据显示到UI上,这时就用到Surface了,我们可以这样理解,Surface是图像显示的介质,Camera2 API 允许我们设置多个Surface为输出目标,比如上面我们说的ImageReader、SurfaceTexutre、MediaRecorder相关的Surface都可以设为目标Surface,底层会帮我们进行数据的填充和显示。这些Surface我们需要预先初始化好参数;

那么有了显示的载体后,就可以进行显示了,Camera2里面就用到了个CaptureRequest 来触发数据的请求,这个request又可以根据自己的使用场景设置不同的请求类型,比如是 预览场景,则可以设置请求类型为 CameraDevice.TEMPLATE_PREVIEW,录像场景下,则设置为CameraDevice.TEMPLATE_RECORD,拍照场景下,则设置为CameraDevice.TEMPLATE_STILL_CAPTURE;

完成了上面这些后,还需要最后一步,就是建立会话了,也就是CaptureSession。我们可以理解为,上面所做的准备,都是为了建立会话,建立了会话后,和Camera之间的交互才真正建立起来。这个会话可以随时关闭,也可以修改参数。


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