视频采集:Android平台基于Camera 1的实现

前言

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

权限配置

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

<uses-permission android:name="android.permission.CAMERA"/>

打开摄像头

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

int cameraNum = Camera.getNumberOfCameras();

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

//获取对应摄像头信息
for (int id = 0; id < cameraNum; id++) {
    Camera.getCameraInfo(id, info);
    if (info.facing == cameraId) {
        //TODO 
    }
}

判断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<Integer> 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<Camera.Size> 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 < lastWeight) {
        curWidth = size.width;
        curHeight = size.height;
    }
}
//设置预览的size尺寸
parms.setPreviewSize(curWidth, curHeight);

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

int weight;
int lastWeight = Integer.MAX_VALUE;
int curRange[] = new int[2];
//获取支持的帧率上下限列表
List<int[]> ranges = parms.getSupportedPreviewFpsRange();
for (int[] range : ranges) {
    //如果帧率在支持的范围之间,直接设置
    if (mConfig.mMinFps >= range[0] && mConfig.mMaxFps <= range[1]) {
        curRange[0] = mConfig.mMinFps;
        curRange[1] = mConfig.mMaxFps;
        break;
    }
    //计算权重,这里采用差值平方来做比较,也可以采用其他方式计算
    weight = (range[0] - mConfig.mMinFps) * (range[0] - mConfig.mMinFps)
            + (range[1] - mConfig.mMaxFps) * (range[1] - mConfig.mMaxFps);
    if (weight < lastWeight) {
        curRange[0] = Math.max(range[0], mConfig.mMinFps);
        curRange[1] = Math.min(range[1], mConfig.mMaxFps);
    }
}
//设置帧率数值
parms.setPreviewFpsRange(curRange[0], curRange[1]);

注意的是,这里的帧率范围是需要乘以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,如下:

<SurfaceView
    android:id="@+id/surface_view"
    android:layout_width="720px"
    android:layout_height="1280px"
    />

在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,如下:

<TextureView
    android:id="@+id/texture_view"
    android:layout_width="720px"
    android:layout_height="1280px"
    />

回到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需要在布局中才能生效:

<android.opengl.GLSurfaceView
    android:id="@+id/gl_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

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!

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

推荐阅读更多精彩内容