Allow ads to be paused/resumed

Controls are still hidden while playing ads, but if the app pauses the player,
controls will be shown. During ads, the player is not seekable.

When the player enters the background then returns to the foreground, the
content period may not be prepared, so also cache the content window duration.
This means that if the app reenters the foreground while an ad is paused the
time bar can be populated.

Issue: #3303

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=171123428
This commit is contained in:
andrewlewis 2017-10-05 01:36:23 -07:00 committed by Oliver Woodman
parent 09165ab870
commit 20e43ac4f8
7 changed files with 78 additions and 32 deletions

View File

@ -150,10 +150,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state. // Fields tracking the player/loader state.
/**
* Whether the player's play when ready flag has temporarily been set to true for playing ads.
*/
private boolean playWhenReadyOverriddenForAds;
/** /**
* Whether the player is playing an ad. * Whether the player is playing an ad.
*/ */
@ -243,7 +239,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
player.addListener(this); player.addListener(this);
if (adPlaybackState != null) { if (adPlaybackState != null) {
eventListener.onAdPlaybackState(adPlaybackState.copy()); eventListener.onAdPlaybackState(adPlaybackState.copy());
if (imaPausedContent) { if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume(); adsManager.resume();
} }
} else { } else {
@ -448,13 +444,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd"); Log.d(TAG, "playAd");
} }
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected playAd while detached");
} else if (!player.getPlayWhenReady()) {
playWhenReadyOverriddenForAds = true;
player.setPlayWhenReady(true);
}
switch (imaAdState) { switch (imaAdState) {
case IMA_AD_STATE_PLAYING: case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content. // IMA does not always call stopAd before resuming content.
@ -472,6 +461,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onResume(); adCallbacks.get(i).onResume();
} }
break;
default:
throw new IllegalStateException();
}
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected playAd while detached");
} else if (!player.getPlayWhenReady()) {
adsManager.pause();
} }
} }
@ -522,7 +520,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
Assertions.checkArgument(timeline.getPeriodCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline; this.timeline = timeline;
contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); long contentDurationUs = timeline.getPeriod(0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState.contentDurationUs = contentDurationUs;
}
updateImaStateForPlayerState(); updateImaStateForPlayerState();
} }
@ -532,6 +534,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
return; return;
} }
if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) {
adsManager.pause();
return;
}
if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) {
adsManager.resume();
return;
}
if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING
&& playWhenReady) { && playWhenReady) {
checkForContentComplete(); checkForContentComplete();
@ -593,10 +605,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private void updateImaStateForPlayerState() { private void updateImaStateForPlayerState() {
boolean wasPlayingAd = playingAd; boolean wasPlayingAd = playingAd;
playingAd = player.isPlayingAd(); playingAd = player.isPlayingAd();
if (!playingAd && playWhenReadyOverriddenForAds) {
playWhenReadyOverriddenForAds = false;
player.setPlayWhenReady(false);
}
if (!sentContentComplete) { if (!sentContentComplete) {
boolean adFinished = (wasPlayingAd && !playingAd) boolean adFinished = (wasPlayingAd && !playingAd)
|| playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup();

View File

@ -51,6 +51,10 @@ public final class AdPlaybackState {
*/ */
public final Uri[][] adUris; public final Uri[][] adUris;
/**
* The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise.
*/
public long contentDurationUs;
/** /**
* The position offset in the first unplayed ad at which to begin playback, in microseconds. * The position offset in the first unplayed ad at which to begin playback, in microseconds.
*/ */
@ -71,15 +75,17 @@ public final class AdPlaybackState {
adUris = new Uri[adGroupCount][]; adUris = new Uri[adGroupCount][];
Arrays.fill(adUris, new Uri[0]); Arrays.fill(adUris, new Uri[0]);
adsLoadedCounts = new int[adGroupTimesUs.length]; adsLoadedCounts = new int[adGroupTimesUs.length];
contentDurationUs = C.TIME_UNSET;
} }
private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
int[] adsPlayedCounts, Uri[][] adUris, long adResumePositionUs) { int[] adsPlayedCounts, Uri[][] adUris, long contentDurationUs, long adResumePositionUs) {
this.adGroupTimesUs = adGroupTimesUs; this.adGroupTimesUs = adGroupTimesUs;
this.adCounts = adCounts; this.adCounts = adCounts;
this.adsLoadedCounts = adsLoadedCounts; this.adsLoadedCounts = adsLoadedCounts;
this.adsPlayedCounts = adsPlayedCounts; this.adsPlayedCounts = adsPlayedCounts;
this.adUris = adUris; this.adUris = adUris;
this.contentDurationUs = contentDurationUs;
this.adResumePositionUs = adResumePositionUs; this.adResumePositionUs = adResumePositionUs;
adGroupCount = adGroupTimesUs.length; adGroupCount = adGroupTimesUs.length;
} }
@ -94,7 +100,8 @@ public final class AdPlaybackState {
} }
return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount), return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount),
Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount), Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount),
Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, adResumePositionUs); Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs,
adResumePositionUs);
} }
/** /**

View File

@ -273,7 +273,8 @@ public final class AdsMediaSource implements MediaSource {
Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs,
adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts,
adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs,
adPlaybackState.contentDurationUs);
listener.onSourceInfoRefreshed(timeline, contentManifest); listener.onSourceInfoRefreshed(timeline, contentManifest);
} }
} }

View File

@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.Assertions;
private final int[] adsPlayedCounts; private final int[] adsPlayedCounts;
private final long[][] adDurationsUs; private final long[][] adDurationsUs;
private final long adResumePositionUs; private final long adResumePositionUs;
private final long contentDurationUs;
/** /**
* Creates a new timeline with a single period containing the specified ads. * Creates a new timeline with a single period containing the specified ads.
@ -48,10 +49,12 @@ import com.google.android.exoplayer2.util.Assertions;
* may be {@link C#TIME_UNSET} if the duration is not yet known. * may be {@link C#TIME_UNSET} if the duration is not yet known.
* @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin
* playback, in microseconds. * playback, in microseconds.
* @param contentDurationUs The content duration in microseconds, if known. {@link C#TIME_UNSET}
* otherwise.
*/ */
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs,
long adResumePositionUs) { long contentDurationUs) {
super(contentTimeline); super(contentTimeline);
Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1);
@ -61,6 +64,7 @@ import com.google.android.exoplayer2.util.Assertions;
this.adsPlayedCounts = adsPlayedCounts; this.adsPlayedCounts = adsPlayedCounts;
this.adDurationsUs = adDurationsUs; this.adDurationsUs = adDurationsUs;
this.adResumePositionUs = adResumePositionUs; this.adResumePositionUs = adResumePositionUs;
this.contentDurationUs = contentDurationUs;
} }
@Override @Override
@ -72,4 +76,14 @@ import com.google.android.exoplayer2.util.Assertions;
return period; return period;
} }
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
if (window.durationUs == C.TIME_UNSET) {
window.durationUs = contentDurationUs;
}
return window;
}
} }

