ExoPlayer播放器剖析(三)流程分析---从build到prepare看ExoPlayer的创建流程

关联博客:

ExoPlayer播放器剖析(一)进入ExoPlayer的世界
ExoPlayer播放器剖析(二)编写exoplayer的demo
ExoPlayer播放器剖析(三)流程分析—从build到prepare看ExoPlayer的创建流程
ExoPlayer播放器剖析(四)从renderer.render函数分析至MediaCodec
ExoPlayer播放器剖析(五)ExoPlayer对AudioTrack的操作
ExoPlayer播放器剖析(六)ExoPlayer同步机制分析
ExoPlayer播放器剖析(七)ExoPlayer对音频时间戳的处理
ExoPlayer播放器扩展(一)DASH流与HLS流简介

一、前言:
上一篇博客介绍了exoplayer的简单demo,对流程有了一个大致的了解,我们都知道exoplayer的本质是调用Android原生的MediaCodec接口,这篇博客将着重分析其内部实现逻辑,看exoplayer是如何完成创建的。

二、流程分析:
先贴出上篇博客中讲exoplayer初始化的五步曲:

    private fun initPlayer(playUri: String?) {

        if (playUri == null){
            Log.d("ExoTest", "playUri is null!")
            return
        }

        /* 1.创建SimpleExoPlayer实例 */
        mPlayer = SimpleExoPlayer.Builder(this).build()

        /* 2.创建播放菜单并添加到播放器 */
        val firstLocalMediaItem = MediaItem.fromUri(playUri)
        mPlayer!!.addMediaItem(firstLocalMediaItem)

        /* 3.设置播放方式为自动播放 */
        mPlayer!!.playWhenReady = true

        /* 4.将SimpleExoPlayer实例设置到StyledPlayerView中 */
        mPlayerView!!.player = mPlayer

        /* 5,设置播放器状态为prepare */
        mPlayer!!.prepare()
    }

实际上,我们可以看到,真正跟播放器相关的只有1、3和5三步,我们就从这三步入手看看exoplayer内部到底是如何实现的。

1.分析SimpleExoPlayer.Builder(this).build():
看build实现如下:

    /**
     * Builds a {@link SimpleExoPlayer} instance.
     *
     * @throws IllegalStateException If this method has already been called.
     */
    public SimpleExoPlayer build() {
      Assertions.checkState(!buildCalled);
      buildCalled = true;
      return new SimpleExoPlayer(/* builder= */ this);
    }
  }

看build方式实际上是实例了一个SimpleExoPlayer对象,继续跟进到SimpleExoPlayer的构造函数:

  protected SimpleExoPlayer(Builder builder) {
  	...
  		/* 构建render */
  	    renderers =
        builder.renderersFactory.createRenderers(
            eventHandler,
            componentListener,
            componentListener,
            componentListener,
            componentListener);
  	...	  
    // Build the player and associated objects.
    player =
        new ExoPlayerImpl(
            renderers,
            builder.trackSelector,
            builder.mediaSourceFactory,
            builder.loadControl,
            builder.bandwidthMeter,
            analyticsCollector,
            builder.useLazyPreparation,
            builder.seekParameters,
            builder.pauseAtEndOfMediaItems,
            builder.clock,
            builder.looper);
    player.addListener(componentListener);
    videoDebugListeners.add(analyticsCollector);
    videoListeners.add(analyticsCollector);
    audioDebugListeners.add(analyticsCollector);
    audioListeners.add(analyticsCollector);
    addMetadataOutput(analyticsCollector);
    	...
  }

可以看到,SimpleExoPlayer的构造里面继续去实例化了一个ExoPlayerImpl的对象,再往下跟进:

  public ExoPlayerImpl(
      Renderer[] renderers,
      TrackSelector trackSelector,
      MediaSourceFactory mediaSourceFactory,
      LoadControl loadControl,
      BandwidthMeter bandwidthMeter,
      @Nullable AnalyticsCollector analyticsCollector,
      boolean useLazyPreparation,
      SeekParameters seekParameters,
      boolean pauseAtEndOfMediaItems,
      Clock clock,
      Looper applicationLooper) {
	...
	    internalPlayer =
        new ExoPlayerImplInternal(
            renderers,
            trackSelector,
            emptyTrackSelectorResult,
            loadControl,
            bandwidthMeter,
            repeatMode,
            shuffleModeEnabled,
            analyticsCollector,
            seekParameters,
            pauseAtEndOfMediaItems,
            applicationLooper,
            clock,
            playbackInfoUpdateListener);
    internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());      
}

