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的idpublic 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() {@Overridepublic 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-facingresult = (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 {@Overridepublic 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() {@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {SopCastLog.d(SopCastConstant.TAG, "SurfaceView destroy");CameraHolder.instance().stopPreview();CameraHolder.instance().releaseCamera();}@TargetApi(Build.VERSION_CODES.GINGERBREAD)@Overridepublic void surfaceCreated(SurfaceHolder holder) {SopCastLog.d(SopCastConstant.TAG, "SurfaceView created");}@Overridepublic 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视频采集与处理相关推荐

  1. Android视频采集实时推送RTP/RTSP/RTMP

    因为在工作中,接触到了视频相关的开发工作:同时,大多数android处理音视频多半都是有C++工程师提供处理库,所以,在这里记录一下我自己在工作中遇到的问题. 主要功能: 采集Android摄像头数据 ...

  2. android视频采集

    视频画面的采集主要是使用各个平台提供的摄像头API来实现的, 在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到 屏幕上提供给用户预览,然后将该视频帧编码到一个视频文件中,其使 用的编码格 ...

  3. Android视频采集方案

    需求:视频录制.30秒保存一个mp4文件. 一.采用MediaRecord录制 优点: 使用方便,得到就是编码和封装好的音视频文件,可以直接使用. 缺点: 无法获取原始数据,从而无法对原始数据添加一些 ...

  4. android视频采集与压缩,视频压缩 Android原生插件

    更新记录 1.0.3(2021-02-23) 1.修复某些类型视频压缩失败问题 1.0.2(2020-10-26) 1.支持视频压缩后,获取第一帧原尺寸图片 2.单独开放出获取视频第一帧原尺寸图片方法 ...

  5. android log 码率,webrtc之Android视频质量提升:保帧率降码率

    前言: 我们的产品是在一款跑着Android系统的特定芯片上使用webrtc开发的一个视频通话业务,当前的情况是在网络正常的情况下帧率也比较低,弱网环境下适应能力较差.基于此,我了解了webrtc A ...

  6. 移动互联网实时视频通讯之视频采集

    原文:http://blog.easemob.com/?p=277 一 .前言 一套完整的实时网络视频通讯系统包括视频采集.视频编码.视频传输.视频解码和播放.对于视频采集,大多数视频编码器对输入原始 ...

  7. 用FFmpeg玩转Android视频录制与压缩

    [置顶] 利用FFmpeg玩转Android视频录制与压缩(二) 标签: Android视频采集Android视频编码Android FFmpegAndroid 视频压缩视频编码 2017-06-10 ...

  8. 【Android RTMP】Android Camera 视频数据采集预览 ( 视频采集相关概念 | 摄像头预览参数设置 | 摄像头预览数据回调接口 )

    文章目录 安卓直播推流专栏博客总结 一. Android 端数据采集涉及到的相关概念 二. Camera 预览图像尺寸设置 三. 获取摄像头采集的数据格式 安卓直播推流专栏博客总结 Android R ...

  9. android camera2 采集,视频采集:Android平台基于Camera 2的实现

    前言 这篇文章简单介绍下移动端Android系统下利用Camera2相关API进行视频采集的方法. Camera2是谷歌在Android 5.0新增的用来替代Camera1操作摄像头的一个全新的API ...

最新文章

  1. 1小时教你做360度全景“小星球”效果图 Skillshare – Create a Panoramic ‘Little Planet’ from Anywhere
  2. 全排列:不含重复元素和含重复元素的全排列
  3. MAX2323E - 原理图系列
  4. Android各大热补丁方案分析和比较
  5. 记录一次postfix无法收取邮件和mysql异常不能启动
  6. 那些用起来很爽,但用不好可能会被人打的Python骚操作
  7. DelphiXE下的字符串变化
  8. python学习笔记(11)--测验3: Python基础语法(下) (第7周)
  9. mysql模糊匹配查询_Mysql之模糊匹配查询
  10. 计算机组装维护安装光驱步骤,光驱怎么安装?教你如何正确安装光驱方法
  11. uni-app银行卡卡号验证
  12. 卡耐基梅隆大学计算机科学课本,美国卡耐基梅隆大学计算机科学专业.pdf
  13. 动态显示电池电量Icon Vue 电量Icon 电池电量
  14. 点击切换图标(收藏和取消收藏)
  15. 如何向Google提交网站?(转)
  16. linux关于压缩解压tar包
  17. LeetCode1446. 连续字符
  18. Algorithm之PrA:PrA之LP线性规划算法经典案例剖析+Matlab编程实现
  19. 山东计算机网络期末试题,山东轻工业学院计算机网络期末试题A(13页)-原创力文档...
  20. Python实现的BeagleBone Black 串口助手(全部源码)

热门文章

  1. 【数据挖掘笔记】基础知识
  2. matlab模拟流动传热,基于MATLAB的快速式汽-水换热器传热与流动特性研究
  3. HS100B音频芯片,替代芯片
  4. 读书也需要「耍心眼」?这 10 个读书策略,你怎么看?
  5. 扫一扫——Zxing
  6. mysql数据库精确匹配搜索_mysql 模糊搜索方法
  7. 达内CEO出席第三届软件服务外包年会
  8. 数控车削加工 的 过程能力分析
  9. Python+Vue计算机毕业设计医院碳排放管理平台r392w(源码+程序+LW+部署)
  10. 计算机考研复试----技术前沿知识