Android平臺Camera開發實踐指南

蘇策發表於2017-12-15

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 Camera實踐指南
    • 1.1 開啟相機
    • 1.2 關閉相機
    • 1.3 開啟預覽
    • 1.4 關閉預覽
    • 1.5 拍照
    • 1.6 開始視訊錄製
    • 1.7 結束視訊錄製
  • 二 Camera2實踐指南
    • 2.1 開啟相機
    • 2.2 關閉相機
    • 2.3 開啟預覽
    • 2.4 關閉預覽
    • 2.5 拍照
    • 2.6 開始視訊錄製
    • 2.7 結束視訊錄製

Android Camera 相關API也是Android生態碎片化最為嚴重的一塊,首先Android本身就有兩套API,Android 5.0以下的Camera和Android 5.0以上的Camera2,而且 更為嚴重的時,各家手機廠商都Camera2的支援程度也各不相同,這就導致我們在相機開發中要花費很大精力來處理相容性問題。

相機開發的一般流程是什麼樣的??

  1. 檢測並訪問相機資源 檢查手機是否存在相機資源,如果存在則請求訪問相機資源。
  2. 建立預覽介面,建立繼承自SurfaceView並實現SurfaceHolder介面的拍攝預覽類。有了拍攝預覽類,即可建立一個佈局檔案,將預覽畫面與設計好的使用者介面控制元件融合在一起,實時顯示相機的預覽影象。
  3. 設定拍照監聽器,給使用者介面控制元件繫結監聽器,使其能響應使用者操作, 開始拍照過程。
  4. 拍照並儲存檔案,將拍攝獲得的影象轉換成點陣圖檔案,最終輸出儲存成各種常用格式的圖片。
  5. 釋放相機資源,相機是一個共享資源,當相機使用完畢後,必須正確地將其釋放,以免其它程式訪問使用時發生衝突。

相機開發一般需要注意哪些問題??

  1. 版本相容性問題,Android 5.0以下的Camera和Android 5.0以上使用Camera2,Android 4.0以下的SurfaceView和Android 4.0以上的TextureView,Android 6.0以上要做相機等執行時許可權相容。
  2. 裝置相容性問題,Camera/Camera2裡的各種特性在有些手機廠商的裝置實現方式和支援程度是不一樣的,這個需要做相容性測試,一點點踩坑。
  3. 各種場景下的生命週期變化問題,最常見的是後臺場景和鎖屏場景,這兩種場景下的相機資源的申請與釋放,Surface的建立與銷燬會帶來一些問題,這個我們 後面會仔細分析。

關於Camera/Camear2

既然要解決這種相容性問題,就要兩套並用,那是不是根據版本來選擇:Android 5.0 以下用Camera,Android 5.0以上用Camera2呢??

事實上,這樣是不可取的。前面說過不同手機廠商對Camera2的支援程度各不相同,即便是Android 5.0 以上的手機,也存在對Camera2支援非常差的情況,這個時候就要降級使用Camera,如何判斷對Camera的支援 程度我們下面會說。

關於SurfaceView/TextureView

  • SurfaceView是一個有自己Surface的View。介面渲染可以放在單獨執行緒而不是主執行緒中。它更像是一個Window,自身不能做變形和動畫。
  • TextureView同樣也有自己的Surface。但是它只能在擁有硬體加速層層的Window中繪製,它更像是一個普通View,可以做變形和動畫。

更多關於SurfaceView與TextureView區別的內容可以參考這篇文章Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView.

那麼如何針對版本進行方案的選擇呢??

官方的開源庫cameraview給出了方案:

Android平臺Camera開發實踐指南

既然要兩套並用,就要定義統一的介面,針對不同場景提供不同的實現,使用的時候也是根據不同的場景來建立不同的例項。

我們不難發現,這個介面一般需要定義以下功能:

  • 開啟相機
  • 關閉相機
  • 開啟預覽
  • 關閉預覽
  • 拍照
  • 開始視訊錄製
  • 結束視訊錄製

定義好了介面,我們就有了思路,針對相機的具體特性實現相應的方案,那麼另一個問題就出來了,相機在日常開發中一般作為一個SDK的形式存在供各個業務方呼叫,那麼如何設計 出一個功能與UI相分離,高度可定製的相機SDK呢??

