From 20e43ac4f8539229f407700da011d008c253fabd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 5 Oct 2017 01:36:23 -0700 Subject: [PATCH] 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 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 42 +++++++++++-------- .../source/ads/AdPlaybackState.java | 11 ++++- .../exoplayer2/source/ads/AdsMediaSource.java | 3 +- .../source/ads/SinglePeriodAdTimeline.java | 18 +++++++- .../exoplayer2/ui/PlaybackControlView.java | 6 +-- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++- .../res/layout/exo_simple_player_view.xml | 8 ++-- 7 files changed, 78 insertions(+), 32 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6b20404a39..cbed5d166e 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -150,10 +150,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // 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. */ @@ -243,7 +239,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A player.addListener(this); if (adPlaybackState != null) { eventListener.onAdPlaybackState(adPlaybackState.copy()); - if (imaPausedContent) { + if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } } else { @@ -448,13 +444,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { 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) { case IMA_AD_STATE_PLAYING: // 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++) { 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); 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(); } @@ -532,6 +534,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A 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 && playWhenReady) { checkForContentComplete(); @@ -593,10 +605,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; playingAd = player.isPlayingAd(); - if (!playingAd && playWhenReadyOverriddenForAds) { - playWhenReadyOverriddenForAds = false; - player.setPlayWhenReady(false); - } if (!sentContentComplete) { boolean adFinished = (wasPlayingAd && !playingAd) || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 97c97dec8f..58fa149b59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -51,6 +51,10 @@ public final class AdPlaybackState { */ 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. */ @@ -71,15 +75,17 @@ public final class AdPlaybackState { adUris = new Uri[adGroupCount][]; Arrays.fill(adUris, new Uri[0]); adsLoadedCounts = new int[adGroupTimesUs.length]; + contentDurationUs = C.TIME_UNSET; } 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.adCounts = adCounts; this.adsLoadedCounts = adsLoadedCounts; this.adsPlayedCounts = adsPlayedCounts; this.adUris = adUris; + this.contentDurationUs = contentDurationUs; this.adResumePositionUs = adResumePositionUs; adGroupCount = adGroupTimesUs.length; } @@ -94,7 +100,8 @@ public final class AdPlaybackState { } return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount), Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount), - Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, adResumePositionUs); + Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs, + adResumePositionUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 41a856f83f..9c75b5ee5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -273,7 +273,8 @@ public final class AdsMediaSource implements MediaSource { Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, - adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); + adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs, + adPlaybackState.contentDurationUs); listener.onSourceInfoRefreshed(timeline, contentManifest); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index c2974681db..0a04c9ab4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.Assertions; private final int[] adsPlayedCounts; private final long[][] adDurationsUs; private final long adResumePositionUs; + private final long contentDurationUs; /** * 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. * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin * 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, - int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, - long adResumePositionUs) { + int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs, + long contentDurationUs) { super(contentTimeline); Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1); @@ -61,6 +64,7 @@ import com.google.android.exoplayer2.util.Assertions; this.adsPlayedCounts = adsPlayedCounts; this.adDurationsUs = adDurationsUs; this.adResumePositionUs = adResumePositionUs; + this.contentDurationUs = contentDurationUs; } @Override @@ -72,4 +76,14 @@ import com.google.android.exoplayer2.util.Assertions; 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; + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index e2ae1f732b..964eda7de0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -656,17 +656,13 @@ public class PlaybackControlView extends FrameLayout { boolean isSeekable = false; boolean enablePrevious = false; boolean enableNext = false; - if (haveNonEmptyTimeline) { + if (haveNonEmptyTimeline && !player.isPlayingAd()) { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != 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(enableNext, nextButton); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 053bc26a6e..f34ede3e6d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; 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. */ private void maybeShowController(boolean isForced) { + if (isPlayingAd()) { + // Never show the controller if an ad is currently playing. + return; + } if (useController) { boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); @@ -777,6 +782,10 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.show(); } + private boolean isPlayingAd() { + return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + } + private void updateForCurrentTrackSelections() { if (player == null) { return; @@ -907,7 +916,18 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override 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(); + } } } diff --git a/library/ui/src/main/res/layout/exo_simple_player_view.xml b/library/ui/src/main/res/layout/exo_simple_player_view.xml index 1f59b7796d..340113da6c 100644 --- a/library/ui/src/main/res/layout/exo_simple_player_view.xml +++ b/library/ui/src/main/res/layout/exo_simple_player_view.xml @@ -38,12 +38,12 @@ - - + +