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

系列文章目录

ExoPlayer架构详解与源码分析(1)------前言

ExoPlayer架构详解与源码分析(2)------Player

ExoPlayer架构详解与源码分析(3)------Timeline

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


前言

根据前篇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实现。

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

    java 复制代码
      protected BasePlayer() {
        window = new Timeline.Window();
      }
      
      @Override
      public 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里的,这里后面看到具体实现。

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

    java 复制代码
      @Override
      public 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);
      }
    
       @Override
      public 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);
          
  • 完成了其他一些可以通过已有方法实现的方法。

    java 复制代码
      //判断当前命令是否可用,对应Player设计的第2点
      @Override
      public final boolean isCommandAvailable(@Command int command) {
        return getAvailableCommands().contains(command);//通过已有的getAvailableCommands来实现,getAvailableCommands由子类实现
      }
      //播放和暂停,实现了Player关于playWhenReady的设计,playWhenReady就是一个标记位,标记用户的一个播放意图
      //所以这里的play并不是立即开始播放的意思,而是调用者希望开始播放,实际播放要等到PlaybackState=STATE_READY的时候,pause同上
      @Override
      public final void play() {
        setPlayWhenReady(true);
      }
      //实现了Player关于isPlaying的设计
      @Override
      public final boolean isPlaying() {
        return getPlaybackState() == Player.STATE_READY
            && getPlayWhenReady()
            && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
      }
    
      //获取直播流的延时
      @Override
      public 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();
      }
      //获取缓冲百分比
      @Override
      public 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开始分析。


版权声明 ©

本文为作者山雨楼原创文章

转载请注明出处

原创不易,觉得有用的话,收藏转发点赞支持

相关推荐
SRC_BLUE_1726 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛5 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器