答案就是利用Fragment,將各種點選事件(點選拍照、點選切換攝像頭、點選切換閃光模式等)對應的功能封裝在Fragment裡,業務方在用的時候可以在Fragment之上蒙一層 UI(當然我們也需要提供預設的實現),這樣就可以讓功能和UI相分離,整合起來也非常的簡便。

相機SDK框架圖如下所示:

Android平臺Camera開發實踐指南
  • CameraActivity:相機介面,主要用來實現UI的定製,實際功能(點選事件)交由CameraFragment完成。
  • CameraFragment:向CameraActivity提供功能介面,完成CameraActivity裡的點選事件,例如:拍照、錄影等。
  • CameraLifecycle:處理相機隨著Activity生命週期變化的情況,內部持有CameraManager,處理相機初始化和釋放,預覽的建立與銷燬等問題。
  • CameraManager:相機的實際管理者,呼叫相機API來操作相機,進行拍照和錄影等操作。
  • Camera/Camera2:相機API。

phoenix專案已經實現了這套方案,效果圖如下所示:

Android平臺Camera開發實踐指南 Android平臺Camera開發實踐指南

理解了整體的架構,我們接著就來分析針對這套架構,Camera/Camera2分別該如何實現。

一 Camera實踐指南

Camera API中主要涉及以下幾個關鍵類:

  • Camera:操作和管理相機資源,支援相機資源切換,設定預覽和拍攝尺寸,設定光圈、曝光等相關引數。
  • SurfaceView:用於繪製相機預覽影象,提供實時預覽的影象。
  • SurfaceHolder:用於控制Surface的一個抽象介面,它可以控制Surface的尺寸、格式與畫素等,並可以監視Surface的變化。
  • SurfaceHolder.Callback:用於監聽Surface狀態變化的介面。

SurfaceView和普通的View相比有什麼區別呢??

普通View都是共享一個Surface的,所有的繪製也都在UI執行緒中進行,因為UI執行緒還要處理其他邏輯,因此對View的更新速度和繪製幀率無法保證。這顯然不適合相機實時 預覽這種情況,因而SurfaceView持有一個單獨的Surface,它負責管理這個Surface的格式、尺寸以及顯示位置,它的Surface繪製也在單獨的執行緒中進行,因而擁有更高 的繪製效率和幀率。

SurfaceHolder.Callback介面裡定義了三個函式:

  • surfaceCreated(SurfaceHolder holder); 當Surface第一次建立的時候呼叫,可以在這個方法裡呼叫camera.open()、camera.setPreviewDisplay()來實現開啟相機以及連線Camera與Surface 等操作。
  • surfaceChanged(SurfaceHolder holder, int format, int width, int height); 當Surface的size、format等發生變化的時候呼叫,可以在這個方法裡呼叫camera.startPreview()開啟預覽。
  • surfaceDestroyed(SurfaceHolder holder); 當Surface被銷燬的時候呼叫,可以在這個方法裡呼叫camera.stopPreview(),camera.release()等方法來實現結束預覽以及釋放

1.1 開啟相機

開啟相機之前我們需要先獲取系統相機的相關資訊。

//有多少個攝像頭
numberOfCameras = Camera.getNumberOfCameras();

for (int i = 0; i < numberOfCameras; ++i) {
    final Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

    Camera.getCameraInfo(i, cameraInfo);
    //後置攝像頭
    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
        faceBackCameraId = i;
        faceBackCameraOrientation = cameraInfo.orientation;
    } 
    //前置攝像頭
    else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        faceFrontCameraId = i;
        faceFrontCameraOrientation = cameraInfo.orientation;
    }
}
複製程式碼

知道了相機相關資訊,就可以通過相機ID開啟相機了。

camera = Camera.open(cameraId);
複製程式碼

另外,開啟相機以後你會獲得一個Camera物件,從這個物件裡可以獲取和設定相機的各種引數資訊。


//獲取相機引數
camera.getParameters();
//設定相機引數
camera.getParameters();
複製程式碼

常見的引數有以下幾種。

閃光燈配置引數,可以通過Parameters.getFlashMode()介面獲取。

  • Camera.Parameters.FLASH_MODE_AUTO 自動模式,當光線較暗時自動開啟閃光燈;
  • Camera.Parameters.FLASH_MODE_OFF 關閉閃光燈;
  • Camera.Parameters.FLASH_MODE_ON 拍照時閃光燈;
  • Camera.Parameters.FLASH_MODE_RED_EYE 閃光燈引數,防紅眼模式。

