概述
视频画面的采集主要是使用各个平台提供的摄像头
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视频采集所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复