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之間的交互才真正建立起來。這個會話可以隨時關閉,也可以修改參數。
=======================================================================