前言

这篇文章简单介绍下移动端Android系统下利用Camera1进行视频数据采集的方法。
按照惯例先上一份源码 AndroidVideo。
Camera1调用摄像头采集视频的核心实现在CameraCapture.java。

权限配置

使用Android平台提供的摄像头,首先必须在配置文件中添加如下权限配置:

打开摄像头

1、首先我们需要获取当前设备的摄像头数量:

int cameraNum = Camera.getNumberOfCameras();

2、一般业务上面都会指定是打开前置摄像头还是后置摄像头:

//获取对应摄像头信息
for (int id = 0; id 

判断info.facing的值,他的值有如下几种:

  • Camera.CameraInfo.CAMERA_FACING_FRONT:前置摄像头
  • Camera.CameraInfo.CAMERA_FACING_BACK:后置摄像头

3、调用打开摄像的接口,它的原型是public static Camera open(int cameraId)
需要传入的是摄像头的ID;一般手机发展来说,都是先有后置摄像头,然后才发展前置摄像头,所以摄像头的ID排列是后置是0,前置是1,其他摄像头再递增。
但是我们最好通过CAMERA_FACING_FRONTCAMERA_FACING_BACK比对才比较靠谱。
open()返回摄像头实例,如果返回NULL或者抛异常,请检查cameraId传入是否有误或者权限申请被禁止情况。

配置摄像头参数

一般来说,我们需要关注摄像头预览格式、帧率和宽高尺寸等配置。
获取参数集合

//获取摄像头参数设置集合
Camera.Parameters parms = mCamera.getParameters();
//进行参数设置
//必须setParameters后,更新的属性才会生效
mCamera.setParameters(parms);

设置预览格式
一般最通用的就是ImageFormat.NV21格式,其实也就是YUV420SP格式,对于YUV的具体格式这里不做扩展分析,其最重要一点就是YUV420SP的UV是交错存放在一个平面的。
我们需要调用parms.getSupportedPreviewFormats()返回一个支持格式列表,然后判断其中是否包含我们所需要的格式。

//获取支持的预览格式集合
List supportedPreviewFormat = parms.getSupportedPreviewFormats();
//一般来说,ImageFormat.NV21通用适配绝大部分手机
if (supportedPreviewFormat.contains(mConfig.mFormat)) {
    parms.setPreviewFormat(mConfig.mFormat);
}
else {
    //格式不兼容,采用默认格式 or 返回客户端处理错误
}

设置预览宽高
通过调用parms.getSupportedPreviewSizes()得出支持的size列表,然后对比我们需要的szie,查询列表中是否存在。
如果不存在,建议取一个相对靠近的支持的size进行设置。

int weight;
int lastWeight = Integer.MAX_VALUE;
int curWidth = 0, curHeight = 0;
//获取支持的预览size列表
List sizes = parms.getSupportedPreviewSizes();
for (Camera.Size size : sizes) {
    //如果height和width都一致,直接设置
    if (size.height == mConfig.mHeight && size.width == mConfig.mWidth) {
        curWidth = size.width;
        curHeight = size.height;
        break;
    }
    //计算权重,这里采用差值平方来做比较,也可以采用其他方式计算
    weight = (size.width - mConfig.mWidth) * (size.width - mConfig.mWidth)
            + (size.height - mConfig.mHeight) * (size.height - mConfig.mHeight);
    if (weight 

设置预览的帧率
设置源码:

int weight;
int lastWeight = Integer.MAX_VALUE;
int curRange[] = new int[2];
//获取支持的帧率上下限列表
List ranges = parms.getSupportedPreviewFpsRange();
for (int[] range : ranges) {
    //如果帧率在支持的范围之间,直接设置
    if (mConfig.mMinFps >= range[0] && mConfig.mMaxFps 

注意的是,这里的帧率范围是需要乘以1000的,也就是说,如果你的一秒是15帧到30帧的话,那么帧率范围应该是[1500, 3000]。
PS:有点需要注意的是,需要设置15帧,而支持列表只有[1500, 2000],在设置setPreviewFpsRange(1500,1500)发生异常了,那么你需要调用setPreviewFpsRange(1500,2000)来进行帧率的设置。

摄像头旋转问题

在Camare1的api中,你会发现size的设置是这样的:
宽的值是1280(或者是640),而对应高的值是720(或者是480)
因为摄像头默认采集出来的视频画面是横版的,那么我们需要获取摄像头的选择角度进行校对视频方向。

int degrees;
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (mConfig.isFront) {
    degrees = info.orientation % 360;
}
else {
    degrees = (info.orientation + 360) % 360;
}
mCamera.setDisplayOrientation(degrees);

根据对应的cameraId取到Camera.CameraInfo,而CameraInfoorientation变量代表的就是该摄像头采集到画面的选择角度。
我们还需要接下来进行处理:
前置摄像头直接对info.orientation进行360取模。
后置摄像头需要对info.orientation先加上360在进行360取模。
最后调用mCamera.setDisplayOrientation()设置旋转角度。

摄像头预览

我们采集到画面后,一般需要提供给用户渲染界面。
根据业务的需求,我们有多种方式可以选择:

SurfaceView
比较简单的一种方式,一般我们在布局文件里面添加一个SurfaceView,如下:

在Java代码监听SurfaceHolder.Callback回调:

SurfaceView surfaceView = findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        //SurfaceView创建成功
        mCamera.setPreviewDisplay(holder);
    }
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //SurfaceView的尺寸发生改变
    }
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //SurfaceView开始销毁
    }
});

