OpenGL ES短视频开发(MediaCodec编码)

2024-05-04 03:38

本文主要是介绍OpenGL ES短视频开发(MediaCodec编码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

https://juejin.im/post/5bef9847e51d451dca476445

 

OpenGL ES短视频开发(MediaCodec编码)

这一章节进行视频录制,选用MediaCodec, ffmpeg软编效率比较低,这里采用MediaCodec进行编码。

MediaCodec

MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。

 

 

 

除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的数据,那就是:

public native final Surface createInputSurface();
复制代码

使用此接口创建一个Surface,然后我们在这个Surface中"作画",MediaCodec就能够自动的编码Surface中的“画作”,我们只需要从输出缓冲区取出编码完成之后的数据即可。

此前,我们使用OpenGL进行绘画显示在屏幕上,然而想要复制屏幕图像到cpu内存中却不是一件非常轻松的事情。所以我们可以直接将OpenGL显示到屏幕中的图像,同时绘制到MediaCodec#createInputSurface当中去。

PBO(Pixel Buffer Object,像素缓冲对象)通过直接的内存访问(Direct Memory Access,DMA)高速的复制屏幕图像像素数据到CPU内存,但这里我们直接使用createInputSurface更简单......

录制我们在另外一个线程中进行(录制现场),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作(通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。

在这个线程中我们需要自己来:

1、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的)

2、完成将显示的图像绘制到MediaCodec的Surface中

3、编码(H.264)与复用(封装mp4)的工作

视频录制

处理录制Button的回调

recordButton.setOnRecordListener(new RecordButton.OnRecordListener() {@Overridepublic void onRecordStart() {douyinView.startRecord();}@Overridepublic void onRecordStop() {douyinView.stopRecord();}});
复制代码

然后Douyinview通过 Render中来录制

public void startRecord(float speed) {try {mMediaRecorder.start(speed);} catch (IOException e) {e.printStackTrace();}}public void stopRecord() {mMediaRecorder.stop();}
复制代码

因为在OpenGL显示到屏幕中的图像的同时绘制到MediaCodec#createInputSurface当中,而这里我们没有GLSurfaceView的EGL环境,所以我们需要自己创建一套EGL环境。

创建编码器MediaRecorder处理类,出入帧率,码率。

/*** @param context 上下文* @param path 保存视频的地址* @param width 视频宽* @param height 视频高* 还可以让人家传递帧率 fps、码率等参数
*/
public MediaRecorder(Context context, String path, int width, int height, EGLContext eglContext){mContext = context.getApplicationContext();mPath = path;mWidth = width;mHeight = height;mEglContext = eglContext;
}
复制代码

给编码器传参:这里的码率、帧率直接写死的。

/*** 开始录制视频
*/
public void start(float speed) throws IOException{mSpeed = speed;/*** 配置MediaCodec 编码器*///视频格式// 类型(avc高级编码 h264) 编码出的宽、高MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);//参数配置// 1500kbs码率mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);//帧率mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);//关键帧间隔mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);//颜色格式(RGB\YUV)//从surface当中回去mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);//编码器mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);//将参数配置给编码器mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);//交给虚拟屏幕 通过opengl 将预览的纹理 绘制到这一个虚拟屏幕中//这样MediaCodec 就会自动编码 inputSurface 中的图像mInputSurface = mMediaCodec.createInputSurface();
。。。。。。。  
}
复制代码

这样就创建了InputSurface,Mediacodec往这里写数据。

播放的时候我们的顺序是 解封装 ——>解码——>渲染, 所以我们编码完成后,还需要处理对应的封装操作:在GLThread线程中把数据交给我们的虚拟屏幕环境,这里我们通过 HandlerThread拿去Looper给到Handler进行

GLThread跟我们创建的这个子线程之间的通信。

/*** 开始录制视频
*/
public void start(float speed) throws IOException{//  H.264// 播放://  MP4 -> 解复用 (解封装) -> 解码 -> 绘制//封装器 复用器// 一个 mp4 的封装器 将h.264 通过它写出到文件就可以了mMediaMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);/*** 配置EGL环境,需要在一个线程中处理,线程间通信* Handler* Handler: 子线程通知主线程* Looper.loop()*/HandlerThread handlerThread = new HandlerThread("VideoCodec");handlerThread.start();Looper looper = handlerThread.getLooper();//用于其他线程 通知子线程mHandler = new Handler(looper);//子线程:EGL的绑定线程,对我们自己创建的opengl操作都在这个线程当中执行mHandler.post(new Runnable() {@Overridepublic void run() {//创建我们的子线程,用于mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);//启动编码器mMediaCodec.start();isStart = true;}});
}
复制代码

创建EGL工作环境

  • 创建EGLContext
  • 创建用于绘制的mEglSurface
  • 双缓冲进行绘画 mEglSurface + mEglDisplay进行交替绘制

创建EGLBase来录制Opengl操作需要的EGL环境配置,传入宽、高,surface,参考GLSurfaceView的配置过程。

如代码中所示,创建mEglContext需要传入mEglDisplay、mEglConfig, mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list, 0);

 private void createEGL(EGLContext eglContext) {//创建 虚拟显示器mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);if (mEglDisplay == EGL14.EGL_NO_DISPLAY){throw new RuntimeException("eglGetDisplay failed");}//初始化显示器int[] version = new int[2];// 12.1020203//major:主版本 记录在 version[0]//minor : 子版本 记录在 version[1]if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {throw new RuntimeException("eglInitialize failed");}// egl 根据我们配置的属性 选择一个配置int[] attrib_list = {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, //egl版本 2EGL14.EGL_NONE};EGLConfig[] configs = new EGLConfig[1];int[] num_config = new int[1];// attrib_list:属性列表+属性列表的第几个开始// configs:获取的配置 (输出参数)//num_config: 长度和 configs 一样就行了if (!EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0,configs, 0, configs.length, num_config, 0)) {throw new IllegalArgumentException("eglChooseConfig#2 failed");}mEglConfig = configs[0];int[] ctx_attrib_list = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //egl版本 2EGL14.EGL_NONE};//创建EGL上下文// 3 share_context: 共享上下文 传绘制线程(GLThread)中的EGL上下文 达到共享资源的目的 发生关系mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list, 0);// 创建失败if (mEglContext == EGL14.EGL_NO_CONTEXT) {throw new RuntimeException("EGL Context Error.");}}
复制代码

创建完 EglContext,需要将surface传递到 EglDisplay中去,创建

// 绘制线程中的图像 就是往这个mEglSurface 上面去画mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
复制代码

绑定并向虚拟屏幕上画:

// 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {throw  new RuntimeException("eglMakeCurrent 失败!");
}
//向虚拟屏幕画
mScreenFilter = new ScreenFiliter(context);
mScreenFilter.onReady(width,height);
复制代码

