当前位置:   article > 正文

Android视频编辑器(一)通过OpenGL预览、录制视频以及断点续录等

Android视频编辑器(一)通过OpenGL预览、录制视频以及断点续录等

前言

如今的视频类app可谓是如日中天,火的不行。比如美拍、快手、VUE、火山小视频、抖音小视频等等。而这类视频的最基础和核心的功能就是视频录制和视频编辑功能。包括了手机视频录制、美白、加滤镜、加水印、给本地视频美白、加水印、加滤镜、视频裁剪、视频拼接和加bgm等等一系列音视频的核心操作。 而本系列的文章,就是作者在视频编辑器开发上的一些个人心得,希望能帮助到大家,另外因个人水平有限,难免有不足之处,还希望大家不惜赐教。
      本系列的文章,计划包括以下几部分:
      1、android视频编辑器之视频录制、断点续录、对焦等
      6、android视频编辑器之通过OpenGL做本地视频拼接
      7、android视频编辑器之音视频裁剪、增加背景音乐等
      通过这一系列的文章,大家就能自己开发出一个具有目前市面上完整功能的视频类app最核心功能的视频编辑器了(当然,如果作者能按计划全部写完的话。。。捂脸)。主要涉及到的核心知识点有Android音视频编解码、OpenGL开发、音视频的基础知识等等。整个过程会忽略掉一些基础知识,只会讲解一些比较核心的技术点。所有代码都会上传到github上面。有兴趣的童鞋,可以从文章末尾进行下载。
      文章中也借鉴和学习了很多其他小伙伴们分享的知识。 每篇文章我都会贴出不完全的相关连接,非常感谢小伙伴们的分享。
       Opengl入门详解
      

方案选择

     
      关于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感兴趣的童鞋,可以自行查阅相关资料。

GLSurfaceView的作用
       其实视频的预览大致流程就是,从Camera中拿到当前摄像头返回的数据,然后显示在屏幕上,我们这里是采用的GLSurfaceView类进行图像的显示。GLSurfaceView类有一个Renderer接口,这个Renderer其实就是GLSurfaceView中很重要的一个监听器,你可以把他看成是GLSurfaceView的生命周期的回调。有三个回调函数:
  1. @Override
  2. public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  3. }
  4. @Override
  5. public void onSurfaceChanged(GL10 gl, int width, int height) {
  6. }
  7. @Override
  8. public void onDrawFrame(GL10 gl) {
  9. }
      onSurfaceCreated:我们主要在这里面做一些初始化的工作
      onSurfaceChanged:就是当surface大小发生变化的时候,会回调,我们主要会在这里面做一些更改相关设置的工作
      onDrawFrame:这个就是返回当前帧的数据,我们对帧数据进行处理,主要就是在这里面进行的
       所以,我们的大致流程就是按照这三个回调方法来进行的:
      
