ExoPlayer架构详解与源码分析(4)——整体架构

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构


文章目录

  • 系列文章目录
  • 前言
  • Player的实现
    • BasePlayer
    • ExoPlayer
  • 线程模型
  • 总结


前言

根据前篇ExoPlayer架构详解与源码分析(2)——Player,想要直接实现Player接口需要非常复杂的代码逻辑,都写在一个类里肯定不现实,需要通过更多层次的扩展简化来实现,当然ExoPlayer就是这么做的,本篇来讲讲的如何通过BasePlayer来简化设计以及ExoPlayer如何将整个复杂的设计划分给一个个子系统来完成的。

Player的实现

先来看下整体架构
在这里插入图片描述
Player接口经过了一层BasePlayer简化,和ExoPlayer扩展。然后由ExoPlayerImpl实现,ExoPlayerImpl内部又依赖ExoPlayerImplInternal,ExoPlayerImplInternal再依据功能划分将任务交由各个组件,主要为MediaSource、Renderer、TrackSelector、LoadControl四大组件。

BasePlayer

先说BasePlayer 是个抽象类,主要作用是简化了Player接口的部分功能。

  • 实现了单文件列表增删改等操作,通过将单个MediaItem转为List,交由xxMediaItems实现。

      @Overridepublic final void setMediaItem(MediaItem mediaItem) {setMediaItems(ImmutableList.of(mediaItem));}
    
  • 实例化出Timeline 中的 Window对象,这里主要用于Timeline getWindow 方法时装填的容器,因为Timeline 本身不持有Window或者Period,Timeline获取Window或者Period时都需要传入一个容器去获取,通过调用容器的set方法给容器赋值。

      protected BasePlayer() {window = new Timeline.Window();}@Overridepublic final long getContentDuration() {//获取播放的总时长Timeline timeline = getCurrentTimeline();//先获取Timeline 由子类实现return timeline.isEmpty()? C.TIME_UNSET//将初始化的window对象传入,方法里会将window对象赋值: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();}
    
  • 实现了Player关于播放列表管理的设计,将MediaItem 播放列表查询相关交由Timeline管理,从这里可以看出上面针对MediaItem 的增删改,最终都是会封装到或者同步到Timeline里的,这里后面看到具体实现。

    @Overridepublic final int getNextMediaItemIndex() {Timeline timeline = getCurrentTimeline();return timeline.isEmpty()? C.INDEX_UNSET: timeline.getNextWindowIndex(getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());}@Override@Nullablepublic final MediaItem getCurrentMediaItem() {Timeline timeline = getCurrentTimeline();return timeline.isEmpty()? null: timeline.getWindow(getCurrentMediaItemIndex(), window).mediaItem;}@Overridepublic final int getMediaItemCount() {return getCurrentTimeline().getWindowCount();}@Overridepublic final MediaItem getMediaItemAt(int index) {return getCurrentTimeline().getWindow(index, window).mediaItem;}
    
  • 基于Timeline将各种媒体的导航操作,如上一曲,下一曲,SEEK等,统一到自己抽象出的一个seekTo方法中。

    @Overridepublic final void seekToNextMediaItem() {seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);}private void seekToNextMediaItemInternal(@Player.Command int seekCommand) {int nextMediaItemIndex = getNextMediaItemIndex();if (nextMediaItemIndex == C.INDEX_UNSET) {return;}if (nextMediaItemIndex == getCurrentMediaItemIndex()) {repeatCurrentMediaItem(seekCommand);} else {seekToDefaultPositionInternal(nextMediaItemIndex, seekCommand);}}private void repeatCurrentMediaItem(@Player.Command int seekCommand) {seekTo(getCurrentMediaItemIndex(),/* positionMs= */ C.TIME_UNSET,seekCommand,/* isRepeatingCurrentItem= */ true);}private void seekToDefaultPositionInternal(int mediaItemIndex, @Player.Command int seekCommand) {seekTo(mediaItemIndex,/* positionMs= */ C.TIME_UNSET,seekCommand,/* isRepeatingCurrentItem= */ false);}@Overridepublic final int getNextMediaItemIndex() {Timeline timeline = getCurrentTimeline();return timeline.isEmpty()? C.INDEX_UNSET//通过Timeline获取下一个索引: timeline.getNextWindowIndex(getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());}/*** Seek到指定的MediaItem中的指定位置** @param mediaItemIndex MediaItem 的索引,可以理解成播放列表中的第几个* @param positionMs MediaItem 中的位置* @param seekCommand Seek 的类型用于权限控制,这里可以不用考虑* @param isRepeatingCurrentItem 是否重复当前播放项目*/public abstract void seekTo(int mediaItemIndex,long positionMs,@Player.Command int seekCommand,boolean isRepeatingCurrentItem);
  • 完成了其他一些可以通过已有方法实现的方法。

      //判断当前命令是否可用,对应Player设计的第2点@Overridepublic final boolean isCommandAvailable(@Command int command) {return getAvailableCommands().contains(command);//通过已有的getAvailableCommands来实现,getAvailableCommands由子类实现}//播放和暂停,实现了Player关于playWhenReady的设计,playWhenReady就是一个标记位,标记用户的一个播放意图//所以这里的play并不是立即开始播放的意思,而是调用者希望开始播放,实际播放要等到PlaybackState=STATE_READY的时候,pause同上@Overridepublic final void play() {setPlayWhenReady(true);}//实现了Player关于isPlaying的设计@Overridepublic final boolean isPlaying() {return getPlaybackState() == Player.STATE_READY&& getPlayWhenReady()&& getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;}//获取直播流的延时@Overridepublic final long getCurrentLiveOffset() {Timeline timeline = getCurrentTimeline();if (timeline.isEmpty()) {return C.TIME_UNSET;}long windowStartTimeMs =timeline.getWindow(getCurrentMediaItemIndex(), window).windowStartTimeMs;if (windowStartTimeMs == C.TIME_UNSET) {return C.TIME_UNSET;}//获取当前播放时间和实际实际的差值,使用当前时间(取服务端的实时时间如果可用)-(播放开始时间+已播放位置【含广告】)return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();}//获取缓冲百分比@Overridepublic final int getBufferedPercentage() {long position = getBufferedPosition();long duration = getDuration();return position == C.TIME_UNSET || duration == C.TIME_UNSET? 0: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);}
    

综上所述,BasePlay实现了部分Player接口的设计,简化了Player接口实现,为后续的子类铺平道路。

ExoPlayer

一个接口定义,继承扩展了Player接口,实现MediaSource的播放。ExoPlayer播放器本体设计都在这里了,将Player接口复杂的设计,通过建立一个运行框架,将功能分散到各个子系统,协调这些子系统完成播放器的播放等最初的设计目标。
我们将文章开头的架构图进一步扩充下。
在这里插入图片描述

ExoPlayer 设计理念之一就是高度可定制化,主要任务是协调各个组件间工作,而对媒体的类型、存储方式、加载方式、如何展示等并不关心。ExoPlayer并不直接实现媒体的加载与渲染,而是将这些工作交给播放器创建或者准备时注入的组件,这些组件包括:

  • MediaSource
    • 主要作用是定义需要播放的媒体基本信息、加载媒体以及定义了从哪里读取已经加载的媒体数据。
    • 通过将MediaItems传入MediaSource.Factory(播放器创建时指定)创建,也可以直接调用setMediaSource方法创建。
    • 播放器默认提供了 DefaultMediaSourceFactory可以根据不同类型的MediaItem创建出不同的MediaSource,包括progressive ,HLS,DASH,SmoothStreaming 。
  • Renderers
    • 包含了用于渲染媒体的各个组件。
    • 提供了像MediaCodecVideoRenderer, MediaCodecAudioRenderer, TextRenderer and MetadataRenderer这些组件用于常见媒体的渲染。
    • Renderer 使用MediaSource提供的数据来渲染。
    • 可以通过ExoPlayer接口提供的getRendererCount获取渲染器的数量,getRendererType获取各自轨道类型。
  • TrackSelector
    • 用于选择由MediaSource提供的可用于渲染器的轨道。
    • 播放器在创建时默认注入了DefaultTrackSelector,可以用于大部分情况的轨道选择 。
  • LoadControl
    • 主要用于控制MediaSource何时缓冲更多媒体数据以及缓冲多少数据。
    • 播放器在创建时默认注入了DefaultLoadControl,可以用于大部分情况的数据加载 。

上面的组件在创建ExoPlayer 时都会注入一个默认的实现,当默认组件无法满足需求时,可以通过自定义的组件来构建播放器。如可以通过设置自定义的LoadControl来更改播放器默认的缓存加载策略,或者通过添加子当以的Renderer来支持Android本身不支持的视频编码格式。

上图可以看到不光ExoPlayer使用了注入组件的概念,上面列出ExoPlayer组件本身就和ExoPlayer一样也使用了组件注入的概念,这些组件本身也是由子组件注入创建而来的,将这些的组件本地的功能又再一次细化分配给各自的子组件来完成,并且这些子组件同样也支持自定义。如上图,默认的在创建MediaSource时就需要注入一个或者多个DataSource 工厂,通过提供不同的DataSource工厂,可以从不同的数据源加载数据。基于这种设计思路下的系统共同打造了一个高度可定制化的ExoPlayer。

线程模型

下图展示了ExoPlayer的线程模型
在这里插入图片描述
可以看出播放器线程主要分为3部分

  • application thread
    • 应用线程只有一个 ,大部分情况是应用的主线程,对应Android的UI线程。
    • 如果使用了ExoPlayer 的UI库或者IMA库也要使用应用的主线程。
    • 可以通过在创建播放器时传递“Looper”来显式指定用于访问 ExoPlayer 实例的线程,如果未指定“Looper”,则使用创建播放器的线程的“Looper”,或者如果该线程没有“Looper”,则使用应用程序主线程的“Looper”。无论哪种情况,都要可以通过Player接口定义的getApplicationLooper获取到访问播放器线程的“Looper”。
    • 由于是主线程应用可以直接在主线程中获取播放器的相关信息,这些信息通常保存在ExoPlayerImpl中无需异步回调即可立刻获取到数据,这也符合ExoPlayer架构详解与源码分析(2)——Player中关于Player的设计。
    • 已注册的监听都是在主线程(通过getApplicationLooper获取)中回调的,这就意味着组测这些监听的地方也必须在同一个主线程中。对于监听类的回调这些都是异步的,这个回调最终会使用主线程的Handler分发到主线程里,这也是为什么创建ExoPlayer是必须要指定主线程的原因。
  • internal playback thread
    • 一个播放器实例只有一个,主要负责播放。renderer、MediaSources、TrackSelectors 和 LoadControls 等注入到播放器组件都是在这个线程里调用的。
    • 这个线程也是一个Looper线程,有一个Handler用于将主线程的请求发送到Looper里进行分发。
    • 当应用程序在播放器上执行操作(如Seek)时,消息会通过主线程持有的Handler发送到内部播放线程的Looper然后分发到内部线程里,并在内部播放线程里调用相关方法执行相应的操作。类似地,当内部播放线程上发生播放事件时,消息将通过另一个Handler分发到主线程。主线程使用队列中的消息,更新应用程序可见状态并调用相应的监听回调。
    • 这部分Exoplayer实现在ExoPlayerImplInternal中,在其初始化过程中创建了一个HandlerThread来实现后面会讲到。
  • background threads
    • 各个注入到ExoPlayer中组件的后台线程,会有多个。
    • 注入的播放器组件可以使用额外的后台线程执行任务。例如,MediaSource 可以使用后台线程来加载数据。这些线程都是由不同的MediaSource实现决定的。

总结

可以看到EoxPlayer架构的高度可定制化,基本每一个组件都可以在创建时自定义,然后注入到播放器中实现自定义的播放器。
EoxPlayer这些设计在后续的分析中都会体现,按顺序下篇应该了解下ExoPlayerImpl和ExoPlayerImplInternal,但是他们中很多功能都是依赖于4大组件的,而且4大组件直接又是相互独立的,所以计划后面几篇先把它的4大组件分析下,最后通过分析ExoPlayerImpl和ExoPlayerImplInternal将前面将的4大组件串联起来,了解ExoPlayerImpl和ExoPlayerImplInternal是如何协调这些组件完成播放的。下篇预计先从最复杂的组件MediaSource开始分析。


版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/129068.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C/C++】结构体内存分配问题

规则1:以多少个字节为单位开辟内存 就是说,该结构体最终所占字节大小,是这个单位的整数倍 给结构体变量分配内存的时候,会去结构体变量中找基本类型的成员 哪个基本类型的成员占字节数多,就以它大大小为单位开辟内存 …

竞赛选题 深度学习 python opencv 火焰检测识别 火灾检测

文章目录 0 前言1 基于YOLO的火焰检测与识别2 课题背景3 卷积神经网络3.1 卷积层3.2 池化层3.3 激活函数:3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 YOLOV54.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 数据集准备5.1 数…

快手商品数据整合API|获取快手商品详情数据价格销量主图宝贝链接

接口名称:ks.item_get 接口路径:https://api-seaver.cn/ks/item_get 功能介绍: 通过调用此API可获取商品详情数据,包括商品ID、宝贝标题、商品简介、价格、原价、掌柜昵称、库存、宝贝链接、宝贝图片、品牌名称、商品详情、商品…

Visual Studio 2022 修改字符集的方法

在射频识别技术课程实验过程中发现的报错问题,搞了半天才找到原因,是字符集设置有问题。下图为报错: 根本原因是默认的字符编码集是Unicode。 改成使用多字节字符集就好了。以下为修改方法。

vue3 antv 静态登录页面

效果图 <template> <!-- 内容区域 --><div class"main"><div class"from"><!-- 表单 model是antv里边的绑定表单数据 --><a-form :model"formState" ref"formRef"><!-- 切换 --><a-tabs…

手写模拟SpringBoot核心流程

通过手写模拟实现一个Spring Boot&#xff0c;让大家能以非常简单的方式就能知道Spring Boot大概是如何工作的。 依赖 建一个工程&#xff0c;两个Module: 1.springboot模块&#xff0c;表示springboot框架的源码实现 2.user包&#xff0c;表示用户业务系统&#xff0c;用来写…

【刷题】只出现一次的数字(三种解法)

【刷题】只出现一次的数字 文章目录 【刷题】只出现一次的数字解法异或运算解法一 : 异或运算解法二:集合类Set集合Map集合 链接: https://www.nowcoder.com/share/jump/2008263481696810321082 https://leetcode.cn/problems/single-number/description/ 题目描述 给定一个整…

基于Java的民宿管理系统设计与实现(源码+lw+部署文档+讲解等)(民宿预约、民宿预订、民宿管理、酒店预约通用)

文章目录 前言具体实现截图论文参考详细视频演示代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技…

运行软件找不到mfc140u.dll怎么解决,mfc140u.dll是什么文件

"找不到 mfc140u.dll"是一条错误信息&#xff0c;表示您的计算机上缺少一个名为 mfc140u.dll 的动态链接库&#xff08;DLL&#xff09;文件。这个文件通常与 Microsoft Visual C Redistributable 相关。Mfc140u.dll 是 Microsoft 基础类库&#xff08;MFC&#xff0…

Linux: 基础IO

学习目标 1.C接口与系统调用接口的差别 2.文件描述符, 重定向, 一切皆文件, 缓冲区 3.fd与FILE, 系统调用和库函数的关系 4.系统中的inode 5.软硬链接 6.动静态库 预备知识 1.文件 内容 属性 2.文件的所有操作: a. 对内容的操作 b.对属性的操作 3.文件在磁盘(硬件)上, 我…

Blender 导出 fbx 到虚幻引擎中丢失材质!!!(使用Blender导出内嵌材质的fbx即可解决)

目录 0 引言1 Blender导出内嵌纹理的fbx模型 0 引言 我在Blender处理了一些fbx模型后再次导出到UE中就经常出现&#xff0c;材质空白的情况&#xff08;如下图所示&#xff09;&#xff0c;今天终于找到问题原因&#xff0c;记录下来&#xff0c;让大家避免踩坑。 其实原因很简…

Pikachu靶场——跨站请求伪造(CSRF)

文章目录 1. 跨站请求伪造&#xff08;CSRF&#xff09;1.1 CSRF(get)1.2 CSRF(post)1.3 CSRF Token1.4 CSRF漏洞防御 1. 跨站请求伪造&#xff08;CSRF&#xff09; 还可以参考我的另一篇文章&#xff1a;跨站请求伪造(CSRF) 全称Cross-site request forgery&#xff0c;翻译…