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

系列文章目录

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


前言

如果让你去开发一款播放器,第一步当然想到的就是设计。使用面向对象的思路,去确定ExoPlayer应该具有哪些功能,对外暴露哪些操作,需要解决哪些问题。将这些功能进一步抽象,最终就产生了本文要说的Player接口,Player接口位于整个播放器的最顶层,相当于描绘了整个播放器的蓝图。

设计播放器

Player接口除了定义了一些用于播放的高阶函数(如play、pause、seek、获取某些状态等)。还对对播放器设计了以下特性:

  • 所有方法(除非有特殊说明)必须在应用线程调用,大部分是主线程,同样回调必须注册在同一线程里。

    这部分主要体现在ExoPlayerImpl和ExoPlayerImplInternal里,ExoPlayerImpl里几乎所有方法都可以在主线程中直接调用,而ExoPlayerImplInternal里维护运行着播放线程(一个HandlerThread),主线程通过播放线程的Handler将消息数据发生给播放线程用于控制播放,播放线程又通过主线程的Handler将播放的状态通过在主线程中回调监听的方法通知主线程。

  • 所提供的方法可能有是否可用的控制,播放器会提供一个可用方法集,用户只能调用里面的可用方法。

    Player中定义了 isCommandAvailable(int command)方法,用来查询当前操作方法是否可用,而可用方法集在播放器初始化时设置,而方法的可用控制主要实现在播放器上层,播放器只提供用于实现的能力。 如PlayerControlView中,在设置播放速度时,就会先判断COMMAND_SET_SPEED_AND_PITCH这个权限。

    java 复制代码
    private void setPlaybackSpeed(float speed) {
        if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) {
          return;
        }
        player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed));
      }
  • 用户通过注册Listener 来监听播放状态变化。

    Player中定义了addListener(Listener listener)方法用于注册监听,在初始化时设置监听,这些监听贯穿了播放器的整个生命周期,从资源的加载到播放过程中的状态变化都可以在Listener中获取到,Listener 只会在主线程回调。通过使用主线程的Handler将回调转发到主线程。 addListener主要实现在ExoPlayerImpl中。

    java 复制代码
    @Override
    public void addListener(Listener listener) {
    // 这里的方法调用前没有像其他方法一样校验是否在主线程,这个方法可以在任何线程调用,因为添加的listener最终都会被转发到主线程执行
    listeners.add(checkNotNull(listener));
    }
    //ExoPlayerImpl初始化时,会将主线程的Looper传入,用于转发Listener转发到主线程
    listeners =
          new ListenerSet<>(
              applicationLooper,//主线程Looper
              clock,
              (listener, flags) -> listener.onEvents(this.wrappingPlayer, new Events(flags)));
  • 必须在方法调用后立即更新播放状态或者信息,即使实际发生变化的代码执行在后台线程甚至是其他设备上。这样是为了方便调用方法,无需考虑异步处理。

    为了实现这个特性ExoPlayerImpl中维护了播放的状态和信息,主要通过playbackInfo存储当前的状态,当调用Player某个方法时,在方法异步执行完毕前,Player会先更新当前的状态,此时用户如果再去获取这个状态时,也无需等待异步方法执行完毕,可以直接获取到当前的状态。 如在ExoPlayerImpl调用prepare时,底层prepare是异步执行于播放线程的,无需等待其调用结束,直接更新状态信息,等底层prepare异步执行完毕后也会设置当前最新的playbackInfo。

    java 复制代码
    @Override
      public void prepare() {
        verifyApplicationThread();//判断主线程调用
        ...
        PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);//创建副本,防止多线程问题
        playbackInfo =//设置状态为STATE_ENDED 或者STATE_BUFFERING
            playbackInfo.copyWithPlaybackState(
                playbackInfo.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING);
        pendingOperationAcks++;
        internalPlayer.prepare();
        updatePlaybackInfo(
            playbackInfo,
            /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
            /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
            /* positionDiscontinuity= */ false,
            /* ignored */ DISCONTINUITY_REASON_INTERNAL,
            /* ignored */ C.TIME_UNSET,
            /* ignored */ C.INDEX_UNSET,
            /* repeatCurrentMediaItem= */ false);
      }
  • 可以操作播放列表,如设置、获取、添加、删除、移动或者替换播放列表。播放器还可以设置重复模式和随机播放。

    Player中定义了add、get、move、removeMediaItem等方法实现了播放列表的操作,setRepeatMode设置重复模式和setShuffleModeEnabled设置随机播放,通过设置ShuffleOrder实现自定义的随机算法。随机功能主要实现在MediaSourceList中,可以参考后续的系列文章。

  • 可以获取轨道及正在播放的轨道,还可以选择设置轨道。

    通过getCurrentTracks获取正在播放的轨道,通过setTrackSelectionParameters选择设置轨道,最终会调用ExoPlayerImplInternal的reselectTracksInternal播放选择的轨道。这部分内容会在系列文章4大组件TrackSelector中详细说明。

  • 可以获取当前播放内容的元数据。

    通过getMediaMetadata()获取元数据,数据存储在MediaMetadata 对象中。

  • 可以获取当前媒体中的广告信息,如当前播放的是否为广告。

    通过isPlayingAd判断是否为广告,getCurrentAdGroupIndex获取当前广告的索引。

  • 可以支持不同的视频渲染输出,如SurfaceView、TextureView。

    setVideoSurfaceView或者setVideoTextureView等方法指定渲染输出对象,这些设置最终会配置到MediaCodec 中,这部分内容会在系列文章4大组件Renderers中详细说明。

  • 可以倍数播放,音频参数调节,音量调节。

    倍数播放通过调用setPlaybackSpeed方法实现,如果当前媒体包含音轨,最终是通过audioTrack.setPlaybackParams将音轨倍数,然后采用音轨的时钟做为参考时钟,没有的音轨则直接倍数标准系统时钟,这部分内容会在系列文章4大组件Renderers中详细说明,通过setAudioAttributes调节音频参数,setVolume调节音量。

    java 复制代码
      //DefaultAudioSink
      @RequiresApi(23)
      private void setAudioTrackPlaybackParametersV23() {
        ...
            audioTrack.setPlaybackParams(playbackParams);
         ...
      }
      //StandaloneMediaClock
      @Override
      public long getPositionUs() {
        long positionUs = baseUs;
        if (started) {
          long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs;
          if (playbackParameters.speed == 1f) {
            positionUs += Util.msToUs(elapsedSinceBaseMs);
          } else {
            positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs);
          }
        }
        return positionUs;
      }
  • 可以获取播放设备的信息,即使是远程设备,这样可以设置这些设备的音量。

    通过getDeviceInfo获取设备信息,设备信息里包含当前设备是否为远程设备,还有音量信息,这里主要用于控制远程设备的音量。