對焦模式配置引數,可以通過Parameters.getFocusMode()介面獲取。

  • Camera.Parameters.FOCUS_MODE_AUTO 自動對焦模式,攝影小白專用模式;
  • Camera.Parameters.FOCUS_MODE_FIXED 固定焦距模式,拍攝老司機模式;
  • Camera.Parameters.FOCUS_MODE_EDOF 景深模式,文藝女青年最喜歡的模式;
  • Camera.Parameters.FOCUS_MODE_INFINITY 遠景模式,拍風景大場面的模式;
  • Camera.Parameters.FOCUS_MODE_MACRO 微焦模式,拍攝小花小草小螞蟻專用模式;

場景模式配置引數,可以通過Parameters.getSceneMode()介面獲取。

  • Camera.Parameters.SCENE_MODE_BARCODE 掃描條碼場景,NextQRCode專案會判斷並設定為這個場景;
  • Camera.Parameters.SCENE_MODE_ACTION 動作場景,就是抓拍跑得飛快的運動員、汽車等場景用的;
  • Camera.Parameters.SCENE_MODE_AUTO 自動選擇場景;
  • Camera.Parameters.SCENE_MODE_HDR 高動態對比度場景,通常用於拍攝晚霞等明暗分明的照片;
  • Camera.Parameters.SCENE_MODE_NIGHT 夜間場景;

1.2 關閉相機

關閉相機很簡單,只需要把相機釋放掉就可以了。

camera.release();
複製程式碼

1.3 開啟預覽

Camera的預覽時通過SurfaceView的SurfaceHolder進行的,先通過,具體說來:

private void startPreview(SurfaceHolder surfaceHolder) {
    try {
        final Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        Camera.getCameraInfo(currentCameraId, cameraInfo);
        int cameraRotationOffset = cameraInfo.orientation;

        //獲取相機引數
        final Camera.Parameters parameters = camera.getParameters();
        //設定對焦模式
        setAutoFocus(camera, parameters);
        //設定閃光模式
        setFlashMode(mCameraConfigProvider.getFlashMode());

        if (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_PHOTO
                || mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)
            turnPhotoCameraFeaturesOn(camera, parameters);
        else if (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_PHOTO)
            turnVideoCameraFeaturesOn(camera, parameters);

        final int rotation = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break; // Natural orientation
            case Surface.ROTATION_90:
                degrees = 90;
                break; // Landscape left
            case Surface.ROTATION_180:
                degrees = 180;
                break;// Upside down
            case Surface.ROTATION_270:
                degrees = 270;
                break;// Landscape right
        }

        //根據前置與後置攝像頭的不同,設定預覽方向,否則會發生預覽影象倒過來的情況。
        if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            displayRotation = (cameraRotationOffset + degrees) % 360;
            displayRotation = (360 - displayRotation) % 360; // compensate
        } else {
            displayRotation = (cameraRotationOffset - degrees + 360) % 360;
        }
        this.camera.setDisplayOrientation(displayRotation);

        if (Build.VERSION.SDK_INT > 13
                && (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_VIDEO
                || mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)) {
//                parameters.setRecordingHint(true);
        }

        if (Build.VERSION.SDK_INT > 14
                && parameters.isVideoStabilizationSupported()
                && (mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_VIDEO
                || mCameraConfigProvider.getMediaAction() == CameraConfig.MEDIA_ACTION_UNSPECIFIED)) {
            parameters.setVideoStabilization(true);
        }

        //設定預覽大小
        parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
        parameters.setPictureSize(photoSize.getWidth(), photoSize.getHeight());

        //設定相機引數
        camera.setParameters(parameters);
        //設定surfaceHolder
        camera.setPreviewDisplay(surfaceHolder);
        //開啟預覽
        camera.startPreview();

    } catch (IOException error) {
        Log.d(TAG, "Error setting camera preview: " + error.getMessage());
    } catch (Exception ignore) {
        Log.d(TAG, "Error starting camera preview: " + ignore.getMessage());
    }
}
複製程式碼

1.4 關閉預覽

關閉預覽很簡單,直接呼叫camera.stopPreview()即可。

camera.stopPreview();
複製程式碼

1.5 拍照

