導讀:本文來自 LiveVideoStack 線上分享第三季,第十期阿里巴巴閒魚事業部無線開發專家陳爐軍帶來的分享內容,針對閒魚APP在當下流行的跨平臺框架Flutter的大規模實踐,介紹其在音視頻領域碰到的一些困難以及解決方案。
你們好,我是阿里巴巴閒魚事業部的陳爐軍,本次分享的主題是 Flutter 浪潮下的音視頻研發探索,主要內容是針對閒魚APP在當下流行的跨平臺框架 Flutter 的大規模實踐,介紹其在音視頻領域碰到的一些困難以及解決方案。算法
分享內容主要分爲四個方面,首先會對 Flutter 有一個簡單介紹以及選擇 Flutter 做爲跨平臺框架的緣由,其次會介紹 Flutter 中與音視頻關係很是大的外接紋理概念,以及對它作出的一些優化。以後會對閒魚在音視頻實踐過程當中碰到的一些 Flutter 問題提出了一些解決方案—— TPM 音視頻框架。最後是閒魚 Flutter 多媒體開源組件的介紹。網絡
Flutter 是一個跨平臺框架,以往的作法是將音頻、視頻和網絡這些模塊都下沉到 C++ 層或者 ARM 層,在其上封裝成一個音視頻的 SDK ,供 UI 層的 PC 、 iOS 和 Android 調用。多線程
而 Flutter 作爲一個 UI 層的跨平臺框架,顧名思義就是在 UI 層也實現了一個跨平臺開發。能夠預想的是未Flutter發展的好的話,會逐漸變爲一個從底層到 UI 層的一個全鏈路的跨平臺開發,技術人員分別負責 SDK 和 UI 層的開發。架構
在 Flutter 以前已經有不少跨平臺 UI 解決方案,那爲何選擇 Flutter 呢?咱們主要考慮性能和跨平臺的能力。併發
以往的跨平臺方案好比 Weex,ReactNative,Cordova 等等由於架構的緣由沒法知足性能要求,尤爲是在音視頻這種性能要求幾乎苛刻的場景。負載均衡
而諸如 Xamarin 等,雖然性能能夠和原生 App 一致,可是大部分邏輯仍是須要分平臺實現。框架
咱們能夠看一下,爲何 Flutter 能夠實現高性能:ide
原生的native組件渲染以 IOS 爲例,蘋果的UIKit經過調用平臺本身的繪製框架 QuaztCore 來實現 UI 的繪製,圖形繪製也是調用底層的 API ,好比 OpenGL 、 Metal 等。函數
而 Flutter 也是和原生 API 邏輯一致,也是經過調用底層的繪製框架層 SKIA 實現 UI 層。這樣至關於 Flutter 他本身實現了一套 UI 框架,提供了一種性能超越原生 API 的跨平臺可能性。性能
可是咱們說一個框架最終性能怎樣,其實取決於設計者和開發者。至於如今究竟是一個什麼情況:
在閒魚的實踐中,咱們發如今正常的開發沒有特地的去優化 UI 代碼的狀況下,在一些低端機上,Flutter界面的流暢性是比Native界面要好的。
雖然如今閒魚某些場景下會有卡頓閃退等狀況,可是這是一個新事物發展過程當中的必然問題,咱們相信將來性能確定不會成爲限制 Flutter 發展的瓶頸的。
在閒魚實踐 Flutter 的過程當中,混合棧和音視頻是其中比較難解決的兩個問題,混合棧是指一個 APP 在 Flutter 過程當中不可能一口氣將全部業務所有重寫爲 Flutter ,因此這是一個逐步迭代的過程,這期間原生 native 界面與 Flutter 界面共存的狀態就稱之爲混合棧。閒魚在混合棧上也有一些比較好的輸出,例如 FlutterBoost 。
在講音視頻以前須要簡要介紹一下外接紋理的概念,咱們將它稱之爲是Flutter和Frame之間的橋樑。
Flutter 渲染一幀屏幕數據首先要作的是, GPU 發出的 VC 信號在 Flutter 的UI 線程,經過 AOT 編譯的機器碼結合當前 Dart Runtime ,生成 Layer Tree UI樹, Layer Tree 上每個葉子節點都表明了當前屏幕上所須要渲染的每個元素,包含了這些元素渲染所須要的內容。將 Layer Tree 拋給 GPU 線程,在 GPU 線程內調用 Skia 去完成整個UI的渲染過程。
Layer Tree 中有 PictureLayer 和 TextureLayer 兩個比較重要的節點。PictureLayer 主要負責屏幕圖片的渲染, Flutter 內部實現了一套圖片解碼邏輯,在 IO 線程將圖片讀取或者從網絡上拉取以後,經過解碼可以在 IO 線程上加載出紋理,交給 GPU 線程將圖片渲染到屏幕上。
可是因爲音視頻場景下系統 API 太過繁多,業務場景過於複雜。Flutter 沒有一套邏輯去實現跨平臺的音視頻組件,因此說 Flutter 提出了一種讓第三方開發者來實現音視頻組件的方式,而這些音視頻組件的視頻渲染出口,就是 TextureLayer 。
在整個 Layer Tree 渲染的過程當中, TextureLayer 的數據紋理須要由外部第三方開發者來指定,能夠把視頻數據和播放器數據送到 TextureLayer 裏,由 Flutter 將這些數據渲染出來。
TextureLayer 渲染過程:首先判斷 Layer 是否已經初始化,若是沒有就建立一個 Texture ,而後將 Texture Attach 到一個 SufaceTexture 上。
這個 SufaceTexture 是音視頻的 native 代碼能夠獲取到的對象,經過這個對象建立的 Suface ,咱們能夠將視頻數據、攝像頭數據解碼放到 Suface 中,而後 Flutter 端經過監聽 SufaceTexture 的數據更新就能夠順利把剛纔建立的數據更新到它的紋理中,而後再將紋理交給 SKIA 渲染到屏幕上。
然而咱們若是須要用 Flutter 實現美顏,濾鏡,人臉貼圖等等功能,就須要將視頻數據讀取出來,更新到紋理中,再將 GPU 紋理通過美顏濾鏡處理後生成一個處理後的紋理。按Flutter提供的現有能力,必須先將紋理中的數據從 GPU 讀出到 CPU 中,生成 Bitmap 後再寫入 Surface 中,這樣在 Flutter 中才能順利的更新到視頻數據,這樣作對系統性能的消耗很大。
經過對 Flutter 渲染過程分析,咱們知道 Flutter 底層須要渲染的數據就是 GPU 紋理,而咱們通過美顏濾鏡處理完成之後的結果也是 GPU 紋理,若是能夠將它直接交給 Flutter 渲染,那就能夠避免 GPU->CPU->GPU 這樣的無用循環。這樣的方法是可行的,可是須要一個條件,就是 OpenGL 上下文共享。
在說上下文以前,得提到一個和上線文息息相關的概念:線程。
Flutter 引擎啓動後會啓動四個線程:
第一個線程是 UI 線程,這是 Flutter 本身定義的UI線程,主要負責 GPU 發出的 VSync 信號時候用當前 Dart 編譯的機器碼和當前運行環境建立出 Layer Tree。
還有就是 IO 線程和 GPU 線程。和大部分 OpenGL 處理解決方案中同樣, Flutter 也採起一個線程責資源加載,一部分負責資源渲染這種思路。
兩個線程之間紋理共享有兩種方式。一種是 EGLImage ( IOS 是 CVOpenGLESTextureCache )。一種是 OpenGL Share Context 。Flutter 經過 Share Context 來實現紋理共享,將 IO 線程的 Context 和 GPU 線程的 Context 進行 Share ,放到同一個 Share Group 下面,這樣兩個線程下資源是互相可見能夠共享的。
Platform 線程是主線程, Flutter 中有一個很奇怪的設定, GPU 線程和主線程共用一個 Context 。而且在主線程也有不少 OpenGL 操做。
這樣的設計會給音視頻開發帶來不少問題,後面會詳細說。
音視頻端美顏處理完成的 OpenGL 紋理可以讓Flutter直接使用的條件就是 Flutter 的上下文須要和平臺音視頻相關的 OpenGL 上下文處在一個 Share Group 下面。
因爲 Flutter 主線程的 Context 就是 GPU 的 Context ,因此在音視頻端主線程中有一些 OpenGL 操做的話,頗有可能使 Flutter 整個 OpenGL 被破壞掉。因此須要將全部的 OpenGL 操做都限制在子線程中。
經過上述這兩個條件的處理,咱們就能夠在沒有增長 GPU 消耗的前提下實現美顏和濾鏡等等功能。
在通過 demo 驗證以後,咱們將這個方案應用到閒魚音視頻組件中,但改造過程當中發現了一些問題。
上圖是攝像頭採集數據轉換爲紋理的一段代碼,其中有兩個操做:首先是切進程,將後面的 OpenGL 操做都切到 cameraQueue 中。而後是設置一次上下文。而後這種限制條件或者說是潛規則每每在開發過程當中容易被忽略的。而這個條件一旦忽略後果就是出現一些莫名其妙的詭異問題極難排查。所以咱們就但願能抽象出一套框架,由框架自己實現線程的切換、上下文和模塊生命週期等的管理,開發者接入框架之後只須要安心實現本身的算法,而不須要關心這些潛規則還有其餘一些重複的邏輯操做。
在引入 Flutter 以前閒魚的音視頻架構與大部分音視頻邏輯同樣採用分層架構:
1:底層是一些獨立模塊
2:SDK 層是對底層模塊的封裝
3:最上層是 UI 層。
引入 Flutter 以後,經過分析各個模塊的使用場景,咱們能夠得出一個假設或者說是抽象:音視頻應用在終端上能夠概括爲視頻幀解碼以後視頻數據幀在各個模塊之間流動的過程,基於這種假設去作 Flutter 音視頻框架的抽象。
整個 Flutter 音視頻框架抽象分爲管線和數據的抽象、模塊的抽象、線程統一管理和上下文同一管理四部分。
管線,其實就是視頻幀流動的管道。數據,音視頻中涉及到的數據包括紋理、 Bit Map 以及時間戳等。結合現有的應用場景咱們定義了管線流通數據以 Texture 爲主數據,同時能夠選擇性的添加 Bit Map 等做爲輔助數據。這樣的數據定義方式,避免重複的建立和銷燬紋理帶來的性能開銷以及多線程訪問紋理帶來的一些問題。也知足一些特殊模塊對特殊數據的需求。同時也設計了紋理池來管理管線中的紋理數據。
模塊:若是把管線和數據比喻成血管和血液,那框架音視頻的場景就能夠比喻成器官,咱們根據模塊所在管線的位置抽象出採集、處理和輸出三個基類。這三個基類裏實現了剛纔說的線程切換,上下文切換,格式轉換等等共同邏輯,各個功能模塊經過集成自這些基類,能夠避免不少重複勞動。
線程:每個模塊初始化的時候,初始化函數就會去線程管理的模塊去獲取本身的線程,線程管理模塊能夠決定給初始化函數分配新的線程或者已經分配過其餘模塊的線程。
這樣有三個好處:
從 Flutter 端修改 Flutter 引擎將 Context 取出後,根據 Context 建立上下文的統一管理模塊,每個模塊在初始化的時候會獲取它的線程,獲取以後會調用上下文管理模塊獲取本身的上下文。這樣能夠保證每個模塊的上下文都是與 Flutter 的上下文進行Share的,每一個模塊之間資源都是共享可見的, Flutter 和音視頻 native 之間也是互相共享可見的。
基於上述框架若是要實現一個簡單的場景,好比畫面實時預覽和濾鏡處理功能,
一、須要選擇功能模塊,功能模塊包括攝像頭模塊、濾鏡處理模塊和 Flutter 畫面渲染模塊;
二、須要配置模塊參數,好比採集分辨率、濾鏡參數和先後攝像頭設置等;
三、在建立視頻管線後使用已配置的參數建立模塊;
四、最後管線搭載模塊,開啓管線就能夠實現這樣簡單的功能;
上圖爲整個功能實現的代碼和結構圖。
結合上述音視頻框架,閒魚實現了 Flutter 多媒體開源組件。
組要包含四個基本組件分別是:
一、視頻圖像拍攝組件
二、播放器組件
三、視頻圖像編輯組件
四、相冊選擇組件
如今這些組件正在走內部開源流程。預計 9 月份,相冊和播放器會實現開源。
一、實現開頭所說的從底層 SDK 到 UI 的全鏈路的跨端開發。目前底層框架層和模塊層都是各個平臺各自實現,反而是 Flutter 的 UI 端進行了跨平臺的統一,因此後續會將底層也按照音視頻經常使用作法把邏輯下沉到 C++ 層,儘量的實現全鏈路跨平臺。
二、第二部份內容爲開源共建,閒魚開源的內容不只包括拍攝、編輯組件,還包括了不少底層模塊,但願有開發者在基於 Flutter 開發音視頻應用時能夠充分利用閒魚開源出的音視頻模塊能力,搭建 APP 框架,開發者只要去負責實現特殊需求模塊就能夠,儘量的減小重複勞動。
本文爲雲棲社區原創內容,未經容許不得轉載。