确定播放需要维护的状态和信息

根据上面的设计需要,播放器主要需要维护以下的状态和信息:

  • 播放列表信息

    • 媒体信息封装在MediaItem类里,可以设置定义播放器需要播放的内容。
    • 当前的播放列表可以通过getCurrentTimeline获取,Timeline是ExoPlayer中一个重要的概念,抽象了播放时序,使其适应各种类型的媒体播放,后续系列文章会讲到。
    • 当播放列表为空的时候,播放器状态只能是STATE_IDLE或者STATE_ENDED
  • 播放状态

    • STATE_IDLE: 初始状态,播放器停止时的状态,以及播放失败时的状态。在这种状态下,播放器只能拥有有限的资源。必须调用prepare 方法才能脱离此状态。
    • STATE_BUFFERING: 播放器无法立即从当前位置开始播放。出现这种情况主要是因为需要加载更多数据。
    • STATE_READY: 播放器可以立即从当前位置开始播放。
    • STATE_ENDED: 播放器以及播放完成所有内容,或者没有内容需要播放。
  • 播放/暂停,播放限制和正在播放状态

    • playWhenReady: 一个boolen值,用于标记用户是否要开始播放,可以通过调用play 或者 pause方法设置。

    • playback suppression: 用于标记播放被限制(即使playWhenReady=true)的原因。

    • isPlaying : 一个boolen值 ,用于标记播放器是否正在播放(播放进度在前进且播放的数据正在读取)。 这个值为true的条件是上面的 播放状态=STATE_READY 且 playWhenReady =true 且 播放没有被限制,可以看到这个状态是由上面三个状态计算而来的。

      java 复制代码
        @Override
        public final boolean isPlaying() {
          return getPlaybackState() == Player.STATE_READY
              && getPlayWhenReady()
              && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
        }
  • 播放位置进度

    • media item index: 播放列表的索引。
    • ad insertion: 插入的广告是否在播放,当前正在播放的广告组索引,以及当前广告在组中的索引,这里可以看到广告是分组播放的,一组可以包含一个或者多个广告。
    • current position: 当前播放的进度。 如果没有播放插入的广告这个进度等于content position,content position比current position多了一个广告的时长。
    • 需要注意的是Play不提供播放进度状态的回调的,需要以适当的频率去查询播放进度。

总结

本篇介绍相关的设计特性已经播放器维护的状态,分别进行一个简单的介绍,因为在后面的文章这些概念都会涉及到,到时候会详细解读。


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

相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_5 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子6 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch10 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391915 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef15 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb