阅读 3067
Flutter 音视频播放器的实现思路及设计理念 🔮 —— 教你手撕源码

Flutter 音视频播放器的实现思路及设计理念 🔮 —— 教你手撕源码

  • 本文旨在介绍个人在研读源码的时的一些浅薄理解 ,希望能对各位有一些帮助 😄 。本文将对所有可能遇到的知识点或细节进行注解或链接🔗 跳转,以保证各位读者都能看懂。如果文中有说的不对的或者引导方向不正确的,欢迎各位批评指正。欢迎在评论区交流补充,感谢阅读 📖 。

为什么要写本文

   写本文的目的是为了提高自己阅读、提炼源码的能力 🐳 ,优化自己的学习路线。因为平时在工作开发中会有很多碎片化 🧩 、重复化的浅显知识点 💡 ,通过此类源码总结和分析来加深自己技术栈的深度。

为什么是音视频播放器 📽

   随着Flutter在越来越多大厂的业务落地,大家不难发现,音视频是一块绕不开的业务。短视频、IM、新媒体等相对较重的业务中都会有音视频的身影 👀 ,那么如何通过一个强大的跨平台框架去实现一个强大 💪 、高性能、可控的音视频播放功能呢?我们是否还仅仅停留在使用插件的上层API 🔌 ?相信能耐心看完本文会,你对Flutter上的音视频实现会比之前有更深入的理解。

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

  video_playerFlutter官方plugin中的音视频播放插件,我们不妨以这个插件为例,细看其中的一些端倪。我会通过几部分的个人认为比较关键的源码,给各位点出该插件的实现方案。

Native Source Code 🍎

  由于本人对iOS相对熟悉,Android不敢妄自推测,本文的Native部分也将以iOS为例。但可以保证每位小伙伴都看得懂,并且看完以后再看Android部分也是思路清晰(亲测有效🤪 )

FLTVideoPlayer

  首先我们可以看到源码中封装了一个叫FLTVideoPlayer的类,很显然,如果仅仅是PlatformView的简单展示,此处无需自己封装如此复杂的一个Player类,我对类中的方法和参数都做了注解(写文章不易啊,为了大家都能看懂,我给每一行都扣了注释 🤣 ,因为源码比较复杂,用Markdown语法写的代码块看起来很不美观,这边我就直接截图了,方便各位阅读)



  注意,其实这个所谓的FLTVideoPlayer的核心点并不是那个看似亮眼的play方法💎 ,这里我要给大家介绍的是上面用虚线标出的初始化方法。看源码就可以发现,无论是加载本地Asset音频,或是url的音频,都调用了该方法。附上加载本地音视频代码:



  那么这个方法到底做了什么呢?🤔 其实调用的是另一个初始化方法,通过PlayerItem进行初始化,AVPlayerItem提供了AVPlayer播放需要的媒体文件,时间、状态、文件大小等信息,是AVPlayer媒体文件的载体。这里我们已经可以看出我们应该是要通过载体获取一些视频的信息📜 。

  继续追查!🔍 来看看到底这些个初始化方法干了什么,于是我们追到了最下层createVideoOutputAndDisplayLink方法,我们可以看到我们在FLTVideoPlayer这个类中定义的好几个变量都被使用了,并且看了源码还能发现videoOutputdisplayLink仅在此处被赋值,可见他是一个核心的方法。这个方法做了什么?🤯 我来给不熟悉iOS这边的同学解释一下,我们通过AVPlayerItemVideoOutput,获得了视频解码后的数据,同时我们开启了一个计时器,进行定时回调,同时我们的定时器CADisplayLink的回调次数是根据屏幕刷新频率来的,这样我们就达到了一个逐帧获取视频解码后的数据的目的!👏 巧妙~fantastic!这个数据对我们来说太重要了。
  那我们拿到这个数据是怎样一个类型呢?大家可以看到我们下面这个方法中有一个NSDictionary字典类型,里面定义了我们整体的一个数据结构,包括多个系统级的枚举值kCVPixelFormatType_32BGRA, kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferIOSurfacePropertiesKey,这样一个字典结构设计的目的是什么呢?目的是将我们获取到的数据注入到一个叫CVPixelBuffer的类当中

  • CVPixelBufferRef 是一种像素图片类型,属于CoreVideo 模块

FLTVideoPlayerPlugin

  以上部分介绍了实现音视频播放插件的基础类,接下来进入到我们的插件具体实现部分🔦 。通俗来说,我们在上面已经获取到了CVPixelBufferRef类的数据,我们只需要将这部分数据存储到内存中,就可以通过映射的方式Flutter页面中展示出来,但是仅仅展示肯定不是一个完整的音视频播放功能。我们依旧需要建立连接,传递我们的点击事件、用户交互等消息以及一些基础的数据流。这就是我们插件要实现的部分。这部分直接贴出核心部分的代码吧。大家可以看到这里,我们选用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的赋值点,我们就找到了这里!

  点击跳转到create方法的实现,哦豁!🤩 看到这个美丽的注解了吗,我们在这里初始化VideoPlayer,同时返回他的textureId。结束了?No ~ No ~,不觉得这个方法很可疑吗,仅仅只有一个报错处理?如何实现所描述的功能?

  于是我们肯定要想,是有extends存在⛓ ,果然!在VideoPlayerPlatformextendsMethodChannelVideoPlayer中找到了实现方法,走到这一步,终于有点眉目了,但仍然没有结束,看其中的回调,来自的是_api.create()方法,这个方法又做了什么呢?首先我们找到我们的_api其实是VideoPlayerApi()类。

  终于,我们到达尽头,尽头是一个BasicMessageChannel,我们在这里通过BasicMessageChannelFlutterNative层进行通信,在其中回调我们的textureId。至此,谜底全部解开。能看到这里的读者应该给自己点一个赞 👍 。

总结

  本文主要给各位介绍了Flutter中实现音视频的一种方案 🤗 ,外接纹理(Texture),这也是Flutter官方视频插件所采用的方案。应该也颠覆了各位以往对Flutter插件的一些理解。再来回忆一下整个流程:iOSCVPixelBufferRef将渲染出来的数据存在内存中,Flutter engine会将Texture的数据在内存中直接进行映射无需通过Channel传输,然后Texture Widget就可以把你提供的这些数据显示出来。在我们传输数据的时候会需要将其与 TextureID 绑定,绑定的过程通过BasicMessageChannel实现数据流的传输,以做到实时展示的效果 🚀。附上一张流程图,方便大家理解:

  那么我们在选择实现方案时是选择PlatformView还是Texture呢?这里引用一张图可以让各位更好的了解。

参考文献

文章分类
前端
文章标签