Android Camera2 API和拍照與錄影過程
簡介:
Android 5.0開始出現了新的相機Camera 2 API,用來替代以前的camera api。
Camera2 API不僅提高了android系統的拍照效能,還支援RAW照片輸出,還可以設定相機的對焦模式,曝光模式,快門等等。
Camera2 中主要的API類
CameraManager類 : 攝像頭管理類,用於檢測、開啟系統攝像頭,通過
getCameraCharacteristics(cameraId)
可以獲取攝像頭特徵。CameraCharacteristics類:相機特性類,例如,是否支援自動調焦,是否支援zoom,是否支援閃光燈一系列特徵。
CameraDevice類: 相機裝置,類似早期的camera類。
CameraCaptureSession類:用於建立預覽、拍照的Session類。通過它的
setRepeatingRequest()
方法控制預覽介面 , 通過它的capture()
方法控制拍照動作或者錄影動作。CameraRequest類:一次捕獲的請求,可以設定一些列的引數,用於控制預覽和拍照引數,例如:對焦模式,曝光模式,zoom引數等等。
接下來,進一步介紹,Camera2 API中的各種常見類和抽象類。
CameraManager類
CameraCharacteristics cameraCharacteristics =manager.getCameraCharacteristics(cameraId);
通過以上程式碼可以獲取攝像頭的特徵物件,例如: 前後攝像頭,解析度等。
CameraCharacteristics類
相機特性類
CameraCharacteristics是一個包含相機引數的物件,可以通過一些key獲取對應的values.
以下幾種常用的引數:
LENS_FACING:獲取攝像頭方向。LENS_FACING_FRONT是前攝像頭,LENS_FACING_BACK是後攝像頭。
SENSOR_ORIENTATION:獲取攝像頭拍照的方向。
FLASH_INFO_AVAILABLE:獲取是否支援閃光燈。
SCALER_AVAILABLE_MAX_DIGITAL_ZOOM:獲取最大的數字調焦值,也就是zoom最大值。
LENS_INFO_MINIMUM_FOCUS_DISTANCE:獲取最小的調焦距離,某些手機上獲取到的該values為null或者0.0。前攝像頭大部分有固定焦距,無法調節。
INFO_SUPPORTED_HARDWARE_LEVEL:獲取攝像頭支援某些特性的程度。
以下手機中支援的若干種程度:
INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬體支援,允許手動控制全高清的攝像、支援連拍模式以及其他新特性。
INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支援,這個需要單獨查詢。
INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有裝置都會支援,也就是和過時的Camera API支援的特性是一致的。
CameraDevice類
CameraDevice的reateCaptureRequest(int templateType)
方法建立CaptureRequest.Builder。
templateType引數有以下幾種:
TEMPLATE_PREVIEW :預覽
TEMPLATE_RECORD:拍攝視訊
TEMPLATE_STILL_CAPTURE:拍照
TEMPLATE_VIDEO_SNAPSHOT:建立視視訊錄製時截圖的請求
TEMPLATE_ZERO_SHUTTER_LAG:建立一個適用於零快門延遲的請求。在不影響預覽幀率的情況下最大化影象質量。
TEMPLATE_MANUAL:建立一個基本捕獲請求,這種請求中所有的自動控制都是禁用的(自動曝光,自動白平衡、自動焦點)。
CameraDevice.StateCallback抽象類
該抽象類用於CemeraDevice相機裝置狀態的回撥。
/**
* 當相機裝置的狀態發生改變的時候,將會回撥。
*/
protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
/**
* 當相機開啟的時候,呼叫
* @param cameraDevice
*/
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
startPreView();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
mCameraDevice = null;
}
/**
* 發生異常的時候呼叫
*
* 這裡釋放資源,然後關閉介面
* @param cameraDevice
* @param error
*/
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
mCameraDevice = null;
}
/**
*當相機被關閉的時候
*/
@Override
public void onClosed(@NonNull CameraDevice camera) {
super.onClosed(camera);
}
};
CameraCaptureSession.StateCallback抽象類
該抽象類用於Session過程中狀態的回撥。
public static abstract class StateCallback {
//攝像頭完成配置,可以處理Capture請求了。
public abstract void onConfigured(@NonNull CameraCaptureSession session);
//攝像頭配置失敗
public abstract void onConfigureFailed(@NonNull CameraCaptureSession session);
//攝像頭處於就緒狀態,當前沒有請求需要處理
public void onReady(@NonNull CameraCaptureSession session) {}
//攝像頭正在處理請求
public void onActive(@NonNull CameraCaptureSession session) {}
//請求佇列中為空,準備著接受下一個請求。
public void onCaptureQueueEmpty(@NonNull CameraCaptureSession session) {}
//會話被關閉
public void onClosed(@NonNull CameraCaptureSession session) {}
//Surface準備就緒
public void onSurfacePrepared(@NonNull CameraCaptureSession session,@NonNull Surface surface) {}
}
接下來,是介紹拍照和錄影流程步驟。
使用流程:
1. 開啟指定的方向的相機
最先獲取CameraManager物件,通過該物件的getCameraIdList()
獲取到一些列的攝像頭引數。
通過迴圈匹配,獲取到指定方向的攝像頭,例如後攝像頭等。
CameraManager manager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);
//獲取到可用的相機
for (String cameraId : manager.getCameraIdList()) {
//獲取到每個相機的引數物件,包含前後攝像頭,解析度等
CameraCharacteristics cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
//攝像頭的方向
Integer facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if(facing==null){
continue;
}
//匹配方向,指定開啟後攝像頭
if(facing!=CameraCharacteristics.LENS_FACING_BACK){
continue;
}
//開啟指定的攝像頭
manager.openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler());
return;
}
當然,實際開發中,還需要獲取相機支援的特性(閃光燈,zoom調焦,手動調焦等),和設定攝像頭的引數(例如:預覽的Size)。
2. 建立預覽的介面:
建立 CameraDevice.StateCallback 物件,且開啟一個相機。當相機開啟後,將出現相機預覽介面。
CameraDevice.StateCallback 物件傳入CameraManager中openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler())
的第二個引數,用於監聽攝像頭的狀態。
/**
* 相機裝置
*/
protected CameraDevice mCameraDevice;
/**
* 當相機裝置的狀態發生改變的時候,將會回撥。
*/
protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
/**
* 當相機開啟的時候,呼叫
* @param cameraDevice
*/
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
// 省略該狀態介面的部分方法
...............
};
/**
* 預覽請求的Builder
*/
private CaptureRequest.Builder mPreviewRequestBuilder;
/**
* 相機開始預覽,建立一個CameraCaptureSession物件
*/
private void createCameraPreviewSession() {
// 將CaptureRequest的構建器與Surface物件繫結在一起
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 為相機預覽,建立一個CameraCaptureSession物件
mCameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), stateCallback, null);
}
建立完預覽的介面後,接下來需要開始重新整理。
3. 在預覽介面過程中,需要間隔重新整理介面
相機預覽使用TextureView來實現。建立一個CameraCaptureSession ,通過一個用於預覽介面的CaptureRequest,間隔複用給CameraCaptureSession。
private CameraCaptureSession mCaptureSession;
CameraCaptureSession.StateCallback stateCallback=new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
//當cameraCaptureSession已經準備完成,開始顯示預覽介面
mCaptureSession = cameraCaptureSession;
setCameraCaptureSession();
}
//省略該介面的部分方法
.......
}
/**
* 設定CameraCaptureSession的特徵:
* <p>
* 自動對焦,閃光燈
*/
private void setCameraCaptureSession() {
//設定預覽介面的特徵,通過mPreviewRequestBuilder.set()方法,例如,閃光燈,zoom調焦等
..........
//為CameraCaptureSession設定間隔的CaptureRequest,用間隔重新整理預覽介面。
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, workThreadManager.getBackgroundHandler());
}
只要未開始拍照動作或者錄影動作,該複用的CaptureRequest會重複的重新整理預覽介面。
接下來,等待使用者點選拍照按鈕或者錄影按鈕,進行拍照動作,或者錄影動作。
4. 拍照動作:
首先鎖住焦點,通過在相機預覽介面個性CaptureRequest。然後,以類似方式,需要執行一個預捕獲序列。接下來,可已經準備好捕捉圖片。建立一個新的CaptureRequest,且拍照。
/**
* 拍照一個靜態的圖片
* ,當在CaptureCallback監聽器響應的時候呼叫該方法。
* <p>
* 當數字調焦縮放的時候,在寫入圖片數中也要設定。
*/
private void captureStillPicture() {
try {
// 建立一個拍照的CaptureRequest.Builder
final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(imageReader.getSurface());
//設定一系列的拍照引數,這裡省略
...........
//先停止以前的預覽狀態
mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();
//執行拍照動作
mCaptureSession.capture(captureBuilder.build(), captureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
拍照介面產生的資料只是在手機記憶體中,圖片是一個磁碟檔案,還需要一個將拍照產生資料寫入檔案中的操作類ImageReader。
先是建立ImageReader物件,和設定監聽器等一些列引數。
/**
* 處理靜態圖片的輸出
*/
private ImageReader imageReader;
//對於靜態圖片,使用可用的最大值來拍攝。
Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizeByArea());
//設定ImageReader,將大小,圖片格式
imageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /*maxImages*/2);
imageReader.setOnImageAvailableListener(onImageAvailableListener, workThreadManager.getBackgroundHandler());
接下來,將ImageReader的surface配置到captureBuilder物件中captureBuilder.addTarget(imageReader.getSurface());
最後,當拍照完成後,會在該監聽狀態中回撥:
/**
* ImageReader的回撥監聽器
* <p>
* onImageAvailable被呼叫的時候,已經拍照完,準備儲存的操作
* 通常寫入磁碟檔案中。
*/
protected final ImageReader.OnImageAvailableListener onImageAvailableListener = (ImageReader reader)
-> writePictureData(reader.acquireNextImage());
public void writePictureData(Image image) {
if (camera2ResultCallBack != null) {
camera2ResultCallBack.callBack(ObservableBuilder.createWriteCaptureImage(appContext, image));
}
}
/**
* 將JPEG圖片的資料,寫入磁碟中
*
* @param context
* @param mImage
* @return
*/
public static Observable<String> createWriteCaptureImage(final Context context, final Image mImage) {
Observable<String> observable = Observable.create(subscriber -> {
File file = FileUtils.createPictureDiskFile(context, FileUtils.createBitmapFileName());
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
try {
output = new FileOutputStream(file);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImage.close();
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
subscriber.onNext(file.getAbsolutePath());
});
return observable;
}
這裡採用RxJava+RxAndroid非同步通訊,避免太多回調介面。
5. 錄影動作
錄影是長時間的動作,錄影過程中需要重複性的重新整理錄製介面。其餘的步驟和拍照動作基本類似。
/**
* 開始視訊錄製。
*/
private void startRecordingVideo() {
try {
//建立錄製的session會話中的請求
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
//設定錄製引數,這裡省略
.........
// 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;
Log.i(TAG, " startRecordingVideo 正式開始錄製 ");
updatePreview();
}
//該介面的方法,部分省略
.............
}, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException | IOException e) {
e.printStackTrace();
}
}
//錄製過程中,不斷重新整理錄製介面
private void updatePreview() {
try {
mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
和拍照類似,將視訊資料寫入磁碟檔案中,也是需要一個操作類 MediaRecorder來實現的。
先是建立該操作類物件,設定一些列引數:
/**
* MediaRecorder
*/
private MediaRecorder mMediaRecorder;
/**
* 設定媒體錄製器的配置引數
* <p>
* 音訊,視訊格式,檔案路徑,頻率,編碼格式等等
*
* @throws IOException
*/
private void setUpMediaRecorder() throws IOException {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mNextVideoAbsolutePath = FileUtils.createVideoDiskFile(appContext, FileUtils.createVideoFileName()).getAbsolutePath();
mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
mMediaRecorder.setVideoEncodingBitRate(10000000);
//每秒30幀
mMediaRecorder.setVideoFrameRate(30);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
switch (mSensorOrientation) {
case SENSOR_ORIENTATION_DEFAULT_DEGREES:
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
break;
case SENSOR_ORIENTATION_INVERSE_DEGREES:
mMediaRecorder.setOrientationHint(ORIENTATIONS.get(rotation));
break;
default:
break;
}
mMediaRecorder.prepare();
}
間隔性的隨著視訊錄製而輸出資料到檔案中。
// 為 MediaRecorder設定Surface
Surface recorderSurface = mMediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);
最後,當錄製視訊結束後,停止輸出:
// 停止錄製
mMediaRecorder.stop();
mMediaRecorder.reset();
6. 恢復到預覽介面:
完成一些列拍照或錄影動作後,重新恢復到預覽介面。
/**
* 完成一些列拍照或錄影動作後,釋放焦點。
*/
private void unlockFocus() {
try {
//向session重新發送,預覽的間隔性請求,出現預覽介面。
mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, workThreadManager.getBackgroundHandler());
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
當然,還有關閉相機操作,和與Activity生命週期繫結的操作,這裡不再做介紹了。
下一篇,開始介紹,如何開發一個真正的相機程式。
資源參考: