Flutter 移動端屏幕采集方案分享
現(xiàn)如今隨著 Flutter 的應(yīng)用越來越廣泛,純 Flutter 項目也越來越多,本篇內(nèi)容主要分享的是 Flutter 移動端(iOS + Android)的屏幕采集的實現(xiàn)。
概述
在視頻會議、線上課堂、游戲直播等場景,屏幕共享是一個最常見的功能。屏幕共享就是對屏幕畫面的實時共享,端到端主要有幾個步驟:錄屏采集、視頻編碼及封裝、實時傳輸、視頻解封裝及解碼、視頻渲染。
一般來說,實時屏幕共享時,共享發(fā)起端以固定采樣頻率(一般 8 - 15幀足夠)抓取到屏幕中指定源的畫面(包括指定屏幕、指定區(qū)域、指定程序等),經(jīng)過視頻編碼壓縮(應(yīng)選擇保持文本/圖形邊緣信息不失真的方案)后,在實時網(wǎng)絡(luò)上以相應(yīng)的幀率分發(fā)。
因此,屏幕采集是實現(xiàn)實時屏幕共享的基礎(chǔ),它的應(yīng)用場景也是非常廣泛的。
實現(xiàn)
準備
首先我們看看原生系統(tǒng)提供了哪些能力來進行屏幕錄制。
iOS 11.0 提供了
ReplayKit 2用于采集跨 App 的全局屏幕內(nèi)容,但僅能通過控制中心啟動;iOS 12.0 則在此基礎(chǔ)上提供了從 App 內(nèi)啟動 ReplayKit 的能力。Android 5.0 系統(tǒng)提供了
MediaProjection功能,只需彈窗獲取用戶的同意即可采集到全局屏幕內(nèi)容。
我們再看一下 Android / iOS 的屏幕采集能力有哪些區(qū)別。
iOS 的
ReplayKit是通過啟動一個Broadcast Upload Extension子進程來采集屏幕數(shù)據(jù),需要解決主 App 進程與屏幕采集子進程之間的通信交互問題,同時,子進程還有諸如運行時內(nèi)存最大不能超過 50M 的限制。Android 的
MediaProjection是直接在 App 主進程內(nèi)運行的,可以很容易獲取到屏幕數(shù)據(jù)的Surface。
解決方案
雖然無法避免原生代碼,但我們可以盡量以最少的原生代碼來實現(xiàn) Flutter 屏幕采集。將兩端的屏幕采集能力抽象封裝為通用的 Dart 層接口,只需一次部署完成后,就能開心地在 Dart 層啟動、停止屏幕采集了。
iOS
打開 Runner Xcode Project,新建一個 Broadcast Upload Extension Target,在此處理 ReplayKit 子進程的業(yè)務(wù)邏輯。
首先需要處理主 App 進程與 ReplayKit 子進程的跨進程通信問題,由于屏幕采集的 audio/video buffer 回調(diào)非常頻繁,出于性能與 Flutter 插件生態(tài)考慮,在原生側(cè)處理音視頻 buffer 顯然是目前最靠譜的方案,那剩下要解決的就是啟動、停止信令以及必要的配置信息的傳輸了。
對于啟動 ReplayKit 的操作,可以通過 Flutter 的 MethodChannel 在原生側(cè) new 一個 RPSystemBroadcastPickerView,這是一個系統(tǒng)提供的 View,包含一個點擊后直接彈出啟動屏幕采集窗口的 Button。通過遍歷 Sub View 的方式找到 Button 并觸發(fā)點擊操作,便解決了啟動 ReplayKit 的問題。
然后是配置信息的同步問題。
一是使用 iOS 的
App Group能力,通過 NSUserDefaults 持久化配置在進程間共享配置信息,分別在RunnerTarget 和Broadcast Upload ExtensionTarget 內(nèi)開啟 App Group 能力并設(shè)置同一個 App Group ID,然后就能通過-[NSUserDefaults initWithSuiteName]讀寫此 App Group 內(nèi)的配置了。二是使用跨進程通知
CFNotificationCenterGetDarwinNotifyCenter攜帶配置信息來實現(xiàn)進程間通信。
接下來是停止 ReplayKit 的操作。也是使用上述的 CFNotification 跨進程通知,在 Flutter 主 App 發(fā)起結(jié)束屏幕采集的通知,ReplayKit 子進程接收到通知后調(diào)用 -[RPBroadcastSampleHandler finishBroadcastWithError:] 來結(jié)束屏幕采集。

Android
啟動屏幕采集時,直接通過 Flutter 的 MethodChannel 在原生側(cè)通過 MediaProjectionManager 彈出一個向用戶請求屏幕采集權(quán)限的彈窗,收到確認后即可調(diào)用 MediaProjectionManager.getMediaProjection() 函數(shù)拿到 MediaProjection 對象。
然后根據(jù)業(yè)務(wù)場景需求從屏幕采集 buffer 的消費者拿到 Surface,例如,要保存屏幕錄制的話,從 MediaRecoder 拿到 Surface,要錄屏直播的話,可調(diào)用音視頻直播 SDK 的接口獲取 Surface。
有了 MediaProjection 和消費者的 Surface,接下來就是調(diào)用 MediaProjection.createVirtualDisplay() 函數(shù)傳入 Surface 來創(chuàng)建 VirtualDisplay 實例,從而獲取到屏幕采集 buffer。
最后是結(jié)束屏幕采集,相比 iOS 復(fù)雜的操作,Android 僅需要將 VirtualDisplay 和 MediaProjection 實例對象釋放即可。
實戰(zhàn)示例
一個實現(xiàn)了 iOS/Android 屏幕采集并使用 ZEGO Express Audio and Video Flutter SDK 進行推流直播的示例 Demo。
https://github.com/zegoim/zego-express-example-screen-capture-flutter