继续实例化对象ExoPlayerImplInternal,需要注意的是ExoPlayerImplInternal实现了Handler.Callback接口,所以实例化的方式是以new Handler来进行,那么
ExoPlayerImplInternal必然实现了handleMessage方法,同时,也知道了ExoPlayerImplInternal的内部是以消息机制来通信的。所以,到目前为止我们清楚了ExoPlayerImplInternal才是内部的核心播放器,上面的几层封装不过是为了服务于api函数而已。
下面我们看下ExoPlayerImplInternal的构造函数:

  public ExoPlayerImplInternal(
      Renderer[] renderers,
      TrackSelector trackSelector,
      TrackSelectorResult emptyTrackSelectorResult,
      LoadControl loadControl,
      BandwidthMeter bandwidthMeter,
      @Player.RepeatMode int repeatMode,
      boolean shuffleModeEnabled,
      @Nullable AnalyticsCollector analyticsCollector,
      SeekParameters seekParameters,
      boolean pauseAtEndOfWindow,
      Looper applicationLooper,
      Clock clock,
      PlaybackInfoUpdateListener playbackInfoUpdateListener) {
	...
	    /* 维护playbackInfo类 */
	    playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
	    ... 
	    /* 获取每个渲染器的属性 */ 
	        for (int i = 0; i < renderers.length; i++) {
      renderers[i].setIndex(i);
      rendererCapabilities[i] = renderers[i].getCapabilities();
    }
      /* 音视频同步会使用该类 */
      mediaClock = new DefaultMediaClock(this, clock);
      ...
      /* 创建一个子线程:用于分担main looper的工作量 */
      // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
    // not normally change to this priority" is incorrect.
    internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
    internalPlaybackThread.start();
    playbackLooper = internalPlaybackThread.getLooper();
    handler = clock.createHandler(playbackLooper, this);
}

我们看到有代码中会获取多个render属性,这里的render有几个?分别是什么意思?是在哪里创建的?通过调试打印发现,一共会有6个render,用于处理6种类型的数据,render的创建是在最外层的SimpleExoPlayer构造函数中完成的,可以看到最上面的代码,renderers = builder.renderersFactory.createRenderers,这里的renderersFactory对应的接口是RenderersFactory,默认情况下实现类为DefaultRenderersFactory,看一下该类的createRenderers方法:

----------------------------------------------------------------------------
createRenderers@ExoPlayer\library\core\src\main\java\com\google\android\exoplayer2\DefaultRenderersFactory.java
----------------------------------------------------------------------------
  @Override
  public Renderer[] createRenderers(
      Handler eventHandler,
      VideoRendererEventListener videoRendererEventListener,
      AudioRendererEventListener audioRendererEventListener,
      TextOutput textRendererOutput,
      MetadataOutput metadataRendererOutput) {
    ArrayList<Renderer> renderersList = new ArrayList<>();
    /* 1.video的render */
    buildVideoRenderers(
        context,
        extensionRendererMode,
        mediaCodecSelector,
        enableDecoderFallback,
        eventHandler,
        videoRendererEventListener,
        allowedVideoJoiningTimeMs,
        renderersList);
    @Nullable
    /* 2.audio的render */
    AudioSink audioSink =
        buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams, enableOffload);
    if (audioSink != null) {
      buildAudioRenderers(
          context,
          extensionRendererMode,
          mediaCodecSelector,
          enableDecoderFallback,
          audioSink,
          eventHandler,
          audioRendererEventListener,
          renderersList);
    }
    /* 3.字幕文本的render */
    buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
        extensionRendererMode, renderersList);
    /* 4.MetaData的render */    
    buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
        extensionRendererMode, renderersList);
    /* 5.动态摄像机的render,这个我也不知道是用来干什么的 */ 
    buildCameraMotionRenderers(context, extensionRendererMode, renderersList);
    /* 6.混合render,可能是用来存放其他类似数据的 */
    buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
    return renderersList.toArray(new Renderer[0]);
  }

从上述代码中可以看到,默认是有6个render的,但是通常第6个不会创建,每种类型的render会实例化进行具体的类实现,这里就不去展开描述了。