双缓存画画:mEglSurface + mEglDisplay进行交替绘制。

public void draw(int textureId, long timestamp){// 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {throw  new RuntimeException("eglMakeCurrent 失败!");}//画画mScreenFilter.onDrawFrame(textureId);//刷新eglsurface的时间戳EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timestamp);//交换数据//EGL的工作模式是双缓存模式,内部有两个frame buffer(fb)//当EGL将一个fb显示到屏幕上,另一个就在后台等待opengl进行交换EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
}
复制代码

添加共享的EGLContext, 在创建的EGL环境下的子线程下进行编码,接受传入的视频宽、高,以及Surface,这里直接把渲染线程中的EGLContext给自定义的绘制EGL,作为share_context.

 mHandler.post(new Runnable() {@Overridepublic void run() {//创建我们的子线程EGL环境mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);//启动编码器mMediaCodec.start();isStart = true;}
});/*** 创建好渲染器* @param gl* @param config*/
@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {。。。。。。。。//注意,必须在Gl线程中创建文件mCameraFiliter = new CameraFilter(mDouyinView.getContext());mScreenFiliter = new ScreenFiliter(mDouyinView.getContext());//渲染线程的上下文,需要给到自己的EGL环境下作为share_contextEGLContext eglContext = EGL14.eglGetCurrentContext();mMediaRecorder = new MediaRecorder(mDouyinView.getContext(), "/sdcard/a.mp4", CameraHelper.HEIGHT,CameraHelper.WIDTH, eglContext);
}
复制代码

绘制、编码、读取output

在子线程中启动编码

//交给虚拟屏幕 通过opengl 将预览的纹理 绘制到这一个虚拟屏幕中//这样MediaCodec 就会自动编码 inputSurface 中的图像mInputSurface = mMediaCodec.createInputSurface();。。。。。。mHandler.post(new Runnable() {@Overridepublic void run() {//创建我们的子线程,用于把预览的图像存储到虚拟Diaplay中去。mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);//启动编码器mMediaCodec.start();isStart = true;}
});
复制代码

