爲何要寫本文
寫本文的目的是爲了提升本身閱讀、提煉源碼的能力 🐳 ,優化本身的學習路線。由於平時在工做開發中會有不少碎片化 🧩 、重複化的淺顯知識點 💡 ,經過此類源碼總結和分析來加深本身技術棧的深度。git
爲何是音視頻播放器 📽
隨着Flutter
在愈來愈多大廠的業務落地,你們不難發現,音視頻是一塊繞不開的業務。短視頻、IM、新媒體等相對較重的業務中都會有音視頻的身影 👀 ,那麼如何經過一個強大的跨平臺框架去實現一個強大 💪 、高性能、可控的音視頻播放功能呢?咱們是否還僅僅停留在使用插件的上層API
🔌 ?相信能耐心看完本文會,你對Flutter
上的音視頻實現會比以前有更深刻的理解。github
Flutter 音頻播放器的兩種實現思路 🤔
開始以前,你們能夠先思考一下若是是你來作一個Flutter
的視頻播放器,你會如何去實現?你會遇到哪些困難呢?帶着問題來看文章每每會更有收穫 🔖 。可能很大一部分同窗都會和我同樣首先跳出來一個詞 —— PlatformView
🔍 。確實,PlatformView
看起來是個不錯的方案,PlatformView
做爲一個在Flutter 1.0
即發佈的技術方案,算是比較成熟且上手較快的方案。但很顯然,今天咱們的主角不是它 😅 ,爲何不是這個可愛的方案呢?請你們思考這樣一個業務場景:
好比咱們想調用攝像頭 📷 來拍照或錄視頻,但在拍照和錄視頻的過程當中咱們須要將預覽畫面顯示到咱們的Flutter UI
中,若是咱們要用Flutter
定義的消息通道機制來實現這個功能,就須要將攝像頭採集的每一幀圖片都要從原生傳遞到Flutter
中,這樣作代價將會很是大,由於將圖像或視頻數據經過消息通道實時傳輸必然會引發內存和CPU
的巨大消耗!—— Flutter中文網
也正是由於有這個業務場景,可能咱們今天的主角就要登場了💥 ——Texture
(外接紋理),會不會有很大一部分好兄弟一臉懵逼?
簡單的介紹一下:Texture
能夠理解爲GPU
內保存將要繪製的圖像數據的一個對象,Flutter engine
會將Texture
的數據在內存中直接進行映射(而無需在原生和Flutter
之間再進行數據傳遞),Flutter
會給每個Texture
分配一個id
,同時Flutter
中提供了一個Texture
組件。🖼 順便附上一個簡單的結構源碼:api
const Texture({ Key key, @required this.textureId, }) 複製代碼
video_player
video_player
是Flutter
官方plugin
中的音視頻播放插件,咱們不妨以這個插件爲例,細看其中的一些端倪。我會經過幾部分的我的認爲比較關鍵的源碼,給各位點出該插件的實現方案。數據結構
Native Source Code 🍎
因爲本人對iOS
相對熟悉,Android
不敢妄自推測,本文的Native
部分也將以iOS
爲例。但能夠保證每位小夥伴都看得懂,而且看完之後再看Android
部分也是思路清晰(親測有效🤪 )框架
FLTVideoPlayer
首先咱們能夠看到源碼中封裝了一個叫FLTVideoPlayer
的類,很顯然,若是僅僅是PlatformView
的簡單展現,此處無需本身封裝如此複雜的一個Player
類,我對類中的方法和參數都作了註解(寫文章不易啊,爲了你們都能看懂,我給每一行都扣了註釋 🤣 ,由於源碼比較複雜,用Markdown語法寫的代碼塊看起來很不美觀,這邊我就直接截圖了,方便各位閱讀)
注意,其實這個所謂的FLTVideoPlayer
的核心點並非那個看似亮眼的play
方法💎 ,這裏我要給你們介紹的是上面用虛線標出的初始化方法。看源碼就能夠發現,不管是加載本地Asset
音頻,或是url
的音頻,都調用了該方法。附上加載本地音視頻代碼:
那麼這個方法到底作了什麼呢?🤔 其實調用的是另外一個初始化方法,經過PlayerItem
進行初始化,AVPlayerItem
提供了AVPlayer
播放須要的媒體文件,時間、狀態、文件大小等信息,是AVPlayer
媒體文件的載體。這裏咱們已經能夠看出咱們應該是要經過載體獲取一些視頻的信息📜 。ide
繼續追查!🔍 來看看到底這些個初始化方法幹了什麼,因而咱們追到了最下層createVideoOutputAndDisplayLink
方法,咱們能夠看到咱們在FLTVideoPlayer
這個類中定義的好幾個變量都被使用了,而且看了源碼還能發現videoOutput
、displayLink
僅在此處被賦值,可見他是一個核心的方法。這個方法作了什麼?🤯 我來給不熟悉iOS
這邊的同窗解釋一下,咱們經過AVPlayerItemVideoOutput
,得到了視頻解碼後的數據,同時咱們開啓了一個計時器,進行定時回調,同時咱們的定時器CADisplayLink
的回調次數是根據屏幕刷新頻率來的,這樣咱們就達到了一個逐幀獲取視頻解碼後的數據的目的!👏 巧妙~fantastic
!這個數據對咱們來講過重要了。
那咱們拿到這個數據是怎樣一個類型呢?你們能夠看到咱們下面這個方法中有一個NSDictionary
字典類型,裏面定義了咱們總體的一個數據結構,包括多個系統級的枚舉值kCVPixelFormatType_32BGRA
, kCVPixelBufferPixelFormatTypeKey
, kCVPixelBufferIOSurfacePropertiesKey
,這樣一個字典結構設計的目的是什麼呢?目的是將咱們獲取到的數據注入到一個叫CVPixelBuffer
的類當中性能
CVPixelBufferRef
是一種像素圖片類型,屬於CoreVideo
模塊
FLTVideoPlayerPlugin
以上部分介紹了實現音視頻播放插件的基礎類,接下來進入到咱們的插件具體實現部分🔦 。通俗來講,咱們在上面已經獲取到了CVPixelBufferRef
類的數據,那咱們如何將Native
層的數據傳輸到Dart
層呢?這就是咱們插件要實現的部分。這部分直接貼出核心部分的代碼吧。你們能夠看到這裏,咱們選用的PlatformChannel
是EventChannel
, 這個地方爲何不是methodChannel
或者說BasicMessageChannel
,其實答案已經在咱們的上文當中了,咱們須要將咱們獲取到的視頻數據進行傳輸,更貼切的是一個流式 🛀 的傳輸,而EventChannel
就是爲了數據流而生的。
再來仔細看看這個方法吧,方法中很顯然,咱們建立咱們的EventChannel
,並無和以往簡單插件同樣用固定的channelName
,此處咱們的channel
和咱們的textureId
相關。爲何這麼設計呢?實際上是爲了咱們的多窗口播放功能,也就是在插件的example
展現的一個界面中多個播放畫面的效果,其實這一類的設計還能夠應用在視頻通話實現中的多窗口會話 📡 ,說白了就是能夠在Flutter
中對應多個不一樣的Widget
。
學習
Flutter Source Code
有關Dart
方面的具體實現策略也是主要經過EventChannel
實現的,在EventChannel
中會加入插件中支持的feature
,包括暫停,輪播等。可是核心給你們介紹的也是如何和Native
層創建連接。咱們在Dart
層來仔細探究一下實現方法。(方法層層嵌套,設計很是巧妙,你們能夠跟着個人思路來找一找🔩 )。咱們首先確定能夠根據iOS
中找到的EventChannel
名字去找一下這個Channel
🔫優化
咱們首先找到了咱們的EventChannel
定義處。看起來一切正常,惟一最大的疑問是,textureId
是怎麼拿到的呢?是如何去和原生創建鏈接的呢?我們繼續往上找,該方法的調用在一個MethodChannelVideoPlayer
類的方法中調用,但仍是看不出來textureId
的來源。
OK,那就繼續找,繼續找此處videoEventsFor
的調用點,但仍是看不出來!僅僅看出來傳入了一個私有變量 🔬 ,很巧合的也叫textureId
.
那麼目標 📮 又變了,咱們如今要找的是_textureId
的賦值點,咱們就找到了這裏!ui
點擊跳轉到create
方法的實現,哦豁!🤩 看到這個美麗的註解了嗎,咱們在這裏初始化VideoPlayer
,同時返回他的textureId
。結束了?No ~ No ~,不以爲這個方法很可疑嗎,僅僅只有一個報錯處理?如何實現所描述的功能?
因而咱們確定要想,是有extends
存在⛓ ,果真!在VideoPlayerPlatform
的extends
類MethodChannelVideoPlayer
中找到了實現方法,走到這一步,終於有點眉目了,但仍然沒有結束,看其中的回調,來自的是_api.create()
方法,這個方法又作了什麼呢?首先咱們找到咱們的_api
實際上是VideoPlayerApi()
類。
終於,咱們到達盡頭,盡頭是一個BasicMessageChannel
,咱們在這裏經過BasicMessageChannel
在Flutter
和Native
層進行通訊,在其中回調咱們的textureId
。至此,謎底所有解開。能看到這裏的讀者應該給本身點一個贊 👍 。
總結
本文主要給各位介紹了Flutter中實現音視頻的一種方案 🤗 ,外接紋理(Texture
),這也是Flutter
官方視頻插件所採用的方案。應該也顛覆了各位以往對Flutter
插件的一些理解。再來回憶一下整個流程:iOS
用CVPixelBufferRef
將渲染出來的數據存在內存中,Flutter engine
會將Texture
的數據在內存中直接進行映射無需經過Channel
傳輸,而後Texture Widget
就能夠把你提供的這些數據顯示出來。在咱們傳輸數據的時候會須要將其與 TextureID
綁定,綁定的過程經過BasicMessageChannel
實現數據流的傳輸,以作到實時展現的效果 🚀。附上一張流程圖,方便你們理解:
那麼咱們在選擇實現方案時是選擇PlatformView
仍是Texture
呢?這裏引用一張圖可讓各位更好的瞭解。