View File

@ -656,17 +656,13 @@ public class PlaybackControlView extends FrameLayout {
boolean isSeekable = false; boolean isSeekable = false;
boolean enablePrevious = false; boolean enablePrevious = false;
boolean enableNext = false; boolean enableNext = false;
if (haveNonEmptyTimeline) { if (haveNonEmptyTimeline && !player.isPlayingAd()) {
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window); timeline.getWindow(windowIndex, window);
isSeekable = window.isSeekable; isSeekable = window.isSeekable;
enablePrevious = isSeekable || !window.isDynamic enablePrevious = isSeekable || !window.isDynamic
|| player.getPreviousWindowIndex() != C.INDEX_UNSET; || player.getPreviousWindowIndex() != C.INDEX_UNSET;
enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET;
if (player.isPlayingAd()) {
// Always hide player controls during ads.
hide();
}
} }
setButtonEnabled(enablePrevious, previousButton); setButtonEnabled(enablePrevious, previousButton);
setButtonEnabled(enableNext, nextButton); setButtonEnabled(enableNext, nextButton);

View File

@ -37,6 +37,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
@ -751,6 +752,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
* Shows the playback controls, but only if forced or shown indefinitely. * Shows the playback controls, but only if forced or shown indefinitely.
*/ */
private void maybeShowController(boolean isForced) { private void maybeShowController(boolean isForced) {
if (isPlayingAd()) {
// Never show the controller if an ad is currently playing.
return;
}
if (useController) { if (useController) {
boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();
@ -777,6 +782,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
controller.show(); controller.show();
} }
private boolean isPlayingAd() {
return player != null && player.isPlayingAd() && player.getPlayWhenReady();
}
private void updateForCurrentTrackSelections() { private void updateForCurrentTrackSelections() {
if (player == null) { if (player == null) {
return; return;
@ -907,7 +916,18 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
maybeShowController(false); if (isPlayingAd()) {
hideController();
} else {
maybeShowController(false);
}
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (isPlayingAd()) {
hideController();
}
} }
} }

View File

@ -38,12 +38,12 @@
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout> </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
<View android:id="@id/exo_controller_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout android:id="@id/exo_overlay" <FrameLayout android:id="@id/exo_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<View android:id="@id/exo_controller_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge> </merge>