Flutter 音影片開發的新思路

語言: CN / TW / HK

閒魚技術——黑荊

引言

音影片功能因為需要依賴於 GPU 或其他系統底層軟硬體,來計算處理和展現結果,所以相關技術總是相對偏向於底層,包括音影片的基礎框架,乃至程式碼功能邏輯也一般都沉澱在底層。我們可以在各個系統平臺構建我們的音影片功能和應用介面,但是 Flutter 是系統平臺之上的跨平臺框架,似乎天然與偏底層的音影片有著一定的隔閡,所以在 Flutter 上開發音影片,似乎會陷入到了僅能開發音影片介面,而無法更深入一步的困境。

Flutter 音影片開發的困境

我們希望通過 Flutter 統一各端的介面 UI 和功能邏輯,Flutter 不僅僅能夠用來構建介面,也能夠用於中間層功能邏輯的跨端,這樣做的好處不言而喻,不僅 UI 介面在多端是統一的,功能邏輯在多端也是統一的,這樣能有更好的功能一致性和可維護性。閒魚在這方面也做了很多實踐和努力,比如對訊息業務的架構升級,抹平了 IM 場景雙端邏輯的差異,提升了整體穩定性和研發效率,具體可見《Flutter IM 跨端架構設計和實現》。

圖示為 Flutter 音影片的分層結構圖

在音影片方面,我們也希望音影片的相關功能邏輯能上移到 Flutter 進行統一開發,但是由於音影片的底層依賴,目前大量的音影片功能邏輯都還是實現在 Native 或者更底層,僅通過 Plugin 傳輸指令和資料,通過 External Texture 將紋理外接到 Flutter 展示,Flutter 僅僅用於構建介面。所以我們不得不面臨一個尷尬的現狀:雖然通過 Flutter 技術,我們能夠做到音影片模組介面的統一,但功能邏輯依然分散在各端底層,需要分別開發維護,不僅研發效率不高,各端功能表現也容易不一致

為了解決這個問題,將更多的功能邏輯上移到 Flutter,讓 Flutter 音影片的也能做到邏輯跨端,我們需要一種新的思路。

新思路

任何一個功能模組,都是由更細粒度的子模組構建出來的。例如一個拍攝功能,我們可以拆分出相機、麥克風、幀處理、音訊處理、預覽、拍照、影片錄製等子功能模組,每個子模組都承擔一定的職責,對外透出屬性和操作介面,和其他模組配合處理資料和邏輯,最終構建出完整的功能。如果要在 Flutter 中實現這個拍攝功能,一般是在底層串聯實現這些功能,將影片幀作為紋理外接到 Flutter 側展示,Flutter 側構建介面,並通過 MethodChannel 下發指令進行拍照、影片錄製等操作控制。

這時候我們想到,如果子模組能夠被 Flutter 側操作控制,是否能夠直接在 Flutter 側完整構建出整個功能呢?

為了方便地控制底層音影片子模組,我們需要在 Flutter 側建立起 Native 子模組的對映,即在 Flutter 側存在一個和 Native 一樣的物件,Native 側子模組的屬性和介面方法都會對映到 Flutter,我們稱這樣的子模組為節點(Node)。Native Node 和 Flutter Node 之間需要有一條資料傳輸通道,對於 Flutter Node 的操作,可以通過資料通道通知到 Native 進行實際的響應。由於音影片的實際處理和操作是在 Native 底層的,因此資料也是產生在底層的,在早期的想法中,我們希望 Native 產生的資料(例如影片幀)可以上拋到 Flutter,Flutter Node 接收到資料進行一些邏輯處理之後再交給底層 Native Node 處理,如下圖所示(空心箭頭代表指令訊息傳輸,實心箭頭代表音影片資料傳輸)。

雖然理論上通過 FFI 也可以做到,但是頻繁的資料通訊還是可能會存在效能問題和不穩定因素,同時我們也意識到,記憶體資料僅在底層流通是更合理的,Flutter 側只需關注邏輯結果,因此 Node 之間的通訊結構簡化為下圖所示。

