Android手机直播(二)摄像机

一、文章说明

上周开始写直播相关的文章,写了一篇手机直播总览的文章,没想到得到大家很多赞和关注,在此感谢大家支持。这篇文章将会讲述Android摄像头相关的知识,希望能对大家有所帮助。文章开始会简单介绍一下摄像头的成像原理,然后会讲述摄像头采集相关的一些图片格式,之后会讲述在直播项目中如何调用Camera。

整个项目已经开源,开源地址:SopCastComponent

二、成像原理

摄像头结构

上图是一个摄像头的结构图,可以看出一个摄像头由镜头、红外滤光片、光学传感器、软板FPC、数字信号处理芯片构成。

1、镜头Lens
镜头对成像有很重要的作用,相当于人眼中的晶状体,利用透镜的折射原理,景物光线透过镜头在聚焦平面上形成清晰的像,然后通过感光材料CMOS或CCD记录影像,并通过电路转换为电信号。
镜头其组成是透镜结构,由几片透镜组成,一般可分为塑胶透镜(plastic)或玻璃透镜(glass)。当然,所谓塑胶透镜也非纯粹塑料,而是树脂镜片,其透光率感光性之类的光学指标是比不上镀膜镜片的。
通常摄像头用的镜头构造有:1P、2P、1G1P、1G2P、2G2P、2G3P、4G、5G等。透镜越多,成本越高,相对成像效果会更出色;而玻璃透镜又比树脂贵。因此一个品质好的摄像头应该是采用多层玻璃镜头。

镜头

2、红外滤光片IR Filter
主要是过滤掉进入镜头的光线中的红外光,这是因为人眼看不到红外光,但是光学传感器却能感受到红外光,所以需要将光线中的红外光滤掉,以便图像更接近人眼看到的效果。

3、光学传感器Sensor

Sensor

sensor是摄像头的核心,负责将通过Lens的光信号转换为电信号,再经过内部AD转换为数字信号。每个pixel像素点只能感受R、G、B中的一种,因此每个像素点中存放的数据是单色光,所以我们通常所说的30万像素或者130万像素,表示的就是有30万或130万个感光点,每个感光点只能感应一种光,这些最原始的感光数据我们称为Raw Data。Raw Data数据要经过ISP(Image Sensor Processor)的处理才能还原出三原色,也就是说如果一个像素点感应为R值,那么ISP会根据该感光点周围的G、B的值,通过插值和特效处理等,计算出该R点的G、B值,这样该点的RGB就被还原了,除此之外,ISP还有很多操作。

目前常用的sensor有两种,一种是CCD(电荷耦合)原件;一种是CMOS(金属氧化物导体)原件。

1、CCD(Charge Coupled Device),电荷耦合器件传感器:使用一种高感光度的半导体材料制成,能把光线转变成电荷,通过模数转换器芯片转换成电信号。CCD由许多独立的感光单位组成,通常以百万像素为单位。当CCD表面受到光照时,每个感光单位都会将电荷反映在组件上,所有的感光单位产生的信号加在一起,就构成了一幅完整的图像。CCD传感器以日本厂商为主导,全球市场上有90%被日本厂商垄断,索尼、松下、夏普是龙头。

2、CMOS(Complementary Metal-Oxide Semiconductor),互补性氧化金属半导体:主要是利用硅和锗做成的半导体,使其在CMOS上共存着带N(-)和P(+)级的半导体,这两个互补效应所产生的电流可以被处理芯片记录并解读成影像。CMOS传感器主要以美国、韩国和中国台湾为主导,主要生产厂家是美国的OmnVison、Agilent、Micron,中国台湾的锐像、原相、泰视等,韩国的三星、现代。

4、数字信号处理芯片 DSP
所谓数字信号处理芯片DSP,其实就是摄像头的大脑,作用等同于个人计算机里的CPU中央处理器,它的功能主要是通过一系列复杂的数学算法运算,对由光学传感器传来的数字图像信号进行优化处理,并把处理后的信号通过USB接口传到PC等设备上,是摄像头的核心设备。一般来说DSP可以对图像进行自动增益补强,自动曝光、自动白平衡,色彩饱和度、对比度、边缘增强以及伽马矫正等相关处理。

5、摄像头工作流程
外部光线穿过镜头后,经过红外滤光片滤波后照射到光学传感器面上,光学传感器将从镜头上传导过来的光线转换为电信号,再通过内部的数模转换转换为数字图像,数字图像经过数字信号处理芯片处理后得到优化后的图片。

整个流程如下图所示:


摄像头流程.png

三、图像格式