CameraController类对camera进行控制
      Camera的使用过程,网上已经有很多资料了,这里就不在过多的介绍了。但是 有几个地方需要注意一下,首先就是你设置的视频尺寸摄像头并不一定支持,所以我们要选取摄像头支持的,跟我们预设的相同或者相近的尺寸,主要代码如下
  1. mCamera = Camera.open(cameraId);
  2. if (mCamera != null){
  3. /**选择当前设备允许的预览尺寸*/
  4. Camera.Parameters param = mCamera.getParameters();
  5. preSize = getPropPreviewSize(param.getSupportedPreviewSizes(), mConfig.rate,
  6. mConfig.minPreviewWidth);
  7. picSize = getPropPictureSize(param.getSupportedPictureSizes(),mConfig.rate,
  8. mConfig.minPictureWidth);
  9. param.setPictureSize(picSize.width, picSize.height);
  10. param.setPreviewSize(preSize.width,preSize.height);
  11. mCamera.setParameters(param);
  12. }
  13. private Camera.Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){
  14. Collections.sort(list, sizeComparator);
  15. int i = 0;
  16. for(Camera.Size s:list){
  17. if((s.height >= minWidth) && equalRate(s, th)){
  18. break;
  19. }
  20. i++;
  21. }
  22. if(i == list.size()){
  23. i = 0;
  24. }
  25. return list.get(i);
  26. }
  27. private Camera.Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){
  28. Collections.sort(list, sizeComparator);
  29. int i = 0;
  30. for(Camera.Size s:list){
  31. if((s.height >= minWidth) && equalRate(s, th)){
  32. break;
  33. }
  34. i++;
  35. }
  36. if(i == list.size()){
  37. i = 0;
  38. }
  39. return list.get(i);
  40. }
  41. private static boolean equalRate(Camera.Size s, float rate){
  42. float r = (float)(s.width)/(float)(s.height);
  43. if(Math.abs(r - rate) <= 0.03) {
  44. return true;
  45. }else{
  46. return false;
  47. }
  48. }
  49. private Comparator<Camera.Size> sizeComparator=new Comparator<Camera.Size>(){
  50. public int compare(Camera.Size lhs, Camera.Size rhs) {
  51. if(lhs.height == rhs.height){
  52. return 0;
  53. }else if(lhs.height > rhs.height){
  54. return 1;
  55. }else{
  56. return -1;
  57. }
  58. }
  59. };
     这个代码还是相当简单,这里就不过多介绍了,网上也有很多不同的但是类似功能的适配方法,大家可以多了解下,相互对照。
     第二个就是,摄像头取数据的坐标系和屏幕显示的坐标系不太相同,简单的说就是,不管是前置还是后置摄像头,我们都需要对摄像头取的数据进行一些坐标系旋转操作,才能正常的显示到屏幕上,不然的话就会出现画面扭曲的情况。因为我们是采用的OpengGL进行视频录制的,所以我们会有一系列的AFilter来进行shader的加载和画面的渲染工作,所以我们将摄像头数据的旋转也放到这个里面来做。这部分后面再说, CameraController类主要就是Camera的一个包装类,还会包括一些视频尺寸控制等代码,具体的请下载完整demo,进行查看。

AFilter的作用   
       我们在这个项目中,我们使用了AFilter来完成加载shader、绘制图像、清除数据等,主要代码包括如下:
加载asset中的shader
  1. public static int uLoadShader(int shaderType,String source){
  2. int shader= GLES20.glCreateShader(shaderType);
  3. if(0!=shader){
  4. GLES20.glShaderSource(shader,source);
  5. GLES20.glCompileShader(shader);
  6. int[] compiled=new int[1];
  7. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS,compiled,0);
  8. if(compiled[0]==0){
  9. glError(1,"Could not compile shader:"+shaderType);
  10. glError(1,"GLES20 Error:"+ GLES20.glGetShaderInfoLog(shader));
  11. GLES20.glDeleteShader(shader);
  12. shader=0;
  13. }
  14. }
  15. return shader;
  16. }

      Buffer的初始化
  1. /**
  2. * Buffer初始化
  3. */
  4. protected void initBuffer(){
  5. ByteBuffer a= ByteBuffer.allocateDirect(32);
  6. a.order(ByteOrder.nativeOrder());
  7. mVerBuffer=a.asFloatBuffer();
  8. mVerBuffer.put(pos);
  9. mVerBuffer.position(0);
  10. ByteBuffer b= ByteBuffer.allocateDirect(32);
  11. b.order(ByteOrder.nativeOrder());
  12. mTexBuffer=b.asFloatBuffer();
  13. mTexBuffer.put(coord);
  14. mTexBuffer.position(0);
  15. }

      绑定默认的纹理
  1. /**
  2. * 绑定默认纹理
  3. */
  4. protected void onBindTexture(){
  5. GLES20.glActiveTexture(GLES20.GL_TEXTURE0+textureType);
  6. GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());
  7. GLES20.glUniform1i(mHTexture,textureType);
  8. }

     每次绘制前需要清理画布
  1. /**
  2. * 清理画布
  3. */
  4. protected void onClear(){
  5. GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  6. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
  7. }

      这些代码在不同的filter中其实都是公用的,所以我们通过一个抽象类,来进行管理。
      上面我们说了摄像头的数据需要进行旋转,所以我们通过一个ShowAFilter来进行画面的旋转操作,核心代码如下,通过传入是前置还是后置摄像头的flag来进行画面旋转
  1. public void setFlag(int flag) {
  2. super.setFlag(flag);
  3. float[] coord;
  4. if(getFlag()==1){ //前置摄像头 顺时针旋转90,并上下颠倒
  5. coord=new float[]{
  6. 1.0f, 1.0f,
  7. 0.0f, 1.0f,
  8. 1.0f, 0.0f,
  9. 0.0f, 0.0f,
  10. };
  11. }else{ //后置摄像头 顺时针旋转90度
  12. coord=new float[]{
  13. 0.0f, 1.0f,
  14. 1.0f, 1.0f,
  15. 0.0f, 0.0f,
  16. 1.0f, 0.0f,
  17. };
  18. }
  19. mTexBuffer.clear();
  20. mTexBuffer.put(coord);
  21. mTexBuffer.position(0);
  22. }

      摄像头和AFilter我们都已经准备好了,下一步,就是我们需要把Camera取的数据显示在GLSurfaceView上面了,也就是需要将AFilter、 CameraController和GLSurfaceView联系起来。然后,因为我们后续会涉及到很多不同AFilter的管理,所以我们创建一个CameraDraw类,来管理AFilter。让其实现GLSurfaceView.Renderer接口,便于管理。

