Camera + opengles录制视频,录制快速,慢速视频(三)

视频录制可以使用android提供的api,如MediaRecorder,对视频的编码也有MediaCodec这样的api可以使用。

MediaCodec的使用,用到两个缓存队列,一个输入缓存队列,一个是输出缓存队列,只需要使用queueInputBuffer把要编码的数据byte数组提交到输入队列,就可以使用enqueueOutputbuffer从输出队列取出编码完成的数据。这个用法有一个前提,就是要拿到待编码的byte数据。

但是,在使用opengl es将摄像头数据绘制到屏幕时,数据的处理是在着色器中处理的。没有办法从着色器中直接拿到byte数组,那怎么完成视频的录制呢?

MediaCodec中有一个方法:public native final Surface createInputSurface();

这个方法会请求一个surface作为待编码数据的输入,这个Surface必须是被硬件加速层渲染的,比如Opengl es。也就是说,如果我们往Surface中绘制数据,那么MediaCodec就可以从Surface中拿数据来完成编码。

那么opengl 怎么往Surface中绘制数据呢?

在Android中Surface就是一块画布,在WindowManagerService中对应了一个窗口WindowState,在SurfaceFlinger中对应了一个Layer,在Surface内部有一块存储数据的内存空间。这些都是Android中的概念,但是Opengl它不认识Surface是什么。

我们接下来要解决的问题就是让opengl认识Surface。

怎么认识呢?Surface除了是一块画布,还有一个概念,就是它是面向应用程序的本地窗口ANativeWindow,openggl要完成绘制,一定是要和本地窗口建立关联的,也即是opengl环境的搭建,完成这个工作的是egl。

EGL是图形渲染api和本地窗口系统之间的一层接口,提供如下功能:

1,创建rendering surface,让应用程序可以在上面作图。

2,创建graphics context,因为opengl是一个状态机,是一个pipeline,所以它需要状态管理,这就是context的工作。

3,同步应用程序和本地平台渲染api。

4,提供对显示设备的访问。

参考GLSurfaceView的源码,在GLSurfaceView的GLThread运行起来后,会借助EglHelper来初始了EGL环境,这个过程中创建了EGLSurface,实现了opengl绘制数据到EGLSurface中,因为EGLSurface和Surface做了绑定,最终实现数据绘制到了Surface中。

EGLSurface是可以跟Surface产生关联的,这就可以实现使用opengl绘制的数据,通过EGLSurface,传递到Surface,进一步传给MediaCodec,完成编码。

最终实现的结构图是这样的:

首先,摄像头的数据到了GLSurfaceView中,GLSurfaceView搭建了EGL环境,在EGL环境中创建了EGLSurface,并与GLSurfaceView中的Surface做了绑定,所以opengl绘制数据到EGLSurface,实际就绘制到了Surface中。

然后,MediaCodec中也有一个surface,我们去搭建一个EGL环境,在我们搭建的EGL环境中创建一个EGLSurface与MediaCodec中的surface绑定起来,让opengl把数据也绘制一份到这个EGL环境中的EGLSurface中,这样Mediacodec就可以拿到要编码的数据了。

下面就看代码实现:

视频录制需要动态开启Camera,Storage的权限。

MediaCodec的使用,配置编码器,创建编码器,得到编码器的inputSurface,开启编码。

然后把编码后的数据通过MediaMuxer封装到一个容器中。

在编码器启动后,就要开始不断的获取输入数据了,所以在启动编码器后,会创建EGL环境,把MediaCodec中的surface绑定到EGL的eglSurface上。

   public void start(float speed) throws IOException {
        mSpeed = speed;
        MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,mWidth, mHeight);
        //颜色空间,从surface当中获取
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //码率
        format.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
        //帧率
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
        //关键帧间隔
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
        //创建编码器
        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        //配置编码器
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //这个surface显示的内容就是要编码的画面数据。
        mSurface = mMediaCodec.createInputSurface();

        //混合器(复用器),将编码的h264封装为mp4,
        mMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        //开始编码
        mMediaCodec.start();


        //创建opengl的环境,
        HandlerThread gl_codec = new HandlerThread("gl_codec");
        gl_codec.start();
        mHandler = new Handler(gl_codec.getLooper());
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                eglEnv = new EGLEnv(mContext, mGlContext, mSurface, mWidth, mHeight);
                isStart = true;
            }
        });
    }

