我是靠谱客的博主 端庄蜜蜂,这篇文章主要介绍android视频采集,现在分享给大家,希望可以做个参考。

视频画面的采集主要是使用各个平台提供的摄像头 API 来实现的,
在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到
屏幕上提供给用户预览,然后将该视频帧编码到一个视频文件中,其使
用的编码格式一般是 H264 。当然,最终我们还要配上音频,否则没有
音频文件的视频就成了早期的默片电影了。
本节将主要学习如何在 Android 和 iOS 平台上利用各自平台提供的摄
像头 API ,采集出正确的视频帧并绘制到屏幕上,具体的编码将会在后
续进行讨论。
6.2.1 Android 平台的视频画面采集
1. 权限配置
要想使用 Android 平台提供的摄像头,首先必须在配置文件中添加
如下权限要求:
<uses-permission android:name="android.permission.CAMERA" />
伴随着 Android 系统的发展, Android 的摄像头 API 也已经有了非常
大的变化,现在选用的 Camera 的使用方式为设置预览纹理的形式,而不
是设置 YUV 数据回调的方式,这是因为得到纹理 ID 之后,可以很方便
地进行视频滤镜处理,并且很容易渲染到界面上。
2. 打开摄像头
Android 平台提供了打开摄像头的 API ,其函数原型如下:
public static Camera open(int cameraId)
需要传入的参数就是摄像头的 ID ,从手机的发展历史可以知道,先
有后置摄像头,然后才有前置摄像头,甚至目前已经有部分手机有了更
多的辅助摄像头,所以摄像头的 ID 排列是后置摄像头是 0 ,前置摄像头
是 1 ,然后才是其他的摄像头,即便是这样,我们也要使用 CameraInfo
类里面的两个常量,它们分别如下。
·CAMERA_FACING_BACK 代表后置摄像头。
·CAMERA_FACING_FRONT 代表前置摄像头。
该函数返回的就是一个摄像头的实例,如果返回的是 NULL ,或者
抛出异常(因为不同厂商所给出的返回是不一样的),则代表用户没有
授权该应用访问摄像头。
3. 配置摄像头参数 获取到该摄像头实例之后,要为该摄像头实例设置对应的参数,参
数的配置主要涉及如下两个参数。
第一个参数是预览格式,一般设置为 NV21 格式的,实际上就是
YUV420SP 的格式,即 UV 是 interleaved (交错 UVUVUV )的存放,代码
设置如下:
List<Integer> supportedPreviewFormats = parameters.getSupportedPreviewFormats();
if (supportedPreviewFormats.contains(ImageFormat.NV21)) {
parameters.setPreviewFormat(ImageFormat.NV21);
} else {
throw new CameraParamSettingException(" 视频参数设置错误 : 设置预览图像格式异常 ");
}
上述代码先取出摄像头所支持的所有预览格式,然后判断其是否包
含我们要设定的格式,如果包含,则设置进去;如果不包含,则抛出异
常,让客户端代码进行处理。
第二是设置预览的尺寸,分辨率的尺寸一般设置为 1280×720 ,当然
对于某些应用来说,可能也会设置为 640×480 的分辨率,代码设置如
下:
List<Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
int previewWidth = 640;// 1280
int previewHeight = 480;// 720
boolean isSupportPreviewSize = isSupportPreviewSize(
supportedPreviewSizes, previewWidth, previewHeight);
if (isSupportPreviewSize) {
parameters.setPreviewSize(previewWidth, previewHeight);
} else {
throw new CameraParamSettingException(" 视频参数设置错误 : 设置预览的尺寸异常 ");
}
上述代码会取出摄像头所支持的所有分辨率列表,然后判断要设置
的分辨率是否在支持的列表中,如果包含,则设置进去,否则抛出异
常,让客户端代码进行处理。
配置完上述参数的设置之后,就需要将该参数设置给 Camera 实例
了,代码如下:
try {
mCamera.setParameters(parameters); } catch (Exception e) {
throw new CameraParamSettingException(" 视频参数设置错误 ");
}
在宽高的设置中,细心的读者可能已经注意到了宽是 1280 (或者
640 ),高是 720 (或者 480 ),这是因为摄像头默认采集出来的视频画
面是横版的,在显示的时候,需要获取当前这个摄像头采集出来的画面
的旋转角度,那么具体的旋转角度应该如何获取呢?代码如下:
int degrees = 0;
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
degrees = (info.orientation) % 360;
} else { // back-facing
degrees = (info.orientation + 360) % 360;
}
根据不同的摄像头取出对应的 CameraInfo ,该 CameraInfo 中的
orientation 变量表示的就是该摄像头采集到的画面的旋转角度,不过,
要想正确地旋转还需要再处理一下,如果是前置摄像头,则直接对 360
进行取模;如果是后置摄像头,则先加上 360 度再取模 360 ,从而就能得
到想要旋转的角度。得到的这个角度对于后续可以将视频帧正确地显示
到屏幕上来说是至关重要的,下面讲述摄像头的预览时就会用到该角度
参数。
4. 摄像头的预览
配置好摄像头之后,剩下的事情就是配置摄像头采集每一帧图像的
回调,并且获取到图像之后将图像渲染到屏幕上。本书的第 4 章已经讲
解过了如何通过 OpenGL ES 来渲染图像,这里先来回顾一下:首先把图
像解码为 RGBA 格式;然后将 RGBA 格式的字节数组上传到一个纹理
上;最终将该纹理渲染到屏幕上。所以这里的渲染到屏幕上也会使用
OpenGL ES 来实现。由于这里要显示的纹理是摄像头按照一定的刷新频
率( fps )来更新的,所以最终显示出来的就是我们预期的预览效果
了。
整个预览过程分为三个阶段,分别为开始预览、刷新预览与结束预
览。我们首先讲解开始预览阶段,整体流程如图 6-2 所示。
图 6-2
如图 6-2 所示,首先在 Activity 的界面层构造一个 SurfaceView 用于显
示渲染结果;然后在 Native 层用 EGL 和 OpenGL ES 构造一个渲染线程用
于渲染该 SurfaceView ,同时在该渲染线程中生成一个纹理 ID 并传递到
Java 层; Java 层利用该纹理 ID 构造出一个 Surface-Texture ,之后再将该
SurfaceTexture 作为 Camera 的预览目标。最终调用 Camera 的开始预览方
法,这样就可以将摄像头采集到的视频帧渲染到设备屏幕上了。
但是如何让摄像头按照频率采集出来的视频帧依次进行渲染呢?答
案是在图 6-3 中构造好了 SurfaceTexture 对象之后,要为该对象设置视频
帧可用时的监听器,实际上就是当 SurfaceTexture 在可以更新的时候调用
该监听器(即当 Camera 设备采集到一帧视频帧的时候会回调该监听器方
法)。将纹理 ID 设置给摄像头的代码如下:
mCameraSurfaceTexture = new SurfaceTexture(textureId);
try {
mCamera.setPreviewTexture(mCameraSurfaceTexture);
mCameraSurfaceTexture.setOnFrameAvailableListener(frameAvailableListener);
mCamera.startPreview();
} catch (Exception e) {
throw new CameraParamSettingException(" 设置预览纹理错误 ");
} 如上所述,代码中的 frameAvailableListener 是继承自
OnFrameAvailableListener 内部类的一个实例,在该内部类中重写
onFrameAvailable 方法,在该方法中调用 Native 层的方法来渲染摄像头刚
刚捕捉的图像。调用到了 Native 层之后,将会委托到渲染线程中去调用
Java 层的 SurfaceTexture 的 updateTexImage 方法(因为必须在 OpenGL ES
的渲染线程中才可以调用该方法,所以绕了一大圈)。更新视频帧的整
体流程如图 6-3 所示。
图 6-3
图 6-3 中,当 VideoCamera 的方法 updateTexture 执行完毕之后,就说
明摄像头采集的视频帧已经更新到 Native 层生成的纹理 ID 上了,渲染线
程就可以把该纹理 ID 渲染到界面上去了。当摄像头再次采集到一帧新视
频帧的时候,就会周而复始地执行上述过程,这样在设备屏幕上就可以
流畅地看到摄像头的预览了。
对于渲染线程的搭建以及如何将一帧纹理绘制到上层界面的知识已
经在前面章节中讲解过了,那么摄像头采集到这一帧视频帧之后是如何
进行渲染的呢?这也是接下来的重点,下面一起来看看。
前面提到过,要在渲染线程中生成一个纹理 ID ,然后传递到 Java
层,再由 Java 层构造成一个 SurfaceTexture 类型的对象,并将 Camera 的
PreviewCallback 设置为该 SurfaceTexture 对象。由于摄像头采集出来的视
频帧的格式是 NV21 ,即采集出来的一帧的格式是 YUV420SP ,
width*height 个像素点共占用了 width*height*3/2 个字节数,即每个像素
点都会有一个 Y 放到数据存储的前 width*height 个数据中,每四个像素点 共享一个 UV 放到后半部分进行交错存储。而在 OpenGL 中使用的绝大部
分纹理 ID 都是 RGBA 的格式,另外之前在讲解播放器项目的时候也曾讲
过 Luminance 格式,但是那里是开辟 3 个纹理 ID 来表示一张 YUV 的图
片,这里必须使用一个纹理 ID 来为 Camera 更新数据,那么应该如何将 3
个 Luminance 的纹理 ID 合并成一个纹理 ID 呢?幸好 OpenGL ES 的扩展
GL_OES_EGL_image_external 定义了一个纹理的扩展类型,即
GL_TEXTURE_EXTERNAL_OES ,否则整个转换过程将会非常复杂。
同时这种纹理目标对纹理的使用方式也会有一些限制,纹理绑定需要绑
定到类型 GL_TEXTURE_EXTERNAL_OES 上,而不是类型
GL_TEXTURE_2D 上,对纹理设置参数也要使用
GL_TEXTURE_EXTERNAL_OES 类型,生成纹理与设置纹理参数的代
码如下:
glGenTextures(1, &texId);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texId);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
在实际的渲染过程中绑定纹理的代码如下:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texId);
glUniform1i(uniformSamplers, 0);
在 OpenGL ES 的 shader 中,任何需要从纹理中采样的 OpenGL ES 2.0
的 shader 都需要声明其对此扩展( GL_OES_EGL_image_external )的使
用,使用指令如下:
#extension GL_OES_EGL_image_external : require
这些 shader 也必须使用 samplerExternalOES 采样方式来声明纹理,其
在 FragmentShader 中的代码如下:
static char* GPU_FRAME_FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require n"
"precision mediump float; n"
"uniform samplerExternalOES yuvTexSampler; n" "varying vec2 yuvTexCoords; n"
" n"
"void main() { n"
" gl_FragColor = texture2D(yuvTexSampler, yuvTexCoords); n"
"} n";
至此,这种扩展类型的纹理 ID 从创建到设置参数,再到真正的渲染
整个过程已经处理完毕,弄清楚了这种特殊纹理 ID 的使用方法之后,接
下来再看一下具体的旋转角度问题,因为在使用摄像头的时候很容易在
这个地方踩到坑,比如手机摄像头预览的时候会出现倒立、镜像等问
题,下面就来彻底地解决这类问题。
摄像头采集出来的视频都是横屏的,比如开发者为摄像头设置的预
览大小是 640×480 ,实际上摄像头采集出来的视频帧宽是 640 ,高是
480 ,并且图片也是横向采集的。正常来讲,用户使用手机时都是竖直
方向的,所以需要旋转 90 度或者 270 度用户才可以正确地看到自己的预
览效果。而具体旋转多大角度需要在当前这颗摄像头的 CameraInfo 中获
得,不同的手机甚至是不同的系统都会不一样。并且如果是前置摄像头
的话,还需要再做一个 VFlip (假设图像是横向采集出来的所以要做竖
直翻转,如果是已经旋转过了的就要做横向翻转)用于修复镜像的问
题,下面就用实际的图片来分别看一下前置摄像头和后置摄像头的具体
渲染流程。
首先我们来看一张摄像头要实际采集的物体,如图 6-4 所示。
图 6-4
在使用手机的摄像头去采集这个物体时,如果是前置摄像头,那么
采集得到的图片将如图 6-5 最左边的图片所示(摄像头的 CameraInfo 中取
出来的角度是 270 度),对此,应该按照摄像头的旋转角度将图片顺时
针旋转(注意这里一定是顺时针),旋转 270 度之后将得到如图 6-5 中间
的图片,最后再进行镜像处理,得到如图 6-5 最右边的图片,最终用户
在手机屏幕中看到的预览才是预期的图像。
图 6-5
如果是后置摄像头,那么一般情况下从摄像头的 CameraInfo 中取出
来的角度是 90 度,当然这一点会根据 ROM 厂商来决定,比如 LG 厂商的
Nexus 5X 设备取出来的角度就是 270 度,不论是多少度,摄像头采集出
来的图像在旋转过该角度之后肯定会是一个正常的图像,旋转流程如图
6-6 所示。
图 6-6
如果是 LG 厂商的 Nexus 5X 或者 HUAWEI 厂商的 Nexus 6P 这两款设
备系统升级之后,我们对图像后置摄像头的处理将如图 6-7 所示。
图 6-7
那么接下来就讲解一下图像的旋转和镜像。在 OpenGL ES 中这个问
题其实就是如何来确定物体的坐标和纹理坐标,尽管在第 4 章中已经讲
解过这部分内容,而在这里由于既要做旋转又要做镜像操作,所以需要 先回顾一下物体的坐标系,如图 6-8 所示。
通过如下数组来规定物体坐标:
GLfloat squareVertices[8] = {
-1.0, -1.0, // 物体左下角
1.0 , -1.0 , // 物体右下角
-1.0 , 1.0 , // 物体左上角
1.0 , 1.0 // 物体右上角
};
下面再回顾一下 OpenGL 的纹理坐标系,如图 6-9 所示。
图 6-8
图 6-9
然后给出不做任何旋转的纹理坐标:
GLfloat textureCoordNoRotation[8] = {
0.0 , 0.0 , // 图像的左下角
1.0 , 0.0 , // 图像的右下角
0.0 , 1.0 , // 图像的左上角
1.0 , 1.0 // 图像的右上角
};
再给出顺时针旋转 90 度的纹理坐标,大家可以想象一下,将图 6-9
顺时针旋转 90 度,然后再把对应的左下、右下、左上、右上的坐标点写
下来,如下所示:
GLfloat textureCoords[8] = {
1.0 , 0.0 , // 图像的右下角
1.0 , 1.0 , // 图像的右上角
0.0 , 0.0 , // 图像的左下角
0.0 , 1.0 // 图像的左上角
};
现在,再给出顺时针旋转 180 度的纹理坐标:
GLfloat textureCoords[8] = { 1.0 , 1.0 , // 图像的右上角
0.0 , 1.0 , // 图像的左上角
1.0 , 0.0 , // 图像的右下角
0.0 , 0.0 // 图像的左下角
};
之后给出顺时针旋转 270 度的纹理坐标:
GLfloat textureCoords[8] = {
0.0 , 1.0 , // 图像的左上角
0.0 , 0.0 , // 图像的左下角
1.0 , 1.0 , // 图像的右上角
1.0 , 0.0 // 图像的右下角
};
还记得第 4 章中讲过的计算机图像的坐标系与 OpenGL 的坐标系有什
么不同吗?它们的 y 轴坐标恰好是相反的,所以这里要对每一个纹理坐
标做一个 VFlip 的变换(即把每一个顶点的 y 值由 0 变为 1 或者由 1 变为
0 ),这样就可以得到一个正确的图像旋转了。而前置摄像头还存在镜
像的问题,因此需要对每一个纹理坐标做一个 HFlip 的变换(即把每一
个顶点的 x 值由 0 变为 1 或者由 1 变为 0 ),从而让图片在预览界面中看起
来就像在镜子中的一样。
上面的步骤其实就是将一个特殊格式( OES )的纹理 ID 经过处理和
旋转,使其变成正常格式( RGBA ),那么接下来就可以把该纹理 ID 渲
染到屏幕上去了,但是在这里还要再啰唆一句,因为该纹理 ID 的宽和高
其实就是摄像头捕捉过来的高和宽(因为我们做了一个 90 度或者 270 度
的旋转),目标是要将其渲染到 SurfaceView 上面去,但是如果 Java 层为
我们提供的 SurfaceView 的宽高和处理过后的该纹理 ID 的宽高不一致,
那么这一帧图像就会出现压缩或者拉伸的问题,所以在渲染到屏幕上的
时候需要进行一个自适配,让纹理按照屏幕比例自动填充。
首先来看一下前面的纹理坐标, x 从 0.0 到 1.0 就说明要把纹理的 x 轴
方向全都绘制到物体表面(整个 SurfaceView )上去,而如果我们只想
绘制一部分,比如中间的一半,那么就可以将 x 轴的坐标写成 0.25 到
0.75 ,相同的原则一样被应用到 y 轴上。那么这个 0.25 和 0.75 是如何出来
的呢?答案很简单,要想不被拉伸,那么 SurfaceView 的宽高比例和纹
理的宽高比例就应该是相同的。假设这一张纹理的宽为 texWidth ,纹理
的高为 texHeight 以及物体的宽为 screenWidth ,物体的高为
screenHeight ,且无论是宽还是高,都是 float 类型的,那么就可以利用下 面的公式来完成自动填充的坐标计算:
float textureAspectRatio = texHeight / texWidth;
float viewAspectRatio = screenHeight / screenWidth;
float xOffset = 0.0f;
float yOffset = 0.0f;
if(textureAspectRatio > viewAspectRatio){
// Update Y Offset
int expectedHeight = texHeight*screenWidth/texWidth+0.5f;
yOffset = (expectedHeight - screenHeight) / (2 * expectedHeight);
} else if(textureAspectRatio < viewAspectRatio){
// Update X Offset
int expectedWidth = texHeight * screenWidth / screenHeight + 0.5);
xOffset = (texWidth - expectedWidth)/(2*texWidth);
}
计算得到的 xOffset 与 yOffset 分别用于在纹理坐标中替换掉 0.0 的位
置,利用 1.0-xOffset 以及 1.0-yOffset 来替换掉 1.0 的位置,最终将得到一
个纹理坐标矩阵如下:
GLfloat textureCoordNoRotation[8] = {
xOffset , yOffset ,
1.0 - xOffset , yOffset ,
xOffset , 1.0 - yOffset ,
1.0 - yOffset , 1.0 - yOffset
};
至此,摄像头预览流程就可以随着摄像头所采集的各帧图像正常地
绘制下去了,从而实现整个预览的过程。
当用户切换摄像头的时候,可以向 Native 层发送一个指令, Native
层会在渲染线程中关闭当前摄像头,然后重新打开另外一个摄像头,并
配置参数,以及设置预览的 Surface-Texture ,最后调用开始预览方法,
这样就可以切换成功,用户看到的就是摄像头切换之后的预览画面了。
当我们最终关闭预览时,首先要停止整个渲染线程,然后释放掉所
建立的 Surface-Texture ,之后再将摄像头的 PreviewCallback 设置为 null ,
最终关闭并且释放摄像头。整个流程代码如下:
if (mCameraSurfaceTexture != null) {
mCameraSurfaceTexture.release();
mCameraSurfaceTexture = null;
}
if (null != mCamera) { mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
至此,摄像头预览部分已经全部讲解完毕,这是十分重要的,对于
后面搭建整个录制视频的项目以及后续视频直播的项目来说,这都是最
基础的部分,所以请读者好好熟悉这一部分。
本节的代码实例在代码仓库中的 CameraPreview 项目中,运行项目
之后进入摄像头预览界面,可以看到摄像头的预览,点击右上角的切换
摄像头按钮可以进行摄像头的切换操作

最后

以上就是端庄蜜蜂最近收集整理的关于android视频采集的全部内容,更多相关android视频采集内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(291)

评论列表共有 0 条评论

立即
投稿
返回
顶部