構成音影片功能模組的子模組稱之為音影片元能力節點(Node),負責實現特定的音影片能力,這些元能力需要提前實現和註冊;根據不同的功能需要,使用不同的元能力節點構建音影片管線(Pipeline),節點之間遵循一定的規則進行連線和標準資料傳輸;音影片管線(Pipeline)在 Flutter 側進行構建,最終在 Native 側實際生成真正的管線和節點連線。通過在 Flutter 側使用不同的音影片元能力,構建不同的管線,並對管線和節點進行操作控制,即可在 Flutter 側實現出不同的音影片功能。

總結起來,基於這種思路,我們需要做到一下幾點:

  • • 合適的元能力抽象和功能對映(Flutter <-> 底層)
  • • 高效地資料通訊(Flutter <-> 底層)
  • • 管線構建與狀態管理機制
  • • 節點連線與資料流轉標準

由此我們沉澱了 Flutter 的流式音影片開發框架 PowerMedia

PowerMedia

PowerMedia 分為 Core 核心框架和 Node 元能力節點兩部分,下圖是 PowerMedia 的整體架構圖。

在核心框架中,PowerMedia 實現了對管線構建和狀態管理的支援,並建立了高效的通訊通道,支援 Flutter 和 Native 之間 Node 以及 Pipeline 的通訊,非同步訊息傳輸使用 Flutter 的 MethodChannel,針對部分需要同步通訊的場景,我們也基於 FFI 實現了高效能的同步通訊通道 PowerPlatformChannel。

音影片元能力節點(Node)的抽象與實現,則與最終功能的底層實現密切相關。節點可以分為生產型(Producer)、消費型(Consumer)和生產消費型(Producer and Consumer)三種,分別代表了節點本身是生產資料、消費資料還是處理資料。我們按照 MIME 規範對管線中傳輸的底層資料進行了標準化收斂,節點需要宣告支援的資料型別,只有型別匹配的節點才能互相連線。實際程式碼中,對於影片幀資料,在 iOS 中流轉的是 CVPixelBuffer 的封裝物件,而 Android 中是 OpenGL Texture 的封裝物件,框架也管理了底層資料的生命週期。Node 在 Flutter 側透出的屬性和介面,可以讓我們以“面向物件”的方式進行音影片功能編碼。

PowerMeida 中 Flutter Node 和 Native Node 的關係,可以類比為 Flutter 中 Widget 與 Element/RenderObject 的關係,或者 Weex/RN 中元件標籤與 Native 中元件實際 View 的關係。Flutter Node 的建立與使用僅僅是一種邏輯描述,真正的功能需要 Native Node 來實現。我們通過 PowerMedia 在 Flutter 中構建 Pipeline 管線圖,這會在 Native 實際建立管線圖,構建這兩張圖的過程也可以類比為 Flutter 中構建三棵樹(雖然兩者不完全等價,但是思想接近)。

簡而言之,我們在 Flutter 通過構建 Pipeline 管線圖實現功能框架,通過對 Pipeline 和 Node 的功能邏輯編碼,實現具體的功能。

下圖顯示了 PowerMedia 框架在整個音影片架構中的位置,針對特定的音影片功能,我們可以實現和沉澱相應的音影片元能力節點,而更多、更穩定的元能力節點,有助於我們更方便地在 Flutter 實現更多的的音影片功能,這也是一種正向的生態演進。

拍攝實踐

我們以上文提到的拍攝的例子,來說明我們如何通過 PowerMedia 在 Flutter 實現拍攝功能。

一個基本的拍攝功能,需要可以預覽相機畫面,可以拍照和錄製影片,當然也可以有一定的濾鏡、美顏能力。基於這些能力要求,我們需要對拍攝模組進行拆分,抽象出完成這些功能所需的音影片元能力:

  1. 1. 相機(Camera):負責採集影片畫面
  2. 2. 麥克風(Microphone):負責採集音訊資料
  3. 3. 裁剪(Cropper):負責裁剪影片幀,調整畫幅
  4. 4. 渲染(Renderer):對影片幀進行處理,實現濾鏡、美顏等能力
  5. 5. 預覽(Previewer):負責影片幀上屏預覽
  6. 6. 幀儲存(FrameSaver):負責將影片幀儲存到本地,實現拍照功能
  7. 7. 影片錄製(VideoRecorder):負責將影片幀和音訊幀儲存寫入到影片檔案

