mirror of
https://github.com/androidx/media.git
synced 2025-05-14 11:09:53 +08:00
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:
parent
de293af3a4
commit
901dd19e3e
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user