本文旨在介绍个人在研读源码的时的一些浅薄理解 ,希望能对各位有一些帮助 😄 。本文将对所有可能遇到的知识点或细节进行注解或链接🔗 跳转,以保证各位读者都能看懂。如果文中有说的不对的或者引导方向不正确的,欢迎各位批评指正。欢迎在评论区交流补充,感谢阅读 📖 。
写本文的目的是为了提高自己阅读、提炼源码的能力 🐳 ,优化自己的学习路线。因为平时在工作开发中会有很多碎片化 🧩 、重复化的浅显知识点 💡 ,通过此类源码总结和分析来加深自己技术栈的深度。
随着Flutter
在越来越多大厂的业务落地,大家不难发现,音视频是一块绕不开的业务。短视频、IM、新媒体等相对较重的业务中都会有音视频的身影 👀 ,那么如何通过一个强大的跨平台框架去实现一个强大 💪 、高性能、可控的音视频播放功能呢?我们是否还仅仅停留在使用插件的上层API
🔌 ?相信能耐心看完本文会,你对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
组件。🖼 顺便附上一个简单的结构源码:
const Texture({ Key key, @required this.textureId, }) 复制代码
video_player
是Flutter
官方plugin
中的音视频播放插件,我们不妨以这个插件为例,细看其中的一些端倪。我会通过几部分的个人认为比较关键的源码,给各位点出该插件的实现方案。
由于本人对iOS
相对熟悉,Android
不敢妄自推测,本文的Native
部分也将以iOS
为例。但可以保证每位小伙伴都看得懂,并且看完以后再看Android
部分也是思路清晰(亲测有效🤪 )
首先我们可以看到源码中封装了一个叫FLTVideoPlayer
的类,很显然,如果仅仅是PlatformView
的简单展示,此处无需自己封装如此复杂的一个Player
类,我对类中的方法和参数都做了注解(写文章不易啊,为了大家都能看懂,我给每一行都扣了注释 🤣 ,因为源码比较复杂,用Markdown语法写的代码块看起来很不美观,这边我就直接截图了,方便各位阅读)
注意,其实这个所谓的FLTVideoPlayer
的核心点并不是那个看似亮眼的play
方法💎 ,这里我要给大家介绍的是上面用虚线标出的初始化方法。看源码就可以发现,无论是加载本地A
sset
音频,或是url
的音频,都调用了该方法。附上加载本地音视频代码:
那么这个方法到底做了什么呢?🤔 其实调用的是另一个初始化方法,通过PlayerItem
进行初始化,AVPlayerItem
提供了AVPlayer
播放需要的媒体文件,时间、状态、文件大小等信息,是AVPlayer
媒体文件的载体。这里我们已经可以看出我们应该是要通过载体获取一些视频的信息📜 。
继续追查!🔍 来看看到底这些个初始化方法干了什么,于是我们追到了最下层createVideoOutputAndDisplayLink
方法,我们可以看到我们在FLTVideoPlayer
这个类中定义的好几个变量都被使用了,并且看了源码还能发现videoOutput
、displayLink
仅在此处被赋值,可见他是一个核心的方法。这个方法做了什么?🤯 我来给不熟悉iOS
这边的同学解释一下,我们通过AVPlayerItemVideoOutput
,获得了视频解码后的数据,同时我们开启了一个计时器,进行定时回调,同时我们的定时器CADisplayLink
的回调次数是根据屏幕刷新频率来的,这样我们就达到了一个逐帧获取视频解码后的数据的目的!👏 巧妙~fantastic
!这个数据对我们来说太重要了。
那我们拿到这个数据是怎样一个类型呢?大家可以看到我们下面这个方法中有一个NSDictionary
字典类型,里面定义了我们整体的一个数据结构,包括多个系统级的枚举值kCVPixelFormatType_32BGRA
, kCVPixelBufferPixelFormatTypeKey
, kCVPixelBufferIOSurfacePropertiesKey
,这样一个字典结构设计的目的是什么呢?目的是将我们获取到的数据注入到一个叫CVPixelBuffer
的类当中
CVPixelBufferRef
是一种像素图片类型,属于CoreVideo
模块
以上部分介绍了实现音视频播放插件的基础类,接下来进入到我们的插件具体实现部分🔦 。通俗来说,我们在上面已经获取到了CVPixelBufferRef
类的数据,那我们如何将Native
层的数据传输到Dart
层呢?这就是我们插件要实现的部分。这部分直接贴出核心部分的代码吧。大家可以看到这里,我们选用的PlatformChannel
是EventChannel
, 这个地方为什么不是methodChannel
或者说BasicMessageChannel
,其实答案已经在我们的上文当中了,我们需要将我们获取到的视频数据进行传输,更贴切的是一个流式 🛀 的传输,而EventChannel
就是为了数据流而生的。
再来仔细看看这个方法吧,方法中很显然,我们创建我们的EventChannel
,并没有和以往简单插件一样用固定的channelName
,此处我们的channel
和我们的textureId
相关。为什么这么设计呢?其实是为了我们的多窗口播放功能,也就是在插件的example
展示的一个界面中多个播放画面的效果,其实这一类的设计还可以应用在视频通话实现中的多窗口会话 📡 ,说白了就是可以在Flutter
中对应多个不同的Widget
。
有关Dart
方面的具体实现策略也是主要通过EventChannel
实现的,在EventChannel
中会加入插件中支持的feature
,包括暂停,轮播等。但是核心给大家介绍的也是如何和Native
层建立链接。我们在Dart
层来仔细探究一下实现方法。(方法层层嵌套,设计非常巧妙,大家可以跟着我的思路来找一找🔩 )。我们首先肯定可以根据iOS
中找到的EventChannel
名字去找一下这个Channel
🔫
我们首先找到了我们的EventChannel
定义处。看起来一切正常,唯一最大的疑问是,textureId
是怎么拿到的呢?是如何去和原生建立连接的呢?咱们继续往上找,该方法的调用在一个MethodChannelVideoPlayer
类的方法中调用,但还是看不出来textureId
的来源。
OK,那就继续找,继续找此处videoEventsFor
的调用点,但还是看不出来!仅仅看出来传入了一个私有变量 🔬 ,很巧合的也叫textureId
.
那么目标 📮 又变了,我们现在要找的是_textureId
的赋值点,我们就找到了这里!
点击跳转到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
呢?这里引用一张图可以让各位更好的了解。
作者:多肉葡萄五分糖