视频录制可以使用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() {@Overridepublic 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.0EGL14.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 surfaceint[] 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

   @Overridepublic 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所在线程绑定,所以需要在这个线程中使用录制的openglmHandler.post(new Runnable() {@Overridepublic 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值,也就是视频显示的时间戳,这个时间戳是递增的。所以快速,慢速视频,对文件大小是没有影响的,

Camera + opengles录制视频,录制快速,慢速视频(三)相关推荐

  1. 英文视频如何翻译成中文视频简单工具推荐

    英文视频如何快速变成中文视频,自动生成文案,自动匹配画面,同时还有中文的配音,看过来,在测试了十几款软件之后,终于找到我心目中最好用的那一款了, 不服来辩. 相信很多小伙伴在日常浏览海外的英文视频的时 ...

  2. Android使用MediaRecorder和Camera实现视频录制及播放功能整理

    转载请注明出处:http://blog.csdn.net/woshizisezise/article/details/51878566 这两天产品经理向我丢来一个新需求,需要在项目里添加一个视频录制的 ...

  3. android camera使用ISO值录制视频

    android camera使用ISO值录制视频 我的应用是使用自定义camera对着一个led灯箱录制视频,该led灯的频率是1000HZ,同时我使用高ISO值录像,最终想要的效果如下图,屏幕中会出 ...

  4. 如何录制一个高品质的网页直播视频,录制在线直播视频的快速教程

    怎么录制网页上的直播视频?怎么录制网页上正在播放的视频?怎么把别人在线直播的视频快速录制下来? 今天就教大家用超级捕快来快速录制一个高品质的网页直播视频,高清无损,没有水印.不仅能录制在线直播视频,还 ...

  5. 如何录制视频?快速录制视频方法介绍:

    你是否想要录制软件操作流程给朋友,想要录制游戏视频分享给网友.现在网上的视频录制软件很多,想要找到一款适合自己的录屏软件确实不容易. 别担心,今天小编就给大家分享两种快速录制视频有什么方法?相信对您会 ...

  6. Android 使用 CameraX 快速实现仿微信短视频录制

    Android 使用 CameraX 快速实现仿微信短视频录制(轻触拍照.长按录像) https://github.com/ldlywt/MyCameraX 微信短视频android端 https:/ ...

  7. 【亲测好用】一站式视频录制编辑软件:Filmage Screen mac中文版

    为大家推荐一款超好用的一站式视频录制编辑软件,Filmage Screen for Mac中文版支持1080P/4K高清屏幕录制.投屏.视频剪辑.格式转换.视频播放.GIF导出等实用的功能,高清录制, ...

  8. Android (系统+自定义)短视频录制(含暂停继续录制功能) 总结

    前言 在Android开发中自然少不了对视频录制的需求,然而视频录制虽然有系统提供给我们能够直接使用的API,但是我们往往在完成需求的过程中需要自定义实现短视频录制.网上虽然也有不少资料,但是总是零零 ...

  9. html5动画怎么做成gif,一种网页版的调用html5视频录制动画GIF图像的方法与流程...

    本发明涉及WEB开发与应用技术领域,特别涉及一种网页版的调用html5视频录制动画GIF图像的方法. 背景技术: GIF 格式指的是图像交换格式(Graphics Interchange Format ...

最新文章

  1. squid日志文件太大,怎样处理?
  2. Custom Client Side Drag and Drop Behavior in ASP.NET AJAX
  3. 分布式系统中处理参数配置的 4 种方案
  4. Keras TensorFlow教程:如何从零开发一个复杂深度学习模型
  5. 如何使用Photoshop制作真实的尺子
  6. 可穿戴计算机硬件技术研究,可穿戴计算机硬件技术应用探究.doc
  7. esp32的GPIO操作
  8. 中国为什么不发展民用计算机,为什么大多数人都错估了计算机与AI的发展?
  9. C++——《算法分析》实验壹——二分搜索算法
  10. java json float_java – Json解析问题(值自动更改为float)
  11. 双点双向路由引入/路由重发布
  12. 单片机控制三相异步电动机正反转c语言程序,请用PLC控制一台普通三相异步电动机的正反转控制,设计其控制程序梯形图及主电路...
  13. Linux 将普通用户改成root用户
  14. 深入浅出Spring源码:IOC原理解析(一)
  15. react-scripts的工作原理
  16. 【Blender】快捷键大全(带导图)
  17. MTK Android 13平台开关机动画铃声客制化
  18. CentOS 8 中配置阿里云的 yum 源
  19. 一篇还不错的介绍make的文章
  20. 一键!构建最大似然树~ 简单又准确

热门文章

  1. 「SDOI2018」物理实验
  2. Nexus 6p 内核编译
  3. 腾讯qq和新浪微博网站登录接口及文档
  4. 用7行Python代码构建自己的有声读物
  5. matlab 批量定义变量及批量传入函数参数
  6. oracle强弱动态游标,Oracle动态游标的使用
  7. java 调用s3 ip 端口,AWS S3 接口调用
  8. 分词计算频次代码-GO
  9. cas重定向次数过多
  10. java为什么不能多继承_java为什么不支持多继承