然后看下EGL环境搭建的代码,在单独的类EGLEnv.java中完成:

EGL环境的搭建流程:

1,获得显示窗口,作为opengl的绘制目标,

2,初始化显示窗口

3,配置属性选项,

4,创建EGL上下文,

5,创建EGL surface,

6,选定当前的上下文,绑定当前线程的显示设备,

  public EGLEnv(Context context, EGLContext mGlContext, Surface surface, int width, int height) {
        //获得显示窗口,作为opengl的绘制目标
        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay,failed.");
        }

        //初始化显示窗口
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
            throw new RuntimeException("eglInitialize,failed.");
        }

        //配置属性选项,
        int[] configAttribs = {
            EGL14.EGL_RED_SIZE, 8, //颜色缓冲区中红色位数,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,// opengl es 2.0
                EGL14.EGL_NONE
        };
        int[] numConfigs = new int[1];
        EGLConfig[] eglConfig = new EGLConfig[1];
        if (!EGL14.eglChooseConfig(mEglDisplay, configAttribs, 0,
                eglConfig, 0, eglConfig.length,
                numConfigs, 0)) {
            throw new RuntimeException("eglChooseConfig,failed."+EGL14.eglGetError());
        }
        mEglConfig = eglConfig[0];

        //创建EGL上下文,
        int[] contex_attrib_list = {
          EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        //与GLSurfaceView中的EGLContext共享数据,只有这样才能拿到处理完之后显示的图像纹理,
        mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, contex_attrib_list,0);
        if (mEglContext == EGL14.EGL_NO_CONTEXT) {
            throw new RuntimeException("eglCreateContext,failed."+EGL14.eglGetError());
        }

        //创建EGL surface
        int[] surface_attrib_list = {
                EGL14.EGL_NONE
        };
        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surface_attrib_list, 0);
        if (mEglSurface == null) {
            throw new RuntimeException("eglCreateWindowSurface,failed."+EGL14.eglGetError());
        }

        //选定当前的上下文,绑定当前线程的显示设备,一个进程中可能创建多个context,
        // 必须选择其中一个作为当前的处理对象。这里选择的是跟GLSurfaceView共享context,
        if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface, mEglSurface, mEglContext)) {
            throw new RuntimeException("eglMakeCurrent,failed."+EGL14.eglGetError());
        }

        recordFilter = new RecordFilter(context);

    }

这里面那么多概念是什么样的关系呢?在理下,EGLContext会管理绘制过程,EGLDisplay是一个跟具体系统无关的显示设备,其中包含着一个EGLSurface,在EGLSurface创建时,关联了一个本地窗口Surface,这个Surface决定了绘制的数据是显示到屏幕,还是作为MediaCodec的输入。

还有一点需要 注意:

//与GLSurfaceView中的EGLContext共享数据,只有这样才能拿到处理完之后显示的图像纹理,

mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, contex_attrib_list,0);

其中的参数mGlContext来自于EGL14.eglGetCurrentContext(),并且是在GLSurfaceView的render实现类中的onSurfaceCreated中赋值的。

原本GLSurfaceView中EGLSurface和我们自己创建的EGL环境中EGLSurface是没有关联的,但是我们用同一个EGLContext上下文,这就让他们之间产生了关联。也就实现了MediaCodec环境中需要的EGLSurface,能够使用到,共享到GLSurfaceView中纹理。

准备工作完成,下面开始绘制,录像。

在有一帧新的数据时,回调GLSurfaceView.Renderer的onDrawFrame

   @Override
    public void onDrawFrame(GL10 gl) {
      //绘制与摄像头绑定的纹理,实际的绘制只是把参数传给着色器,
       int id =cameraFilter.onDraw(texture[0]);
      mRecorder.fireFrame(id, mCameraTexture.getTimestamp());        
}

这里的纹理id,是代表绘制到屏幕上的纹理,我们要拿这个纹理id,用opengl绘制到mediacodec的EGLSurface上去。