CameraDraw类
      首先实现GLSurfaceView.Renderer接口
    public class CameraDrawer implements GLSurfaceView.Renderer
       然后,在类的构造函数中,进行AFilter的初始化
  1. public CameraDrawer(Resources resources){
  2. //初始化一个滤镜 也可以叫控制器
  3. showFilter = new ShowFilter(resources);
  4. }
      在 onSurfaceCreated中,进行SurfaceTextured的创建,并且和AFilter进行绑定
  1. @Override
  2. public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
  3. textureID = createTextureID();
  4. mSurfaceTextrue = new SurfaceTexture(textureID);
  5. showFilter.create();
  6. showFilter.setTextureId(textureID);
  7. }
     在onSurfaceChanged中,进行一些参数的更改和纹理的重新绑定
  1. @Override
  2. public void onSurfaceChanged(GL10 gl10, int i, int i1) {
  3. width = i;
  4. height = i1;
  5. /**创建一个帧染缓冲区对象*/
  6. GLES20.glGenFramebuffers(1,fFrame,0);
  7. /**根据纹理数量 返回的纹理索引*/
  8. GLES20.glGenTextures(1, fTexture, 0);
  9. /**将生产的纹理名称和对应纹理进行绑定*/
  10. GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[0]);
  11. /**根据指定的参数 生产一个2D的纹理 调用该函数前 必须调用glBindTexture以指定要操作的纹理*/
  12. GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mPreviewWidth, mPreviewHeight,
  13. 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
  14. useTexParameter();
  15. GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
  16. }
      在 onDrawFrame中进行图像的绘制工作。
  1. @Override
  2. public void onDrawFrame(GL10 gl10) {
  3. /**更新界面中的数据*/
  4. mSurfaceTextrue.updateTexImage();
  5. /**绘制显示的filter*/
  6. GLES20.glViewport(0,0,width,height);
  7. showFilter.draw();
  8. }
     CameraDraw目前所做的主要工作就是这样,然后我们将CameraController、CameraDraw和自定义的CameraView控件进行绑定,就可以实现摄像头数据预览了。

自定义的CameraView控件
      首先,在构造函数中进行OpenGL、CameraController、CameraDraw 的初始化
  1. private void init() {
  2. /**初始化OpenGL的相关信息*/
  3. setEGLContextClientVersion(2);//设置版本
  4. setRenderer(this);//设置Renderer
  5. setRenderMode(RENDERMODE_WHEN_DIRTY);//主动调用渲染
  6. setPreserveEGLContextOnPause(true);//保存Context当pause时
  7. setCameraDistance(100);//相机距离
  8. /**初始化Camera的绘制类*/
  9. mCameraDrawer = new CameraDrawer(getResources());
  10. /**初始化相机的管理类*/
  11. mCamera = new CameraController();
  12. }
      然后,分别在三个生命周期的函数中调用CameraController和CameraDrawer的相关方法,以及打开摄像头
  1. @Override
  2. public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  3. mCameraDrawer.onSurfaceCreated(gl,config);
  4. if (!isSetParm){
  5. open(0);
  6. stickerInit();
  7. }
  8. mCameraDrawer.setPreviewSize(dataWidth,dataHeight);
  9. }
  10. @Override
  11. public void onSurfaceChanged(GL10 gl, int width, int height) {
  12. mCameraDrawer.onSurfaceChanged(gl,width,height);
  13. }
  14. @Override
  15. public void onDrawFrame(GL10 gl) {
  16. if (isSetParm){
  17. mCameraDrawer.onDrawFrame(gl);
  18. }
  19. }
     然后在onFrameAvailable函数中,调用即可
  1. @Override
  2. public void onFrameAvailable(SurfaceTexture surfaceTexture) {
  3. this.requestRender();
  4. }
 
     我们的视频预览的主体流程就是这样,然后我们可以直接在布局中使用CameraView类即可。
