No-op restructure of playlist update handling.
This restructure moves all the position resolving code to a static method and removes the dependency of the MediaPeriodQueue on having an up-to-date timeline. Both steps allow simplified reasoning about the code as the static method can't change the state of the player, and there is no risk the queue can use the wrong timeline. These propoerties allow to fix a bug causing inconsistent states in a follow-up step. PiperOrigin-RevId: 290616395
This commit is contained in:
parent
df41ae3f42
commit
21c76b012c
@ -839,7 +839,7 @@ import java.util.concurrent.TimeoutException;
|
||||
long startPositionUs = playbackInfo.positionUs;
|
||||
if (clearPlaylist) {
|
||||
timeline = Timeline.EMPTY;
|
||||
mediaPeriodId = playbackInfo.getDummyPeriodForEmptyTimeline();
|
||||
mediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline();
|
||||
contentPositionUs = C.TIME_UNSET;
|
||||
startPositionUs = C.TIME_UNSET;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
@ -663,7 +664,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)
|
||||
throws ExoPlaybackException {
|
||||
this.repeatMode = repeatMode;
|
||||
if (!queue.updateRepeatMode(repeatMode)) {
|
||||
if (!queue.updateRepeatMode(playbackInfo.timeline, repeatMode)) {
|
||||
seekToCurrentPosition(/* sendDiscontinuity= */ true);
|
||||
}
|
||||
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
||||
@ -672,7 +673,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
|
||||
throws ExoPlaybackException {
|
||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||
if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {
|
||||
if (!queue.updateShuffleModeEnabled(playbackInfo.timeline, shuffleModeEnabled)) {
|
||||
seekToCurrentPosition(/* sendDiscontinuity= */ true);
|
||||
}
|
||||
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
|
||||
@ -867,7 +868,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
boolean seekPositionAdjusted;
|
||||
@Nullable
|
||||
Pair<Object, Long> resolvedSeekPosition =
|
||||
resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
|
||||
resolveSeekPosition(
|
||||
playbackInfo.timeline,
|
||||
seekPosition,
|
||||
/* trySubsequentPeriods= */ true,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
window,
|
||||
period);
|
||||
if (resolvedSeekPosition == null) {
|
||||
// The seek position was valid for the timeline that it was performed into, but the
|
||||
// timeline has changed or is not ready and a suitable seek position could not be resolved.
|
||||
@ -879,7 +887,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
// Update the resolved seek position to take ads into account.
|
||||
Object periodUid = resolvedSeekPosition.first;
|
||||
contentPositionUs = resolvedSeekPosition.second;
|
||||
periodId = queue.resolveMediaPeriodIdForAds(periodUid, contentPositionUs);
|
||||
periodId =
|
||||
queue.resolveMediaPeriodIdForAds(playbackInfo.timeline, periodUid, contentPositionUs);
|
||||
if (periodId.isAd()) {
|
||||
periodPositionUs = 0;
|
||||
seekPositionAdjusted = true;
|
||||
@ -1131,7 +1140,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
if (clearPlaylist) {
|
||||
timeline = playlist.clear(/* shuffleOrder= */ null);
|
||||
queue.setTimeline(timeline);
|
||||
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
|
||||
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
|
||||
}
|
||||
@ -1144,7 +1152,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
if (resetPosition) {
|
||||
mediaPeriodId =
|
||||
timeline.isEmpty()
|
||||
? playbackInfo.getDummyPeriodForEmptyTimeline()
|
||||
? PlaybackInfo.getDummyPeriodForEmptyTimeline()
|
||||
: getDummyFirstMediaPeriodForAds();
|
||||
contentPositionUs = C.TIME_UNSET;
|
||||
if (!mediaPeriodId.equals(playbackInfo.periodId)) {
|
||||
@ -1175,11 +1183,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
private MediaPeriodId getDummyFirstMediaPeriodForAds() {
|
||||
MediaPeriodId dummyFirstMediaPeriodId =
|
||||
playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period);
|
||||
getDummyFirstMediaPeriodId(
|
||||
playbackInfo.timeline, playbackInfo.periodId, shuffleModeEnabled, window, period);
|
||||
if (!playbackInfo.timeline.isEmpty()) {
|
||||
// add ad metadata if any and propagate the window sequence number to new period id.
|
||||
dummyFirstMediaPeriodId =
|
||||
queue.resolveMediaPeriodIdForAds(dummyFirstMediaPeriodId.periodUid, /* positionUs= */ 0);
|
||||
queue.resolveMediaPeriodIdForAds(
|
||||
playbackInfo.timeline, dummyFirstMediaPeriodId.periodUid, /* positionUs= */ 0);
|
||||
}
|
||||
return dummyFirstMediaPeriodId;
|
||||
}
|
||||
@ -1260,13 +1270,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) {
|
||||
if (pendingMessageInfo.resolvedPeriodUid == null) {
|
||||
// Position is still unresolved. Try to find window in current timeline.
|
||||
@Nullable
|
||||
Pair<Object, Long> periodPosition =
|
||||
resolveSeekPosition(
|
||||
playbackInfo.timeline,
|
||||
new SeekPosition(
|
||||
pendingMessageInfo.message.getTimeline(),
|
||||
pendingMessageInfo.message.getWindowIndex(),
|
||||
C.msToUs(pendingMessageInfo.message.getPositionMs())),
|
||||
/* trySubsequentPeriods= */ false);
|
||||
/* trySubsequentPeriods= */ false,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
window,
|
||||
period);
|
||||
if (periodPosition == null) {
|
||||
return false;
|
||||
}
|
||||
@ -1509,100 +1525,53 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
|
||||
private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
|
||||
Timeline oldTimeline = playbackInfo.timeline;
|
||||
queue.setTimeline(timeline);
|
||||
PositionUpdateForPlaylistChange positionUpdate =
|
||||
resolvePositionForPlaylistChange(
|
||||
timeline,
|
||||
playbackInfo,
|
||||
pendingInitialSeekPosition,
|
||||
queue,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
window,
|
||||
period);
|
||||
|
||||
playbackInfo = playbackInfo.copyWithTimeline(timeline);
|
||||
resolvePendingMessagePositions();
|
||||
if (timeline.isEmpty()) {
|
||||
@Nullable SeekPosition pendingInitialSeekPosition = this.pendingInitialSeekPosition;
|
||||
handleEndOfPlaylist();
|
||||
// Retain seek position if any.
|
||||
this.pendingInitialSeekPosition = pendingInitialSeekPosition;
|
||||
return;
|
||||
}
|
||||
MediaPeriodId oldPeriodId = playbackInfo.periodId;
|
||||
Object newPeriodUid = oldPeriodId.periodUid;
|
||||
long oldContentPositionUs =
|
||||
oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
|
||||
long newContentPositionUs = oldContentPositionUs;
|
||||
boolean forceBufferingState = false;
|
||||
if (pendingInitialSeekPosition != null) {
|
||||
// Resolve initial seek position.
|
||||
@Nullable
|
||||
Pair<Object, Long> periodPosition =
|
||||
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
|
||||
if (periodPosition == null) {
|
||||
// The initial seek in the empty old timeline is invalid in the new timeline.
|
||||
handleEndOfPlaylist();
|
||||
// Use the period resulting from the reset.
|
||||
newPeriodUid = playbackInfo.periodId.periodUid;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else {
|
||||
// The pending seek has been resolved successfully in the new timeline.
|
||||
newPeriodUid = periodPosition.first;
|
||||
newContentPositionUs =
|
||||
pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET
|
||||
? C.TIME_UNSET
|
||||
: periodPosition.second;
|
||||
forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED;
|
||||
}
|
||||
if (!timeline.isEmpty()) {
|
||||
// Retain pending seek position only while the timeline is still empty.
|
||||
pendingInitialSeekPosition = null;
|
||||
} else if (oldTimeline.isEmpty()) {
|
||||
// Resolve to default position if the old timeline is empty and no seek is requested above.
|
||||
Pair<Object, Long> defaultPosition =
|
||||
getPeriodPosition(
|
||||
timeline,
|
||||
timeline.getFirstWindowIndex(shuffleModeEnabled),
|
||||
/* windowPositionUs= */ C.TIME_UNSET);
|
||||
newPeriodUid = defaultPosition.first;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) {
|
||||
// The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
|
||||
// window we can restart from.
|
||||
@Nullable
|
||||
Object subsequentPeriodUid =
|
||||
resolveSubsequentPeriod(
|
||||
window, period, repeatMode, shuffleModeEnabled, newPeriodUid, oldTimeline, timeline);
|
||||
if (subsequentPeriodUid == null) {
|
||||
// We failed to resolve a suitable restart position but the timeline is not empty.
|
||||
handleEndOfPlaylist();
|
||||
// Use period and position resulting from the reset.
|
||||
newPeriodUid = playbackInfo.periodId.periodUid;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else {
|
||||
// We resolved a subsequent period. Start at the default position in the corresponding
|
||||
// window.
|
||||
Pair<Object, Long> defaultPosition =
|
||||
getPeriodPosition(
|
||||
timeline,
|
||||
timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex,
|
||||
C.TIME_UNSET);
|
||||
newPeriodUid = defaultPosition.first;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure ad insertion metadata is up to date.
|
||||
long contentPositionForAdResolution = newContentPositionUs;
|
||||
if (contentPositionForAdResolution == C.TIME_UNSET) {
|
||||
contentPositionForAdResolution =
|
||||
timeline.getWindow(timeline.getPeriodByUid(newPeriodUid, period).windowIndex, window)
|
||||
.defaultPositionUs;
|
||||
MediaPeriodId oldPeriodId = playbackInfo.periodId;
|
||||
long oldContentPositionUs =
|
||||
oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
|
||||
MediaPeriodId newPeriodId = positionUpdate.periodId;
|
||||
long newContentPositionUs = positionUpdate.contentPositionUs;
|
||||
boolean forceBufferingState = positionUpdate.forceBufferingState;
|
||||
long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
|
||||
|
||||
if (positionUpdate.endPlayback) {
|
||||
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
||||
setState(Player.STATE_ENDED);
|
||||
}
|
||||
playbackInfo = copyWithNewPosition(newPeriodId, newPositionUs, newContentPositionUs);
|
||||
// Reset, but retain the playlist and new position.
|
||||
resetInternal(
|
||||
/* resetRenderers= */ false,
|
||||
/* resetPosition= */ false,
|
||||
/* releasePlaylist= */ false,
|
||||
/* clearPlaylist= */ false,
|
||||
/* resetError= */ true);
|
||||
if (timeline.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
MediaPeriodId periodIdWithAds =
|
||||
queue.resolveMediaPeriodIdForAds(newPeriodUid, contentPositionForAdResolution);
|
||||
boolean oldAndNewPeriodIdAreSame =
|
||||
oldPeriodId.periodUid.equals(newPeriodUid)
|
||||
&& !oldPeriodId.isAd()
|
||||
&& !periodIdWithAds.isAd();
|
||||
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
|
||||
// only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
|
||||
// discontinuity until we reach the former next ad group position.
|
||||
MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds;
|
||||
|
||||
if (oldPeriodId.equals(newPeriodId) && oldContentPositionUs == newContentPositionUs) {
|
||||
// We can keep the current playing period. Update the rest of the queued periods.
|
||||
if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) {
|
||||
if (!queue.updateQueuedPeriods(
|
||||
timeline, rendererPositionUs, getMaxRendererReadPositionUs())) {
|
||||
seekToCurrentPosition(/* sendDiscontinuity= */ false);
|
||||
}
|
||||
} else {
|
||||
@ -1613,11 +1582,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
while (periodHolder.getNext() != null) {
|
||||
periodHolder = periodHolder.getNext();
|
||||
if (periodHolder.info.id.equals(newPeriodId)) {
|
||||
periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info);
|
||||
periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info);
|
||||
}
|
||||
}
|
||||
}
|
||||
long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs;
|
||||
if (!newPeriodId.isAd() && newContentPositionUs == C.TIME_UNSET) {
|
||||
// Get the default position for the first new period that is not an ad.
|
||||
int windowIndex = timeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex;
|
||||
@ -1657,95 +1625,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
return maxReadPositionUs;
|
||||
}
|
||||
|
||||
private void handleEndOfPlaylist() {
|
||||
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
||||
setState(Player.STATE_ENDED);
|
||||
}
|
||||
// Reset, but retain the playlist so that it can still resume after a seek or be modified.
|
||||
resetInternal(
|
||||
/* resetRenderers= */ false,
|
||||
/* resetPosition= */ true,
|
||||
/* releasePlaylist= */ false,
|
||||
/* clearPlaylist= */ false,
|
||||
/* resetError= */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the
|
||||
* internal timeline.
|
||||
*
|
||||
* @param seekPosition The position to resolve.
|
||||
* @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching
|
||||
* period if the original period is no longer available.
|
||||
* @return The resolved position, or null if resolution was not successful.
|
||||
* @throws IllegalSeekPositionException If the window index of the seek position is outside the
|
||||
* bounds of the timeline.
|
||||
*/
|
||||
@Nullable
|
||||
private Pair<Object, Long> resolveSeekPosition(
|
||||
SeekPosition seekPosition, boolean trySubsequentPeriods) {
|
||||
Timeline timeline = playbackInfo.timeline;
|
||||
Timeline seekTimeline = seekPosition.timeline;
|
||||
if (timeline.isEmpty()) {
|
||||
// We don't have a valid timeline yet, so we can't resolve the position.
|
||||
return null;
|
||||
}
|
||||
if (seekTimeline.isEmpty()) {
|
||||
// The application performed a blind seek with an empty timeline (most likely based on
|
||||
// knowledge of what the future timeline will be). Use the internal timeline.
|
||||
seekTimeline = timeline;
|
||||
}
|
||||
// Map the SeekPosition to a position in the corresponding timeline.
|
||||
Pair<Object, Long> periodPosition;
|
||||
try {
|
||||
periodPosition =
|
||||
seekTimeline.getPeriodPosition(
|
||||
window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
// The window index of the seek position was outside the bounds of the timeline.
|
||||
return null;
|
||||
}
|
||||
if (timeline.equals(seekTimeline)) {
|
||||
// Our internal timeline is the seek timeline, so the mapped position is correct.
|
||||
return periodPosition;
|
||||
}
|
||||
// Attempt to find the mapped period in the internal timeline.
|
||||
int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
|
||||
if (periodIndex != C.INDEX_UNSET) {
|
||||
// We successfully located the period in the internal timeline.
|
||||
return periodPosition;
|
||||
}
|
||||
if (trySubsequentPeriods) {
|
||||
// Try and find a subsequent period from the seek timeline in the internal timeline.
|
||||
@Nullable
|
||||
Object periodUid =
|
||||
resolveSubsequentPeriod(
|
||||
window,
|
||||
period,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
periodPosition.first,
|
||||
seekTimeline,
|
||||
timeline);
|
||||
if (periodUid != null) {
|
||||
// We found one. Use the default position of the corresponding window.
|
||||
return getPeriodPosition(
|
||||
timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET);
|
||||
}
|
||||
}
|
||||
// We didn't find one. Give up.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
|
||||
* current timeline.
|
||||
*/
|
||||
private Pair<Object, Long> getPeriodPosition(
|
||||
Timeline timeline, int windowIndex, long windowPositionUs) {
|
||||
return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
|
||||
}
|
||||
|
||||
private void updatePeriods() throws ExoPlaybackException, IOException {
|
||||
if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
|
||||
// We're waiting to get information about periods.
|
||||
@ -2199,6 +2078,234 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
.sendToTarget();
|
||||
}
|
||||
|
||||
private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange(
|
||||
Timeline timeline,
|
||||
PlaybackInfo playbackInfo,
|
||||
@Nullable SeekPosition pendingInitialSeekPosition,
|
||||
MediaPeriodQueue queue,
|
||||
@RepeatMode int repeatMode,
|
||||
boolean shuffleModeEnabled,
|
||||
Timeline.Window window,
|
||||
Timeline.Period period) {
|
||||
if (timeline.isEmpty()) {
|
||||
return new PositionUpdateForPlaylistChange(
|
||||
PlaybackInfo.getDummyPeriodForEmptyTimeline(),
|
||||
/* contentPositionUs= */ C.TIME_UNSET,
|
||||
/* forceBufferingState= */ false,
|
||||
/* endPlayback= */ true);
|
||||
}
|
||||
MediaPeriodId oldPeriodId = playbackInfo.periodId;
|
||||
Object newPeriodUid = oldPeriodId.periodUid;
|
||||
long oldContentPositionUs =
|
||||
oldPeriodId.isAd() ? playbackInfo.contentPositionUs : playbackInfo.positionUs;
|
||||
long newContentPositionUs = oldContentPositionUs;
|
||||
boolean forceBufferingState = false;
|
||||
boolean endPlayback = false;
|
||||
if (pendingInitialSeekPosition != null) {
|
||||
// Resolve initial seek position.
|
||||
@Nullable
|
||||
Pair<Object, Long> periodPosition =
|
||||
resolveSeekPosition(
|
||||
timeline,
|
||||
pendingInitialSeekPosition,
|
||||
/* trySubsequentPeriods= */ true,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
window,
|
||||
period);
|
||||
if (periodPosition == null) {
|
||||
// The initial seek in the empty old timeline is invalid in the new timeline.
|
||||
endPlayback = true;
|
||||
newPeriodUid =
|
||||
getDummyFirstMediaPeriodId(
|
||||
timeline, playbackInfo.periodId, shuffleModeEnabled, window, period)
|
||||
.periodUid;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else {
|
||||
// The pending seek has been resolved successfully in the new timeline.
|
||||
newPeriodUid = periodPosition.first;
|
||||
newContentPositionUs =
|
||||
pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET
|
||||
? C.TIME_UNSET
|
||||
: periodPosition.second;
|
||||
forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED;
|
||||
}
|
||||
} else if (playbackInfo.timeline.isEmpty()) {
|
||||
// Resolve to default position if the old timeline is empty and no seek is requested above.
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
period,
|
||||
timeline.getFirstWindowIndex(shuffleModeEnabled),
|
||||
/* windowPositionUs= */ C.TIME_UNSET);
|
||||
newPeriodUid = defaultPosition.first;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) {
|
||||
// The current period isn't in the new timeline. Attempt to resolve a subsequent period whose
|
||||
// window we can restart from.
|
||||
@Nullable
|
||||
Object subsequentPeriodUid =
|
||||
resolveSubsequentPeriod(
|
||||
window,
|
||||
period,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
newPeriodUid,
|
||||
playbackInfo.timeline,
|
||||
timeline);
|
||||
if (subsequentPeriodUid == null) {
|
||||
// We failed to resolve a suitable restart position but the timeline is not empty.
|
||||
endPlayback = true;
|
||||
newPeriodUid =
|
||||
getDummyFirstMediaPeriodId(
|
||||
timeline, playbackInfo.periodId, shuffleModeEnabled, window, period)
|
||||
.periodUid;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
} else {
|
||||
// We resolved a subsequent period. Start at the default position in the corresponding
|
||||
// window.
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
period,
|
||||
timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex,
|
||||
/* windowPositionUs= */ C.TIME_UNSET);
|
||||
newPeriodUid = defaultPosition.first;
|
||||
newContentPositionUs = C.TIME_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure ad insertion metadata is up to date.
|
||||
long contentPositionForAdResolution = newContentPositionUs;
|
||||
if (contentPositionForAdResolution == C.TIME_UNSET) {
|
||||
contentPositionForAdResolution =
|
||||
timeline.getWindow(timeline.getPeriodByUid(newPeriodUid, period).windowIndex, window)
|
||||
.defaultPositionUs;
|
||||
}
|
||||
MediaPeriodId periodIdWithAds =
|
||||
queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolution);
|
||||
boolean oldAndNewPeriodIdAreSame =
|
||||
oldPeriodId.periodUid.equals(newPeriodUid)
|
||||
&& !oldPeriodId.isAd()
|
||||
&& !periodIdWithAds.isAd();
|
||||
// Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and
|
||||
// only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential
|
||||
// discontinuity until we reach the former next ad group position.
|
||||
MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds;
|
||||
|
||||
return new PositionUpdateForPlaylistChange(
|
||||
newPeriodId, newContentPositionUs, forceBufferingState, endPlayback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the
|
||||
* internal timeline.
|
||||
*
|
||||
* @param seekPosition The position to resolve.
|
||||
* @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching
|
||||
* period if the original period is no longer available.
|
||||
* @return The resolved position, or null if resolution was not successful.
|
||||
* @throws IllegalSeekPositionException If the window index of the seek position is outside the
|
||||
* bounds of the timeline.
|
||||
*/
|
||||
@Nullable
|
||||
private static Pair<Object, Long> resolveSeekPosition(
|
||||
Timeline timeline,
|
||||
SeekPosition seekPosition,
|
||||
boolean trySubsequentPeriods,
|
||||
@RepeatMode int repeatMode,
|
||||
boolean shuffleModeEnabled,
|
||||
Timeline.Window window,
|
||||
Timeline.Period period) {
|
||||
Timeline seekTimeline = seekPosition.timeline;
|
||||
if (timeline.isEmpty()) {
|
||||
// We don't have a valid timeline yet, so we can't resolve the position.
|
||||
return null;
|
||||
}
|
||||
if (seekTimeline.isEmpty()) {
|
||||
// The application performed a blind seek with an empty timeline (most likely based on
|
||||
// knowledge of what the future timeline will be). Use the internal timeline.
|
||||
seekTimeline = timeline;
|
||||
}
|
||||
// Map the SeekPosition to a position in the corresponding timeline.
|
||||
Pair<Object, Long> periodPosition;
|
||||
try {
|
||||
periodPosition =
|
||||
seekTimeline.getPeriodPosition(
|
||||
window, period, seekPosition.windowIndex, seekPosition.windowPositionUs);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
// The window index of the seek position was outside the bounds of the timeline.
|
||||
return null;
|
||||
}
|
||||
if (timeline.equals(seekTimeline)) {
|
||||
// Our internal timeline is the seek timeline, so the mapped position is correct.
|
||||
return periodPosition;
|
||||
}
|
||||
// Attempt to find the mapped period in the internal timeline.
|
||||
int periodIndex = timeline.getIndexOfPeriod(periodPosition.first);
|
||||
if (periodIndex != C.INDEX_UNSET) {
|
||||
// We successfully located the period in the internal timeline.
|
||||
return periodPosition;
|
||||
}
|
||||
if (trySubsequentPeriods) {
|
||||
// Try and find a subsequent period from the seek timeline in the internal timeline.
|
||||
@Nullable
|
||||
Object periodUid =
|
||||
resolveSubsequentPeriod(
|
||||
window,
|
||||
period,
|
||||
repeatMode,
|
||||
shuffleModeEnabled,
|
||||
periodPosition.first,
|
||||
seekTimeline,
|
||||
timeline);
|
||||
if (periodUid != null) {
|
||||
// We found one. Use the default position of the corresponding window.
|
||||
return timeline.getPeriodPosition(
|
||||
window,
|
||||
period,
|
||||
timeline.getPeriodByUid(periodUid, period).windowIndex,
|
||||
/* windowPositionUs= */ C.TIME_UNSET);
|
||||
}
|
||||
}
|
||||
// We didn't find one. Give up.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dummy media period id for the first-to-be-played period of the current timeline.
|
||||
*
|
||||
* @param timeline The timeline whose first-to-be-played period needs to be found.
|
||||
* @param currentMediaPeriodId The current media period id, not guaranteed to be part of {@code
|
||||
* timeline}.
|
||||
* @param shuffleModeEnabled Whether shuffle mode is enabled.
|
||||
* @param window A writable {@link Timeline.Window}.
|
||||
* @param period A writable {@link Timeline.Period}.
|
||||
* @return A dummy media period id for the first-to-be-played period of the current timeline.
|
||||
*/
|
||||
private static MediaPeriodId getDummyFirstMediaPeriodId(
|
||||
Timeline timeline,
|
||||
MediaPeriodId currentMediaPeriodId,
|
||||
boolean shuffleModeEnabled,
|
||||
Timeline.Window window,
|
||||
Timeline.Period period) {
|
||||
if (timeline.isEmpty()) {
|
||||
return PlaybackInfo.getDummyPeriodForEmptyTimeline();
|
||||
}
|
||||
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
|
||||
int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;
|
||||
int currentPeriodIndex = timeline.getIndexOfPeriod(currentMediaPeriodId.periodUid);
|
||||
long windowSequenceNumber = C.INDEX_UNSET;
|
||||
if (currentPeriodIndex != C.INDEX_UNSET) {
|
||||
int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex;
|
||||
if (firstWindowIndex == currentWindowIndex) {
|
||||
// Keep window sequence number if the new position is still in the same window.
|
||||
windowSequenceNumber = currentMediaPeriodId.windowSequenceNumber;
|
||||
}
|
||||
}
|
||||
return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a period index into an old timeline, finds the first subsequent period that also exists
|
||||
* in a new timeline. The uid of this period in the new timeline is returned.
|
||||
@ -2260,6 +2367,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PositionUpdateForPlaylistChange {
|
||||
public final MediaPeriodId periodId;
|
||||
public final long contentPositionUs;
|
||||
public final boolean forceBufferingState;
|
||||
public final boolean endPlayback;
|
||||
|
||||
public PositionUpdateForPlaylistChange(
|
||||
MediaPeriodId periodId,
|
||||
long contentPositionUs,
|
||||
boolean forceBufferingState,
|
||||
boolean endPlayback) {
|
||||
this.periodId = periodId;
|
||||
this.contentPositionUs = contentPositionUs;
|
||||
this.forceBufferingState = forceBufferingState;
|
||||
this.endPlayback = endPlayback;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> {
|
||||
|
||||
public final PlayerMessage message;
|
||||
|
@ -43,7 +43,6 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
private final Timeline.Window window;
|
||||
|
||||
private long nextWindowSequenceNumber;
|
||||
private Timeline timeline;
|
||||
private @RepeatMode int repeatMode;
|
||||
private boolean shuffleModeEnabled;
|
||||
@Nullable private MediaPeriodHolder playing;
|
||||
@ -57,33 +56,32 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
public MediaPeriodQueue() {
|
||||
period = new Timeline.Period();
|
||||
window = new Timeline.Window();
|
||||
timeline = Timeline.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued
|
||||
* media periods to take into account the new timeline.
|
||||
*/
|
||||
public void setTimeline(Timeline timeline) {
|
||||
this.timeline = timeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled.
|
||||
* If not, it is necessary to seek to the current playback position.
|
||||
*
|
||||
* @param timeline The current timeline.
|
||||
* @param repeatMode The new repeat mode.
|
||||
* @return Whether the repeat mode change has been fully handled.
|
||||
*/
|
||||
public boolean updateRepeatMode(@RepeatMode int repeatMode) {
|
||||
public boolean updateRepeatMode(Timeline timeline, @RepeatMode int repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
return updateForPlaybackModeChange();
|
||||
return updateForPlaybackModeChange(timeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully
|
||||
* handled. If not, it is necessary to seek to the current playback position.
|
||||
*
|
||||
* @param timeline The current timeline.
|
||||
* @param shuffleModeEnabled Whether shuffling mode is enabled.
|
||||
* @return Whether the shuffle mode change has been fully handled.
|
||||
*/
|
||||
public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||
public boolean updateShuffleModeEnabled(Timeline timeline, boolean shuffleModeEnabled) {
|
||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||
return updateForPlaybackModeChange();
|
||||
return updateForPlaybackModeChange(timeline);
|
||||
}
|
||||
|
||||
/** Returns whether {@code mediaPeriod} is the current loading media period. */
|
||||
@ -124,7 +122,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
long rendererPositionUs, PlaybackInfo playbackInfo) {
|
||||
return loading == null
|
||||
? getFirstMediaPeriodInfo(playbackInfo)
|
||||
: getFollowingMediaPeriodInfo(loading, rendererPositionUs);
|
||||
: getFollowingMediaPeriodInfo(playbackInfo.timeline, loading, rendererPositionUs);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -286,13 +284,15 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* current playback position. The method assumes that the first media period in the queue is still
|
||||
* consistent with the new timeline.
|
||||
*
|
||||
* @param timeline The new timeline.
|
||||
* @param rendererPositionUs The current renderer position in microseconds.
|
||||
* @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read
|
||||
* the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they
|
||||
* have read to the end.
|
||||
* @return Whether the timeline change has been handled completely.
|
||||
*/
|
||||
public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) {
|
||||
public boolean updateQueuedPeriods(
|
||||
Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) {
|
||||
// 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.
|
||||
@ -307,9 +307,10 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
// The id and start position of the first period have already been verified by
|
||||
// ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
|
||||
// and isLastInPeriod flags.
|
||||
newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo);
|
||||
newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo);
|
||||
} else {
|
||||
newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
|
||||
newPeriodInfo =
|
||||
getFollowingMediaPeriodInfo(timeline, previousPeriodHolder, rendererPositionUs);
|
||||
if (newPeriodInfo == null) {
|
||||
// We've loaded a next media period that is not in the new timeline.
|
||||
return !removeAfter(previousPeriodHolder);
|
||||
@ -349,13 +350,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* account the current timeline. This method must only be called if the period is still part of
|
||||
* the current timeline.
|
||||
*
|
||||
* @param timeline The current timeline used to update the media period.
|
||||
* @param info Media period info for a media period based on an old timeline.
|
||||
* @return The updated media period info for the current timeline.
|
||||
*/
|
||||
public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info) {
|
||||
public MediaPeriodInfo getUpdatedMediaPeriodInfo(Timeline timeline, MediaPeriodInfo info) {
|
||||
MediaPeriodId id = info.id;
|
||||
boolean isLastInPeriod = isLastInPeriod(id);
|
||||
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
||||
boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod);
|
||||
timeline.getPeriodByUid(info.id.periodUid, period);
|
||||
long durationUs =
|
||||
id.isAd()
|
||||
@ -378,13 +380,16 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* played, returning an identifier for an ad group if one needs to be played before the specified
|
||||
* position, or an identifier for a content media period if not.
|
||||
*
|
||||
* @param timeline The timeline the period is part of.
|
||||
* @param periodUid The uid of the timeline period to play.
|
||||
* @param positionUs The next content position in the period to play.
|
||||
* @return The identifier for the first media period to play, taking into account unplayed ads.
|
||||
*/
|
||||
public MediaPeriodId resolveMediaPeriodIdForAds(Object periodUid, long positionUs) {
|
||||
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodUid);
|
||||
return resolveMediaPeriodIdForAds(periodUid, positionUs, windowSequenceNumber);
|
||||
public MediaPeriodId resolveMediaPeriodIdForAds(
|
||||
Timeline timeline, Object periodUid, long positionUs) {
|
||||
long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(timeline, periodUid);
|
||||
return resolveMediaPeriodIdForAds(
|
||||
timeline, periodUid, positionUs, windowSequenceNumber, period);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
@ -394,14 +399,20 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* played, returning an identifier for an ad group if one needs to be played before the specified
|
||||
* position, or an identifier for a content media period if not.
|
||||
*
|
||||
* @param timeline The timeline the period is part of.
|
||||
* @param periodUid The uid of the timeline period to play.
|
||||
* @param positionUs The next content position in the period to play.
|
||||
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
|
||||
* windows this period is part of.
|
||||
* @param period A scratch {@link Timeline.Period}.
|
||||
* @return The identifier for the first media period to play, taking into account unplayed ads.
|
||||
*/
|
||||
private MediaPeriodId resolveMediaPeriodIdForAds(
|
||||
Object periodUid, long positionUs, long windowSequenceNumber) {
|
||||
private static MediaPeriodId resolveMediaPeriodIdForAds(
|
||||
Timeline timeline,
|
||||
Object periodUid,
|
||||
long positionUs,
|
||||
long windowSequenceNumber,
|
||||
Timeline.Period period) {
|
||||
timeline.getPeriodByUid(periodUid, period);
|
||||
int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
|
||||
if (adGroupIndex == C.INDEX_UNSET) {
|
||||
@ -418,10 +429,11 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
* the window sequence number of an existing matching media period or by creating a new window
|
||||
* sequence number.
|
||||
*
|
||||
* @param timeline The timeline the period is part of.
|
||||
* @param periodUid The uid of the timeline period.
|
||||
* @return A window sequence number for a media period created for this timeline period.
|
||||
*/
|
||||
private long resolvePeriodIndexToWindowSequenceNumber(Object periodUid) {
|
||||
private long resolvePeriodIndexToWindowSequenceNumber(Timeline timeline, Object periodUid) {
|
||||
int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex;
|
||||
if (oldFrontPeriodUid != null) {
|
||||
int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid);
|
||||
@ -481,8 +493,10 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param timeline The current timeline.
|
||||
*/
|
||||
private boolean updateForPlaybackModeChange() {
|
||||
private boolean updateForPlaybackModeChange(Timeline timeline) {
|
||||
// Find the last existing period holder that matches the new period order.
|
||||
MediaPeriodHolder lastValidPeriodHolder = playing;
|
||||
if (lastValidPeriodHolder == null) {
|
||||
@ -514,7 +528,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder);
|
||||
|
||||
// Update the period info for the last holder, as it may now be the last period in the timeline.
|
||||
lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info);
|
||||
lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info);
|
||||
|
||||
// If renderers may have read from a period that's been removed, it is necessary to restart.
|
||||
return !readingPeriodRemoved;
|
||||
@ -525,13 +539,17 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
*/
|
||||
private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
|
||||
return getMediaPeriodInfo(
|
||||
playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs);
|
||||
playbackInfo.timeline,
|
||||
playbackInfo.periodId,
|
||||
playbackInfo.contentPositionUs,
|
||||
playbackInfo.startPositionUs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s
|
||||
* media period.
|
||||
*
|
||||
* @param timeline The current timeline.
|
||||
* @param mediaPeriodHolder The media period holder.
|
||||
* @param rendererPositionUs The current renderer position in microseconds.
|
||||
* @return The following media period's info, or {@code null} if it is not yet possible to get the
|
||||
@ -539,7 +557,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
*/
|
||||
@Nullable
|
||||
private MediaPeriodInfo getFollowingMediaPeriodInfo(
|
||||
MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) {
|
||||
Timeline timeline, MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) {
|
||||
// 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
|
||||
// until the timeline is updated. Store whether the next timeline period is ready when the
|
||||
@ -571,6 +589,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
// want it to be from its default start position, so project the default start position
|
||||
// forward by the duration of the buffer, and start buffering from this point.
|
||||
contentPositionUs = C.TIME_UNSET;
|
||||
@Nullable
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
@ -595,8 +614,9 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
contentPositionUs = 0;
|
||||
}
|
||||
MediaPeriodId periodId =
|
||||
resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
|
||||
return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
|
||||
resolveMediaPeriodIdForAds(
|
||||
timeline, nextPeriodUid, startPositionUs, windowSequenceNumber, period);
|
||||
return getMediaPeriodInfo(timeline, periodId, contentPositionUs, startPositionUs);
|
||||
}
|
||||
|
||||
MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
|
||||
@ -614,6 +634,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup)
|
||||
? null
|
||||
: getMediaPeriodInfoForAd(
|
||||
timeline,
|
||||
currentPeriodId.periodUid,
|
||||
adGroupIndex,
|
||||
nextAdIndexInAdGroup,
|
||||
@ -625,6 +646,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
if (startPositionUs == C.TIME_UNSET) {
|
||||
// If we're transitioning from an ad group to content starting from its default position,
|
||||
// project the start position forward as if this were a transition to a new window.
|
||||
@Nullable
|
||||
Pair<Object, Long> defaultPosition =
|
||||
timeline.getPeriodPosition(
|
||||
window,
|
||||
@ -638,7 +660,10 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
startPositionUs = defaultPosition.second;
|
||||
}
|
||||
return getMediaPeriodInfoForContent(
|
||||
currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber);
|
||||
timeline,
|
||||
currentPeriodId.periodUid,
|
||||
startPositionUs,
|
||||
currentPeriodId.windowSequenceNumber);
|
||||
}
|
||||
} else {
|
||||
// Play the next ad group if it's available.
|
||||
@ -646,6 +671,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
if (nextAdGroupIndex == C.INDEX_UNSET) {
|
||||
// The next ad group can't be played. Play content from the previous end position instead.
|
||||
return getMediaPeriodInfoForContent(
|
||||
timeline,
|
||||
currentPeriodId.periodUid,
|
||||
/* startPositionUs= */ mediaPeriodInfo.durationUs,
|
||||
currentPeriodId.windowSequenceNumber);
|
||||
@ -654,6 +680,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup)
|
||||
? null
|
||||
: getMediaPeriodInfoForAd(
|
||||
timeline,
|
||||
currentPeriodId.periodUid,
|
||||
nextAdGroupIndex,
|
||||
adIndexInAdGroup,
|
||||
@ -663,24 +690,27 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
}
|
||||
|
||||
private MediaPeriodInfo getMediaPeriodInfo(
|
||||
MediaPeriodId id, long contentPositionUs, long startPositionUs) {
|
||||
Timeline timeline, MediaPeriodId id, long contentPositionUs, long startPositionUs) {
|
||||
timeline.getPeriodByUid(id.periodUid, period);
|
||||
if (id.isAd()) {
|
||||
if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
|
||||
return null;
|
||||
}
|
||||
return getMediaPeriodInfoForAd(
|
||||
timeline,
|
||||
id.periodUid,
|
||||
id.adGroupIndex,
|
||||
id.adIndexInAdGroup,
|
||||
contentPositionUs,
|
||||
id.windowSequenceNumber);
|
||||
} else {
|
||||
return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber);
|
||||
return getMediaPeriodInfoForContent(
|
||||
timeline, id.periodUid, startPositionUs, id.windowSequenceNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaPeriodInfo getMediaPeriodInfoForAd(
|
||||
Timeline timeline,
|
||||
Object periodUid,
|
||||
int adGroupIndex,
|
||||
int adIndexInAdGroup,
|
||||
@ -707,11 +737,11 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
}
|
||||
|
||||
private MediaPeriodInfo getMediaPeriodInfoForContent(
|
||||
Object periodUid, long startPositionUs, long windowSequenceNumber) {
|
||||
Timeline timeline, Object periodUid, long startPositionUs, long windowSequenceNumber) {
|
||||
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
|
||||
MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
|
||||
boolean isLastInPeriod = isLastInPeriod(id);
|
||||
boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
|
||||
boolean isLastInTimeline = isLastInTimeline(timeline, id, isLastInPeriod);
|
||||
long endPositionUs =
|
||||
nextAdGroupIndex != C.INDEX_UNSET
|
||||
? period.getAdGroupTimeUs(nextAdGroupIndex)
|
||||
@ -734,7 +764,8 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
|
||||
private boolean isLastInTimeline(
|
||||
Timeline timeline, MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
|
||||
int periodIndex = timeline.getIndexOfPeriod(id.periodUid);
|
||||
int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex;
|
||||
return !timeline.getWindow(windowIndex, window).isDynamic
|
||||
|
@ -151,35 +151,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
this.positionUs = positionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns dummy media period id for the first-to-be-played period of the current timeline.
|
||||
*
|
||||
* @param shuffleModeEnabled Whether shuffle mode is enabled.
|
||||
* @param window A writable {@link Timeline.Window}.
|
||||
* @param period A writable {@link Timeline.Period}.
|
||||
* @return A dummy media period id for the first-to-be-played period of the current timeline.
|
||||
*/
|
||||
public MediaPeriodId getDummyFirstMediaPeriodId(
|
||||
boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) {
|
||||
if (timeline.isEmpty()) {
|
||||
return getDummyPeriodForEmptyTimeline();
|
||||
}
|
||||
int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
|
||||
int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex;
|
||||
int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid);
|
||||
long windowSequenceNumber = C.INDEX_UNSET;
|
||||
if (currentPeriodIndex != C.INDEX_UNSET) {
|
||||
int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex;
|
||||
if (firstWindowIndex == currentWindowIndex) {
|
||||
// Keep window sequence number if the new position is still in the same window.
|
||||
windowSequenceNumber = periodId.windowSequenceNumber;
|
||||
}
|
||||
}
|
||||
return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber);
|
||||
}
|
||||
|
||||
/** Returns dummy period id for an empty timeline. */
|
||||
public MediaPeriodId getDummyPeriodForEmptyTimeline() {
|
||||
public static MediaPeriodId getDummyPeriodForEmptyTimeline() {
|
||||
return DUMMY_MEDIA_PERIOD_ID;
|
||||
}
|
||||
|
||||
|
@ -202,7 +202,7 @@ public final class MediaPeriodQueueTest {
|
||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||
boolean changeHandled =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
/* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0);
|
||||
playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0);
|
||||
|
||||
assertThat(changeHandled).isTrue();
|
||||
assertThat(getQueueLength()).isEqualTo(3);
|
||||
@ -228,7 +228,9 @@ public final class MediaPeriodQueueTest {
|
||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||
boolean changeHandled =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
/* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US);
|
||||
playbackInfo.timeline,
|
||||
/* rendererPositionUs= */ 0,
|
||||
/* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US);
|
||||
|
||||
assertThat(changeHandled).isFalse();
|
||||
assertThat(getQueueLength()).isEqualTo(1);
|
||||
@ -256,6 +258,7 @@ public final class MediaPeriodQueueTest {
|
||||
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;
|
||||
boolean changeHandled =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
playbackInfo.timeline,
|
||||
/* rendererPositionUs= */ 0,
|
||||
/* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds);
|
||||
|
||||
@ -285,6 +288,7 @@ public final class MediaPeriodQueueTest {
|
||||
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;
|
||||
boolean changeHandled =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
playbackInfo.timeline,
|
||||
/* rendererPositionUs= */ 0,
|
||||
/* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds);
|
||||
|
||||
@ -313,7 +317,9 @@ public final class MediaPeriodQueueTest {
|
||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||
boolean changeHandled =
|
||||
mediaPeriodQueue.updateQueuedPeriods(
|
||||
/* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE);
|
||||
playbackInfo.timeline,
|
||||
/* rendererPositionUs= */ 0,
|
||||
/* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
assertThat(changeHandled).isFalse();
|
||||
assertThat(getQueueLength()).isEqualTo(3);
|
||||
@ -332,12 +338,11 @@ public final class MediaPeriodQueueTest {
|
||||
|
||||
Timeline timeline = createPlaylistTimeline();
|
||||
periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
|
||||
mediaPeriodQueue.setTimeline(timeline);
|
||||
|
||||
playbackInfo =
|
||||
new PlaybackInfo(
|
||||
timeline,
|
||||
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0),
|
||||
mediaPeriodQueue.resolveMediaPeriodIdForAds(timeline, periodUid, /* positionUs= */ 0),
|
||||
/* startPositionUs= */ 0,
|
||||
/* contentPositionUs= */ 0,
|
||||
Player.STATE_READY,
|
||||
@ -361,7 +366,7 @@ public final class MediaPeriodQueueTest {
|
||||
SinglePeriodAdTimeline adTimeline =
|
||||
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
|
||||
fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null);
|
||||
mediaPeriodQueue.setTimeline(createPlaylistTimeline());
|
||||
playbackInfo = playbackInfo.copyWithTimeline(createPlaylistTimeline());
|
||||
}
|
||||
|
||||
private Playlist.PlaylistTimeline createPlaylistTimeline() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user