本文首发于微信公众号——世界上有意思的事,搬运转载请注明出处,不然将追究版权责任。交流qq群:859640274 前端
你们很久不见,又有两个多月没有发文章了。最近新型肺炎闹得挺凶,但愿你们身体健康。本篇博客是视频编辑 SDK 解析文章中的第二篇,文章中我会介绍将上一篇文章 中解码出来的视频帧经过 OpenGL 绘制出来的方式。WsVideoEditor 中的代码也已经更新了。你们在看文章的时候必定要结合项目中的代码来看。c++
本文分为如下章节,读者可按需阅读: git
1.OpenGL之个人理解
2.Android层的框架搭建
3.C/C++渲染视频帧
4.尾巴
1、OpenGL之个人理解
讲解 OpenGL 的教程目前有不少,因此这一章笔者不会去教你们如何入门或者使用 OpenGL。本章笔者只会从抽象的角度来和你们讨论一下笔者对于 OpenGL 的理解。至于如何入门 OpenGL 则会推荐几个有用的网站。程序员
1.OpenGL是什么?能够干什么?
如图1,咱们知道 OpenGL/OpenGL ES 是一个图形图像渲染框架 ,它的规范由Khronos组织 制定,各个显卡厂商在驱动中实现规范,再由各个系统厂商集成到系统中,最终提供各类语言的 API 给开发者使用。github
固然图形图像渲染框架 不只仅只有 OpenGL 这一种。Apple 的 Metal(不跨平台)、Google 的 vulkan(跨平台)、微软的 DirectX(不跨平台) 都是 OpenGL 的竞品。编程
那么什么是图形图像渲染框架 呢?作 Android、iOS、前端、Flutter 的同窗必定都用过 Canvas,在各自的平台中 Canvas 就是一个比较上层的图形图像渲染框架 。后端
如图2,咱们在使用 Canvas 绘制一个三角形的时候通常有如下步骤,在 OpenGL 中也是相似:api
1.肯定坐标系
2.根据坐标系定义三角形的三个点
3.调用绘制函数/触发的渲染函数
除了像 Canvas 同样绘制 2D 图像,OpenGL 最主要的功能仍是进行 3D 绘制,这就是 Canvas 们没法企及的地方了。缓存
2.OpenGL是如何工做的?
要了解 OpenGL 是如何工做的,首先咱们得知道:OpenGL 运行在哪里? 没错有些读者已经知道了:OpenGL 运行在 GPU 上面 ,至于在 GPU 上运行的好坏我就不赘述了。微信
咱们在平时的开发当中,绝大部分时间都在与内存和 CPU 打交道。忽然让咱们写运行在 GPU 上面的程序,我想大部分人都会水土不服,毕竟这是一个思惟上的转变 。大多数教程一上来就是告诉你们如何调用 OpenGL 的 api,而后拼凑出一个程序来,你们也照猫画虎的敲出代码,但最终不少人并无理解 OpenGL 是如何运行的,这也是它难学的地方。那么下面我会经过一张图来粗略的讲讲 OpenGL 是如何运行的。
图3中有一、二、三、四、5 个步骤,这几个步骤组合起来的代码就表示绘制一个三角形到屏幕上。可运行的代码能够在
learning-opengl 这里找到,图中的代码只是关键步骤。我这里也只是讲解 OpenGL 的运行方式,更具体的代码使用还须要读者去前面的网站中学习。
1.首先咱们能够在 Java/c/c++ 等等语言中使用 OpenGL 的 api,因此这里我使用 c 来说解。
2.如图咱们能够看见:GPU 内部会包括显存 和GPU核心 。咱们平时开发 CPU 程序基本能够总结为:获取数据到内存中-->经过各类语言定义函数让 CPU 改变数据-->将改变后的数据输出 。
3.那么开发 GPU 程序就能够类比成:将内存的数据交给 GPU 的显存-->经过 GLSL 语言定义函数让 GPU 改变数据-->将改变后的数据经过必定的方式绘制到屏幕上。
4.图中代码片断1 就是经过 CPU 将 GLSL 的代码编译成 GPU 指令
5.图中代码片断2 是在内存中定义好数据,而后将数据拷贝到 GPU 显存中,在显存中数据是以对象的形式存在的。
6.图中代码片断3 是告诉 GPU 我须要运行代码片断1 中编译好的 GPU 指令了。
7.图中代码片断4 是用 GPU 运行咱们 GLSL 产生的指令以刷新屏幕
8.图中代码片断5 是和 c/c++ 同样手动进行内存回收
9.以上5个代码片断连起来,一个三角形就绘制完成了。
这里我推荐两个教程,让让你们可以学习 OpenGL 的具体用法,毕竟仰望星空的同时脚踏实地也很是重要:
2、Android层的框架搭建
个人老本行是 Android 开发,因此这一章我会讲解视频编辑SDK在 Android 层的代码。代码已经更新 WsVideoEditor ,本章需结合代码食用。另外本章节强依赖 从零开始仿写一个抖音App——视频编辑SDK开发(一) 的第三章SDK架构以及运行机制介绍 ,你们必定要先读一下。本章会省略不少已知知识。
1.WsMediaPlayer
图4是编辑 SDK 的架构图,从中咱们能够看见 WsMediaPlayer 代理了 Native 的 NativeWSMediaPlayer 的 Java 类。该类具备一个播放器应该有的各类 API,例如 play、pause、seek 等等。其实不少 Android 中的系统类都是以这种形式存在的,例如 Bitmap、Surface、Canvas 等等。说到底 Java 只是 Android 系统方便开发者开发 App 的上层语言,系统中大部分的功能最终都会走到 Native 中去,因此读者须要习惯这种代码逻辑 。那么咱们就来看看这个类的运行方式吧。
1.看代码咱们能够知道,WsMediaPlayer 的全部 API,最终都走到了 NativeWSMediaPlayer 中。
2.咱们能够看 VideoActivity 的代码里面有建立和使用 WsMediaPlayer 的流程。
3.mPlayer = new WsMediaPlayer()
:会建立一个 NativeWSMediaPlayer 对象,初始化的时候会建立两个对象
1.VideoDecodeService:这个对象用于解码视频帧,具体代码解析在从零开始仿写一个抖音App——视频编辑SDK开发(一) 的第四章VideoDecodeService解析
2.FrameRenderer:这个对象用于将 VideoDecodeService 对象解码出来的视频帧,经过 OpenGL 绘制到屏幕上。
4.mPlayer.setProject(videoEditorProjectBuilder.build())
:最终走到了 NativeWSMediaPlayer::SetProject
中,这里只是将 EditorProject 设置给 VideoDecodeService 和 FrameRenderer。
5.mPlayer.loadProject()
:最终走到了 wsvideoeditor::LoadProject 中,这里的主要逻辑是对每一段视频使用 FFmpeg 进行解封装 ,获取到各个视频的时长、长宽、等等信息,而后存入 EditorProject 中以便以后使用。至于 FFmpeg 的使用能够参见这几篇文章:从零开始仿写一个抖音App——音视频开篇 、零开始仿写一个抖音App——基于FFmpeg的极简视频播放器
6.至此咱们的 WsMediaPlayer 就建立完了,其余 Api 例如 play、pause、seek 等等就交给读者去了解吧。
2.WsMediaPlayerView
若是把播放视频比做:一个绘画者 每隔 30ms 就向画布 上绘制一幅连环画 的话。那么绘画者 就是 WsMediaPlayer,连环画 就是视频,画布 就是 WsMediaPlayerView。
1.WsMediaPlayerView 是基础于 TextureView 的。因此其生命周期会被系统自动调用,咱们也须要在这些回调中作一些事情
1.init()
:建立 WsMediaPlayerView 是调用,初始化一些参数,注册回调。
2.setPreviewPlayer
:将 WsMediaPlayer 交给 PlayerGLThread,以绘制。
3.onResume()/onPause()
:须要手动在 Activity 中调用,用于启动/暂停绘制。
4.onSurfaceTextureAvailable
:在 TextureView.draw
的时候被系统调用,表示咱们能够开始进行绘制了。咱们在这里就建立了一个 PlayerGLThread,用于在非主线程进行 30ms 的定时循环绘制。同时还获取了绘制窗口的大小。
5.onSurfaceTextureSizeChanged
:当绘制窗口改变的时候,更新窗口大小,最终会做用在 OpenGL 的绘制窗口上。
6.onSurfaceTextureDestroyed
:资源销毁。
2.再来看看 PlayerGLThread,它是一个无限循环的线程,也是 OpenGL 环境的建立者,仍是 WsMediaPlayer 的主要调用者。
1.根据对 WsMediaPlayerView 的描述咱们知道:PlayerGLThread 会在 TextureView.draw
调用与 WsMediaPlayer 被设置,这两个条件同时知足时启动线程。
2.PlayerGLThread 有 mFinished 以控制线程是否结束。
3.PlayerGLThread 有 mRenderPaused 以控制是否调用 WsMediaPlayer.draw 进行绘制。
4.PlayerGLThread 有 mWidth 和 mHeight 以记录绘制窗口的大小,也即 OpenGL 的绘制区域。
5.线程循环的开始,runInternal 会首先检查 OpenGL 的环境是否可用,而后根据 WsMediaPlayer 选择是否建立新的 OpenGL 环境。
6.OpenGL 环境建立好以后,会调用 mPlayer.onAttachedView(mWidth, mHeight)
来向 Native 同步绘制区域的大小。
7.若是全部环境准备就绪,!mFinished && !mRenderPaused
为 true,那么调用 mPlayer.drawFrame()
进行绘制。
8.runInternal 中每次循环为 33ms,在 finally 中经过 sleep 保证。
3.另外须要注意的是,OpenGL 在每一个线程中有一个 OpenGL Context,这至关于一个线程单例。因此即便咱们在 Java 层建立了 OpenGL 的环境,只要 C/C++ 层中运行的代码也处于同一个线程,绘制仍是能够正常进行的,OpenGL Context 也是共用的。
3、C/C++渲染视频帧
我在从零开始仿写一个抖音App——视频编辑SDK开发(一) 的第四章VideoDecodeService解析 中讲解了如何解码出视频帧,在上一章中讲解了如何在 Android 层准备好 OpenGL 的渲染环境。这些都为本章打下了基础,没有看过的同窗必定要仔细阅读啊。一样本章的代码已经上传至WsVideoEditor ,请结合代码食用本章。
1.FrameRender绘制流程解析
图5是视频编辑 SDK 的运行机制,本次咱们解析的功能是在 FrameRender 中渲染 VideoDecodeService 提供的视频帧,也就是视频播放功能。下面咱们就从第二章中提到的 WsMediaPlayer.draw 方法入手。
1.经过第二章你们都知道在视频播放的状况下,WsMediaPlayer.draw 会以 33ms 为间隔不断的进行循环调用。
2.就像你们想的那样,WsMediaPlayer.draw 最终会调用到 Native 的 NativeWSMediaPlayer::DrawFrame 方法中。这个方法目前还不完善里面只有测试代码,由于咱们目前只能播放图像,尚未播放声音,因此目前 current_time_ = current_time = GetRenderPos()
获取到的时间戳,是我构造的测试代码。
3.current_time_ 有了,咱们就能够用 decoded_frames_unit = video_decode_service_->GetRenderFrameAtPtsOrNull(current_time)
来从 VideoDecodeService 中获取到视频帧,由于 VideoDecodeService 有一个单独的线程本身对视频进行解码(代码解析前面提到过)。因此这里可能出现获取不到视频帧的状况,这也是后续须要完善的地方。
4.获取到了视频帧时候会用 frame_renderer_.Render(current_time, std::move(decoded_frames_unit))
来渲染。
5.这里咱们先回忆一下,frame_renderer_ 是怎么来的。经过第二章的讲解咱们知道,frame_renderer_ 是在 NativeWSMediaPlayer 被建立的时候同时建立的。
6.咱们进入 FrameRenderer 类中,会发现几个参数,我这里先简单解释一下,后面一些会分析其代码:
1.ShaderProgramPool:提供各类 "ShaderProgram",例如将 Yuv420 转化为 Argb 的 Yuv420ToRgbShaderProgram、拷贝 Argb 的 CopyArgbShaderProgram、将 Argb 图像绘制到屏幕上的 WsFinalDrawProgram。同时它还提供纹理数据对象的封装类 WsTexture。
2.AVFrameRgbaTextureConverter:整合了 Yuv420ToRgbShaderProgram 和 WsFinalDrawProgram,能够将 AVFrame 转化成 WsTexture。
7.简单了解了 FrameRenderer,咱们回到 FrameRenderer::Render,而后进入 FrameRenderer::RenderInner。
1.代码中先更新了一些数据 render_width/height 这个表示咱们在第二章中提到的渲染区域的宽高。project_width/height 则表示视频的宽/高。showing_media_asset_rotation_ 表示视频旋转的角度,showing_media_asset_index_ 表示正在播放的是第几个视频(咱们的 EditorProject 支持按顺序添加多个视频)。
2.而后若是传入的 current_frame_unit_ 是一个新视频帧的话,那么就经过 current_original_frame_texture_ = avframe_rgba_texture_converter_.Convert
来将AVFrame 转化成 WsTexture。此时视频帧已经从内存中被拷贝到了显存中了,WsTexture.gl_texture_ 能够理解为显存中纹理(视频帧)数据对象的指针。
3.再继续给 WsFinalDrawProgram 设置 render_width/height 和 project_width/height 以保证视频帧可以正确的绘制到渲染区域中。
4.最终经过 GetWsFinalDrawProgram()->DrawGlTexture
将视频帧真正的绘制到屏幕上。
2.OpenGL缓存和绘制解析
经过上一小结的介绍,咱们知道了绘制视频帧的大体流程,可是咱们只是粗略的介绍了整个渲染流程。因此这一节做为上一节的补充,会简单介绍一下咱们的 OpenGL 缓存逻辑和绘制逻辑。
1.咱们在第一章介绍 OpenGL 的运行机制的时候提到:OpenGL 须要用到的数据所有都是从内存中发送到显存中的。若是是普通的坐标数据还好数据量比较小,但若是是像咱们提到的视频帧数据 的话,每次绘制都进行申请和释放的话,那样会形成很大的浪费。因此咱们首先要讲到的就是视频帧数据对象 的复用(后面以纹理对象 来代替)。
1.还记得咱们上一节中提到的 WsTexture 吗?这个对象就是我对纹理对象的封装。它里面有几个参数:width_/height_ 分别像素数量、gl_texture_ 就是纹理对象的地址、is_deleted_ 表示纹理对象是否已经被回收。
2.既然要复用对象,那么 pool 就少不了。因此 WsTexturePool 就是为了复用 WsTexture 而定义的。咱们能够看见其内部有一个 texture_map_,用于存储 WsTexture,key 就是纹理对象的长宽。每次调用 WsTexturePool::GetWsTexturePtr 获取 WsTexture 的时候,都会先从 texture_map_ 中寻找是否有合适的。若是有就直接返回,若是没有则建立一个而后添加到 texture_map_ 中。
3.再继续看 WsTextureFbo,这个对象是对 WsTexture + fbo 的封装。fbo 是什么?若是把纹理对象比做 Bitmap 的话,那么 fbo 能够被认为是 Canvas。
2.咱们前面提到了 shader program 是由 cpu 编译而成,编译又是一个须要耗费时间的过程。那么咱们是否能够缓存 shader program 呢,毕竟某一个操做的 shader program 是固定的,例如咱们在上一节提到的:将 Yuv420 转化为 Argb 的操做。shader program 固然也是能够缓存的, 因此咱们就使用了 Yuv420ToRgbShaderProgram、CopyArgbShaderProgram 等等类来封装某一个 shader program。
3.介绍完了 shader program 和 纹理对象的缓存,上一节提到的 ShaderProgramPool 的用处就水落石出了。
4.剩下的 OpenGL 的绘制逻辑就交给读者们本身去分析啦!
4、尾巴
又是一篇大几千字的干货出炉,但愿这篇文章能让你满意,废话很少说,咱们下篇文章见。
连载文章
不贩卖焦虑,也不标题党。分享一些这个世界上有意思的事情。题材包括且不限于:科幻、科学、科技、互联网、程序员、计算机编程。下面是个人微信公众号:世界上有意思的事 ,干货多多等你来看。