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不提供播放进度状态的回调的,需要以适当的频率去查询播放进度。

总结

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


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

相关推荐
断墨先生13 分钟前
uniapp—android原生插件开发(3Android真机调试)
android·uni-app
无极程序员2 小时前
PHP常量
android·ide·android studio
萌面小侠Plus3 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
慢慢成长的码农3 小时前
Android Profiler 内存分析
android
大风起兮云飞扬丶3 小时前
Android——多线程、线程通信、handler机制
android
L72563 小时前
Android的Handler
android
清风徐来辽3 小时前
Android HandlerThread 基础
android
HerayChen4 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野4 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11234 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机