在 int id =cameraFilter.onDraw(texture[0]);这之前,可能会有多个filter来处理数据,都会返回一个纹理id,录像这里需要的纹理id是最后一级需要绘制到屏幕上的纹理id,也就是包含了所有处理效果的数据,当然也可以中间部分的纹理id,比如说某些特效,只想预览,不想保存下来。

在MyMediaRecorder中,新启一个线程,处理绘制,编码:

    public void fireFrame(final int textureId, final long timeStamp) {
        if (!isStart) {
            return;
        }
        //录制用的opengl已经和mHandler所在线程绑定,所以需要在这个线程中使用录制的opengl
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                //绘制
                eglEnv.draw(textureId, timeStamp);
                //编码
                surfaceCodec(false);
            }
        });
    }

EGLEnv的绘制,

    public void draw(int textureId, long timestamp) {
        recordFilter.onDraw(textureId, mFilterChain);
        //设置绘制时间
        EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timestamp);
        //egl surface是双缓冲模式,绘制完成后,需要交换前后台buffer,才能将绘制数据显示到屏幕上。
        EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
    }

这里的RecordFilter的绘制流程,跟普通的opengl往屏幕绘制没有区别,直接使用了父类AbstractFilter的绘制操作;

public class RecordFilter extends AbstractFilter{}

看到这里,你可能会有疑问,既然跟opengl往屏幕绘制没有区别,那数据怎么绘制到mediacodec中的EGLSurface中的呢?

还记得在创建EGL环境时,最后一句调用

EGL14.eglMakeCurrent(mEglDisplay,mEglSurface, mEglSurface, mEglContext)

这句话的作用就是指定当前的上下文,设定opengl当前处理的显示器,当前处理的EGLSurface,这样数据就绘制到了指定的mEGLSurface中。

 

绘制完后,获取编码后的数据,封装成MP4文件,这块实现是MyMediaRecorder中代码:

  private void surfaceCodec(boolean endOfStream) {
        //标记结束信号
        if (endOfStream) {
            mMediaCodec.signalEndOfInputStream();
        }

        while (true) {
            //从输出缓冲区中,获取编码后的数据,所以先获取到输出缓冲区。
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int encoderStatus = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
            //还需要更多数据才能编码,需要在等一会
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                //如果还没结束录像,就退出循环,等待下次拿到更多Camera数据完成编码。
                if (!endOfStream) {
                    break;
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //输出格式发生改变,开启混合器,第一次总会调用,
                MediaFormat outputFormat = mMediaCodec.getOutputFormat();
                track = mMuxer.addTrack(outputFormat);
                mMuxer.start();
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //忽略,不处理
            } else {
                //调整时间戳,实现快速,慢速录像,
                bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
                //bufferInfo.presentationTimeUs <= mLastTimeStamp可能会有异常。
                if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
                    bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 24/ mSpeed);
                }
                mLastTimeStamp = bufferInfo.presentationTimeUs;

                //获取输出缓冲区编码后的数据,正常情况下,encoderStatus表示缓冲区的下标
                ByteBuffer encodedData = mMediaCodec.getOutputBuffer(encoderStatus);
                //如果当前的buffer是配置信息,不用写进去。
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) !=0) {
                    bufferInfo.size = 0;
                }

                if (bufferInfo.size !=0) {
                    //设置从哪里开始读数据(读出来就是编码后的数据)
                    encodedData.position(bufferInfo.offset);
                    //设置可读数据的总长度
                    encodedData.limit(bufferInfo.offset + bufferInfo.size);
                    //写到mp4文件中
                    mMuxer.writeSampleData(track, encodedData, bufferInfo);
                }

                //释放缓冲区,后续可以存放新的编码后的数据
                mMediaCodec.releaseOutputBuffer(encoderStatus, false);

                //如果给了结束信号,
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    break;
                }
            }
        }
    }

实现快速,慢速录制代码:

               //调整时间戳,实现快速,慢速录像,
                bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
                //bufferInfo.presentationTimeUs <= mLastTimeStamp可能会有异常。
                if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
                    bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 24/ mSpeed);
                }
                mLastTimeStamp = bufferInfo.presentationTimeUs;

就是通过调整mSpeed参数,大于1时表示快速,小于1表示慢速。实际调整的就是每一帧图像pts值,也就是视频显示的时间戳,这个时间戳是递增的。所以快速,慢速视频,对文件大小是没有影响的,

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值