拍照時通過呼叫Camera的takePicture()方法來完成的,

takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg)
複製程式碼

該方法有三個引數:

  • ShutterCallback shutter:在拍照的瞬間被回撥,這裡通常可以播放"咔嚓"這樣的拍照音效。
  • PictureCallback raw:返回未經壓縮的影象資料。
  • PictureCallback postview:返回postview型別的影象資料
  • PictureCallback jpeg:返回經過JPEG壓縮的影象資料。

我們一般用的就是最後一個,實現最後一個PictureCallback即可。

camera.takePicture(null, null, new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] bytes, Camera camera) {
            //儲存返回的影象資料
            final File pictureFile = outputPath;
            if (pictureFile == null) {
                Log.d(TAG, "Error creating media file, check storage permissions.");
                return;
            }
            try {
                FileOutputStream fileOutputStream = new FileOutputStream(pictureFile);
                fileOutputStream.write(bytes);
                fileOutputStream.close();
            } catch (FileNotFoundException error) {
                Log.e(TAG, "File not found: " + error.getMessage());
            } catch (IOException error) {
                Log.e(TAG, "Error accessing file: " + error.getMessage());
            } catch (Throwable error) {
                Log.e(TAG, "Error saving file: " + error.getMessage());
            }
        }
 });
複製程式碼

拍照完成後如果還要繼續拍照則呼叫camera.startPreview()繼續開啟預覽,否則關閉預覽,釋放相機資源。

1.6 開始視訊錄製

視訊的錄製時通過MediaRecorder來完成的。

if (prepareVideoRecorder()) {
            mediaRecorder.start();
            isVideoRecording = true;
            uiHandler.post(new Runnable() {
                @Override
                public void run() {
                    videoListener.onVideoRecordStarted(videoSize);
                }
            });
}
複製程式碼

MediaRecorder主要用來錄製音訊和視訊,在使用之前要進行初始化和相關引數的設定,如下所示:

protected boolean preparemediaRecorder() {
    mediaRecorder = new MediaRecorder();
    try {
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        
        //輸出格式
        mediaRecorder.setOutputFormat(camcorderProfile.fileFormat);
        //視訊幀率
        mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate);
        //視訊大小
        mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight());
        //視訊位元率
        mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate);
        //視訊編碼器
        mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec);
        
        //音訊編位元速率
        mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate);
        //音訊聲道
        mediaRecorder.setAudioChannels(camcorderProfile.audioChannels);
        //音訊取樣率
        mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate);
        //音訊編碼器
        mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec);
        
        File outputFile = outputPath;
        String outputFilePath = outputFile.toString();
        //輸出路徑
        mediaRecorder.setOutputFile(outputFilePath);
        
        //設定視訊輸出的最大尺寸
        if (mCameraConfigProvider.getVideoFileSize() > 0) {
            mediaRecorder.setMaxFileSize(mCameraConfigProvider.getVideoFileSize());
            mediaRecorder.setOnInfoListener(this);
        }
        
        //設定視訊輸出的最大時長
        if (mCameraConfigProvider.getVideoDuration() > 0) {
            mediaRecorder.setMaxDuration(mCameraConfigProvider.getVideoDuration());
            mediaRecorder.setOnInfoListener(this);
        }
        mediaRecorder.setOrientationHint(getVideoOrientation(mCameraConfigProvider.getSensorPosition()));
        
        //準備
        mediaRecorder.prepare();

        return true;
    } catch (IllegalStateException error) {
        Log.e(TAG, "IllegalStateException preparing MediaRecorder: " + error.getMessage());
    } catch (IOException error) {
        Log.e(TAG, "IOException preparing MediaRecorder: " + error.getMessage());
    } catch (Throwable error) {
        Log.e(TAG, "Error during preparing MediaRecorder: " + error.getMessage());
    }
    releasemediaRecorder();
    return false;
}
複製程式碼

值得一提的是,日常的業務中經常對拍攝視訊的時長或者大小有要求,這個可以通過mediaRecorder.setOnInfoListener()來處理,OnInfoListener會監聽正在錄製的視訊,然後我們 可以在它的回撥方法裡處理。

   @Override
public void onInfo(MediaRecorder mediaRecorder, int what, int extra) {
    if (MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED == what) {
        //到達最大時長
    } else if (MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED == what) {
        //到達最大尺寸
    }
}
複製程式碼

