Mask ad media periods before the URI is available

Previously `MediaPeriodQueue` would return null if an ad media URI hadn't
loaded yet, but this meant that the player could be stuck in `STATE_READY` if
an `AdsLoader` unexpectedly didn't provide an ad URI. Fix this behavior by
masking ad media periods. `MaskingMediaPeriod` no longer requires a
`MediaSource` to instantiate it.

This also fixes a specific case where playback gets stuck when using the IMA
extension with an empty ad where the IMA SDK unexpectedly doesn't notify the ad
group fetch error.

Issue: #8205
PiperOrigin-RevId: 344984824
This commit is contained in:
andrewlewis 2020-12-01 11:02:30 +00:00 committed by Ian Baker
parent d8df5411b8
commit fe754f313e
7 changed files with 142 additions and 90 deletions

View File

@ -1,9 +1,15 @@
# Release notes
### 2.12.3 (???-??-??) ###
* IMA extension:
* Fix a condition where playback can get stuck before an empty ad
([#8205](https://github.com/google/ExoPlayer/issues/8205)).
### 2.12.2 (2020-12-01) ###
* Core library:
* Suppress exceptions from registering/unregistering the stream volume
* Suppress exceptions from registering and unregistering the stream volume
receiver ([#8087](https://github.com/google/ExoPlayer/issues/8087)),
([#8106](https://github.com/google/ExoPlayer/issues/8106)).
* Suppress ProGuard warnings caused by Guava's compile-only dependencies

View File

@ -672,15 +672,13 @@ import com.google.common.collect.ImmutableList;
period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup);
if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
// Play the next ad in the ad group if it's available.
return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup)
? null
: getMediaPeriodInfoForAd(
timeline,
currentPeriodId.periodUid,
adGroupIndex,
nextAdIndexInAdGroup,
mediaPeriodInfo.requestedContentPositionUs,
currentPeriodId.windowSequenceNumber);
return getMediaPeriodInfoForAd(
timeline,
currentPeriodId.periodUid,
adGroupIndex,
nextAdIndexInAdGroup,
mediaPeriodInfo.requestedContentPositionUs,
currentPeriodId.windowSequenceNumber);
} else {
// Play content from the ad group position.
long startPositionUs = mediaPeriodInfo.requestedContentPositionUs;
@ -720,15 +718,13 @@ import com.google.common.collect.ImmutableList;
currentPeriodId.windowSequenceNumber);
}
int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);
return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup)
? null
: getMediaPeriodInfoForAd(
timeline,
currentPeriodId.periodUid,
nextAdGroupIndex,
adIndexInAdGroup,
/* contentPositionUs= */ mediaPeriodInfo.durationUs,
currentPeriodId.windowSequenceNumber);
return getMediaPeriodInfoForAd(
timeline,
currentPeriodId.periodUid,
nextAdGroupIndex,
adIndexInAdGroup,
/* contentPositionUs= */ mediaPeriodInfo.durationUs,
currentPeriodId.windowSequenceNumber);
}
}
@ -737,9 +733,6 @@ import com.google.common.collect.ImmutableList;
Timeline timeline, MediaPeriodId id, long requestedContentPositionUs, long startPositionUs) {
timeline.getPeriodByUid(id.periodUid, period);
if (id.isAd()) {
if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
return null;
}
return getMediaPeriodInfoForAd(
timeline,
id.periodUid,

View File

@ -609,19 +609,6 @@ public abstract class Timeline {
return adPlaybackState.adGroups[adGroupIndex].count;
}
/**
* Returns whether the URL for the specified ad is known.
*
* @param adGroupIndex The ad group index.
* @param adIndexInAdGroup The ad index in the ad group.
* @return Whether the URL for the specified ad is known.
*/
public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
return adGroup.count != C.LENGTH_UNSET
&& adGroup.states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE;
}
/**
* Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at
* {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known.

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.Nullable;
@ -25,12 +27,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Media period that wraps a media source and defers calling its {@link
* MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link
* #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
* period immediately but the media source that should create it is not yet prepared.
* Media period that defers calling {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)}
* on a given source until {@link #createPeriod(MediaPeriodId)} has been called. This is useful if
* you need to return a media period immediately but the media source that should create it is not
* yet available or prepared.
*/
public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
@ -46,33 +49,32 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception);
}
/** The {@link MediaSource} which will create the actual media period. */
public final MediaSource mediaSource;
/** The {@link MediaPeriodId} used to create the masking media period. */
public final MediaPeriodId id;
private final long preparePositionUs;
private final Allocator allocator;
@Nullable private MediaPeriod mediaPeriod;
/** The {@link MediaSource} that will create the underlying media period. */
private @MonotonicNonNull MediaSource mediaSource;
private @MonotonicNonNull MediaPeriod mediaPeriod;
@Nullable private Callback callback;
private long preparePositionUs;
@Nullable private PrepareListener listener;
private boolean notifiedPrepareError;
private long preparePositionOverrideUs;
/**
* Creates a new masking media period.
* Creates a new masking media period. The media source must be set via {@link
* #setMediaSource(MediaSource)} before preparation can start.
*
* @param mediaSource The media source to wrap.
* @param id The identifier used to create the masking media period.
* @param allocator The allocator used to create the media period.
* @param preparePositionUs The expected start position, in microseconds.
*/
public MaskingMediaPeriod(
MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {
public MaskingMediaPeriod(MediaPeriodId id, Allocator allocator, long preparePositionUs) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
this.preparePositionUs = preparePositionUs;
preparePositionOverrideUs = C.TIME_UNSET;
}
@ -108,6 +110,12 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
return preparePositionOverrideUs;
}
/** Sets the {@link MediaSource} that will create the underlying media period. */
public void setMediaSource(MediaSource mediaSource) {
checkState(this.mediaSource == null);
this.mediaSource = mediaSource;
}
/**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
@ -117,18 +125,16 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
*/
public void createPeriod(MediaPeriodId id) {
long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
mediaPeriod = checkNotNull(mediaSource).createPeriod(id, allocator, preparePositionUs);
if (callback != null) {
mediaPeriod.prepare(this, preparePositionUs);
mediaPeriod.prepare(/* callback= */ this, preparePositionUs);
}
}
/**
* Releases the period.
*/
/** Releases the period. */
public void releasePeriod() {
if (mediaPeriod != null) {
mediaSource.releasePeriod(mediaPeriod);
checkNotNull(mediaSource).releasePeriod(mediaPeriod);
}
}
@ -136,7 +142,8 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback;
if (mediaPeriod != null) {
mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
mediaPeriod.prepare(
/* callback= */ this, getPreparePositionWithOverride(this.preparePositionUs));
}
}
@ -145,10 +152,10 @@ public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
try {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else {
} else if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
} catch (final IOException e) {
} catch (IOException e) {
if (listener == null) {
throw e;
}

View File

@ -111,8 +111,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override
public MaskingMediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
MaskingMediaPeriod mediaPeriod =
new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);
MaskingMediaPeriod mediaPeriod = new MaskingMediaPeriod(id, allocator, startPositionUs);
mediaPeriod.setMediaSource(mediaSource);
if (isPrepared) {
MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid));
mediaPeriod.createPeriod(idInSource);

