利用Android Camera2 的照相機api 實現 實時的圖像採集與預覽

   最近想要做一個客戶端往服務器推送實時畫面的功能,首先可以考慮到兩種思路,一種是在客戶端進行視頻流的推送,主要利用RTSP等流媒體協議進行傳輸,而另外一種是通過攝像頭獲取當前畫面,將每一幀作爲對象單獨傳輸。

   項目想要實現的功能最終目的是對實時畫面的每一幀進行處理,可以考慮客戶端推流到服務器,再在服務器進行幀解析的操作,但由於目前很多的流媒體推送框架在推流端或者服務端都或多或少存在限制,很少有完全開源的項目,再加上傳送畫面的同時需要附帶部分的數據,仍然需要另外建立連接進行傳輸,所以暫時擱置這一方案。選擇第二種思路,獲取每一幀的畫面,單獨傳輸。

   要想獲取實時畫面,我們必須通過對安卓設備上的攝像頭進行調用。

   從API21開始,安卓引入了android.hardware.camera2這個包,來替代原有的camera類,原有的camera類已經不再建議使用了。camera2中最重要的變化是,攝像頭的調用不再是簡單地進行實例化,而是用一種類似服務申請的方式來進行調用。通過CameraManager來管理攝像服務,需要通過建立CameraCaptureSession來建立一個調用攝像設備CameraDevices的會話,來實現對攝像頭的調用。而CaptureRequest.Builder類用於建立實際的調用請求,具體的參數設置也可以通過這個類來實現(而不是對camera設備進行直接設置),這樣做的目的是把對攝像頭的控制與攝像頭本身分離開來,用戶可以通過不同的session根據不同的配置來使用攝像頭。

   我們可以結合具體的代碼來分析新api中攝像頭調用的過程。

   首先我們想要對攝像設備進行操作,需要獲得CameraManager的實例

    CameraManager cameraManager;
    cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

   我們可以調用openCamera函數打開攝像頭設備

    cameraManager.openCamera(cameraId, cameraCallback, mainHandler);

   這裏需要傳入三個參數,cameraId是設備編號,cameraCallback控制攝像服務的回調,最後一個參數指定HandlerThread對象 

     cameraId = Integer.toString(CameraCharacteristics.LENS_FACING_FRONT);
     
     private CameraDevice.StateCallback cameraCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            Log.d("CameraCallback", "Camera Opened");
            cameraDevice = camera;
            takePreview();
        }

        @Override
        public void onDisconnected(CameraDevice cameraDevice) {
            Log.d("CameraCallback", "Camera Disconnected");
            closeCameraDevice();
        }

        @Override
        public void onError(CameraDevice cameraDevice, int i) {
            Log.d("CameraCallback", "Camera Error");
            Toast.makeText(PusherSurface.this, "攝像頭開啓失敗", Toast.LENGTH_SHORT).show();
        }
    };

 回調函數用於指定連接攝像頭設備時不同狀態的操作。在這裏,我們在攝像頭成功連接的時候調用  takePreview()函數開啓攝像頭畫面的預覽。

private void takePreview() {
    try {
        final CaptureRequest.Builder previewRequestBuilder
                = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        previewRequestBuilder.addTarget(surfaceHolder.getSurface());
        previewRequestBuilder.addTarget(previewReader.getSurface());
        cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(),
                previewReader.getSurface(),
                p_w_picpathReader.getSurface()), new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                if (cameraDevice == null) return;
                mCameraCaptureSession = cameraCaptureSession;

                try {
                    previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                    previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_OFF);

                    CaptureRequest previewRequest = previewRequestBuilder.build();
                    mCameraCaptureSession.setRepeatingRequest(previewRequest, null, childHandler);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
                Toast.makeText(PusherSurface.this, "配置失敗", Toast.LENGTH_SHORT).show();
            }
        }, childHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

要從攝像設備中獲取圖像,我們首先需要建立一個camera capture session。函數

createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)的第一個參數傳入了我們想要繪製的視圖列表,第二個參數傳入的是建立攝像會話的狀態回調函數,第三個參數傳入相應的handler處理器。然後,我們需要利用capturerequest來定義攝像頭捕獲圖像時候的具體參數,比如是否開啓攝像頭,是否自動對焦等。最後通過CamraCaptureSession.setRepeatingRequest來開啓請求。這樣我們就可以從capturesession傳入的list中的surface列表獲得連續的圖像。留意到

previewRequestBuilder.addTarget(surfaceHolder.getSurface());
previewRequestBuilder.addTarget(previewReader.getSurface());

這裏除了傳入xml界面佈局中的surfaceHolder的surface外,還傳入了一個previewReader的surface。

previewReader是一個自定義的ImageReader對象。

previewReader = ImageReader.newInstance(1080, 1920, ImageFormat.YUV_420_888, 2);
        previewReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader p_w_picpathReader) {
                Image p_w_picpath = null;
                try {
                    p_w_picpath = p_w_picpathReader.acquireLatestImage();
                    Log.d("PreviewListener", "GetPreviewImage");
                    if (p_w_picpath == null) {
                        return;
                    }
                    byte[] bytes = ImageUtil.p_w_picpathToByteArray(p_w_picpath);
                    if (pushFlag == false)
                        uploadImg(bytes);
                } finally {
                    if (p_w_picpath != null) {
                        p_w_picpath.close();
                    }
                }

            }
        }, mainHandler);

ImageReader是一個可以讓我們對繪製到surface的圖像進行直接操作的類。在這裏我們從攝像設備中傳入了連續的預覽圖片,也就是我們在屏幕上看到的畫面,它們的格式都是未經壓縮的YUV_420_888類型的(同樣的如果要操作拍攝後的圖片,就要設置成jpeg格式)。我們調用p_w_picpathReader.acquireLatestImage或者acquireNextImage來獲取圖像隊列中的圖片。並進行操作。在這裏我利用一個函數將圖像壓縮後轉化成byte[]格式,並調用uploadImg函數上傳至服務器。這樣,整個攝像頭的調用到預覽圖像的處理也就完成了。想要實現拍照功能也是大同小異,在這裏我就不一一貼出了。

  歡迎更多安卓開發者一同交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章