1、RGB
RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。目前的显示器大都是采用了RGB颜色标准,在显示器上,是通过电子枪打在屏幕的红、绿、蓝三色发光极上来产生色彩的,目前的电脑一般都能显示32位颜色,有一千万种以上的颜色。电脑屏幕上的所有颜色,都由这红色绿色蓝色三种色光按照不同的比例混合而成的。一组红色绿色蓝色就是一个最小的显示单位。屏幕上的任何一个颜色都可以由一组RGB值来记录和表达。因此这红色绿色蓝色又称为三原色光,用英文表示就是R(red)、G(green)、B(blue)。在电脑中,RGB的所谓“多少”就是指亮度,并使用整数来表示。通常情况下,RGB各有256级亮度,用数字表示为从0、1、2...直到255。注意虽然数字最高是255,但0也是数值之一,因此共256级。按照计算,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216。通常也被简称为1600万色或千万色。也称为24位色(2的24次方)。


RGB

在摄像头成像过程中已经讲述,当光线经过光学传感器后得到最原始的感光数据,也就是Raw Data,也叫做Raw RGB Data。Raw Data数据经过ISP(Image Sensor Processor)处理还原出三原色,如果一个像素点感应为R值,那么ISP会根据该感光点周围的G、B的值,通过插值和特效处理等,计算出该R点的G、B值,这样该点的RGB就被还原了。RGB888是最通用的RGB格式,每个像素点用8位表示红色,8位表示绿色,8位表示蓝色。RGB565和RGB888原理一致,只不过为了节省空间,使用5位表示红色,6位表示绿色,5位表示蓝色,绿色多一位,原因是人眼对绿色比较敏感。

2、YUV
与我们熟知的RGB类似,YUV也是一种颜色编码方法,有些时候也被称作Y'UV, YUV, YCbCr,YPbPr等,主要用于电视系统以及模拟视频领域,它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。

YUV颜色表,横轴表示U,纵轴表示V,UV范围为-1到1

YUV有三种存储方式:

  • 紧缩格式(packed formats):Y、U、V值的是连续交替存储的,和RGB的存放方式类似。
  • 平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。
  • 半平面格式(semi-planar formats):将Y存储在一个矩阵中,将UV存储在一个矩阵中。

为了节省带宽,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样格式有YUV 4:2:0、YUV 4:2:2、YUV 4:1:1和YUV 4:4:4。

下面三幅图直观地表示了不同的采样方式,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量。


YUV抽样

YUV 4:4:4采样,每一个Y对应一组UV分量,完全取样。
YUV 4:2:2采样,每两个Y共用一组UV分量,2:1的水平取样,垂直完全采样。
YUV 4:2:0采样,每四个Y共用一组UV分量,2:1的水平取样,垂直2:1采样。
YUV 4:1:1采样,每四个Y共用一组UV分量,4:1的水平取样,垂直完全采样。

根据上面的分析,根据YUV的存储方式、抽样格式、其实还有Y和UV分量的先后顺序可以分成各种个样的YUV格式。

下面来具体看看一些YUV格式
YUV422 Planar
由于是平面格式存储,因此这里YUV数据是分开存储的,每两个水平Y采样点,有一个U和一个V采样点,如下图。

YUV422 Planar

YUV420 Planar
这个格式跟YUV422 Planar 类似也是平面格式存储,但对于U和V的采样在水平和垂直方向都减少为2:1,如下图。

YUV420 Planar

下面一张图可以帮助我们更好地理解:
YUV420 Planar详细说明.png

**YUV422 Semi-Planar **
这个格式的数据量跟YUV422 Planar的一样,但是U、V是交叉存放的,如下图。


YUV422 Semi-Planar

YUV420 Semi-Planar
这个格式的数据量跟YUV420 Planar的一样,但是U、V是交叉存放的,如下图。

YUV420 Semi-Planar

YUV422 Interleaved
这个格式的数据量跟YUV422 Planar的一样,但是Y、U、V是交叉存放的,这个是打包(packed)模式的,如下图。

YUV422 Interleaved

当然还有很多其他的格式,但是也无非是根据YUV的存储方式、抽样格式、其实还有Y和UV分量的先后顺序变化产生的。根据上面几种格式的分析,相信大家知道了一种YUV格式的存储方式、抽样格式、Y和UV分量的先后顺序,也能推倒出相应的存储形式。

3、Android图片格式
在Android中定义了一个ImageFormat类,其中定义了一些图片格式,其实都是可以对应到前面讲述的RGB和YUV格式的。

1、I420对应YUV420P,平面格式存储,4:2:0采样,U在前,V在后。
2、YV12对应YUV420P,平面格式存储,4:2:0采样,V在前,U在后。
3、NV12对应YUV420SP,半平面格式存储,4:2:0采样,U在前,V在后。
4、NV21对应YUV420SP,半平面格式存储,4:2:0采样,V在前,U在后。
5、NV16对应YUV422SP,半平面格式存储,4:2:2采样,U在前,V在后。
6、YUY2对应YUV422,紧缩格式存储,4:2:2采样,U在前,V在后。

