前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始仿写一个抖音App——视频编辑SDK开发(二)

从零开始仿写一个抖音App——视频编辑SDK开发(二)

作者头像
何时夕
发布2020-02-24 12:33:12
1.6K0
发布2020-02-24 12:33:12
举报
文章被收录于专栏:求索之路

搬运转载请注明出处,否则将追究版权责任。交流qq群:859640274

大家好久不见,又有两个多月没有发文章了。最近新型肺炎闹得挺凶,希望大家不要出去乱跑且身体健康。本篇博客是视频编辑 SDK 解析文章中的第二篇,文章中我会介绍将上一篇文章中解码出来的视频帧通过 OpenGL 绘制出来的方式。WsVideoEditor 中的代码也已经更新了。大家在看文章的时候一定要结合项目中的代码来看。

本文分为以下章节,读者可按需阅读:

  • 1.OpenGL之我的理解
  • 2.Android层的框架搭建
  • 3.C/C++渲染视频帧
  • 4.尾巴

一、OpenGL之我的理解

讲解 OpenGL 的教程目前有很多,所以这一章笔者不会去教大家如何入门或者使用 OpenGL。本章笔者只会从抽象的角度来和大家讨论一下笔者对于 OpenGL 的理解。至于如何入门 OpenGL 则会推荐几个有用的网站。

1.OpenGL是什么?可以干什么?

图1:OpenGL之我的理解.png

如图1,我们知道 OpenGL/OpenGL ES 是一个图形图像渲染框架,它的规范由Khronos组织制定,各个显卡厂商在驱动中实现规范,再由各个系统厂商集成到系统中,最终提供各种语言的 API 给开发者使用。

当然图形图像渲染框架不仅仅只有 OpenGL 这一种。Apple 的 Metal(不跨平台)、Google 的 vulkan(跨平台)、微软的 DirectX(不跨平台) 都是 OpenGL 的竞品。

那么什么是图形图像渲染框架呢?做 Android、iOS、前端、Flutter 的同学一定都用过 Canvas,在各自的平台中 Canvas 就是一个比较上层的图形图像渲染框架

图2:Canvas对比.png

如图2,我们在使用 Canvas 绘制一个三角形的时候一般有以下步骤,在 OpenGL 中也是类似:

  • 1.确定坐标系
  • 2.根据坐标系定义三角形的三个点
  • 3.调用绘制函数/触发的渲染函数

除了像 Canvas 一样绘制 2D 图像,OpenGL 最主要的功能还是进行 3D 绘制,这就是 Canvas 们无法企及的地方了。

2.OpenGL是如何工作的?

要了解 OpenGL 是如何工作的,首先我们得知道:OpenGL 运行在哪里? 没错有些读者已经知道了:OpenGL 运行在 GPU 上面,至于在 GPU 上运行的好坏我就不赘述了。

我们在平时的开发当中,绝大部分时间都在与内存和 CPU 打交道。突然让我们写运行在 GPU 上面的程序,我想大部分人都会水土不服,毕竟这是一个思维上的转变。大多数教程一上来就是告诉大家如何调用 OpenGL 的 api,然后拼凑出一个程序来,大家也照猫画虎的敲出代码,但最终很多人并没有理解 OpenGL 是如何运行的,这也是它难学的地方。那么下面我会通过一张图来粗略的讲讲 OpenGL 是如何运行的。

图3:OpenGL是如何运行的

图3中有1、2、3、4、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 的具体用法,毕竟仰望星空的同时脚踏实地也非常重要:

二、Android层的框架搭建

我的老本行是 Android 开发,所以这一章我会讲解视频编辑SDK在 Android 层的代码。代码已经更新 WsVideoEditor,本章需结合代码食用。另外本章节强依赖 从零开始仿写一个抖音App——视频编辑SDK开发(一) 的第三章SDK架构以及运行机制介绍,大家一定要先读一下。本章会省略很多已知知识。

图4:编辑SDK架构.png

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 对象,初始化的时候会创建两个对象
  • 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 也是共用的。

三、C/C++渲染视频帧

我在从零开始仿写一个抖音App——视频编辑SDK开发(一) 的第四章VideoDecodeService解析中讲解了如何解码出视频帧,在上一章中讲解了如何在 Android 层准备好 OpenGL 的渲染环境。这些都为本章打下了基础,没有看过的同学一定要仔细阅读啊。同样本章的代码已经上传至WsVideoEditor,请结合代码食用本章。

图5:编辑SDK运行机制

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 的绘制逻辑就交给读者们自己去分析啦!

四、尾巴

又是一篇大几千字的干货出炉,希望这篇文章能让你满意,废话不多说,我们下篇文章见。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、OpenGL之我的理解
    • 1.OpenGL是什么?可以干什么?
      • 2.OpenGL是如何工作的?
      • 二、Android层的框架搭建
      • 1.WsMediaPlayer
      • 2.WsMediaPlayerView
      • 三、C/C++渲染视频帧
        • 1.FrameRender绘制流程解析
          • 2.OpenGL缓存和绘制解析
          • 四、尾巴
          相关产品与服务
          云点播
          面向音视频、图片等媒体,提供制作上传、存储、转码、媒体处理、媒体 AI、加速分发播放、版权保护等一体化的高品质媒体服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档