Fix media period queue updating for ads

Resolve the media period for ad playback when resolving a subsequent period and
when receiving a timeline where the playing period in range (but wasn't before).

Fix the seek position calculation when a current ad must be skipped and is
followed by another ad.

Check MediaPeriodInfos match when checking MediaPeriodHolders, to handle cases
where a future ad should no longer be played. This may involve playing two
content media periods consecutively.

Issue: #3584

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=184514558
This commit is contained in:
andrewlewis 2018-02-05 05:15:40 -08:00 committed by Andrew Lewis
parent de293af3a4
commit 901dd19e3e
9 changed files with 249 additions and 141 deletions

View File

@ -212,8 +212,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase {
// Create media source with ad child source. // Create media source with ad child source.
Timeline timelineContentOnly = new FakeTimeline( Timeline timelineContentOnly = new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
Timeline timelineWithAds = new FakeTimeline( Timeline timelineWithAds =
new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); new FakeTimeline(
new TimelineWindowDefinition(
2,
222,
true,
false,
10 * C.MICROS_PER_SECOND,
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0)));
FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);
FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly,

View File

@ -597,8 +597,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase {
// Create dynamic media source with ad child source. // Create dynamic media source with ad child source.
Timeline timelineContentOnly = new FakeTimeline( Timeline timelineContentOnly = new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
Timeline timelineWithAds = new FakeTimeline( Timeline timelineWithAds =
new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); new FakeTimeline(
new TimelineWindowDefinition(
2,
222,
true,
false,
10 * C.MICROS_PER_SECOND,
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0)));
FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);
FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);
mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceContentOnly);

View File

