门户网站制作定做,asp做学生信息网站,微信营销推广,东莞百推网络本文转载自公众号#xff1a;流利说技术团队(lls_tech)版权归原作者所有本文主要介绍了流利说团队基于 Cocos 游戏引擎进行音视频相关需求开发过程中所遇到的问题和解决方案。文章中将依次阐述 Cocos 引擎直接渲染视频的方案#xff0c;继而引申出多线程环境下 OpenGL 环境的… 本文转载自公众号流利说技术团队(lls_tech)版权归原作者所有本文主要介绍了流利说团队基于 Cocos 游戏引擎进行音视频相关需求开发过程中所遇到的问题和解决方案。文章中将依次阐述 Cocos 引擎直接渲染视频的方案继而引申出多线程环境下 OpenGL 环境的管理方法最后说明音视频处理流水线模型需要解决的问题与我们的方案。让 Cocos 引擎直接渲染视频为什么可能大家首先会疑惑为什么要让 Cocos 引擎来负责渲染视频呢而不利用原生平台的渲染机制如使用Android平台的SurfaceView或TextureView。让我们来分析下利弊原生机制优势1.常规播放器接口的直接支持。2.视频渲染性能稳定。3.代码简单。原生机制劣势1.无法更精确的调整视频与游戏元素的层级关系(只能置游戏其上或其下)2.游戏控制播放器动画有性能损耗(游戏→ native 损耗)。因此如果要在视频层上播放游戏显示游戏元素或游戏动画利用原生机制无疑会转移大量的游戏业务在native端完成。这无论从项目管理和实现复杂度考虑都是不可取的。因此我们决定让 Cocos 引擎使用外接纹理渲染视频。实现方案分析 Cocos 引擎本身的渲染机制我们发现cocos引擎中封装了cocos2d::Sprite 对象用于渲染显示Sprite对象需要我们提供 cocos2d::Texture2D 对象和尺寸信息。因此我们可以封装一个 Cocos 引擎中的基础节点 cocos2d::ui::Widget 专门用于视频显示Cocos 中 widge 对象是类似于 native 中 View 功能的组件负责管理组织绘制大小绘制位置和绘制内容。我们可以在其回调的 Draw 方法中完成视频绘制。在 native 层我们将视频输出数据转为纹理再传递至Widget中转化为 Cocos 的Texture2D对象交由 Sprite 绘制。而 Sprite 渲染尺寸则由 widget 提供。由此我们可以得出下面这样一个简易的转化链多线程 OpenGL但是上面这条转化链并不能简单的实现。首先 Cocos 引擎是在单独开启的一个线程中进行工作的以下简称 Cocos 线程。也就是说我们最终 OpenGL 的绘制都会在 Cocos 线程中操作。我们用 Cocos 线程的 OpenGL context 去进行纹理转化甚至增加贴图美颜等功能都是不合适的。音视频中有一些 OpenGL 操作很有可能使 Cocos 整个 OpenGL 状态机被破坏掉。所以需要将所有的音视频转化、处理操作都限制在子线程中。假如我们需要在多线程下共享纹理数据需要让 OpenGL Context 共享同一个ShareGroup。因此我们需要接管整个架构环境中所有 OpenGLContext的构建过程。如Android端我们需要在 Cocos 引擎Cocos2dxActivity的中将 Cocos2dxGLSurfaceView::setEGLContextFactory 修改为我们自己的提供的方法。除此之外纹理转化和处理模块的 OpenGL 环境也需要统一构建共享 ShareGroup 的 context。Android端有一点特殊之处屏蔽了 ShareGroup 的概念。但是我们只要在 OpenGL Context 的构造函数传入一个 Context即可让两个 Context共享 ShareGroup。音视频处理流水线模型建立模型为了整合音视频处理的各个环节构建统一的错误处理、线程管理、生命周期管理机制我们对音视频处理流程进行了抽象建立起一个以音视频源、线程分发器、消费者链组成的流水线模型。抽象出的音视频源负责加载本地或网络视频资源而后进行解码操作。亦或者为采集摄像头数据的采集器最终输出视频帧数据。而消费者组成消费处理链负责接收处理帧数据或纹理数据。如我们自定义的 cocos::Widget 可以作为消费链的最后一个消费者。线程分发器即是负责连接源与消费者。线程分发器创建管理音视频各自的工作线程把外部命令和音视频数据分发至目标线程再回调给消费者保证消费者内部方法在同一线程执行从而消除消费者模块的中的线程安全问题。按照这样的方式建立的流水线模型具备较好的稳定性和扩展性可以保证如 OpenGL 上下文管理视频帧数据转化为纹理等诸多模块的复用。另外由于消费者和生产者的完全解耦也能够实现诸如动态切换音视频源的功能。此外多线程流水线也能很好的发挥多核 CPU 的性能。尽管模型已经建立但在细节方面还存在不少问题等待我们去解决。下面我就简单说明几个问题以及我们的探索。音视频帧数据的复用为了避免内存频繁分配而造成的不必要的耗时。我们通常会对构建的之前生成过得音视频数据进行复用因为存在多分辨率切换的问题由此会生成诸多大小不一的内存块。因此复用的前提是被复用的内存需要分配的内存。在 Android 端即是指 ByteBuffer 的capacity需要满足上述条件。因此我们可以建立一个ByteBuffer对象池用于缓存已经被消费完成的ByteBuffer在复用时遍历缓存池找寻符合大小条件的 ByteBuffer。在一般情况下视频数据需要 ByteBuffer 数组来存储因此我们可以对对象池的每个对象增加标签属性保证相同分辨率的视频数据可以快速找到可被复用的内存。那么音视频数据被回收的时机是什么呢单线程模型下是极为容易确定的但是多线程环境下事情就变得复杂了我们无法知道什么时候数据才被真正的消费完成。因此我们参考图片加载框架Fresco中对Bitmap回收问题的解决方案引入 Closeable References(可回收引用)概念。CloseableRef 对象包裹我们需要缓存的对象内置的引用计数会在我们所有线程持有的引用都被 close 后才会回收。在回收的回调方法中我们将其加入缓存对象池中。工作线程的阻塞监控开发多线程复杂项目我们必须考虑到在低配机型下工作线程积攒大量任务无法被消费处理的情况。如音频和视频的处理线程如果视频处理过慢可能会导致严重的音画不同步。因此我们需要建立可以被管理的工作线程任务队列。我们在 Android 端的实践为基于 HandlerThread 另外增加一个可以被管理的Queue。每当产生任务我们将任务入栈并向 HandlerThread 发送一条出栈指令HandlerThread 从 Queue 末尾出栈处理任务。Queue 中可以记录多项重要参数用于决策处理。如综合任务预期执行时间、任务的处理时间和队列积攒数量进行进行策略性丢弃。或者根据一段时间的综合情况来决定是否降级生产音视频的分辨率等参数等。总结以上讲述的几个技术关键点是我们团队在项目开发过程中不断探讨与发现得出的。整套框架方案已经在项目中落地获得了还不错的开发结果。希望能为大家带来些许帮助。另外敬请期待我们少儿流利说即将上线的直播课功能。