ExoPlayer实现无缝插播广告(下)

  上篇文章 介绍了在不使用 ExoPlayer-IMA 扩展的情况下,利用 AdsMediaSource 和自定义的 AdsLoader 实现无缝插播广告的详细操作。示例代码运行起来后,会发现有两个细节问题需要处理:

  1. 播放广告的时候只有播放画面,没有 暂停/播放、 以及其它供用户点击的广告 UI;

  2. 创建自定义 AdsLoader 的时候,把所有的广告点通过 AdPlaybackState 的构造函数 adPlaybackState = AdPlaybackState(*adGroupTimesUs) 告诉了播放器,对于实时竞价广告,如果在这个广告点没有返回广告没有更新 AdPlaybackState 的数据,播放到广告点时,内容视频默认会暂停,这种情况该怎么处理?

    这篇文章就详细介绍这两个问题的解决方案。

    对于第一个问题,添加广告 UI,ExoPlayer 已经预留了让用户自定义广告 UI 布局的接口,再回顾下AdsMediaSource 的构造函数和参数说明:

1
/**
2
   * Constructs a new source that inserts ads linearly with the content specified by {@code
3
   * contentMediaSource}.
4
   *
5
   * @param contentMediaSource The {@link MediaSource} providing the content to play.
6
   * @param adMediaSourceFactory Factory for media sources used to load ad media.
7
   * @param adsLoader The loader for ads.
8
   * @param adViewProvider Provider of views for the ad UI.
9
   */ 
10
public AdsMediaSource(
11
      MediaSource contentMediaSource,
12
      MediaSourceFactory adMediaSourceFactory,
13
      AdsLoader adsLoader,
14
      AdsLoader.AdViewProvider adViewProvider) {
15
  	...  
16
  }

其中 AdsLoader.AdViewProvider 用来提供 广告 UI,它有两个函数,功能如下所述:

1
/** Provides views for the ad UI. */
2
  interface AdViewProvider {
3
4
    /** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */
5
    ViewGroup getAdViewGroup();
6
7
    /**
8
     * Returns an array of views that are shown on top of the ad view group, but that are 
9
     * essential for controlling playback and should be excluded from ad viewability 
10
     * measurements by the {@link AdsLoader} (if it supports this).
11
     *
12
     * Each view must be either a fully transparent overlay (for capturing touch events), 
13
     * or a small piece of transient UI that is essential to the user experience of playback
14
     *(such as a button to pause/resume playback or a transient full-screen or cast button). 
15
     *For more information see the documentation for your ads loader.
16
     */
17
    View[] getAdOverlayViews();
18
  }

  其中 getAdViewGroup() 函数提供展示广告 UI 的 ViewGroup, 有了它,就可以通过 ViewGroup.addView(..) 添加广告 UI。同时,自定义 AdsLoader 里有播放器 Player 的实例,有了 UI 元素和响应用户点击事件的执行者,实现基本的 暂停/播放 等操作就是水到渠成的事情了。 关键代码如下:

1
class FancyAdsLoader : AdsLoader, Player.EventListener {
2
    //...
3
    private lateinit var mContext: Context
4
    private var mPlayer: Player? = null
5
    // Ad UI
6
    private lateinit var mControllerView = AdControllerView(mContext)
7
    
8
    override fun onPositionDiscontinuity(reason: Int) {
9
          //...
10
          if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
11
              val playingAd = mPlayer?.isPlayingAd ?: false
12
              if (playingAd) {
13
                  // 开始播放广告,添加广告UI
14
                  mPlayer?.let {
15
                      mControllerView.setPlayer(it)
16
                  }
17
                  val adViewGroup = mAdViewProvider.adViewGroup
18
                  adViewGroup.removeView(mControllerView)
19
                  adViewGroup.addView(mControllerView)
20
                  return
21
              }
22
23
              // 切换到内容视频,移除广告UI
24
              mAdViewProvider.adViewGroup.removeView(mControllerView)
25
              //...
26
          }
27
    }
28
  
29
    override fun setPlayer(player: Player?) {
30
          mPlayer = player
31
          mPlayer?.addListener(this)
32
    }
33
  
34
}

  那么创建 AdsMediaSource 的时候,这个 AdViewProvider 从哪里来呢?如果你使用的播放器 UI 是 ExoPlayer 提供的 PlayerView 类,它已经实现了 AdViewProvider 接口,可以直接使用。如果你使用自定义播放器 UI, 让它继承 AdViewProvider 接口,实现接口的方法。

  第一个问题解决后,来看第二个问题,怎么处理在某一个广告点没有广告返回的情况?

  默认情况下,如果声明了一个广告点,但是没有传广告的 URL, ExoPlayer 会到了广告点自动暂停,之前的文章里我们了解到 AdPlaybackState 类封装了要播放的广告数据,对于在某个广告点没有返回广告的情况,调用 AdPlaybackState.withSkippedAdGroup(adGroupIndex) 就可以跳过该广告组,继续播放视频内容。关键代码如下:

1
class FancyAdsLoader: AdsLoader, Player.EventListener{
2
  private var mCurrentAdGroupSkipped = false
3
  private var mCurrentAdList = mutableListOf<String>()
4
  private var mEventListener: AdsLoader.EventListener? = null
5
  private var adPlaybackState: AdPlaybackState? = null
6
7
  private fun maybeFetchAd(positionMs: Long) {
8
      // 模拟请求广告
9
      val positionS = positionMs / 1000
10
      val currentCuePoint = mCuePointList.first()
11
      if (currentCuePoint - positionS <= 5) {
12
        // 广告点提前5s 取广告
13
        fetchingAds = true
14
        mCurrentAdList.clear()
15
16
        if (mNextAdGroupIndex == 0) {
17
          // 模拟第一个广告点没有广告数据,跳过该广告点
18
          adPlaybackState = adPlaybackState?.withSkippedAdGroup(0)
19
          mCurrentAdGroupSkipped = true
20
        } 
21
        fetchingAds = false
22
        mCuePointList.remove(currentCuePoint)
23
        updateAdPlaybackState()
24
      }
25
  }
26
  
27
  override fun onPositionDiscontinuity(reason: Int) {
28
          //...
29
          if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
30
              val playingAd = mPlayer?.isPlayingAd ?: false
31
              if (playingAd) {
32
                  // 开始播放广告
33
                  //...
34
                  return
35
              }
36
37
              // 切换到内容视频
38
              if (!mCurrentAdGroupSkipped) {
39
                  adPlaybackState = adPlaybackState?.withPlayedAd(
40
                      mCurrentAdGroupIndex,
41
                      mCurrentAdIndexInGroup
42
                  )
43
              }
44
            	updateAdPlaybackState()
45
              //...
46
          }
47
    }
48
  
49
  private fun updateAdPlaybackState() {
50
     mEventListener?.onAdPlaybackState(adPlaybackState)
51
  }
52
 
53
}

  到目前为止,使用 ExoPlayer 的 AdsMediaSurce 和 自定义 AdsLoader 实现无缝插播广告的功能已经介绍完毕,希望这些内容能让用户观看广告的同时减少等待时间,让你的产品体验更加流畅。