這是Android音視訊的第一篇文章,終於回到了我的老本行上。之前好像從來沒有做過Camera相關的開發,這塊對我來說還是有點陌生,所以從頭開始學起。
Camera2
在Android API21中Google釋出了Camera2來取代原本的Camera,兩者的變動也是比較大的。
Camera2中Google採用了pipeline(管道)的概念,將Camera Device相機裝置和Android Device安卓裝置連線起來, Android Device通過管道傳送CaptureRequest請求給Camera Device,Camera Device通過管道返回CameraMetadata資料給Android Device,這一切建立在一個叫作CameraCaptureSession的會話中。
Camera2主要類說明
在Camera2 架構在核心參與類角色有:CameraManager、CameraDevice、CameraCharacteristics、CameraRequest與CameraRequest.Builder、CameraCaptureSession以及CaptureResult。
CameraManager
位於android.hardware.camera2.CameraManager下,也是Android 21(5.0)新增的,和其他系統服務一樣通過 Context.getSystemService(Context.CAMERA_SERVICE)
來完成初始化,主要用於管理系統攝像頭。
manager.getCameraIdList()
獲取Android裝置的攝像頭列表manager.getCameraCharacteristics(cameraId)
獲取指定攝像頭的相關特性manager.openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)
開啟指定Id的攝像頭,StateCallback 是開啟狀態的一個監聽回撥,Handler 表示使用哪個執行緒處理回撥,如果為null則表示當前執行緒。
CameraDevice
CameraDevice是Camera2抽象出來的一個物件,直接與系統硬體攝像頭相聯絡。
-
通過CameraDevice.StateCallback監聽攝像頭的狀態
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback(){ @Override public void onOpened(@NonNull CameraDevice camera) { //攝像頭開啟,可以建立會話,開始預覽 } @Override public void onDisconnected(@NonNull CameraDevice camera) { } @Override public void onError(@NonNull CameraDevice camera, int error) { } }; 複製程式碼
-
管理CameraCaptureSession會話,相當於Android Device和Camera Device之間的管道,後面的資料交流都在這個會話中完成。
-
管理CaptureRequest,主要包括通過createCaptureRequest(int templateType)建立捕獲請求,在需要預覽、拍照、再次預覽的時候都需要通過建立請求來完成。
CameraCaptureSession
正如前面所說,系統向攝像頭髮送 Capture 請求,而攝像頭會返回 CameraMetadata,這一切都是在由對應的CameraDevice建立的CameraCaptureSession 會話完成,當程式需要預覽、拍照、再次預覽時,都需要先通過會話。CameraCaptureSession一旦被建立,直到對應的CameraDevice關閉才會死掉。雖然CameraCaptureSession會話用於從攝像頭中捕獲影象,但是隻有同一個會話才能再次從同一攝像頭中捕獲影象。
- 管理CameraCaptureSession.StateCallback狀態回撥,用於接收有關CameraCaptureSession狀態的更新的回撥物件,主要回撥方法有兩個當CameraDevice 完成配置,對應的會話開始處理捕獲請求時觸發onConfigured(CameraCaptureSession session)方法,反之配置失敗時候觸發onConfigureFailed(CameraCaptureSession session)方法。
- 管理CameraCaptureSession.CaptureCallback捕獲回撥,用於接收捕獲請求狀態的回撥,當請求觸發捕獲已啟動時、捕獲完成時、在捕獲影象時發生錯誤的情況下都會觸發該回撥對應的方法。
- 通過呼叫方法capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)提交捕獲影象請求,即拍照。
- 通過呼叫方法setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)請求不斷重複捕獲影象,即實現預覽。
- 通過方法呼叫stopRepeating()實現停止捕獲影象,即停止預覽。
CameraCharacteristics
描述Cameradevice屬性的物件,可以使用CameraManager通過getCameraCharacteristics(String cameraId)進行查詢。
CameraRequest與CameraRequest.Builder
CameraRequest代表了一次捕獲請求
CameraRequest.Builder用於描述捕獲圖片的各種引數設定,包含捕獲硬體(感測器,鏡頭,快閃記憶體),對焦模式、曝光模式,處理流水線,控制演算法和輸出緩衝區的配置,然後傳遞到對應的會話中進行設定。CameraRequest.Builder負責生成CameraRequest物件。
CaptureResult
CaptureRequest描述是從影象感測器捕獲單個影象的結果的子集的物件。
程式碼實現
谷歌有寫兩個示例程式,介紹如何使用Camera2
示例程式中有預覽、拍照、錄影等功能,非常好的入門學習程式碼。
在這裡還是過一遍整個流程,加深理解。
1、開啟攝像頭
private void openCamera(int width, int height) {
// 判斷許可權
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
return;
}
// 設定引數,獲取攝像頭ID、設定預覽寬高等
setUpCameraOutputs(width, height);
// 配置TextureView的紋理轉換,解決Camera顯示變形問題
configureTransform(width, height);
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
//Semaphore 訊號量,保證只能有一條執行緒使用Camera裝置
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
//真正去開啟攝像頭,在mStateCallback中獲取成功或失敗的回撥
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
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、建立會話,開始預覽
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);
//這裡是指實時影象資料的輸出目標,以後錄製視訊、直播等都需要在這裡新增對應的Target
mPreviewRequestBuilder.addTarget(surface);
// 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.
mCaptureSession = cameraCaptureSession;
try {
// 設定自動對焦
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();
//不斷捕獲影象,顯示預覽影象
mCaptureSession.setRepeatingRequest(mPreviewRequest,
mCaptureCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
showToast("Failed");
}
}, null
);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製程式碼
3、拍照
/**
* Initiate a still image capture.
*/
private void takePicture() {
lockFocus();
}
/**
* Lock the focus as the first step for a still image capture.
*/
private void lockFocus() {
try {
// 告訴攝像機開始對焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_START);
// Tell #mCaptureCallback to wait for the lock.
mState = STATE_WAITING_LOCK;
// 傳送CaptureRequest要求攝像機捕捉影象
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製程式碼
private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback() {
private void process(CaptureResult result) {
//處理對焦、閃光燈等
.......
case STATE_WAITING_LOCK: {
.......
//對焦完成,儲存一張圖片
captureStillPicture();
.......
break;
}
}
}
複製程式碼
private void captureStillPicture() {
try {
final Activity activity = getActivity();
if (null == activity || null == mCameraDevice) {
return;
}
// This is the CaptureRequest.Builder that we use to take a picture.
// 將mImageReader作為目標,獲得的圖片資料會交給mImageReader處理
// 初始化時 mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
// 所以資料最終會在mOnImageAvailableListener中處理
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mImageReader.getSurface());
// Use the same AE and AF modes as the preview.
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
setAutoFlash(captureBuilder);
// Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
// 拍照事件的最終回撥,圖片處理完後呼叫onCaptureCompleted
CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
showToast("Saved: " + mFile);
Log.d(TAG, mFile.toString());
unlockFocus();
}
};
mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();
mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
複製程式碼
// 資料處理、儲存
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
Image mImage = reader.acquireNextImage();
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
// 檔案操作
// ......
// 這裡獲取到的是視訊的原始資料,要對資料做進一步的演算法處理、或者採用第三方的編碼庫進行壓縮、以及網路傳輸等,都可以從這裡拿到資料。
mImage.close();
}
};
複製程式碼
4、錄影
主要看一下MediaRecorder錄製視訊相關程式碼
private void startRecordingVideo() {
if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
return;
}
try {
// 關閉之前的會話,新的會話會新增錄影的Target
closePreviewSession();
// 配置MediaRecorder,音訊、視訊來源,編碼格式等
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 重要的一步,視訊資訊會交給mMediaRecorder處理
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;
// 開始錄製
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();
}
}
複製程式碼
// 配置MediaRecorder
private void setUpMediaRecorder() throws IOException {
final Activity activity = getActivity();
if (null == activity) {
return;
}
// 設定要用於錄製的音訊源。
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// 設定要用於錄製的視訊源。
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
// 設定錄製期間生成的輸出檔案的格式。
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 生成MP4檔案路徑
if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
mNextVideoAbsolutePath = getVideoFilePath(getActivity());
}
mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
// 設定用於錄製的視訊編碼位元率。
mMediaRecorder.setVideoEncodingBitRate(10000000);
// 設定要捕獲的視訊的幀速率。
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(INVERSE_ORIENTATIONS.get(rotation));
break;
}
// 在呼叫start前必須的一步
mMediaRecorder.prepare();
}
複製程式碼
/**
* 常規使用MediaRecorder去錄製視訊的例子如下:
* MediaRecorder recorder = new MediaRecorder();
* recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
* recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
* recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
* recorder.setOutputFile(PATH_NAME);
* recorder.prepare();
* recorder.start(); // Recording is now started
* ...
* recorder.stop();
* recorder.reset(); // You can reuse the object by going back to setAudioSource() step
* recorder.release(); // Now the object cannot be reused
**/
複製程式碼
Camera2預覽、拍照和錄影的大概流程就是這樣,相對來說挺複雜的,但也非常重要,後面會繼續深入分析原理和原始碼。