上边的mMediaCodec.start()之后会从mInputSurface获取data, 而mEglBase会在draw方法里向mInputSurface写入data图像。

public void encodeFrame(final int textureId,final long timestamp) {if (!isStart){return;}mHandler.post(new Runnable() {@Overridepublic void run() {//把图像画到虚拟屏幕mEglBase.draw(textureId, timestamp);//从编码器的输出缓冲区获取编码后的数据就ok了getCodec(false);}});
}
复制代码

最后在看从输出缓冲区拿取编码后的数据通过mMediaMuxer进行封装,生成path路径对应的MP4文件。

/*** 获取编码后 的数据** @param endOfStream 标记是否结束录制*/private void getCodec(boolean endOfStream) {//不录了, 给mediacodec一个标记if (endOfStream) {mMediaCodec.signalEndOfInputStream();}//输出缓冲区MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();// 希望将已经编码完的数据都 获取到 然后写出到mp4文件while (true) {//等待10 msint status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);//让我们重试  1、需要更多数据  2、可能还没编码为完(需要更多时间)if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {// 如果是停止 我继续循环// 继续循环 就表示不会接收到新的等待编码的图像// 相当于保证mediacodec中所有的待编码的数据都编码完成了,不断地重试 取出编码器中的编码好的数据// 标记不是停止 ,我们退出 ,下一轮接收到更多数据再来取输出编码后的数据if (!endOfStream) {//不写这个 会卡太久了,没有必要 你还是在继续录制的,还能调用这个方法的!break;}//否则继续} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//开始编码 就会调用一次MediaFormat outputFormat = mMediaCodec.getOutputFormat();//配置封装器// 增加一路指定格式的媒体流 视频index = mMediaMuxer.addTrack(outputFormat);mMediaMuxer.start();} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {//忽略} else {//成功 取出一个有效的输出ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status);//如果获取的ByteBuffer 是配置信息 ,不需要写出到mp4if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {bufferInfo.size = 0;}if (bufferInfo.size != 0) {bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);//写到mp4//根据偏移定位outputBuffer.position(bufferInfo.offset);//ByteBuffer 可读写总长度outputBuffer.limit(bufferInfo.offset + bufferInfo.size);//写出mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);}//输出缓冲区 我们就使用完了,可以回收了,让mediacodec继续使用mMediaCodec.releaseOutputBuffer(status, false);//结束if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {break;}}}}
复制代码

关注下面的标签,发现更多相似文章

这篇关于OpenGL ES短视频开发(MediaCodec编码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/958267

相关文章

流媒体平台/视频监控/安防视频汇聚EasyCVR播放暂停后视频画面黑屏是什么原因?

视频智能分析/视频监控/安防监控综合管理系统EasyCVR视频汇聚融合平台,是TSINGSEE青犀视频垂直深耕音视频流媒体技术、AI智能技术领域的杰出成果。该平台以其强大的视频处理、汇聚与融合能力,在构建全栈视频监控系统中展现出了独特的优势。视频监控管理系统EasyCVR平台内置了强大的视频解码、转码、压缩等技术,能够处理多种视频流格式,并以多种格式(RTMP、RTSP、HTTP-FLV、WebS

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