mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
09165ab870
commit
20e43ac4f8
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user