更多關於MediaRecorder的介紹可以參考MediaRecorder官方文件

1.7 結束視訊錄製

結束視訊錄製也很簡單,只需要呼叫mediaRecorder.stop()方法即可。

mediaRecorder.stop();
複製程式碼

此外,如果不再使用相機,也要注意釋放相機資源。

以上便是Camera的全部內容,還是比較簡單的,下面我們接著來講Camera2的相關內容,注意體會兩者的區別。

二 Camera2實踐指南

Camera2 API中主要涉及以下幾個關鍵類:

  • CameraManager:攝像頭管理器,用於開啟和關閉系統攝像頭
  • CameraCharacteristics:描述攝像頭的各種特性,我們可以通過CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法來獲取。
  • CameraDevice:描述系統攝像頭,類似於早期的Camera。
  • CameraCaptureSession:Session類,當需要拍照、預覽等功能時,需要先建立該類的例項,然後通過該例項裡的方法進行控制(例如:拍照 capture())。
  • CaptureRequest:描述了一次操作請求,拍照、預覽等操作都需要先傳入CaptureRequest引數,具體的引數控制也是通過CameraRequest的成員變數來設定。
  • CaptureResult:描述拍照完成後的結果。

Camera2拍照流程如下所示:

Android平臺Camera開發實踐指南

開發者通過建立CaptureRequest向攝像頭髮起Capture請求,這些請求會排成一個佇列供攝像頭處理,攝像頭將結果包裝在CaptureMetadata中返回給開發者。整個流程建立在一個CameraCaptureSession的會話中。

2.1 開啟相機

開啟相機之前,我們首先要獲取CameraManager,然後獲取相機列表,進而獲取各個攝像頭(主要是前置攝像頭和後置攝像頭)的引數。

mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
try {
    final String[] ids = mCameraManager.getCameraIdList();
    numberOfCameras = ids.length;
    for (String id : ids) {
        final CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);

        final int orientation = characteristics.get(CameraCharacteristics.LENS_FACING);
        if (orientation == CameraCharacteristics.LENS_FACING_FRONT) {
            faceFrontCameraId = id;
            faceFrontCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
            frontCameraCharacteristics = characteristics;
        } else {
            faceBackCameraId = id;
            faceBackCameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
            backCameraCharacteristics = characteristics;
        }
    }
} catch (Exception e) {
    Log.e(TAG, "Error during camera initialize");
}
複製程式碼

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還是使用Camera2,具體方法如下:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static boolean hasCamera2(Context mContext) {
    if (mContext == null) return false;
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false;
    try {
        CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
        String[] idList = manager.getCameraIdList();
        boolean notFull = true;
        if (idList.length == 0) {
            notFull = false;
        } else {
            for (final String str : idList) {
                if (str == null || str.trim().isEmpty()) {
                    notFull = false;
                    break;
                }
                final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str);

                final int supportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                if (supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    notFull = false;
                    break;
                }
            }
        }
        return notFull;
    } catch (Throwable ignore) {
        return false;
    }
}
複製程式碼

更多ameraCharacteristics引數,可以參見CameraCharacteristics官方文件

開啟相機主要呼叫的是mCameraManager.openCamera(currentCameraId, stateCallback, backgroundHandler)方法,如你所見,它有三個引數:

  • String cameraId:攝像頭的唯一ID。
  • CameraDevice.StateCallback callback:攝像頭開啟的相關回撥。
  • Handler handler:StateCallback需要呼叫的Handler,我們一般可以用當前執行緒的Handler。
 mCameraManager.openCamera(currentCameraId, stateCallback, backgroundHandler);
複製程式碼

上面我們提到了CameraDevice.StateCallback,它是攝像頭開啟的一個回撥,定義了開啟,關閉以及出錯等各種回撥方法,我們可以在 這些回撥方法裡做對應的操作。

private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        //獲取CameraDevice
        mcameraDevice = cameraDevice;
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice cameraDevice) {
        //關閉CameraDevice
        cameraDevice.close();

    }

    @Override
    public void onError(@NonNull CameraDevice cameraDevice, int error) {
        //關閉CameraDevice
        cameraDevice.close();
    }
};
複製程式碼

2.2 關閉相機

通過上面的描述,關閉就很簡單了。

