從零開始仿寫一個抖音App——視頻編輯SDK開發(二)

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。交流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之個人理解.png

如圖1,咱們知道 OpenGL/OpenGL ES 是一個圖形圖像渲染框架,它的規範由Khronos組織制定,各個顯卡廠商在驅動中實現規範,再由各個系統廠商集成到系統中,最終提供各類語言的 API 給開發者使用。github

固然圖形圖像渲染框架不只僅只有 OpenGL 這一種。Apple 的 Metal(不跨平臺)、Google 的 vulkan(跨平臺)、微軟的 DirectX(不跨平臺) 都是 OpenGL 的競品。編程

那麼什麼是圖形圖像渲染框架呢?作 Android、iOS、前端、Flutter 的同窗必定都用過 Canvas,在各自的平臺中 Canvas 就是一個比較上層的圖形圖像渲染框架後端

圖2:Canvas對比.png

如圖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: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架構以及運行機制介紹,你們必定要先讀一下。本章會省略不少已知知識。

圖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 也是共用的。

3、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 的繪製邏輯就交給讀者們本身去分析啦!

4、尾巴

又是一篇大幾千字的乾貨出爐,但願這篇文章能讓你滿意,廢話很少說,咱們下篇文章見。

連載文章

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事
相關文章
相關標籤/搜索