一般我们在surfaceCreated()中调用了Camera.setPreviewDisplay(holder)就完成了预览界面的设置。

TextureView
这个也是一种比较简单的预览方式,一般我们在布局文件里面添加一个TextureView,如下:

回到Java中,需要如下逻辑:

TextureView textureView = findViewById(R.id.texture_view);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        //SurfaceTexture初始化完毕
        mCamera.setPreviewTexture(surface);
    }
    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    }
    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        return false;
    }
    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    }
});

只需要在onSurfaceTextureAvailable()回调中调用Camera.setPreviewTexture(surface)即可。

SurfaceTexure
如果我们并不需要预览界面,而是需要获取到采集画面进行预处理(例如美颜、人脸识别),然后在进行预览的话。
那就需要用到静默渲染的实现,也就是使用SurfaceTexure来渲染采集画面。

//textId,是申请的一个纹理ID,属于OpenGL范畴,这里不展开讲解
SurfaceTexture texture = new SurfaceTexture(texId);
mCamera.setPreviewTexture(texture);

texture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //每帧回调,这里我们可以利用纹理ID去获取采集画面
    }
});

GLSurfaceView
SurfaceView的基础上封装了OpenGL的一些通用性处理功能,提供一个较为简单的OpenGL的使用环境。
GLSurfaceView需要在布局中才能生效:

java中我们需要对GLSurfaceView设置一个Renderer:

GLSurfaceView glSurfaceView = findViewById(R.id.gl_view);
glSurfaceView.setRenderer(new GLSurfaceView.Renderer() {
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //OpenGL纹理构造和其他初始化操作
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //GLSurfaceView的尺寸发生改变
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        //每帧渲染,这里利用OpenGL进行渲染
    }
});

GLSurfaceView的相关源码解析可以看这篇博客,由于OpenGL相关范畴比较大,所以本篇文章不对OpenGL的知识做讲解。

视频数据获取

采集画面获取,一般来讲有两种主要方式。
原始数据bytes获取

//setPreviewCallback 在启动预览后,每产生一帧都会回调
//但是没产生一帧都需要开辟一个新的buffer,GC频繁,效率较低
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //data数据就是采集的画面数据
    }
});

或者

//setPreviewCallbackWithBuffer 在启动预览后,需要手动调用Camera.addCallbackBuffer(data)
//触发回调,byte[]数据需要根据一帧画面的尺寸提前创建传入
//例如NV21格式,size = width * height * 3 / 2;
mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //data数据就是采集的画面数据
        //回收缓存,下次仍然会使用,所以不需要再开辟新的缓存,达到优化的目的
        mCamera.addCallbackBuffer(data);
    }
});

利用纹理ID进行静默渲染

//textId,是申请的一个纹理ID,属于OpenGL范畴,这里不展开讲解
SurfaceTexture texture = new SurfaceTexture(texId);
mCamera.setPreviewTexture(texture);

texture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        //每帧回调,这里我们可以利用纹理ID去获取采集画面
    }
});

采集线程

由于采集需要消耗一定的时间,所以我们建议Camera的调用需要在一个新的子线程进行调用,避免调用UI线程导致了ANR的发生。
比较推荐使用HandlerThread创建一个子线程Looper循环来处理Camera的相关业务。

结语

这篇文章简单介绍了Android平台基于Camera1的api进行摄像头采集的功能。
需要注意的是谷歌已经将Camera1置为废弃状态了,转而建议使用Camera2相关api进行采集,下一篇文章将会简单介绍下怎么利用Camera2相关api进行画面采集。
Camera1虽然被废弃,但是由于厂商兼容性问题,Camera1的通用支持性还是比Camera2好不少,所以可以预知短时间内Camera1的采集框架还是会被主流采纳使用。

本文同步发布于简书、CSDN。

End!

文章来源于互联网,如有雷同请联系站长删除:视频采集:Android平台基于Camera 1的实现

发表评论