概述
Android中很多基本的架构都是C/S层架构,客户端提供调用接口,而实现工作则是在服务端完成。Android Camera的架构也是C/S架构,Client进程虽然不曾拥有任何实质的Camera数据,但是service端为它提供了丰富的接口,它可以轻松的获得Camera数据的地址,然后处理这些数据。两者通过Binder进行通讯。
在Android中调用摄像头需要相应的权限,需要注意的是:权限申请在Android 6.0后变成了动态申请。在本项目中使用了Camera1相关的API对摄像头进行调用,所以对Camera2相关的API不做讲述,以后有时间再补上。
学习如何使用Camera一个好的方式是阅读其他优秀项目的源码,而Android自身就带有Camera的应用,因此阅读Android自身Camera应用的源码无疑是最好的选择。
Google Camera 开源地址:Android Camera App
打开摄像头步骤为:
1、检查摄像头;
2、打开摄像头;
3、设置摄像头参数;
4、设置预览界面。
在本文中使用一个单例CameraHolder来维持Camera相应的状态,也方便在其他地方得到Camera相关的参数。在CameraHolder中定义了Camera的三个状态:Init、Opened、Preview。三个状态的关系图如下所示:
一、检查摄像头
在使用摄像头之前先要检查摄像头服务是否可用,下面一段代码是检查摄像头服务是否可用的。
public static void checkCameraService(Context context)
throws CameraDisabledException {
// Check if device policy has disabled the camera.
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm.getCameraDisabled(null)) {
throw new CameraDisabledException();
}
}
检查完摄像头服务后,还需要检查手机上摄像头的个数,如果个数为0,则说明手机上没有摄像头,这样的话也是不能直播的。在项目中,我将摄像头个数的检测和摄像头数据的初始化放在了一起,如下面代码所示:
public static List<CameraData> getAllCamerasData(boolean isBackFirst) {
ArrayList<CameraData> cameraDatas = new ArrayList<>();
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
CameraData cameraData = new CameraData(i, CameraData.FACING_FRONT);
if(isBackFirst) {
cameraDatas.add(cameraData);
} else {
cameraDatas.add(0, cameraData);
}
} else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
CameraData cameraData = new CameraData(i, CameraData.FACING_BACK);
if(isBackFirst) {
cameraDatas.add(0, cameraData);
} else {
cameraDatas.add(cameraData);
}
}
}
return cameraDatas;
}
在上面的方法中,需要传入一个是否先开启背面摄像头的boolean变量,如果变量为true,则把背面摄像头放在列表第一个,之后打开摄像头的时候,直接获取列表中第一个摄像头相关参数,然后进行打开。这样的设计使得切换摄像头也变得十分简单,切换摄像头时,先关闭当前摄像头,然后变化摄像头列表中的顺序,然后再打开摄像头即可,也就是每次打开摄像头都打开摄像头列表中第一个摄像头参数所指向的摄像头。
二、打开摄像头
打开摄像头之前,先从摄像头列表中获取第一个摄像头参数,之后根据参数中的CameraId来打开摄像头,打开成功后改变相关状态。相关代码如下:
public synchronized Camera openCamera()
throws CameraHardwareException, CameraNotSupportException {
CameraData cameraData = mCameraDatas.get(0);
if(mCameraDevice != null && mCameraData == cameraData) {
return mCameraDevice;
}
if (mCameraDevice != null) {
releaseCamera();
}
try {
SopCastLog.d(TAG, "open camera " + cameraData.cameraID);
mCameraDevice = Camera.open(cameraData.cameraID);
} catch (RuntimeException e) {
SopCastLog.e(TAG, "fail to connect Camera");
throw new CameraHardwareException(e);
}
if(mCameraDevice == null) {
throw new CameraNotSupportException();
}
mCameraData = cameraData;
mState = State.OPENED;
return mCameraDevice;
}
上面需要注意的是,在Android提供的Camera源码中,Camera.open(cameraData.cameraID)抛出异常则说明Camera不可用,否则说明Camera可用,但是在一些手机上Camera.open(cameraData.cameraID)不是抛出异常,而是返回null。
三、设置摄像头参数
在给摄像头设置参数后,需要记录这些参数,以方便其他地方使用。比如记录当前摄像头是否有闪光点,从而可以决定UI界面上是否显示打开闪光灯按钮。在直播项目中使用CameraData来记录这些参数,CameraData类如下所示:
public class CameraData {
public static final int FACING_FRONT = 1;
public static final int FACING_BACK = 2;
public int cameraID; //camera的id
public int cameraFacing; //区分前后摄像头
public int cameraWidth; //camera的采集宽度
public int cameraHeight; //camera的采集高度
public boolean hasLight; //camera是否有闪光灯
public int orientation; //camera旋转角度
public boolean supportTouchFocus; //camera是否支持手动对焦
public boolean touchFocusMode; //camera是否处在自动对焦模式
public CameraData(int id, int facing, int width, int height){
cameraID = id;
cameraFacing = facing;
cameraWidth = width;
cameraHeight = height;
}
public CameraData(int id, int facing) {
cameraID = id;
cameraFacing = facing;
}
}
给摄像头设置参数的时候,有一点需要注意:设置的参数不生效会抛出异常,因此需要每个参数单独设置,这样就避免一个参数不生效后抛出异常,导致之后所有的参数都没有设置。
设置预览界面
设置预览界面有两种方式:1、通过SurfaceView显示;2、通过GLSurfaceView显示。当为SurfaceView显示时,需要传给Camera这个SurfaceView的SurfaceHolder。当使用GLSurfaceView显示时,需要使用Renderer进行渲染,先通过OpenGL生成纹理,通过生成纹理的纹理id生成SurfaceTexture,将SurfaceTexture交给Camera,那么在Renderer中便可以使用这个纹理进行相应的渲染,最后通过GLSurfaceView显示。
通过GLSurfaceView显示流程图如下:
对摄像机设置SurfaceHolder和SurfaceTexture只需要调用相应的接口即可,关于使用GLSurfaceView显示的整套流程将会在OpenGL相关的文章中进行讲述。
设置预览回调
首先需要设置预览回调的图片格式。
public static void setPreviewFormat(Camera camera, Camera.Parameters parameters) {
//设置预览回调的图片格式
try {
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
当设置预览好预览回调的图片格式后,需要设置预览回调的Callback。
Camera.PreviewCallback myCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//得到相应的图片数据
//Do something
}
};
public static void setPreviewCallback(Camera camera, Camera.PreviewCallback callback) {
camera.setPreviewCallback(callback);
}
Android推荐的PreView Format时NV21,在PreviewCallback中会返回Preview的N21图片。如果是软编的话,由于H264支持I420的图片格式,因此需要将N21格式转为I420格式,然后交给x264编码库。如果是硬编的话,由于Android硬编编码器支持I420(COLOR_FormatYUV420Planar)和NV12(COLOR_FormatYUV420SemiPlanar),因此可以将N21的图片转为I420或者NV12,然后交给硬编编码器。
设置预览图像大小
在摄像头相关处理中,一个比较重要的是屏幕显示大小和摄像头预览大小比例不一致的处理。在Android中,摄像头有一系列的Preview Size,我们需要从中选出适合的Preview Size。选择合适的摄像头Preview Size的代码如下所示:
public static Camera.Size getOptimalPreviewSize(Camera camera, int width, int height) {
Camera.Size optimalSize = null;
double minHeightDiff = Double.MAX_VALUE;
double minWidthDiff = Double.MAX_VALUE;
List<Camera.Size> sizes = camera.getParameters().getSupportedPreviewSizes();
if (sizes == null) return null;
//找到宽度差距最小的
for(Camera.Size size:sizes){
if (Math.abs(size.width - width) < minWidthDiff) {
minWidthDiff = Math.abs(size.width - width);
}
}
//在宽度差距最小的里面,找到高度差距最小的
for(Camera.Size size:sizes){
if(Math.abs(size.width - width) == minWidthDiff) {
if(Math.abs(size.height - height) < minHeightDiff) {
optimalSize = size;
minHeightDiff = Math.abs(size.height - height);
}
}
}
return optimalSize;
}
public static void setPreviewSize(Camera camera, Camera.Size size, Camera.Parameters parameters) {
try {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
}
catch (Exception e) {
e.printStackTrace();
}
}
在设置好最适合的Preview Size之后,将size信息存储在CameraData中。当选择了SurfaceView显示的方式,可以将SurfaceView放置在一个LinearLayout中,然后根据摄像头Preview Size的比例改变SurfaceView的大小,从而使得两者比例一致,确保图像正常。当选择了GLSurfaceView显示的时候,可以通过裁剪纹理,使得纹理的大小比例和GLSurfaceView的大小比例保持一致,从而确保图像显示正常。
图像的旋转
在Android中摄像头出来的图像需要进行一定的旋转,然后才能交给屏幕显示,而且如果应用支持屏幕旋转的话,也需要根据旋转的状况实时调整摄像头的角度。在Android中旋转摄像头图像同样有两种方法,一是通过摄像头的setDisplayOrientation(result)方法,一是通过OpenGL的矩阵进行旋转。下面是通过setDisplayOrientation(result)方法进行旋转的代码:
public static int getDisplayRotation(Activity activity) {
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_0: return 0;
case Surface.ROTATION_90: return 90;
case Surface.ROTATION_180: return 180;
case Surface.ROTATION_270: return 270;
}
return 0;
}
public static void setCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
// See android.hardware.Camera.setCameraDisplayOrientation for
// documentation.
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int degrees = getDisplayRotation(activity);
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
通过OpenGL的矩阵进行旋转之后在OpenGL相关的文章中进行讲述。
设置预览帧率
通过Camera.Parameters中getSupportedPreviewFpsRange()可以获得摄像头支持的帧率变化范围,从中选取合适的设置给摄像头即可。相关的代码如下:
public static void setCameraFps(Camera camera, int fps) {
Camera.Parameters params = camera.getParameters();
int[] range = adaptPreviewFps(fps, params.getSupportedPreviewFpsRange());
params.setPreviewFpsRange(range[0], range[1]);
camera.setParameters(params);
}
private static int[] adaptPreviewFps(int expectedFps, List<int[]> fpsRanges) {
expectedFps *= 1000;
int[] closestRange = fpsRanges.get(0);
int measure = Math.abs(closestRange[0] - expectedFps) + Math.abs(closestRange[1] - expectedFps);
for (int[] range : fpsRanges) {
if (range[0] <= expectedFps && range[1] >= expectedFps) {
int curMeasure = Math.abs(range[0] - expectedFps) + Math.abs(range[1] - expectedFps);
if (curMeasure < measure) {
closestRange = range;
measure = curMeasure;
}
}
}
return closestRange;
}
需要注意的是:对于Oppo和Vivo的前置摄像头,当fps不为15的时候,在弱光环境下预览图像会很黑。
设置对焦方式
一般摄像头对焦的方式有两种:手动对焦和触摸对焦。下面的代码分别是设置自动对焦和触摸对焦的模式:
public static void setAutoFocusMode(Camera camera) {
try {
Camera.Parameters parameters = camera.getParameters();
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
camera.setParameters(parameters);
} else if (focusModes.size() > 0) {
parameters.setFocusMode(focusModes.get(0));
camera.setParameters(parameters);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void setTouchFocusMode(Camera camera) {
try {
Camera.Parameters parameters = camera.getParameters();
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.size() > 0 && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
camera.setParameters(parameters);
} else if (focusModes.size() > 0) {
parameters.setFocusMode(focusModes.get(0));
camera.setParameters(parameters);
}
} catch (Exception e) {
e.printStackTrace();
}
}
对于自动对焦这样设置后就完成了工作,但是对于触摸对焦则需要设置对应的对焦区域。
要准确地设置对焦区域,有三个步骤:一、得到当前点击的坐标位置;二、通过点击的坐标位置转换到摄像头预览界面坐标系统上的坐标;三、根据坐标生成对焦区域并且设置给摄像头。
整个摄像头预览界面定义了如下的坐标系统,对焦区域也需要对应到这个坐标系统中。
如果摄像机预览界面是通过SurfaceView显示的则比较简单,由于要确保不变形,会将SurfaceView进行拉伸,从而使得SurfaceView和预览图像大小比例一致,因此整个SurfaceView相当于预览界面,只需要得到当前点击点在整个SurfaceView上对应的坐标,然后转化为相应的对焦区域即可。
如果摄像机预览界面是通过GLSurfaceView显示的则要复杂一些,由于纹理需要进行裁剪,才能使得显示不变形,这样的话,我们要还原出整个预览界面的大小,然后通过当前点击的位置换算成预览界面坐标系统上的坐标,然后得到相应的对焦区域,然后设置给摄像机。
当设置好对焦区域后,通过调用Camera的autoFocus()方法即可完成触摸对焦。
整个过程代码量较多,请自行阅读项目源码。
设置缩放
当检测到手势缩放的时候,我们往往希望摄像头也能进行相应的缩放,其实这个实现还是比较简单的。首先需要加入缩放的手势识别,当识别到缩放的手势的时候,根据缩放的大小来对摄像头进行缩放。代码如下所示:
/**
* Handles the pinch-to-zoom gesture
*/
private class ZoomGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (!mIsFocusing) {
float progress = 0;
if (detector.getScaleFactor() > 1.0f) {
progress = CameraHolder.instance().cameraZoom(true);
} else if (detector.getScaleFactor() < 1.0f) {
progress = CameraHolder.instance().cameraZoom(false);
} else {
return false;
}
if(mZoomListener != null) {
mZoomListener.onZoomProgress(progress);
}
}
return true;
}
}
public float cameraZoom(boolean isBig) {
if(mState != State.PREVIEW || mCameraDevice == null || mCameraData == null) {
return -1;
}
Camera.Parameters params = mCameraDevice.getParameters();
if(isBig) {
params.setZoom(Math.min(params.getZoom() + 1, params.getMaxZoom()));
} else {
params.setZoom(Math.max(params.getZoom() - 1, 0));
}
mCameraDevice.setParameters(params);
return (float) params.getZoom()/params.getMaxZoom();
}
闪光灯操作
一个摄像头可能有相应的闪光灯,也可能没有,因此在使用闪光灯功能的时候先要确认是否有相应的闪光灯。检测摄像头是否有闪光灯的代码如下:
public static boolean supportFlash(Camera camera){
Camera.Parameters params = camera.getParameters();
List<String> flashModes = params.getSupportedFlashModes();
if(flashModes == null) {
return false;
}
for(String flashMode : flashModes) {
if(Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)) {
return true;
}
}
return false;
}
切换闪光灯的代码如下:
public static void switchLight(Camera camera, Camera.Parameters cameraParameters) {
if (cameraParameters.getFlashMode().equals(Camera.Parameters.FLASH_MODE_OFF)) {
cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
} else {
cameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
}
try {
camera.setParameters(cameraParameters);
}catch (Exception e) {
e.printStackTrace();
}
}
四、开始预览
当打开了摄像头,并且设置好了摄像头相关的参数后,便可以通过调用Camera的startPreview()方法开始预览。有一个需要说明,无论是SurfaceView还是GLSurfaceView,都可以设置SurfaceHolder.Callback,当界面开始显示的时候打开摄像头并且开始预览,当界面销毁的时候停止预览并且关闭摄像头,这样的话当程序退到后台,其他应用也能调用摄像头。
private SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
SopCastLog.d(SopCastConstant.TAG, "SurfaceView destroy");
CameraHolder.instance().stopPreview();
CameraHolder.instance().releaseCamera();
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
@Override
public void surfaceCreated(SurfaceHolder holder) {
SopCastLog.d(SopCastConstant.TAG, "SurfaceView created");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
SopCastLog.d(SopCastConstant.TAG, "SurfaceView width:" + width + " height:" + height);
CameraHolder.instance().openCamera();
CameraHolder.instance().startPreview();
}
};
最后
以上就是贪玩铅笔为你收集整理的Android视频采集与处理 一、检查摄像头 二、打开摄像头三、设置摄像头参数四、开始预览的全部内容,希望文章能够帮你解决Android视频采集与处理 一、检查摄像头 二、打开摄像头三、设置摄像头参数四、开始预览所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复