总结:
a.SimpleExoPlayer是供外部调用的api类,构造函数里面会去创建5~6个类型的render用于各自类型的数据处理;
b.ExoPlayerImplInternal是最终的调用实现,再往下调用将是exoplayer的内部实现;

在这里插入图片描述

2.setPlayWhenReady分析:

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    verifyApplicationThread();
    /* 与音频焦点相关 */
    @AudioFocusManager.PlayerCommand
    int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
    updatePlayWhenReady(
        playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
  }

跟进updatePlayWhenReady函数:

  private void updatePlayWhenReady(
      boolean playWhenReady,
      @AudioFocusManager.PlayerCommand int playerCommand,
      @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
    playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
    @PlaybackSuppressionReason
    int playbackSuppressionReason =
        playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY
            ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS
            : Player.PLAYBACK_SUPPRESSION_REASON_NONE;
    /* 调用ExoPlayerImpl的对应函数 */
    player.setPlayWhenReady(playWhenReady, playbackSuppressionReason, playWhenReadyChangeReason);
  }

跟进到ExoPlayerImpl:

  public void setPlayWhenReady(
      boolean playWhenReady,
      @PlaybackSuppressionReason int playbackSuppressionReason,
      @PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
	...
	    internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
	...      
}

这里的internalPlayer是ExoPlayerImplInternal,继续看下setPlayWhenReady函数:

  public void setPlayWhenReady(
      boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
    handler
        .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason)
        .sendToTarget();
  }

前面我们分析构造函数的时候说过,ExoPlayerImplInternal是一个Hander,直接看handleMessage中是如何处理MSG_SET_PLAY_WHEN_READY消息的:

      case MSG_SET_PLAY_WHEN_READY:
          setPlayWhenReadyInternal(
              /* playWhenReady= */ msg.arg1 != 0,
              /* playbackSuppressionReason= */ msg.arg2,
              /* operationAck= */ true,
              Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
          break;

继续跟进,看下setPlayWhenReadyInternal函数实现:

  private void setPlayWhenReadyInternal(
      boolean playWhenReady,
      @PlaybackSuppressionReason int playbackSuppressionReason,
      boolean operationAck,
      @Player.PlayWhenReadyChangeReason int reason)
      throws ExoPlaybackException {
    playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0);
    playbackInfoUpdate.setPlayWhenReadyChangeReason(reason);
    playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
    rebuffering = false;
    /* 正常情况下,初始化不会进入这个分支 */
    if (!shouldPlayWhenReady()) {
      stopRenderers();
      updatePlaybackPositions();
    } else {
      /* 如果播放器状态为ready,则开始渲染并发送MSG_DO_SOME_WORK消息 */
      if (playbackInfo.playbackState == Player.STATE_READY) {
        startRenderers();
        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
      /* 如果播放器状态为buffring,发送MSG_DO_SOME_WORK消息 */
      } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
      }
    }
  }

是否还记得第一篇博客中将的播放器一共存在四种状态机,分别是idle、buffring、ready和ended,实际上,此时的播放器状态是idle,因此进入else分支之后什么都不会去执行。

总结:
setPlayWhenReady在底层维护了一些类变量的更新,没有去做太多关键性的操作。