將上述音影片元能力按照 PowerMedia 的介面規範實現為元能力節點(Node),並註冊給 PowerMedia。

下面簡單以 Camera 和 Renderer 節點為例看下 Flutter 側部分程式碼:

基於上述元能力節點,可以在 Flutter 側構建出如下的 Pipeline 管線圖:

部分 Pipeline 構建程式碼如下:

如果要實現的拍攝模組,不需要濾鏡、美顏等功能,我們只要不新增和連線 Renderer 節點即可,那就會構建出另外一張 Pipeline 管線圖。

構建完 Pipeline 之後,我們通過操作 Pipeline 來控制整個功能模組的生命週期狀態,通過操作相應節點的介面方法,實現特定的功能邏輯如開關閃光燈、新增濾鏡、拍照、錄影等。例如下圖即 Flutter 側封裝的利用 Renderer 節點來應用濾鏡的方法:

通過 PowerMedia,我們在 Flutter 實現了拍攝模組的功能邏輯,當然介面部分還需要額外編碼實現。

總結起來,通過 PowerMedia 構建拍攝功能,我們得到了如下收益:

  1. 1. 沉澱了多個可複用的音影片元能力節點。新功能需求需要的元能力 Node 是需要從零實現的,但是已經實現後的元能力節點,可以沉澱下來,因為其職責單一,可以複用到其他功能模組的構建中。例如我們在圖片編輯功能模組的實現中,就複用了 Cropper、Renderer、FrameSaver、Previewer 這幾個 Node,大大減少了開發工作量,提升了效率。後續其他功能需要使用相機或者麥克風能力,也可以直接複用 Node 能力。
  2. 2. 拍攝功能邏輯上移到了 Flutter,原先需要在各端分別開發的功能邏輯,只需在 Flutter 開發即可,既提升了開發效率,也能夠保證邏輯表現上的一致性,也有利於後續功能維護。
  3. 3. 開發體驗上更加友好,對於 Node 的操作更加符合面向物件的程式設計體驗。

最後提一下,實現一個音影片功能模組所需的元能力的抽象是沒有標準答案的,理論上來說我們可以將整個功能全部實現在底層,將其包裝成一個元能力節點註冊給 PowerMedia,但顯然這樣做的話,沒有太大意義。同樣的,我們可以非常細粒度的拆分音影片基礎能力,例如將 Renderer 節點再細分為 Makeup 節點和 Filter 節點,單獨負責美顏功能和濾鏡功能;將影片錄製細分為影片編碼、音訊編碼、合流和影片檔案儲存節點。

理論上,更細粒度的元能力節點,更符合單一職責的原則,穩定性更好,可複用性更強,功能解耦的同時,也賦予了 Flutter 側邏輯對於整個功能模組更大的控制力,更能體現 PowerMedia 的優勢。但實踐過程中,元能力節點的粒度需要綜合考慮成本、效能和穩定性,來選擇最佳的方案。

結語

音影片在現代 App 中越來越重要,對於 Flutter 開發來說,音影片場景也是不可或缺的一環,功能邏輯跨端與 UI 跨端同樣重要,這樣才能做到真正的跨平臺開發,提升研發效率。文中提到的技術框架 PowerMedia 已經在閒魚多個場景應用落地,我們也還在不斷完善演進中,如何能使用 PowerMedia 解決更多的 Flutter 音影片場景問題,提升框架的整體效能和穩定性,是我們後續重點演進和提升的方向,同時我們也在不斷沉澱高質量的音影片元能力 Node,豐富節點生態。希望通過本文,能給大家做 Flutter 音影片帶來一些不一樣的視角和啟發。