ExoPlayer实现无缝插播广告(下)
上篇文章 介绍了在不使用 ExoPlayer-IMA 扩展的情况下,利用 AdsMediaSource 和自定义的 AdsLoader 实现无缝插播广告的详细操作。示例代码运行起来后,会发现有两个细节问题需要处理:
播放广告的时候只有播放画面,没有 暂停/播放、 以及其它供用户点击的广告 UI;
创建自定义 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 实现无缝插播广告的功能已经介绍完毕,希望这些内容能让用户观看广告的同时减少等待时间,让你的产品体验更加流畅。