@ -1145,8 +1145,9 @@ import java.util.Collections;
int periodIndex = periodPosition.first; int periodIndex = periodPosition.first;
long positionUs = periodPosition.second; long positionUs = periodPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs); MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs);
playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, playbackInfo =
positionUs); playbackInfo.fromNewPosition(
periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs);
} }
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) { } else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
@ -1157,18 +1158,30 @@ import java.util.Collections;
int periodIndex = defaultPosition.first; int periodIndex = defaultPosition.first;
long startPositionUs = defaultPosition.second; long startPositionUs = defaultPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs);
playbackInfo = playbackInfo.fromNewPosition(periodId, playbackInfo =
periodId.isAd() ? 0 : startPositionUs, startPositionUs); playbackInfo.fromNewPosition(
periodId,
periodId.isAd() ? 0 : startPositionUs,
/* contentPositionUs= */ startPositionUs);
} }
} }
return; return;
} }
int playingPeriodIndex = playbackInfo.periodId.periodIndex; int playingPeriodIndex = playbackInfo.periodId.periodIndex;
MediaPeriodHolder periodHolder = queue.getFrontPeriod(); long contentPositionUs = playbackInfo.contentPositionUs;
if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { if (oldTimeline.isEmpty()) {
// If the old timeline is empty, the period queue is also empty.
if (!timeline.isEmpty()) {
MediaPeriodId periodId =
queue.resolveMediaPeriodIdForAds(playingPeriodIndex, contentPositionUs);
playbackInfo =
playbackInfo.fromNewPosition(
periodId, periodId.isAd() ? 0 : contentPositionUs, contentPositionUs);
}
return; return;
} }
MediaPeriodHolder periodHolder = queue.getFrontPeriod();
Object playingPeriodUid = periodHolder == null Object playingPeriodUid = periodHolder == null
? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid;
int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid);
@ -1185,7 +1198,8 @@ import java.util.Collections;
Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline,
timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
newPeriodIndex = defaultPosition.first; newPeriodIndex = defaultPosition.first;
long newPositionUs = defaultPosition.second; contentPositionUs = defaultPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs);
timeline.getPeriod(newPeriodIndex, period, true); timeline.getPeriod(newPeriodIndex, period, true);
if (periodHolder != null) { if (periodHolder != null) {
// Clear the index of each holder that doesn't contain the default position. If a holder // Clear the index of each holder that doesn't contain the default position. If a holder
@ -1202,9 +1216,8 @@ import java.util.Collections;
} }
} }
// Actually do the seek. // Actually do the seek.
MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); long seekPositionUs = seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET);
return; return;
} }
@ -1213,54 +1226,21 @@ import java.util.Collections;
playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
} }
if (playbackInfo.periodId.isAd()) { MediaPeriodId playingPeriodId = playbackInfo.periodId;
// Check that the playing ad hasn't been marked as played. If it has, skip forward. if (playingPeriodId.isAd()) {
MediaPeriodId periodId = MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs);
queue.resolveMediaPeriodIdForAds(periodIndex, playbackInfo.contentPositionUs); if (!periodId.equals(playingPeriodId)) {
if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { // The previously playing ad should no longer be played, so skip it.
long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long seekPositionUs =
long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
return; return;
} }
} }
if (periodHolder == null) { if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) {
// We don't have any period holders, so we're done.
return;
}
// Update the holder indices. If we find a subsequent holder that's inconsistent with the new
// timeline then take appropriate action.
periodHolder = updatePeriodInfo(periodHolder, periodIndex);
while (periodHolder.next != null) {
MediaPeriodHolder previousPeriodHolder = periodHolder;
periodHolder = periodHolder.next;
periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode,
shuffleModeEnabled);
if (periodIndex != C.INDEX_UNSET
&& periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
// The holder is consistent with the new timeline. Update its index and continue.
periodHolder = updatePeriodInfo(periodHolder, periodIndex);
} else {
// The holder is inconsistent with the new timeline.
boolean readingPeriodRemoved = queue.removeAfter(previousPeriodHolder);
if (readingPeriodRemoved) {
seekToCurrentPosition(/* sendDiscontinuity= */ false); seekToCurrentPosition(/* sendDiscontinuity= */ false);
} }
break;
}
}
}
private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) {
while (true) {
periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) {
return periodHolder;
}
periodHolder = periodHolder.next;
}
} }
private void handleSourceInfoRefreshEndedPlayback() { private void handleSourceInfoRefreshEndedPlayback() {

View File

@ -36,8 +36,9 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
*/ */
public final long contentPositionUs; public final long contentPositionUs;
/** /**
* The duration of the media to play within the media period, in microseconds, or {@link * The duration of the media period, like {@link #endPositionUs} but with {@link
* C#TIME_UNSET} if not known. * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if
* the end position is not known.
*/ */
public final long durationUs; public final long durationUs;
/** /**

View File

@ -59,8 +59,8 @@ import com.google.android.exoplayer2.util.Assertions;
} }
/** /**
* Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the
* taking into account the new timeline. * queued media periods to take into account the new timeline.
*/ */
public void setTimeline(Timeline timeline) { public void setTimeline(Timeline timeline) {
this.timeline = timeline; this.timeline = timeline;
@ -121,8 +121,7 @@ import com.google.android.exoplayer2.util.Assertions;
long rendererPositionUs, PlaybackInfo playbackInfo) { long rendererPositionUs, PlaybackInfo playbackInfo) {
return loading == null return loading == null
? getFirstMediaPeriodInfo(playbackInfo) ? getFirstMediaPeriodInfo(playbackInfo)
: getFollowingMediaPeriodInfo( : getFollowingMediaPeriodInfo(loading, rendererPositionUs);
loading.info, loading.getRendererOffset(), rendererPositionUs);
} }
/** /**
@ -289,6 +288,61 @@ import com.google.android.exoplayer2.util.Assertions;
length = 0; length = 0;
} }
/**
* Updates media periods in the queue to take into account the latest timeline, and returns
* whether the timeline change has been fully handled. If not, it is necessary to seek to the
* current playback position.
*
* @param playingPeriodId The current playing media period identifier.
* @param rendererPositionUs The current renderer position in microseconds.
* @return Whether the timeline change has been handled completely.
*/
public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) {
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
// handled here.
int periodIndex = playingPeriodId.periodIndex;
// The front period is either playing now, or is being loaded and will become the playing
// period.
MediaPeriodHolder previousPeriodHolder = null;
MediaPeriodHolder periodHolder = getFrontPeriod();
while (periodHolder != null) {
if (previousPeriodHolder == null) {
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
} else {
// Check this period holder still follows the previous one, based on the new timeline.
MediaPeriodInfo periodInfo =
getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
if (periodInfo == null) {
// We've loaded a next media period that is not in the new timeline.
return !removeAfter(previousPeriodHolder);
}
// Update the period index.
periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
// Check the media period information matches the new timeline.
if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
return !removeAfter(previousPeriodHolder);
}
}
if (periodHolder.info.isLastInTimelinePeriod) {
// Move on to the next timeline period, if there is one.
periodIndex =
timeline.getNextPeriodIndex(
periodIndex, period, window, repeatMode, shuffleModeEnabled);
if (periodIndex == C.INDEX_UNSET
|| !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
// The holder is inconsistent with the new timeline.
return previousPeriodHolder == null || !removeAfter(previousPeriodHolder);
}
}
previousPeriodHolder = periodHolder;
periodHolder = periodHolder.next;
}
return true;
}
/** /**
* Returns new media period info based on specified {@code mediaPeriodInfo} but taking into * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into
* account the current timeline, and with the period index updated to {@code newPeriodIndex}. * account the current timeline, and with the period index updated to {@code newPeriodIndex}.
@ -325,6 +379,17 @@ import com.google.android.exoplayer2.util.Assertions;
// Internal methods. // Internal methods.
/**
* Returns whether {@code periodHolder} can be kept for playing the media period described by
* {@code info}.
*/
private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
MediaPeriodInfo periodHolderInfo = periodHolder.info;
return periodHolderInfo.startPositionUs == info.startPositionUs
&& periodHolderInfo.endPositionUs == info.endPositionUs
&& periodHolderInfo.id.equals(info.id);
}
/** /**
* Updates the queue for any playback mode change, and returns whether the change was fully * Updates the queue for any playback mode change, and returns whether the change was fully
* handled. If not, it is necessary to seek to the current playback position. * handled. If not, it is necessary to seek to the current playback position.
@ -375,28 +440,25 @@ import com.google.android.exoplayer2.util.Assertions;
} }
/** /**
* Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}. * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s
* media period.
* *
* @param currentMediaPeriodInfo The current media period info. * @param mediaPeriodHolder The media period holder.
* @param rendererOffsetUs The current renderer offset in microseconds.
* @param rendererPositionUs The current renderer position in microseconds. * @param rendererPositionUs The current renderer position in microseconds.
* @return The following media period info, or {@code null} if it is not yet possible to get the * @return The following media period's info, or {@code null} if it is not yet possible to get the
* next media period info. * next media period info.
*/ */
private MediaPeriodInfo getFollowingMediaPeriodInfo( private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo(
MediaPeriodInfo currentMediaPeriodInfo, long rendererOffsetUs, long rendererPositionUs) { MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) {
// TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod
// but if the timeline is not ready to provide the next period it can't return a non-null value // but if the timeline is not ready to provide the next period it can't return a non-null value
// until the timeline is updated. Store whether the next timeline period is ready when the // until the timeline is updated. Store whether the next timeline period is ready when the
// timeline is updated, to avoid repeatedly checking the same timeline. // timeline is updated, to avoid repeatedly checking the same timeline.
if (currentMediaPeriodInfo.isLastInTimelinePeriod) { MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;
if (mediaPeriodInfo.isLastInTimelinePeriod) {
int nextPeriodIndex = int nextPeriodIndex =
timeline.getNextPeriodIndex( timeline.getNextPeriodIndex(
currentMediaPeriodInfo.id.periodIndex, mediaPeriodInfo.id.periodIndex, period, window, repeatMode, shuffleModeEnabled);
period,
window,
repeatMode,
shuffleModeEnabled);
if (nextPeriodIndex == C.INDEX_UNSET) { if (nextPeriodIndex == C.INDEX_UNSET) {
// We can't create a next period yet. // We can't create a next period yet.
return null; return null;
@ -411,7 +473,7 @@ import com.google.android.exoplayer2.util.Assertions;
// interruptions). Hence we project the default start position forward by the duration of // interruptions). Hence we project the default start position forward by the duration of
// the buffer, and start buffering from this point. // the buffer, and start buffering from this point.
long defaultPositionProjectionUs = long defaultPositionProjectionUs =
rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
Pair<Integer, Long> defaultPosition = Pair<Integer, Long> defaultPosition =
timeline.getPeriodPosition( timeline.getPeriodPosition(
window, window,
@ -431,10 +493,10 @@ import com.google.android.exoplayer2.util.Assertions;
return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs);
} }
MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
timeline.getPeriod(currentPeriodId.periodIndex, period);
if (currentPeriodId.isAd()) { if (currentPeriodId.isAd()) {
int currentAdGroupIndex = currentPeriodId.adGroupIndex; int currentAdGroupIndex = currentPeriodId.adGroupIndex;
timeline.getPeriod(currentPeriodId.periodIndex, period);
int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex);
if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
return null; return null;
@ -448,29 +510,24 @@ import com.google.android.exoplayer2.util.Assertions;
currentPeriodId.periodIndex, currentPeriodId.periodIndex,
currentAdGroupIndex, currentAdGroupIndex,
nextAdIndexInAdGroup, nextAdIndexInAdGroup,
currentMediaPeriodInfo.contentPositionUs); mediaPeriodInfo.contentPositionUs);
} else { } else {
// Play content from the ad group position. // Play content from the ad group position.
int nextAdGroupIndex =
period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs);
long endUs =
nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_END_OF_SOURCE
: period.getAdGroupTimeUs(nextAdGroupIndex);
return getMediaPeriodInfoForContent( return getMediaPeriodInfoForContent(
currentPeriodId.periodIndex, currentMediaPeriodInfo.contentPositionUs, endUs); currentPeriodId.periodIndex, mediaPeriodInfo.contentPositionUs);
} }
} else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
// Play the next ad group if it's available. // Play the next ad group if it's available.
int nextAdGroupIndex = int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); if (nextAdGroupIndex == C.INDEX_UNSET) {
// The next ad group can't be played. Play content from the ad group position instead.
return getMediaPeriodInfoForContent(
currentPeriodId.periodIndex, mediaPeriodInfo.endPositionUs);
}
return !period.isAdAvailable(nextAdGroupIndex, 0) return !period.isAdAvailable(nextAdGroupIndex, 0)
? null ? null
: getMediaPeriodInfoForAd( : getMediaPeriodInfoForAd(
currentPeriodId.periodIndex, currentPeriodId.periodIndex, nextAdGroupIndex, 0, mediaPeriodInfo.endPositionUs);
nextAdGroupIndex,
0,
currentMediaPeriodInfo.endPositionUs);
} else { } else {
// Check if the postroll ad should be played. // Check if the postroll ad should be played.
int adGroupCount = period.getAdGroupCount(); int adGroupCount = period.getAdGroupCount();
@ -516,12 +573,7 @@ import com.google.android.exoplayer2.util.Assertions;
return getMediaPeriodInfoForAd( return getMediaPeriodInfoForAd(
id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs); id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs);
} else { } else {
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs);
long endUs =
nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_END_OF_SOURCE
: period.getAdGroupTimeUs(nextAdGroupIndex);
return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs);
} }
} }
@ -548,12 +600,16 @@ import com.google.android.exoplayer2.util.Assertions;
isLastInTimeline); isLastInTimeline);
} }
private MediaPeriodInfo getMediaPeriodInfoForContent( private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs) {
int periodIndex, long startPositionUs, long endUs) {
MediaPeriodId id = new MediaPeriodId(periodIndex); MediaPeriodId id = new MediaPeriodId(periodIndex);
timeline.getPeriod(id.periodIndex, period);
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
long endUs =
nextAdGroupIndex == C.INDEX_UNSET
? C.TIME_END_OF_SOURCE
: period.getAdGroupTimeUs(nextAdGroupIndex);
boolean isLastInPeriod = isLastInPeriod(id, endUs); boolean isLastInPeriod = isLastInPeriod(id, endUs);
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
timeline.getPeriod(id.periodIndex, period);
long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
return new MediaPeriodInfo( return new MediaPeriodInfo(
id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline);

View File

@ -70,11 +70,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
this.trackSelectorResult = trackSelectorResult; this.trackSelectorResult = trackSelectorResult;
} }
public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs,
long contentPositionUs) {
return fromNewPosition(new MediaPeriodId(periodIndex), startPositionUs, contentPositionUs);
}
public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs,
long contentPositionUs) { long contentPositionUs) {
return new PlaybackInfo( return new PlaybackInfo(
@ -82,7 +77,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
manifest, manifest,
periodId, periodId,
startPositionUs, startPositionUs,
contentPositionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET,
playbackState, playbackState,
isLoading, isLoading,
trackSelectorResult); trackSelectorResult);

View File

@ -211,7 +211,7 @@ public final class AdPlaybackState {
public static final int AD_STATE_ERROR = 4; public static final int AD_STATE_ERROR = 4;
/** Ad playback state with no ads. */ /** Ad playback state with no ads. */
public static final AdPlaybackState NONE = new AdPlaybackState(new long[0]); public static final AdPlaybackState NONE = new AdPlaybackState();
/** The number of ad groups. */ /** The number of ad groups. */
public final int adGroupCount; public final int adGroupCount;
@ -233,7 +233,7 @@ public final class AdPlaybackState {
* @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value
* {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
*/ */
public AdPlaybackState(long[] adGroupTimesUs) { public AdPlaybackState(long... adGroupTimesUs) {
int count = adGroupTimesUs.length; int count = adGroupTimesUs.length;
adGroupCount = count; adGroupCount = count;
this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count); this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);

View File

@ -26,6 +26,7 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
@ -384,6 +385,57 @@ public final class ExoPlayerTest {
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
} }
@Test
public void testAdGroupWithLoadErrorIsSkipped() throws Exception {
AdPlaybackState initialAdPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 5 * C.MICROS_PER_SECOND);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ C.MICROS_PER_SECOND,
initialAdPlaybackState));
AdPlaybackState errorAdPlaybackState = initialAdPlaybackState.withAdLoadError(0, 0);
final Timeline adErrorTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ C.MICROS_PER_SECOND,
errorAdPlaybackState));
final FakeMediaSource fakeMediaSource =
new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new Runnable() {
@Override
public void run() {
fakeMediaSource.setNewSourceInfo(adErrorTimeline, null);
}
})
.waitForTimelineChanged(adErrorTimeline)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
// There is still one discontinuity from content to content for the failed ad insertion.
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AD_INSERTION);
}
@Test @Test
public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);

