Android视频编辑器(一)通过OpenGL预览、录制视频以及断点续录等
前言如今的视频类app可谓是如日中天,火的不行。比如美拍、快手、VUE、火山小视频、抖音小视频等等。而这类视频的最基础和核心的功能就是视频录制和视频编辑功能。包括了手机视频录制、美白、加滤镜、加水印、给本地视频美白、加水印、加滤镜、视频裁剪、视频拼接和加bgm等等一系列音视频的核心操作。而本系列的文章,就是作者在视频编辑器开发上的一些个人心得,希望能帮助到大家,另外因个人水平有限,难免有不足之
·
前言
如今的视频类app可谓是如日中天,火的不行。比如美拍、快手、VUE、火山小视频、抖音小视频等等。而这类视频的最基础和核心的功能就是视频录制和视频编辑功能。包括了手机视频录制、美白、加滤镜、加水印、给本地视频美白、加水印、加滤镜、视频裁剪、视频拼接和加bgm等等一系列音视频的核心操作。
而本系列的文章,就是作者在视频编辑器开发上的一些个人心得,希望能帮助到大家,另外因个人水平有限,难免有不足之处,还希望大家不惜赐教。
GLSurfaceView的作用
第二个就是,摄像头取数据的坐标系和屏幕显示的坐标系不太相同,简单的说就是,不管是前置还是后置摄像头,我们都需要对摄像头取的数据进行一些坐标系旋转操作,才能正常的显示到屏幕上,不然的话就会出现画面扭曲的情况。因为我们是采用的OpengGL进行视频录制的,所以我们会有一系列的AFilter来进行shader的加载和画面的渲染工作,所以我们将摄像头数据的旋转也放到这个里面来做。这部分后面再说, CameraController类主要就是Camera的一个包装类,还会包括一些视频尺寸控制等代码,具体的请下载完整demo,进行查看。
Buffer的初始化
绑定默认的纹理
每次绘制前需要清理画布
这些代码在不同的filter中其实都是公用的,所以我们通过一个抽象类,来进行管理。
摄像头和AFilter我们都已经准备好了,下一步,就是我们需要把Camera取的数据显示在GLSurfaceView上面了,也就是需要将AFilter、 CameraController和GLSurfaceView联系起来。然后,因为我们后续会涉及到很多不同AFilter的管理,所以我们创建一个CameraDraw类,来管理AFilter。让其实现GLSurfaceView.Renderer接口,便于管理。
我们的视频预览的主体流程就是这样,然后我们可以直接在布局中使用CameraView类即可。
这里就不贴出 TexureMovieEncoder和VideoEncoderCore类的详细代码了。
本系列的文章,计划包括以下几部分:
1、android视频编辑器之视频录制、断点续录、对焦等
6、android视频编辑器之通过OpenGL做本地视频拼接
7、android视频编辑器之音视频裁剪、增加背景音乐等
通过这一系列的文章,大家就能自己开发出一个具有目前市面上完整功能的视频类app最核心功能的视频编辑器了(当然,如果作者能按计划全部写完的话。。。捂脸)。主要涉及到的核心知识点有Android音视频编解码、OpenGL开发、音视频的基础知识等等。整个过程会忽略掉一些基础知识,只会讲解一些比较核心的技术点。所有代码都会上传到github上面。有兴趣的童鞋,可以从文章末尾进行下载。
文章中也借鉴和学习了很多其他小伙伴们分享的知识。 每篇文章我都会贴出不完全的相关连接,非常感谢小伙伴们的分享。
方案选择
关于android平台的视频录制,首先我们要确定我们的需求,录制音视频本地保存为mp4文件。实现音视频录制的方案有很多,比如原生的sdk,通过Camera进行数据采集,注意:是Android.hardware包下的Camera而不是Android.graphics包下的,后者是用于矩阵变换的一个类,而前者才是通过硬件采集摄像头的数据,并且返回到java层。然后使用SurfaceView进行画面预览,使用MediaCodec对数据进行编解码,最后通过MediaMuxer将音视频混合打包成为一个mp4文件。当然也可以直接使用MediaRecorder类进行录制,该类是封装好了的视频录制类,但是不利于功能扩展,比如如果我们想在录制的视频上加上我们自己的logo,也就是常说的加水印,或者是录制一会儿 然后暂停 然后继续录制的功能,也就是断点续录的话 就不是那么容易实现了。而本篇文章,作为后面系列的基础,我们就不讲解常规的视频录制的方案了,有兴趣的可以查看本文前面附上的一些链接。因为我们后期会涉及到给视频加滤镜、加水印、加美颜等功能,所以就不能使用常规的视频录制方案了,而是采用Camera + OpengGL + MediaCodec +进MediaMuxer行视频录制。
视频预览
为了实现录制的效果,首先我们得实现摄像头数据预览的功能。
Camera的使用
android在5.0的版本加载了hardware.camera2包对android平台的视频录制功能进行了增强,但是为了兼容低版本,我们将使用的是Camera类,而不是5.0之后加入的新类。对新api感兴趣的童鞋,可以自行查阅相关资料。
其实视频的预览大致流程就是,从Camera中拿到当前摄像头返回的数据,然后显示在屏幕上,我们这里是采用的GLSurfaceView类进行图像的显示。GLSurfaceView类有一个Renderer接口,这个Renderer其实就是GLSurfaceView中很重要的一个监听器,你可以把他看成是GLSurfaceView的生命周期的回调。有三个回调函数:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
onSurfaceCreated:我们主要在这里面做一些初始化的工作
onSurfaceChanged:就是当surface大小发生变化的时候,会回调,我们主要会在这里面做一些更改相关设置的工作
onDrawFrame:这个就是返回当前帧的数据,我们对帧数据进行处理,主要就是在这里面进行的
所以,我们的大致流程就是按照这三个回调方法来进行的:
CameraController类对camera进行控制
Camera的使用过程,网上已经有很多资料了,这里就不在过多的介绍了。但是 有几个地方需要注意一下,首先就是你设置的视频尺寸摄像头并不一定支持,所以我们要选取摄像头支持的,跟我们预设的相同或者相近的尺寸,主要代码如下
mCamera = Camera.open(cameraId);
if (mCamera != null){
/**选择当前设备允许的预览尺寸*/
Camera.Parameters param = mCamera.getParameters();
preSize = getPropPreviewSize(param.getSupportedPreviewSizes(), mConfig.rate,
mConfig.minPreviewWidth);
picSize = getPropPictureSize(param.getSupportedPictureSizes(),mConfig.rate,
mConfig.minPictureWidth);
param.setPictureSize(picSize.width, picSize.height);
param.setPreviewSize(preSize.width,preSize.height);
mCamera.setParameters(param);
}
private Camera.Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){
Collections.sort(list, sizeComparator);
int i = 0;
for(Camera.Size s:list){
if((s.height >= minWidth) && equalRate(s, th)){
break;
}
i++;
}
if(i == list.size()){
i = 0;
}
return list.get(i);
}
private Camera.Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){
Collections.sort(list, sizeComparator);
int i = 0;
for(Camera.Size s:list){
if((s.height >= minWidth) && equalRate(s, th)){
break;
}
i++;
}
if(i == list.size()){
i = 0;
}
return list.get(i);
}
private static boolean equalRate(Camera.Size s, float rate){
float r = (float)(s.width)/(float)(s.height);
if(Math.abs(r - rate) <= 0.03) {
return true;
}else{
return false;
}
}
private Comparator<Camera.Size> sizeComparator=new Comparator<Camera.Size>(){
public int compare(Camera.Size lhs, Camera.Size rhs) {
if(lhs.height == rhs.height){
return 0;
}else if(lhs.height > rhs.height){
return 1;
}else{
return -1;
}
}
};
这个代码还是相当简单,这里就不过多介绍了,网上也有很多不同的但是类似功能的适配方法,大家可以多了解下,相互对照。
第二个就是,摄像头取数据的坐标系和屏幕显示的坐标系不太相同,简单的说就是,不管是前置还是后置摄像头,我们都需要对摄像头取的数据进行一些坐标系旋转操作,才能正常的显示到屏幕上,不然的话就会出现画面扭曲的情况。因为我们是采用的OpengGL进行视频录制的,所以我们会有一系列的AFilter来进行shader的加载和画面的渲染工作,所以我们将摄像头数据的旋转也放到这个里面来做。这部分后面再说, CameraController类主要就是Camera的一个包装类,还会包括一些视频尺寸控制等代码,具体的请下载完整demo,进行查看。
AFilter的作用
我们在这个项目中,我们使用了AFilter来完成加载shader、绘制图像、清除数据等,主要代码包括如下:
加载asset中的shader
public static int uLoadShader(int shaderType,String source){
int shader= GLES20.glCreateShader(shaderType);
if(0!=shader){
GLES20.glShaderSource(shader,source);
GLES20.glCompileShader(shader);
int[] compiled=new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS,compiled,0);
if(compiled[0]==0){
glError(1,"Could not compile shader:"+shaderType);
glError(1,"GLES20 Error:"+ GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader=0;
}
}
return shader;
}
Buffer的初始化
/**
* Buffer初始化
*/
protected void initBuffer(){
ByteBuffer a= ByteBuffer.allocateDirect(32);
a.order(ByteOrder.nativeOrder());
mVerBuffer=a.asFloatBuffer();
mVerBuffer.put(pos);
mVerBuffer.position(0);
ByteBuffer b= ByteBuffer.allocateDirect(32);
b.order(ByteOrder.nativeOrder());
mTexBuffer=b.asFloatBuffer();
mTexBuffer.put(coord);
mTexBuffer.position(0);
}
绑定默认的纹理
/**
* 绑定默认纹理
*/
protected void onBindTexture(){
GLES20.glActiveTexture(GLES20.GL_TEXTURE0+textureType);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());
GLES20.glUniform1i(mHTexture,textureType);
}
每次绘制前需要清理画布
/**
* 清理画布
*/
protected void onClear(){
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
}
这些代码在不同的filter中其实都是公用的,所以我们通过一个抽象类,来进行管理。
上面我们说了摄像头的数据需要进行旋转,所以我们通过一个ShowAFilter来进行画面的旋转操作,核心代码如下,通过传入是前置还是后置摄像头的flag来进行画面旋转
public void setFlag(int flag) {
super.setFlag(flag);
float[] coord;
if(getFlag()==1){ //前置摄像头 顺时针旋转90,并上下颠倒
coord=new float[]{
1.0f, 1.0f,
0.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
};
}else{ //后置摄像头 顺时针旋转90度
coord=new float[]{
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
}
mTexBuffer.clear();
mTexBuffer.put(coord);
mTexBuffer.position(0);
}
摄像头和AFilter我们都已经准备好了,下一步,就是我们需要把Camera取的数据显示在GLSurfaceView上面了,也就是需要将AFilter、 CameraController和GLSurfaceView联系起来。然后,因为我们后续会涉及到很多不同AFilter的管理,所以我们创建一个CameraDraw类,来管理AFilter。让其实现GLSurfaceView.Renderer接口,便于管理。
CameraDraw类
首先实现GLSurfaceView.Renderer接口
public class CameraDrawer implements GLSurfaceView.Renderer
然后,在类的构造函数中,进行AFilter的初始化
public CameraDrawer(Resources resources){
//初始化一个滤镜 也可以叫控制器
showFilter = new ShowFilter(resources);
}
在
onSurfaceCreated中,进行SurfaceTextured的创建,并且和AFilter进行绑定
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
textureID = createTextureID();
mSurfaceTextrue = new SurfaceTexture(textureID);
showFilter.create();
showFilter.setTextureId(textureID);
}
在onSurfaceChanged中,进行一些参数的更改和纹理的重新绑定
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
width = i;
height = i1;
/**创建一个帧染缓冲区对象*/
GLES20.glGenFramebuffers(1,fFrame,0);
/**根据纹理数量 返回的纹理索引*/
GLES20.glGenTextures(1, fTexture, 0);
/**将生产的纹理名称和对应纹理进行绑定*/
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[0]);
/**根据指定的参数 生产一个2D的纹理 调用该函数前 必须调用glBindTexture以指定要操作的纹理*/
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mPreviewWidth, mPreviewHeight,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
useTexParameter();
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
}
在
onDrawFrame中进行图像的绘制工作。
@Override
public void onDrawFrame(GL10 gl10) {
/**更新界面中的数据*/
mSurfaceTextrue.updateTexImage();
/**绘制显示的filter*/
GLES20.glViewport(0,0,width,height);
showFilter.draw();
}
CameraDraw目前所做的主要工作就是这样,然后我们将CameraController、CameraDraw和自定义的CameraView控件进行绑定,就可以实现摄像头数据预览了。
自定义的CameraView控件
首先,在构造函数中进行OpenGL、CameraController、CameraDraw
的初始化
private void init() {
/**初始化OpenGL的相关信息*/
setEGLContextClientVersion(2);//设置版本
setRenderer(this);//设置Renderer
setRenderMode(RENDERMODE_WHEN_DIRTY);//主动调用渲染
setPreserveEGLContextOnPause(true);//保存Context当pause时
setCameraDistance(100);//相机距离
/**初始化Camera的绘制类*/
mCameraDrawer = new CameraDrawer(getResources());
/**初始化相机的管理类*/
mCamera = new CameraController();
}
然后,分别在三个生命周期的函数中调用CameraController和CameraDrawer的相关方法,以及打开摄像头
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mCameraDrawer.onSurfaceCreated(gl,config);
if (!isSetParm){
open(0);
stickerInit();
}
mCameraDrawer.setPreviewSize(dataWidth,dataHeight);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
mCameraDrawer.onSurfaceChanged(gl,width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
if (isSetParm){
mCameraDrawer.onDrawFrame(gl);
}
}
然后在onFrameAvailable函数中,调用即可
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
this.requestRender();
}
我们的视频预览的主体流程就是这样,然后我们可以直接在布局中使用CameraView类即可。
视频录制和断点录制
在上面部分,我们实现了通过OpenGL预览视频,下面部分,我们就需要实现录制视频了.
我们使用的opengl录制视频方案,采用的是谷歌的工程师编写的grafika 这个项目,项目链接 在文章开头部分。这个项目包含了很多GLSurfaceView和视频编解码的知识,还是非常值得学习一下的。主要核心的类就是TextureMovieEncoder和VideoEncoderCore类。
采用的是MediaMuxer和MeidaCodec类进行视频的编码和音视频合成。后面我们涉及到音视频编解码、视频拼接、音视频裁剪的时候会详细的介绍一下android里面音视频编解码的相关类的用法。这里就暂时先不深入讲解了。当然音视频的编解码也可以使用FFmpeg进行软编码,但是因为硬编码的速度比软编码要快得多,所以我们这个项目,不会涉及到FFmpeg的使用。
好了,现在回到我们的从GLSurfaceView读取数据,通过TexureMovieEncoder进行视频的录制和断点续录。
上面我们说了我们通过AFilter的相关类,进行opengl的相关操作,实现了视频的预览,这里我们还需要一个AFilter类将摄像头的数据交给我们的编码类进行编码。所以初始化的时候 再初始化一个
drawFilter = new ShowFilter(resources);
这里需要注意一下,为了显示在屏幕上是正常的,我们进行了旋转的操作。所以,我们在录制的AFilter里面需要加上矩阵翻转的控制。
OM= MatrixUtils.getOriginalMatrix();
MatrixUtils.flip(OM,false,true);//矩阵上下翻转
drawFilter.setMatrix(OM);
然后同样分别进行drawFilter的create,在onDrawFrame里面讲textureId进行绑定以及绘制。还有就是添加录制控制的相关代码
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[0], 0);
GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
drawFilter.setTextureId(fTexture[0]);
drawFilter.draw();
//解绑
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
if (recordingEnabled){
/**说明是录制状态*/
switch (recordingStatus){
case RECORDING_OFF:
videoEncoder = new TextureMovieEncoder();
videoEncoder.setPreviewSize(mPreviewWidth,mPreviewHeight);
videoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
savePath, mPreviewWidth, mPreviewHeight,
3500000, EGL14.eglGetCurrentContext(),
null));
recordingStatus = RECORDING_ON;
break;
case RECORDING_ON:
case RECORDING_PAUSED:
break;
case RECORDING_RESUMED:
videoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
videoEncoder.resumeRecording();
recordingStatus = RECORDING_ON;
break;
case RECORDING_RESUME:
videoEncoder.resumeRecording();
recordingStatus=RECORDING_ON;
break;
case RECORDING_PAUSE:
videoEncoder.pauseRecording();
recordingStatus=RECORDING_PAUSED;
break;
default:
throw new RuntimeException("unknown recording status "+recordingStatus);
}
}else {
switch (recordingStatus) {
case RECORDING_ON:
case RECORDING_RESUMED:
case RECORDING_PAUSE:
case RECORDING_RESUME:
case RECORDING_PAUSED:
videoEncoder.stopRecording();
recordingStatus = RECORDING_OFF;
break;
case RECORDING_OFF:
break;
default:
throw new RuntimeException("unknown recording status " + recordingStatus);
}
}
if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON){
videoEncoder.setTextureId(fTexture[0]);
videoEncoder.frameAvailable(mSurfaceTextrue);
}
上面主要逻辑是,在数据返回的视频判断当前的录制状态,如果是正在录制,就将SurfaceTexture给到VideoEncoder进行数据的编码,如果没有录制,就跳过该帧,这样就可以实现断点续录,即录制 —>暂停录制—>继续录制,而且这样录制出来的是一个整体的视频文件。
这里就不贴出 TexureMovieEncoder和VideoEncoderCore类的详细代码了。
这样我们就完成了,摄像头数据的预览和视频的录制功能。然后呢,还有一些额外的功能。
Camera的手动对焦
不管是视频录制还是拍照的时候,对焦是非常重要的,如果没有对焦功能,那录制出来的视频的效果会非常的差,但是网上的很多讲解android的摄像头对焦功能的文章,其实并不准确,他们实现的功能其实有一些小问题的,主要是涉及到摄像头坐标系和屏幕显示坐标系的变化。手动聚焦,主要是点击屏幕 然后就调用Camera聚焦的相关函数 进行对焦。
聚焦的主要代码如下,在CameraConroller类里面
Camera.Parameters parameters = mCamera.getParameters();
boolean supportFocus=true;
boolean supportMetering=true;
//不支持设置自定义聚焦,则使用自动聚焦,返回
if (parameters.getMaxNumFocusAreas() <= 0) {
supportFocus=false;
}
if (parameters.getMaxNumMeteringAreas() <= 0){
supportMetering=false;
}
List<Camera.Area> areas = new ArrayList<Camera.Area>();
List<Camera.Area> areas1 = new ArrayList<Camera.Area>();
//再次进行转换
point.x= (int) (((float)point.x)/ MyApplication.screenWidth*2000-1000);
point.y= (int) (((float)point.y)/MyApplication.screenHeight*2000-1000);
int left = point.x - 300;
int top = point.y - 300;
int right = point.x + 300;
int bottom = point.y + 300;
left = left < -1000 ? -1000 : left;
top = top < -1000 ? -1000 : top;
right = right > 1000 ? 1000 : right;
bottom = bottom > 1000 ? 1000 : bottom;
areas.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
areas1.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
if(supportFocus){
parameters.setFocusAreas(areas);
}
if(supportMetering){
parameters.setMeteringAreas(areas1);
}
try {
mCamera.setParameters(parameters);// 部分手机 会出Exception(红米)
mCamera.autoFocus(callback);
} catch (Exception e) {
e.printStackTrace();
}
主要涉及到了一下坐标变换,因为大部分的手机的前置摄像头不支持对焦功能,所以我们不进行前置摄像头的对焦。
结语
到这里的话,本篇文章的主要内容就已经结束了,再次回顾一下,我们其实本篇文章主要涉及到的内容有通过OpenGl预览视频,通过MediaCodec录制视频,以及一些其他的知识点。这里并没有讲解OpenGL的一些基础知识,比如顶点着色器等等,这部分如果要涉及到的话,也是一个很庞大的内容,所以就不会在系列文章中进行介绍了。大家不太清楚的话,请自行查询相关资料。
本篇仅仅是一个开始,下一篇文章,我们就会在录制视频的时候通过opengl加上水印和美白效果。请大家持续关注。
因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!
其他
项目的github地址
更多推荐
已为社区贡献1条内容
所有评论(0)