目錄
- Camera基礎知識
- 視頻采集的流程
- 遇到的問題和常見的坑(重點)
- 收獲
一窟赏、 Camera基礎知識
Camera 有幾個重要的基礎概念枫弟。
facing相機的方向究反,一般后置攝像頭和前置攝像頭慈参。
Orientation:相機采集圖片的角度窍帝,攝像頭的傳感器在手機中是橫向的榕茧,在預覽的時候根據(jù)Camera的預覽方向進行順時針旋轉對應角度進行設置即可正常預覽垃沦。如果不正確設置會導致預覽時出現(xiàn)倒立、鏡像等問題用押。把預覽的圖片保存為相冊也要單獨設置方向肢簿,注意這個方向和預覽方向互不相干。
預覽圖片的大小 預覽容器的大小和攝像頭支持的圖片預覽的圖片大小蜻拨,如果設置了Camera不支持的預覽大小池充,會導致黑屏。
可以設置幀回調然后缎讼,在每一幀中進行業(yè)務處理收夸,比如,人臉識別等功能
Camera預覽的圖片格式有NV21 YUV420sp等
Camera需要一個容器把的Surface顯示在屏幕上血崭,一般SurfaceView卧惜,TextureView等。
二夹纫、視頻采集的流程
通過SurfaceView拿到SurfaceHolder咽瓷,然后設置addCallback回調,當Surface創(chuàng)建舰讹、銷毀茅姜、改變時觸發(fā)對應的回調,在其中可以進行Camera的初始化以及參數(shù)設置
通過new Camera(cameralId)生成一個對象月匣。然后Camera.getParams獲取到相關的參數(shù)钻洒,可以把重要的或者說比較關系的parmas打印出來,比如說支持多少個攝像頭锄开、支持的預覽圖片的大小素标、每個攝像頭的方向等信息∑笺玻可以根據(jù)需要設置對應的參數(shù)糯钙,比如圖片的格式粪狼、圖片的預覽大小等。當然有一個必須要設置的就是Camera的預覽展示方向任岸,否則預覽的到圖片和正常的方向不一致再榄。
可以設置Camera的setPreviewCallback獲取每一幀的回調,根據(jù)需要設置處理享潜,開始預覽startPreview以及幀回調的處理
攝像頭的切換
如果出發(fā)Camera的切換困鸥,需要把前一個Camera釋放,重新生成和設置Camera切換預覽
和Activity生命周期想的關系剑按,這個是有SurfaceView決定的疾就,當頁面可見時(onResume)創(chuàng)建或重新創(chuàng)建,頁面不可見時(onPause)銷毀釋放
具體實現(xiàn)如下
- SurfaceView的設置
private SurfaceHolder mSurfaceHolder;
private void initSurfaceView() {
mSurfaceHolder = surfaceview.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "surfaceCreated: ");
handleSurfaceCreated();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "surfaceDestroyed: ");
handleSurfaceDestroyed();
}
});
}
private void handleSurfaceDestroyed() {
releaseCamera();
mSurfaceHolder = null;
Log.i(TAG, "handleSurfaceDestroyed: ");
}
private void handleSurfaceCreated() {
Log.i(TAG, "handleSurfaceCreated: start");
if (mSurfaceHolder == null) {
mSurfaceHolder = surfaceview.getHolder();
}
if (mCamera == null) {
initCamera(curCameraId);
}
try {
//問題2:頁面重新打開后SurfaceView的內容黑屏
//Camera is being used after Camera.release() was called
//在surfaceDestroyed時調用了Camera的release 但是沒有設置為null,
//--》如何解耦合艺蝴,把生命周期相關的方法和Camera的生命周期綁定而不時在回調中處理猬腰,方便業(yè)務實現(xiàn)
//onResume--》surfaceCreated
//onPause--》surfaceDestroyed
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "handleSurfaceCreated: " + e.getMessage());
}
startPreview();
Log.i(TAG, "handleSurfaceCreated: end");
}
private void startPreview() {
// mCamera.setPreviewCallback(new Camera.PreviewCallback() {
// @Override
// public void onPreviewFrame(byte[] data, Camera camera) {
// Log.i(TAG, "onPreviewFrame: setPreviewCallback");
// }
// });
//問題:很多時候,不僅僅要預覽猜敢,在預覽視頻的時候姑荷,希望能做一些檢測,比如人臉檢測等缩擂。這就需要獲得預覽幀視頻鼠冕,該如何做吶?
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Log.i(TAG, "onPreviewFrame: setOneShotPreviewCallback");
Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, previewSize.width, previewSize.height, null);
ByteArrayOutputStream os = new ByteArrayOutputStream(data.length);
if(!yuvImage.compressToJpeg(new Rect(0,0,previewSize.width,previewSize.height),100,os)){
Log.e(TAG, "onPreviewFrame: compressToJpeg error" );
return;
}
byte[] bytes = os.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//這里的處理方式是簡單的把預覽的一幀圖保存下胯盯。如果需要做人臉設別或者其他操作懈费,可以拿到這個bitmap進行分析處理
//我們可以通過找出這張圖片發(fā)現(xiàn)預覽保存的圖片的方向是不對的,還是Camera的原始方向博脑,需要旋轉一定角度才可以憎乙。
if(curCameraId == Camera.CameraInfo.CAMERA_FACING_BACK){
bitmap = BitmapUtils.rotate(bitmap,90);
}else {
bitmap = BitmapUtils.mirror(BitmapUtils.rotate(bitmap,270));
}
FileUtils.saveBitmapToFile(bitmap,"oneShot.jpg");
}
});
// mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
// @Override
// public void onPreviewFrame(byte[] data, Camera camera) {
// Log.i(TAG, "onPreviewFrame: setPreviewCallbackWithBuffer");
// }
// });
mCamera.startPreview();
}
2: Camera的初始化和Params設置
private void initCamera(int cameraId) {
curCameraId = cameraId;
mCamera = Camera.open(curCameraId);
Log.d(TAG, "initCamera: Camera Open ");
setCamerDisplayOrientation(this, curCameraId, mCamera);
if (!hadPrinted) {
printCameraInfo();
hadPrinted = true;
}
Camera.Parameters parameters = mCamera.getParameters();
Camera.Size closelyPreSize = CameraUtil.getCloselyPreSize(true, SystemUtils.getDisplayWidth(), SystemUtils.getDisplayHeight(), parameters.getSupportedPreviewSizes());
Log.i(TAG, "initCamera: closelyPreSizeW="+closelyPreSize.width+" closelyPreSizeH="+closelyPreSize.height);
parameters.setPreviewSize(closelyPreSize.width, closelyPreSize.height);
mCamera.setParameters(parameters);
}
private void printCameraInfo() {
//1. 調用getParameters獲取Parameters
Camera.Parameters parameters = mCamera.getParameters();
//2. 獲取Camera預覽支持的圖片格式(常見的是NV21和YUV420sp)
int previewFormat = parameters.getPreviewFormat();
Log.d(TAG, "initCamera: previewFormat=" + previewFormat); // NV21
//3. 獲取Camera預覽支持的W和H的大小,
// 手動設置Camera的W和H時叉趣,要檢測camera是否支持寨闹,如果設置了Camera不支持的預覽大小,會出現(xiàn)黑屏君账。
// 那么這里有一個問,由于Camera不同廠商支持的預覽大小不同沈善,如果做到兼容吶乡数?
// 需要使用方采用一定策略進行選擇(比如:選擇和預設置的最接近的支持的WH)
//通過輸出信息,我們可以看到Camera是橫向的即 W>H
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size item : supportedPreviewSizes
) {
Log.d(TAG, "initCamera: supportedPreviewSizes w= " + item.width + " h=" + item.height);
}
//可以看到Camera的寬高是屏幕的寬高是不一致的闻牡,手機屏幕是豎屏的H>W净赴,而Camera的寬高是橫向的W>H
Camera.Size previewSize = parameters.getPreviewSize();
int[] physicalSS = SystemUtils.getPhysicalSS(this);
Log.i(TAG, "initCamera: w=" + previewSize.width + " h=" + previewSize.height
+ " screenW=" + SystemUtils.getDisplayWidth() + " screenH=" + SystemUtils.getDisplayHeight()
+ " physicalW=" + physicalSS[0] + " physicalH=" + physicalSS[1]);
//4. 獲取Camera支持的幀率 一般是10~30
List<Integer> supportedPreviewFrameRates = parameters.getSupportedPreviewFrameRates();
for (Integer item : supportedPreviewFrameRates
) {
Log.i(TAG, "initCamera: supportedPreviewFrameRates frameRate=" + item);
}
//5. 獲取Camera的個數(shù)信息,以及每一個Camera的orientation罩润,這個很關鍵玖翅,如果根據(jù)Camera的orientation正確的設置Camera的DisplayOrientation可能會導致預覽倒止或者出現(xiàn)鏡像的情況
int numberOfCameras = Camera.getNumberOfCameras();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int i = 0; i < numberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
Log.i(TAG, "initCamera: facing=" + cameraInfo.facing
+ " orientation=" + cameraInfo.orientation);
}
}
/**
*
* @param activity
* @param cameraId
* @param camera
*/
public static void setCamerDisplayOrientation(Activity activity, int cameraId, Camera camera) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
Log.i(TAG, "setCamerDisplayOrientation: rotation=" + rotation + " cameraId=" + cameraId);
int degress = 0;
switch (rotation) {
case Surface.ROTATION_0:
degress = 0;
break;
case Surface.ROTATION_90:
degress = 90;
break;
case Surface.ROTATION_180:
degress = 180;
break;
case Surface.ROTATION_270:
degress = 270;
break;
}
int result = 0;
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (cameraInfo.orientation + degress) % 360;
result = (360 - result) % 360;
} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
result = (cameraInfo.orientation - degress + 360) % 360;
}
Log.i(TAG, "setCamerDisplayOrientation: result=" + result + " cameraId=" + cameraId + " facing=" + cameraInfo.facing + " cameraInfo.orientation=" + cameraInfo.orientation);
camera.setDisplayOrientation(result);
}
- Camera的預覽站削、幀回調處理田炭、保存圖片旋轉和鏡像處理
private void startPreview() {
//問題六:很多時候,不僅僅要預覽,在預覽視頻的時候狮崩,希望能做一些檢測,比如人臉檢測等缨叫。這就需要獲得預覽幀視頻
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Log.i(TAG, "onPreviewFrame: setOneShotPreviewCallback");
Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, previewSize.width, previewSize.height, null);
ByteArrayOutputStream os = new ByteArrayOutputStream(data.length);
if(!yuvImage.compressToJpeg(new Rect(0,0,previewSize.width,previewSize.height),100,os)){
Log.e(TAG, "onPreviewFrame: compressToJpeg error" );
return;
}
byte[] bytes = os.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//這里的處理方式是簡單的把預覽的一幀圖保存下色查。如果需要做人臉設別或者其他操作,可以拿到這個bitmap進行分析處理
//我們可以通過找出這張圖片發(fā)現(xiàn)預覽保存的圖片的方向是不對的跟伏,還是Camera的原始方向丢胚,需要旋轉一定角度才可以。
if(curCameraId == Camera.CameraInfo.CAMERA_FACING_BACK){
bitmap = rotate(bitmap,90);
}else {
bitmap = mirror(rotate(bitmap,270));
}
saveBitmapToFile(bitmap,"oneShot.jpg");
}
});
mCamera.startPreview();
}
void saveBitmapToFile(Bitmap bitmap, String fileName) {
File file = new File(MyApplication.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName);
if (file.exists()) {
file.delete();
}
try {
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//水平鏡像翻轉
public Bitmap mirror(Bitmap rawBitmap) {
Matrix matrix = new Matrix();
matrix.postScale(-1f, 1f);
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.getWidth(), rawBitmap.getHeight(), matrix, true);
}
//旋轉
public Bitmap rotate(Bitmap rawBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.getWidth(), rawBitmap.getHeight(), matrix, true);
}
- Camera的切換
private void switchCamera() {
if (mCamera != null) {
releaseCamera();
initCamera((curCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT);
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
}
startPreview();
}
}
private void releaseCamera() {
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
三受扳、遇到的問題 (頁面卡住携龟、黑屏、倒立等)
問題一 :切換攝像頭后畫面卡住
解決:需要先關閉Camera釋放資源勘高,然后重新打開切換后的Camera峡蟋,重新設置PreviewDisplay 然后開始預覽
問題二: 頁面重新打開后(在預覽頁按Home鍵推到后臺,然后再回到前臺)SurfaceView的內容黑屏
解決:通過查看log看到有一個異常信息:“Camera is being used after Camera.release() was called” 原來是在surfaceDestroyed時調用了Camera的release 但是沒有設置為null, 在surfaceCreated的時候是根據(jù)camera是否為空來判斷是否需要重新初始化相满。
問題三:前攝像頭預覽出現(xiàn)倒立并且是鏡像狀態(tài)
public static void setCamerDisplayOrientation(Activity activity, int cameraId, Camera camera) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int result = 0;
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (cameraInfo.orientation) % 360;
}
camera.setDisplayOrientation(result);
解決:
`public static void setCamerDisplayOrientation(Activity activity, int cameraId, Camera camera) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
int result = 0;
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (cameraInfo.orientation) % 360;
result = (360 - result) % 360;
}
camera.setDisplayOrientation(result);
圖片來自[Android: Camera相機開發(fā)詳解(上)]
問題四:很多時候层亿,不僅僅要預覽,在預覽視頻的時候立美,希望能做一些檢測匿又,比如人臉檢測等。這就需要獲得預覽幀視頻建蹄,該如何做吶碌更?
Camera提供了setPreviewCallback、setOneShotPreviewCallback以及setPreviewCallbackWithBuffer三個方法供使用者進行幀回調處理洞慎。比如下面的處理時痛单,通過setOneShotPreviewCallback獲取一幀的bitmap,然后進行保存到文件
mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Log.i(TAG, "onPreviewFrame: setOneShotPreviewCallback");
Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, previewSize.width, previewSize.height, null);
ByteArrayOutputStream os = new ByteArrayOutputStream(data.length);
if(!yuvImage.compressToJpeg(new Rect(0,0,previewSize.width,previewSize.height),100,os)){
Log.e(TAG, "onPreviewFrame: compressToJpeg error" );
return;
}
byte[] bytes = os.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//這里的處理方式是簡單的把預覽的一幀圖保存下劲腿。如果需要做人臉設別或者其他操作旭绒,可以拿到這個bitmap進行分析處理
FileUtils.saveBitmapToFile(bitmap,"oneShot.jpg");
}
});
問題五:發(fā)現(xiàn)保存的圖片和預覽的圖片的方向不一致
解決: 預覽通過Camera的setDisplay Orientation根據(jù)前后攝像頭的需旋轉的角度進行了處理,但是保存為圖片是和預覽時的設置時不相關的焦人,需要單獨處理
圖片來自[Android: Camera相機開發(fā)詳解(上)]
//我們可以通過找出這張圖片發(fā)現(xiàn)預覽保存的圖片的方向是不對的挥吵,還是Camera的原始方向,需要旋轉一定角度才可以花椭。
if(curCameraId == Camera.CameraInfo.CAMERA_FACING_BACK){
bitmap = BitmapUtils.rotate(bitmap,90);
}else {
bitmap = BitmapUtils.mirror(BitmapUtils.rotate(bitmap,270));
}
public class BitmapUtils {
//水平鏡像翻轉
public static Bitmap mirror(Bitmap rawBitmap) {
Matrix matrix = new Matrix();
matrix.postScale(-1f, 1f);
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.getWidth(), rawBitmap.getHeight(), matrix, true);
}
//旋轉
public static Bitmap rotate(Bitmap rawBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.getWidth(), rawBitmap.getHeight(), matrix, true);
}
}
參考資料
《音視頻開發(fā)進階指南》
[Android: Camera相機開發(fā)詳解(上)]
Android camera2 實現(xiàn)相機預覽及獲取預覽幀數(shù)據(jù)流
Activity啟動后View何時開始繪制(onCreate中還是onResume之后忽匈?)
收獲
- Camera的Facing、Orientation 矿辽、Size丹允、PreviewCallback等基礎知識的實踐和了解
- camera預覽的流程熟悉
- 黑屏郭厌、卡頓、倒立雕蔽、鏡像等問題的分析和處理處理
感謝你的閱讀折柠。
下一篇我們來一起學習實踐下MediaExtractor和MediaMuxer 來解析和封裝MP4文件。