View File

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source.ads;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
@ -116,7 +118,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
*/
public RuntimeException getRuntimeExceptionForUnexpected() {
Assertions.checkState(type == TYPE_UNEXPECTED);
return (RuntimeException) Assertions.checkNotNull(getCause());
return (RuntimeException) checkNotNull(getCause());
}
}
@ -257,12 +259,10 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState);
AdPlaybackState adPlaybackState = checkNotNull(this.adPlaybackState);
if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
int adGroupIndex = id.adGroupIndex;
int adIndexInAdGroup = id.adIndexInAdGroup;
Uri adUri =
Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]);
if (adMediaSourceHolders[adGroupIndex].length <= adIndexInAdGroup) {
int adCount = adIndexInAdGroup + 1;
adMediaSourceHolders[adGroupIndex] =
@ -272,16 +272,14 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
AdMediaSourceHolder adMediaSourceHolder =
adMediaSourceHolders[adGroupIndex][adIndexInAdGroup];
if (adMediaSourceHolder == null) {
MediaSource adMediaSource =
adMediaSourceFactory.createMediaSource(MediaItem.fromUri(adUri));
adMediaSourceHolder = new AdMediaSourceHolder(adMediaSource);
adMediaSourceHolder = new AdMediaSourceHolder(id);
adMediaSourceHolders[adGroupIndex][adIndexInAdGroup] = adMediaSourceHolder;
prepareChildSource(id, adMediaSource);
maybeUpdateAdMediaSources();
}
return adMediaSourceHolder.createMediaPeriod(adUri, id, allocator, startPositionUs);
return adMediaSourceHolder.createMediaPeriod(id, allocator, startPositionUs);
} else {
MaskingMediaPeriod mediaPeriod =
new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs);
MaskingMediaPeriod mediaPeriod = new MaskingMediaPeriod(id, allocator, startPositionUs);
mediaPeriod.setMediaSource(contentMediaSource);
mediaPeriod.createPeriod(id);
return mediaPeriod;
}
@ -293,10 +291,10 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
MediaPeriodId id = maskingMediaPeriod.id;
if (id.isAd()) {
AdMediaSourceHolder adMediaSourceHolder =
Assertions.checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]);
checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]);
adMediaSourceHolder.releaseMediaPeriod(maskingMediaPeriod);
if (adMediaSourceHolder.isInactive()) {
releaseChildSource(id);
adMediaSourceHolder.release();
adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup] = null;
}
} else {
@ -307,7 +305,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
Assertions.checkNotNull(componentListener).release();
checkNotNull(componentListener).release();
componentListener = null;
contentTimeline = null;
adPlaybackState = null;
@ -321,7 +319,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
if (mediaPeriodId.isAd()) {
int adGroupIndex = mediaPeriodId.adGroupIndex;
int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;
Assertions.checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
.handleSourceInfoRefresh(timeline);
} else {
Assertions.checkArgument(timeline.getPeriodCount() == 1);
@ -346,9 +344,41 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]);
}
this.adPlaybackState = adPlaybackState;
maybeUpdateAdMediaSources();
maybeUpdateSourceInfo();
}
/**
* Initializes any {@link AdMediaSourceHolder AdMediaSourceHolders} where the ad media URI is
* newly known.
*/
private void maybeUpdateAdMediaSources() {
@Nullable AdPlaybackState adPlaybackState = this.adPlaybackState;
if (adPlaybackState == null) {
return;
}
for (int adGroupIndex = 0; adGroupIndex < adMediaSourceHolders.length; adGroupIndex++) {
for (int adIndexInAdGroup = 0;
adIndexInAdGroup < this.adMediaSourceHolders[adGroupIndex].length;
adIndexInAdGroup++) {
@Nullable
AdMediaSourceHolder adMediaSourceHolder =
this.adMediaSourceHolders[adGroupIndex][adIndexInAdGroup];
if (adMediaSourceHolder != null
&& !adMediaSourceHolder.hasMediaSource()
&& adPlaybackState.adGroups[adGroupIndex] != null
&& adIndexInAdGroup < adPlaybackState.adGroups[adGroupIndex].uris.length) {
@Nullable Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup];
if (adUri != null) {
MediaSource adMediaSource =
adMediaSourceFactory.createMediaSource(MediaItem.fromUri(adUri));
adMediaSourceHolder.initializeWithMediaSource(adMediaSource, adUri);
}
}
}
}
}
private void maybeUpdateSourceInfo() {
@Nullable Timeline contentTimeline = this.contentTimeline;
if (adPlaybackState != null && contentTimeline != null) {
@ -461,22 +491,38 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private final class AdMediaSourceHolder {
private final MediaSource adMediaSource;
private final MediaPeriodId id;
private final List<MaskingMediaPeriod> activeMediaPeriods;
private @MonotonicNonNull Uri adUri;
private @MonotonicNonNull MediaSource adMediaSource;
private @MonotonicNonNull Timeline timeline;
public AdMediaSourceHolder(MediaSource adMediaSource) {
this.adMediaSource = adMediaSource;
public AdMediaSourceHolder(MediaPeriodId id) {
this.id = id;
activeMediaPeriods = new ArrayList<>();
}
public void initializeWithMediaSource(MediaSource adMediaSource, Uri adUri) {
this.adMediaSource = adMediaSource;
this.adUri = adUri;
for (int i = 0; i < activeMediaPeriods.size(); i++) {
MaskingMediaPeriod maskingMediaPeriod = activeMediaPeriods.get(i);
maskingMediaPeriod.setMediaSource(adMediaSource);
maskingMediaPeriod.setPrepareListener(new AdPrepareListener(adUri));
}
prepareChildSource(id, adMediaSource);
}
public MediaPeriod createMediaPeriod(
Uri adUri, MediaPeriodId id, Allocator allocator, long startPositionUs) {
MediaPeriodId id, Allocator allocator, long startPositionUs) {
MaskingMediaPeriod maskingMediaPeriod =
new MaskingMediaPeriod(adMediaSource, id, allocator, startPositionUs);
maskingMediaPeriod.setPrepareListener(new AdPrepareListener(adUri));
new MaskingMediaPeriod(id, allocator, startPositionUs);
activeMediaPeriods.add(maskingMediaPeriod);
if (adMediaSource != null) {
maskingMediaPeriod.setMediaSource(adMediaSource);
maskingMediaPeriod.setPrepareListener(new AdPrepareListener(checkNotNull(adUri)));
}
if (timeline != null) {
Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);
@ -510,6 +556,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
maskingMediaPeriod.releasePeriod();
}
public void release() {
if (hasMediaSource()) {
releaseChildSource(id);
}
}
public boolean hasMediaSource() {
return adMediaSource != null;
}
public boolean isInactive() {
return activeMediaPeriods.isEmpty();
}

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf;
@ -103,7 +102,8 @@ public final class MediaPeriodQueueTest {
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
setupAdTimeline(/* adGroupTimesUs...= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ C.TIME_UNSET);
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ C.TIME_UNSET);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid,
@ -128,12 +128,14 @@ public final class MediaPeriodQueueTest {
/* isLastInPeriod= */ false,
/* isLastInWindow= */ false,
/* nextAdGroupIndex= */ 0);
// The next media period info should be null as we haven't loaded the ad yet.
advance();
assertNull(getNextMediaPeriodInfo());
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 0,
/* adDurationUs= */ C.TIME_UNSET,
/* contentPositionUs= */ FIRST_AD_START_TIME_US);
setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
/* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid,
@ -147,7 +149,7 @@ public final class MediaPeriodQueueTest {
advance();
setAdGroupLoaded(/* adGroupIndex= */ 1);
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 1, /* contentPositionUs= */ SECOND_AD_START_TIME_US);
/* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ SECOND_AD_START_TIME_US);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid,
@ -175,7 +177,7 @@ public final class MediaPeriodQueueTest {
advance();
setAdGroupLoaded(/* adGroupIndex= */ 0);
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 0, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
/* adGroupIndex= */ 0, AD_DURATION_US, /* contentPositionUs= */ FIRST_AD_START_TIME_US);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid,
@ -189,7 +191,7 @@ public final class MediaPeriodQueueTest {
advance();
setAdGroupLoaded(/* adGroupIndex= */ 1);
assertNextMediaPeriodInfoIsAd(
/* adGroupIndex= */ 1, /* contentPositionUs= */ CONTENT_DURATION_US);
/* adGroupIndex= */ 1, AD_DURATION_US, /* contentPositionUs= */ CONTENT_DURATION_US);
advance();
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
/* periodUid= */ firstPeriodUid,
@ -531,7 +533,8 @@ public final class MediaPeriodQueueTest {
/* isFinal= */ isLastInWindow));
}
private void assertNextMediaPeriodInfoIsAd(int adGroupIndex, long contentPositionUs) {
private void assertNextMediaPeriodInfoIsAd(
int adGroupIndex, long adDurationUs, long contentPositionUs) {
assertThat(getNextMediaPeriodInfo())
.isEqualTo(
new MediaPeriodInfo(
@ -543,7 +546,7 @@ public final class MediaPeriodQueueTest {
/* startPositionUs= */ 0,
contentPositionUs,
/* endPositionUs= */ C.TIME_UNSET,
/* durationUs= */ AD_DURATION_US,
adDurationUs,
/* isLastInTimelinePeriod= */ false,
/* isLastInTimelineWindow= */ false,
/* isFinal= */ false));