<Image_1>

视频录制和断点录制

       在上面部分,我们实现了通过OpenGL预览视频,下面部分,我们就需要实现录制视频了. 我们使用的opengl录制视频方案,采用的是谷歌的工程师编写的grafika 这个项目,项目链接 在文章开头部分。这个项目包含了很多GLSurfaceView和视频编解码的知识,还是非常值得学习一下的。主要核心的类就是TextureMovieEncoder和VideoEncoderCore类。 采用的是MediaMuxer和MeidaCodec类进行视频的编码和音视频合成。后面我们涉及到音视频编解码、视频拼接、音视频裁剪的时候会详细的介绍一下android里面音视频编解码的相关类的用法。这里就暂时先不深入讲解了。当然音视频的编解码也可以使用FFmpeg进行软编码,但是因为硬编码的速度比软编码要快得多,所以我们这个项目,不会涉及到FFmpeg的使用。
        好了,现在回到我们的从GLSurfaceView读取数据,通过TexureMovieEncoder进行视频的录制和断点续录。
         上面我们说了我们通过AFilter的相关类,进行opengl的相关操作,实现了视频的预览,这里我们还需要一个AFilter类将摄像头的数据交给我们的编码类进行编码。所以初始化的时候 再初始化一个
     drawFilter = new ShowFilter(resources);
        这里需要注意一下,为了显示在屏幕上是正常的,我们进行了旋转的操作。所以,我们在录制的AFilter里面需要加上矩阵翻转的控制。
  1. OM= MatrixUtils.getOriginalMatrix();
  2. MatrixUtils.flip(OM,false,true);//矩阵上下翻转
  3. drawFilter.setMatrix(OM);
     然后同样分别进行drawFilter的create,在onDrawFrame里面讲textureId进行绑定以及绘制。还有就是添加录制控制的相关代码
  1. GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
  2. GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
  3. GLES20.GL_TEXTURE_2D, fTexture[0], 0);
  4. GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
  5. drawFilter.setTextureId(fTexture[0]);
  6. drawFilter.draw();
  7. //解绑
  8. GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);
  9. if (recordingEnabled){
  10. /**说明是录制状态*/
  11. switch (recordingStatus){
  12. case RECORDING_OFF:
  13. videoEncoder = new TextureMovieEncoder();
  14. videoEncoder.setPreviewSize(mPreviewWidth,mPreviewHeight);
  15. videoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
  16. savePath, mPreviewWidth, mPreviewHeight,
  17. 3500000, EGL14.eglGetCurrentContext(),
  18. null));
  19. recordingStatus = RECORDING_ON;
  20. break;
  21. case RECORDING_ON:
  22. case RECORDING_PAUSED:
  23. break;
  24. case RECORDING_RESUMED:
  25. videoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
  26. videoEncoder.resumeRecording();
  27. recordingStatus = RECORDING_ON;
  28. break;
  29. case RECORDING_RESUME:
  30. videoEncoder.resumeRecording();
  31. recordingStatus=RECORDING_ON;
  32. break;
  33. case RECORDING_PAUSE:
  34. videoEncoder.pauseRecording();
  35. recordingStatus=RECORDING_PAUSED;
  36. break;
  37. default:
  38. throw new RuntimeException("unknown recording status "+recordingStatus);
  39. }
  40. }else {
  41. switch (recordingStatus) {
  42. case RECORDING_ON:
  43. case RECORDING_RESUMED:
  44. case RECORDING_PAUSE:
  45. case RECORDING_RESUME:
  46. case RECORDING_PAUSED:
  47. videoEncoder.stopRecording();
  48. recordingStatus = RECORDING_OFF;
  49. break;
  50. case RECORDING_OFF:
  51. break;
  52. default:
  53. throw new RuntimeException("unknown recording status " + recordingStatus);
  54. }
  55. }
  56. if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON){
  57. videoEncoder.setTextureId(fTexture[0]);
  58. videoEncoder.frameAvailable(mSurfaceTextrue);
  59. }
      上面主要逻辑是,在数据返回的视频判断当前的录制状态,如果是正在录制,就将SurfaceTexture给到VideoEncoder进行数据的编码,如果没有录制,就跳过该帧,这样就可以实现断点续录,即录制 —>暂停录制—>继续录制,而且这样录制出来的是一个整体的视频文件。
       这里就不贴出 TexureMovieEncoder和VideoEncoderCore类的详细代码了。
       这样我们就完成了,摄像头数据的预览和视频的录制功能。然后呢,还有一些额外的功能。