//關閉CameraDevice
cameraDevice.close();
複製程式碼

2.3 開啟預覽

Camera2都是通過建立請求會話的方式進行呼叫的,具體說來:

  1. 呼叫mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)方法建立CaptureRequest,呼叫
  2. mCameraDevice.createCaptureSession()方法建立CaptureSession。
CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
複製程式碼

createCaptureRequest()方法裡引數templateType代表了請求型別,請求型別一共分為六種,分別為:

  • TEMPLATE_PREVIEW:建立預覽的請求
  • TEMPLATE_STILL_CAPTURE:建立一個適合於靜態影象捕獲的請求,影象質量優先於幀速率。
  • TEMPLATE_RECORD:建立視訊錄製的請求
  • TEMPLATE_VIDEO_SNAPSHOT:建立視視訊錄製時截圖的請求
  • TEMPLATE_ZERO_SHUTTER_LAG:建立一個適用於零快門延遲的請求。在不影響預覽幀率的情況下最大化影象質量。
  • TEMPLATE_MANUAL:建立一個基本捕獲請求,這種請求中所有的自動控制都是禁用的(自動曝光,自動白平衡、自動焦點)。
createCaptureSession(@NonNull List<Surface> outputs, @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
複製程式碼

createCaptureSession()方法一共包含三個引數:

  • List outputs:我們需要輸出到的Surface列表。
  • CameraCaptureSession.StateCallback callback:會話狀態相關回撥。
  • Handler handler:callback可以有多個(來自不同執行緒),這個handler用來區別那個callback應該被回撥,一般寫當前執行緒的Handler即可。

關於CameraCaptureSession.StateCallback裡的回撥方法:

  • onConfigured(@NonNull CameraCaptureSession session); 攝像頭完成配置,可以處理Capture請求了。
  • onConfigureFailed(@NonNull CameraCaptureSession session); 攝像頭配置失敗
  • onReady(@NonNull CameraCaptureSession session); 攝像頭處於就緒狀態,當前沒有請求需要處理。
  • onActive(@NonNull CameraCaptureSession session); 攝像頭正在處理請求。
  • onClosed(@NonNull CameraCaptureSession session); 會話被關閉
  • onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface); Surface準備就緒

理解了這些東西,建立預覽請求就十分簡單了。

previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(workingSurface);

//注意這裡除了預覽的Surface,我們還新增了imageReader.getSurface()它就是負責拍照完成後用來獲取資料的
mCameraDevice.createCaptureSession(Arrays.asList(workingSurface, imageReader.getSurface()),
        new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                cameraCaptureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                Log.d(TAG, "Fail while starting preview: ");
            }
        }, null);
複製程式碼

可以發現,在onConfigured()裡呼叫了cameraCaptureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler),這樣我們就可以 持續的進行預覽了。

注:上面我們說了新增了imageReader.getSurface()它就是負責拍照完成後用來獲取資料,具體操作就是為ImageReader設定一個OnImageAvailableListener,然後在它的onImageAvailable() 方法裡獲取。

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));
        }

 };
複製程式碼

2.4 關閉預覽

關閉預覽就是關閉當前預覽的會話,結合上面開啟預覽的內容,具體實現如下:

if (captureSession != null) {
    captureSession.close();
    try {
        captureSession.abortCaptures();
    } catch (Exception ignore) {
    } finally {
        captureSession = null;
    }
}
複製程式碼

2.5 拍照

拍照具體來說分為三步:

  1. 對焦
try {
    //相機對焦
    previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
    //修改狀態
    previewState = STATE_WAITING_LOCK;
    //傳送對焦請求
    captureSession.capture(previewRequestBuilder.build(), captureCallback, backgroundHandler);
} catch (Exception ignore) {
}
複製程式碼

我們定義了一個CameraCaptureSession.CaptureCallback來處理對焦請求返回的結果。

private CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {

    @Override
    public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                    @NonNull CaptureRequest request,
                                    @NonNull CaptureResult partialResult) {
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
            //等待對焦
            final 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
                    || CaptureResult.CONTROL_AF_STATE_INACTIVE == afState
                    || CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN == afState) {
                Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
                if (aeState == null ||
                        aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
                    previewState = STATE_PICTURE_TAKEN;
                    //對焦完成,進行拍照
                    captureStillPicture();
                } else {
                    runPreCaptureSequence();
                }
            }
    }
};
複製程式碼
  1. 拍照