关于RGB格式的命名比较清晰,这里就不进行讲述。

4、YUV和RGB变换
在之前一篇文章中已经讲述,需要将得到的视频进行编码后进行传输,这样的话会大大减少带宽。一般情况下都会使用H264进行编码,这样的话我们需要传入H264认可的格式。H264支持I420格式的输入和输出,这样的话要求我们在编码前将图片格式转换为I420,当要显示图像的时候又要将I420格式的图片转换为RGB格式,然后由显示器显示。

YUV转换为RGB分为两步:1、根据YUV的采样特点将其他格式的YUV转换为YUV444;2、根据公式将YUV444转换为RGB。将RGB转换为YUV则步骤相反。

转换的时候需要区分YUV和YCbCr
YUV色彩模型来源于RGB模型,
该模型的特点是将亮度和色度分离开,从而适合于图像处理领域。
应用:模拟领域

Y'= 0.299*R' + 0.587*G' + 0.114*B'
U'= -0.147*R' - 0.289*G' + 0.436*B' = 0.492*(B'- Y')
V'= 0.615*R' - 0.515*G' - 0.100*B' = 0.877*(R'- Y')
R' = Y' + 1.140*V'
G' = Y' - 0.394*U' - 0.581*V'
B' = Y' + 2.032*U'

YCbCr模型来源于YUV模型。YCbCr是 YUV 颜色空间的偏移版本。
应用:数字视频,ITU-R BT.601建议

Y’ = 0.257*R' + 0.504*G' + 0.098*B' + 16
Cb' = -0.148*R' - 0.291*G' + 0.439*B' + 128
Cr' = 0.439*R' - 0.368*G' - 0.071*B' + 128
R' = 1.164*(Y’-16) + 1.596*(Cr'-128)
G' = 1.164*(Y’-16) - 0.813*(Cr'-128) - 0.392*(Cb'-128)
B' = 1.164*(Y’-16) + 2.017*(Cb'-128)

上面各个符号都带了一撇,表示该符号在原值基础上进行了伽马校正,伽马校正有助于弥补在抗锯齿的过程中,线性分配伽马值所带来的细节损失,使图像细节更加丰富。在没有采用伽马校正的情况下,暗部细节不容易显现出来,而采用了这一图像增强技术以后,图像的层次更加明晰了。

比如YUV422格式,每两个像素共用一个UV。现在有四个像素,用YUV422表示,存储格式为Y0 U0 Y1 V0 Y2 U1 Y3 V1。四个像素的RGB值计算如下:

RGB0 = Y'UV444toRGB888(Y0, U0, V0);
RGB1 = Y'UV444toRGB888(Y1, U0, V0);
RGB2 = Y'UV444toRGB888(Y2, U1, V1);
RGB3 = Y'UV444toRGB888(Y3, U1, V1);

关于YUV和RGB的转换,可以参考开源项目:LibYuv(LibYuv还可以对图片进行旋转、缩放等)

四、摄像头调用

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。三个状态的关系图如下所示:


Camera State

1、检查摄像头
在使用摄像头之前先要检查摄像头服务是否可用,下面一段代码是检查摄像头服务是否可用的。

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,则把背面摄像头放在列表第一个,之后打开摄像头的时候,直接获取列表中第一个摄像头相关参数,然后进行打开。这样的设计使得切换摄像头也变得十分简单,切换摄像头时,先关闭当前摄像头,然后变化摄像头列表中的顺序,然后再打开摄像头即可,也就是每次打开摄像头都打开摄像头列表中第一个摄像头参数所指向的摄像头。

2、打开摄像头
打开摄像头之前,先从摄像头列表中获取第一个摄像头参数,之后根据参数中的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。

3、设置摄像头参数
在给摄像头设置参数后,需要记录这些参数,以方便其他地方使用。比如记录当前摄像头是否有闪光点,从而可以决定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显示流程图如下:


GLSurfaceView预览.jpeg

对摄像机设置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();
    }
}

对于自动对焦这样设置后就完成了工作,但是对于触摸对焦则需要设置对应的对焦区域。
要准确地设置对焦区域,有三个步骤:一、得到当前点击的坐标位置;二、通过点击的坐标位置转换到摄像头预览界面坐标系统上的坐标;三、根据坐标生成对焦区域并且设置给摄像头。
整个摄像头预览界面定义了如下的坐标系统,对焦区域也需要对应到这个坐标系统中。


Camera Focus Area

如果摄像机预览界面是通过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();
    }
}

4、开始预览
当打开了摄像头,并且设置好了摄像头相关的参数后,便可以通过调用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手机直播(二)摄像机
Android手机直播(三)声音采集
Android手机直播(四)Android Media API

六、结束语

终于写完了,各位看官觉得文章不错的话不妨点个喜欢~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容