Camera的手动对焦

     不管是视频录制还是拍照的时候,对焦是非常重要的,如果没有对焦功能,那录制出来的视频的效果会非常的差,但是网上的很多讲解android的摄像头对焦功能的文章,其实并不准确,他们实现的功能其实有一些小问题的,主要是涉及到摄像头坐标系和屏幕显示坐标系的变化。手动聚焦,主要是点击屏幕 然后就调用Camera聚焦的相关函数 进行对焦。
     聚焦的主要代码如下,在CameraConroller类里面 
  1. Camera.Parameters parameters = mCamera.getParameters();
  2. boolean supportFocus=true;
  3. boolean supportMetering=true;
  4. //不支持设置自定义聚焦,则使用自动聚焦,返回
  5. if (parameters.getMaxNumFocusAreas() <= 0) {
  6. supportFocus=false;
  7. }
  8. if (parameters.getMaxNumMeteringAreas() <= 0){
  9. supportMetering=false;
  10. }
  11. List<Camera.Area> areas = new ArrayList<Camera.Area>();
  12. List<Camera.Area> areas1 = new ArrayList<Camera.Area>();
  13. //再次进行转换
  14. point.x= (int) (((float)point.x)/ MyApplication.screenWidth*2000-1000);
  15. point.y= (int) (((float)point.y)/MyApplication.screenHeight*2000-1000);
  16. int left = point.x - 300;
  17. int top = point.y - 300;
  18. int right = point.x + 300;
  19. int bottom = point.y + 300;
  20. left = left < -1000 ? -1000 : left;
  21. top = top < -1000 ? -1000 : top;
  22. right = right > 1000 ? 1000 : right;
  23. bottom = bottom > 1000 ? 1000 : bottom;
  24. areas.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
  25. areas1.add(new Camera.Area(new Rect(left, top, right, bottom), 100));
  26. if(supportFocus){
  27. parameters.setFocusAreas(areas);
  28. }
  29. if(supportMetering){
  30. parameters.setMeteringAreas(areas1);
  31. }
  32. try {
  33. mCamera.setParameters(parameters);// 部分手机 会出Exception(红米)
  34. mCamera.autoFocus(callback);
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. }
       主要涉及到了一下坐标变换,因为大部分的手机的前置摄像头不支持对焦功能,所以我们不进行前置摄像头的对焦。
      

结语

      到这里的话,本篇文章的主要内容就已经结束了,再次回顾一下,我们其实本篇文章主要涉及到的内容有通过OpenGl预览视频,通过MediaCodec录制视频,以及一些其他的知识点。这里并没有讲解OpenGL的一些基础知识,比如顶点着色器等等,这部分如果要涉及到的话,也是一个很庞大的内容,所以就不会在系列文章中进行介绍了。大家不太清楚的话,请自行查询相关资料。
     本篇仅仅是一个开始,下一篇文章,我们就会在录制视频的时候通过opengl加上水印和美白效果。请大家持续关注。
     因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!

其他
      项目的github地址



声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/575298
推荐阅读
相关标签
  

闽ICP备14008679号