我們定義了一個captureStillPicture()來進行拍照。

private void captureStillPicture() {
    try {
        if (null == mCameraDevice) {
            return;
        }
        
        //構建用來拍照的CaptureRequest
        final CaptureRequest.Builder captureBuilder =
                mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureBuilder.addTarget(imageReader.getSurface());

        //使用相同的AR和AF模式作為預覽
        captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
        //設定方向
        captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getPhotoOrientation(mCameraConfigProvider.getSensorPosition()));

        //建立會話
        CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() {
            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                           @NonNull CaptureRequest request,
                                           @NonNull TotalCaptureResult result) {
                Log.d(TAG, "onCaptureCompleted: ");
            }
        };
        //停止連續取景
        captureSession.stopRepeating();
        //捕獲照片
        captureSession.capture(captureBuilder.build(), CaptureCallback, null);

    } catch (CameraAccessException e) {
        Log.e(TAG, "Error during capturing picture");
    }
}
複製程式碼
  1. 取消對焦

拍完照片後,我們還要解鎖相機焦點,讓相機恢復到預覽狀態。

try {
    //重置自動對焦
    previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
    captureSession.capture(previewRequestBuilder.build(), captureCallback, backgroundHandler);
    //相機恢復正常的預覽狀態
    previewState = STATE_PREVIEW;
    //開啟連續取景模式
    captureSession.setRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
} catch (Exception e) {
    Log.e(TAG, "Error during focus unlocking");
}
複製程式碼

2.6 開始視訊錄製


//先關閉預覽,因為需要新增一個預覽輸出的Surface,也就是mediaRecorder.getSurface()
closePreviewSession();

//初始化MediaRecorder,設定相關引數
if (preparemediaRecorder()) {

    final SurfaceTexture texture = Camera2Manager.this.texture;
    texture.setDefaultBufferSize(videoSize.getWidth(), videoSize.getHeight());

    try {
        //構建視訊錄製aptureRequest
        previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
        final List<Surface> surfaces = new ArrayList<>();

        //設定預覽Surface
        final Surface previewSurface = workingSurface;
        surfaces.add(previewSurface);
        previewRequestBuilder.addTarget(previewSurface);

        //設定預覽輸出Surface
        workingSurface = mediaRecorder.getSurface();
        surfaces.add(workingSurface);
        previewRequestBuilder.addTarget(workingSurface);

        mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                captureSession = cameraCaptureSession;

                previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
                try {
                    //持續傳送Capture請求,實現實時預覽。
                    captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
                } catch (Exception e) {
                }

                try {
                    //開始錄影
                    mediaRecorder.start();
                } catch (Exception ignore) {
                    Log.e(TAG, "mediaRecorder.start(): ", ignore);
                }

                isVideoRecording = true;

                uiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        cameraVideoListener.onVideoRecordStarted(videoSize);
                    }
                });
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                Log.d(TAG, "onConfigureFailed");
            }
        }, backgroundHandler);
    } catch (Exception e) {
        Log.e(TAG, "startVideoRecord: ", e);
    }
}
複製程式碼

關於MediaRecorder上面講Camera的時候我們就已經說過,這裡不再贅述。

以上便是視訊錄製的全部內容,就是簡單的API使用,還是比較簡單的。

2.7 結束視訊錄製

結束視訊錄製主要也是關閉會話以及釋放一些資源,具體說來:

  1. 關閉預覽會話
  2. 停止mediaRecorder
  3. 釋放mediaRecorder
//關閉預覽會話
if (captureSession != null) {
    captureSession.close();
    try {
        captureSession.abortCaptures();
    } catch (Exception ignore) {
    } finally {
        captureSession = null;
    }
}

//停止mediaRecorder
if (mediaRecorder != null) {
    try {
        mediaRecorder.stop();
    } catch (Exception ignore) {
    }
}

//釋放mediaRecorder
try {
    if (mediaRecorder != null) {
        mediaRecorder.reset();
        mediaRecorder.release();
    }
} catch (Exception ignore) {

} finally {
    mediaRecorder = null;
}
複製程式碼

以上便是Camera/Camera2實踐的相關內容,更多關於影象、視訊處理的內容可以參見phoenix專案。

相關文章