3.prepare分析:
还是从SimpleExoPlayer开始:

  @Override
  public void prepare() {
    verifyApplicationThread();
    /* 同setPlayWhenReady操作无异 */
    boolean playWhenReady = getPlayWhenReady();
    @AudioFocusManager.PlayerCommand
    int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
    updatePlayWhenReady(
        playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
    /* 执行prepare */
    player.prepare();
  }

跟进到prepare:

  @Override
  public void prepare() {
  	/* 如果状态不是STATE_IDLE则异常,直接返回 */
    if (playbackInfo.playbackState != Player.STATE_IDLE) {
      return;
    }
    PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);
    /* 更新playbackInfo的状态为STATE_BUFFERING */
    playbackInfo =
        playbackInfo.copyWithPlaybackState(
            playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
    // Trigger internal prepare first before updating the playback info and notifying external
    // listeners to ensure that new operations issued in the listener notifications reach the
    // player after this prepare. The internal player can't change the playback info immediately
    // because it uses a callback.
    pendingOperationAcks++;
    /* 调用下一级的prepare */
    internalPlayer.prepare();
    /* 更新播放信息 */
    updatePlaybackInfo(
        playbackInfo,
        /* positionDiscontinuity= */ false,
        /* ignored */ DISCONTINUITY_REASON_INTERNAL,
        /* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
        /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
        /* seekProcessed= */ false);
  }

这里需要注意的时候,播放信息状态机已经更新为STATE_BUFFERING,后面将会赋值给播放器进行状态更新, 继续进入到ExoPlayerImplInternal的prepare函数:

  public void prepare() {
    handler.obtainMessage(MSG_PREPARE).sendToTarget();
  }

看下是如何处理MSG_PREPARE消息的:

case MSG_PREPARE:
	prepareInternal();
	break;

继续看下prepareInternal函数:

  private void prepareInternal() {
    playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
    resetInternal(
        /* resetRenderers= */ false,
        /* resetPosition= */ false,
        /* releaseMediaSourceList= */ false,
        /* resetError= */ true);
    loadControl.onPrepared();
    /* 设置播放器状态为STATE_BUFFERING */
    setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
    mediaSourceList.prepare(bandwidthMeter.getTransferListener());
    /* 重点是发送MSG_DO_SOME_WORK消息 */
    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
  }

看下如何处理MSG_DO_SOME_WORK消息:

 case MSG_DO_SOME_WORK:
   doSomeWork();
   break;

doSomeWork函数非常长,选择重点来分析:

  private void doSomeWork() throws ExoPlaybackException, IOException {

	...
	/* 1.更新音频时间戳 */
    updatePlaybackPositions();
    ...
      /* 2.调用各个类型的render进行数据处理 */
      for (int i = 0; i < renderers.length; i++) {
    	...
    	/* 核心处理方法 */
        renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);  
        ....
	}
	...
	    if (finishedRendering && playingPeriodHolder.info.isFinal) {
      setState(Player.STATE_ENDED);
      stopRenderers();
      /* 更新播放器状态为STATE_READY */
    } else if (playbackInfo.playbackState == Player.STATE_BUFFERING
        && shouldTransitionToReadyState(renderersAllowPlayback)) {
      setState(Player.STATE_READY);
      /* 如果playWhenReady为true,则开始渲染 */
      if (shouldPlayWhenReady()) {
        startRenderers();
      }
    } else if (playbackInfo.playbackState == Player.STATE_READY
        && !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) {
      rebuffering = shouldPlayWhenReady();
      setState(Player.STATE_BUFFERING);
      stopRenderers();
    }
    ...
        if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
        || playbackInfo.playbackState == Player.STATE_BUFFERING) {
      /* 开启渲染之后将进入这个分支:ACTIVE_INTERVAL_MS为10ms */
      maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS);
    } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
      scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
    } else {
      handler.removeMessages(MSG_DO_SOME_WORK);
    }
}

整个doSomeWork函数将会从prepare之后开始不断执行,updatePlaybackPositions是更新音频的时间戳,便于后面同步时使用,紧接着是调用各个类型的render进行渲染操作,其中最核心的代码就是renderer.render,这个方法的实现最终会调用到MediaCodec里面去,后面为更新状态,开启渲染操作,最后一点需要注意的是,maybeScheduleWakeup函数的实现,这里面会发送MSG_DO_SOME_WORK从而进行循环处理,看下这个函数:

  private void maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) {
    if (offloadSchedulingEnabled && requestForRendererSleep) {
      return;
    }

	/* 执行下次循环操作 */
    scheduleNextWork(operationStartTimeMs, intervalMs);
  }

继续跟进:

  private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
    handler.removeMessages(MSG_DO_SOME_WORK);
    /* intervalMs值为10ms */
    handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
  }

这里就可以看到,整个exoplayer的不停渲染操作实际上是通过消息自发送消息处理来完成的,并且每次处理渲染的时间间隔为10ms。

总结:
如果仅仅是看exoplayer的内部运行机制,基本上就已经看完了,核心就是doSomeWork做的工作,不停地调用各种render来进行数据处理同时发送消息来实现10ms的循环处理,下面是对目前代码流程的一个创建时序图,需要注意其中播放器的状态机变化:
在这里插入图片描述
在下一篇博客中,将重点分析doSomeWork函数,看看exoplayer到底是如何封装MediaCodec的。

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值