前言
这篇文章简单介绍下移动端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_FRONT
和CAMERA_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
,而CameraInfo
的orientation
变量代表的就是该摄像头采集到画面的选择角度。
我们还需要接下来进行处理:
前置摄像头直接对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的采集框架还是会被主流采纳使用。
End!