View File

@ -40,8 +40,7 @@ public final class FakeTimeline extends Timeline {
public final boolean isSeekable; public final boolean isSeekable;
public final boolean isDynamic; public final boolean isDynamic;
public final long durationUs; public final long durationUs;
public final int adGroupsPerPeriodCount; public final AdPlaybackState adPlaybackState;
public final int adsPerAdGroupCount;
/** /**
* Creates a seekable, non-dynamic window definition with one period with a duration of * Creates a seekable, non-dynamic window definition with one period with a duration of
@ -86,7 +85,7 @@ public final class FakeTimeline extends Timeline {
*/ */
public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable,
boolean isDynamic, long durationUs) { boolean isDynamic, long durationUs) {
this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); this(periodCount, id, isSeekable, isDynamic, durationUs, AdPlaybackState.NONE);
} }
/** /**
@ -98,19 +97,21 @@ public final class FakeTimeline extends Timeline {
* @param isSeekable Whether the window is seekable. * @param isSeekable Whether the window is seekable.
* @param isDynamic Whether the window is dynamic. * @param isDynamic Whether the window is dynamic.
* @param durationUs The duration of the window in microseconds. * @param durationUs The duration of the window in microseconds.
* @param adGroupsCountPerPeriod The number of ad groups in each period. The position of the ad * @param adPlaybackState The ad playback state.
* groups is equally distributed in each period starting.
* @param adsPerAdGroupCount The number of ads in each ad group.
*/ */
public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, public TimelineWindowDefinition(
boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { int periodCount,
Object id,
boolean isSeekable,
boolean isDynamic,
long durationUs,
AdPlaybackState adPlaybackState) {
this.periodCount = periodCount; this.periodCount = periodCount;
this.id = id; this.id = id;
this.isSeekable = isSeekable; this.isSeekable = isSeekable;
this.isDynamic = isDynamic; this.isDynamic = isDynamic;
this.durationUs = durationUs; this.durationUs = durationUs;
this.adGroupsPerPeriodCount = adGroupsCountPerPeriod; this.adPlaybackState = adPlaybackState;
this.adsPerAdGroupCount = adsPerAdGroupCount;
} }
} }
@ -120,6 +121,27 @@ public final class FakeTimeline extends Timeline {
private final TimelineWindowDefinition[] windowDefinitions; private final TimelineWindowDefinition[] windowDefinitions;
private final int[] periodOffsets; private final int[] periodOffsets;
/**
* Returns an ad playback state with the specified number of ads in each of the specified ad
* groups, each ten seconds long.
*
* @param adsPerAdGroup The number of ads per ad group.
* @param adGroupTimesUs The times of ad groups, in microseconds.
* @return The ad playback state.
*/
public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... adGroupTimesUs) {
int adGroupCount = adGroupTimesUs.length;
AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs);
long[][] adDurationsUs = new long[adGroupCount][];
for (int i = 0; i < adGroupCount; i++) {
adPlaybackState = adPlaybackState.withAdCount(i, adsPerAdGroup);
adDurationsUs[i] = new long[adsPerAdGroup];
Arrays.fill(adDurationsUs[i], AD_DURATION_US);
}
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
return adPlaybackState;
}
/** /**
* Creates a fake timeline with the given number of seekable, non-dynamic windows with one period * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period
* with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each.
@ -173,27 +195,13 @@ public final class FakeTimeline extends Timeline {
Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null;
long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount;
long positionInWindowUs = periodDurationUs * windowPeriodIndex; long positionInWindowUs = periodDurationUs * windowPeriodIndex;
if (windowDefinition.adGroupsPerPeriodCount == 0) {
return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs);
} else {
int adGroups = windowDefinition.adGroupsPerPeriodCount;
long[] adGroupTimesUs = new long[adGroups];
long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0;
for (int i = 0; i < adGroups; i++) {
adGroupTimesUs[i] = i * adGroupOffset;
}
AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs);
long[][] adDurationsUs = new long[adGroups][];
for (int i = 0; i < adGroups; i++) {
int adCount = windowDefinition.adsPerAdGroupCount;
adPlaybackState = adPlaybackState.withAdCount(i, adCount);
adDurationsUs[i] = new long[adCount];
Arrays.fill(adDurationsUs[i], AD_DURATION_US);
}
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
return period.set( return period.set(
id, uid, windowIndex, periodDurationUs, positionInWindowUs, adPlaybackState); id,
} uid,
windowIndex,
periodDurationUs,
positionInWindowUs,
windowDefinition.adPlaybackState);
} }
@Override @Override