summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java743
1 files changed, 743 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java
new file mode 100644
index 0000000000..941fb61848
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodQueue.java
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.RepeatMode;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Holds a queue of media periods, from the currently playing media period at the front to the
+ * loading media period at the end of the queue, with methods for controlling loading and updating
+ * the queue. Also has a reference to the media period currently being read.
+ */
+/* package */ final class MediaPeriodQueue {
+
+ /**
+ * Limits the maximum number of periods to buffer ahead of the current playing period. The
+ * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
+ * small periods to be buffered if the period count were not limited.
+ */
+ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
+
+ private final Timeline.Period period;
+ private final Timeline.Window window;
+
+ private long nextWindowSequenceNumber;
+ private Timeline timeline;
+ private @RepeatMode int repeatMode;
+ private boolean shuffleModeEnabled;
+ @Nullable private MediaPeriodHolder playing;
+ @Nullable private MediaPeriodHolder reading;
+ @Nullable private MediaPeriodHolder loading;
+ private int length;
+ @Nullable private Object oldFrontPeriodUid;
+ private long oldFrontPeriodWindowSequenceNumber;
+
+ /** Creates a new media period queue. */
+ 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.
+ */
+ public boolean updateRepeatMode(@RepeatMode int repeatMode) {
+ this.repeatMode = repeatMode;
+ return updateForPlaybackModeChange();
+ }
+
+ /**
+ * 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.
+ */
+ public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ return updateForPlaybackModeChange();
+ }
+
+ /** Returns whether {@code mediaPeriod} is the current loading media period. */
+ public boolean isLoading(MediaPeriod mediaPeriod) {
+ return loading != null && loading.mediaPeriod == mediaPeriod;
+ }
+
+ /**
+ * If there is a loading period, reevaluates its buffer.
+ *
+ * @param rendererPositionUs The current renderer position.
+ */
+ public void reevaluateBuffer(long rendererPositionUs) {
+ if (loading != null) {
+ loading.reevaluateBuffer(rendererPositionUs);
+ }
+ }
+
+ /** Returns whether a new loading media period should be enqueued, if available. */
+ public boolean shouldLoadNextMediaPeriod() {
+ return loading == null
+ || (!loading.info.isFinal
+ && loading.isFullyBuffered()
+ && loading.info.durationUs != C.TIME_UNSET
+ && length < MAXIMUM_BUFFER_AHEAD_PERIODS);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} for the next media period to load.
+ *
+ * @param rendererPositionUs The current renderer position.
+ * @param playbackInfo The current playback information.
+ * @return The {@link MediaPeriodInfo} for the next media period to load, or {@code null} if not
+ * yet known.
+ */
+ public @Nullable MediaPeriodInfo getNextMediaPeriodInfo(
+ long rendererPositionUs, PlaybackInfo playbackInfo) {
+ return loading == null
+ ? getFirstMediaPeriodInfo(playbackInfo)
+ : getFollowingMediaPeriodInfo(loading, rendererPositionUs);
+ }
+
+ /**
+ * Enqueues a new media period holder based on the specified information as the new loading media
+ * period, and returns it.
+ *
+ * @param rendererCapabilities The renderer capabilities.
+ * @param trackSelector The track selector.
+ * @param allocator The allocator.
+ * @param mediaSource The media source that produced the media period.
+ * @param info Information used to identify this media period in its timeline period.
+ * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
+ * renderer.
+ */
+ public MediaPeriodHolder enqueueNextMediaPeriodHolder(
+ RendererCapabilities[] rendererCapabilities,
+ TrackSelector trackSelector,
+ Allocator allocator,
+ MediaSource mediaSource,
+ MediaPeriodInfo info,
+ TrackSelectorResult emptyTrackSelectorResult) {
+ long rendererPositionOffsetUs =
+ loading == null
+ ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET
+ ? info.contentPositionUs
+ : 0)
+ : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
+ MediaPeriodHolder newPeriodHolder =
+ new MediaPeriodHolder(
+ rendererCapabilities,
+ rendererPositionOffsetUs,
+ trackSelector,
+ allocator,
+ mediaSource,
+ info,
+ emptyTrackSelectorResult);
+ if (loading != null) {
+ loading.setNext(newPeriodHolder);
+ } else {
+ playing = newPeriodHolder;
+ reading = newPeriodHolder;
+ }
+ oldFrontPeriodUid = null;
+ loading = newPeriodHolder;
+ length++;
+ return newPeriodHolder;
+ }
+
+ /**
+ * Returns the loading period holder which is at the end of the queue, or null if the queue is
+ * empty.
+ */
+ @Nullable
+ public MediaPeriodHolder getLoadingPeriod() {
+ return loading;
+ }
+
+ /**
+ * Returns the playing period holder which is at the front of the queue, or null if the queue is
+ * empty.
+ */
+ @Nullable
+ public MediaPeriodHolder getPlayingPeriod() {
+ return playing;
+ }
+
+ /** Returns the reading period holder, or null if the queue is empty. */
+ @Nullable
+ public MediaPeriodHolder getReadingPeriod() {
+ return reading;
+ }
+
+ /**
+ * Continues reading from the next period holder in the queue.
+ *
+ * @return The updated reading period holder.
+ */
+ public MediaPeriodHolder advanceReadingPeriod() {
+ Assertions.checkState(reading != null && reading.getNext() != null);
+ reading = reading.getNext();
+ return reading;
+ }
+
+ /**
+ * Dequeues the playing period holder from the front of the queue and advances the playing period
+ * holder to be the next item in the queue.
+ *
+ * @return The updated playing period holder, or null if the queue is or becomes empty.
+ */
+ @Nullable
+ public MediaPeriodHolder advancePlayingPeriod() {
+ if (playing == null) {
+ return null;
+ }
+ if (playing == reading) {
+ reading = playing.getNext();
+ }
+ playing.release();
+ length--;
+ if (length == 0) {
+ loading = null;
+ oldFrontPeriodUid = playing.uid;
+ oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
+ }
+ playing = playing.getNext();
+ return playing;
+ }
+
+ /**
+ * Removes all period holders after the given period holder. This process may also remove the
+ * currently reading period holder. If that is the case, the reading period holder is set to be
+ * the same as the playing period holder at the front of the queue.
+ *
+ * @param mediaPeriodHolder The media period holder that shall be the new end of the queue.
+ * @return Whether the reading period has been removed.
+ */
+ public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) {
+ Assertions.checkState(mediaPeriodHolder != null);
+ boolean removedReading = false;
+ loading = mediaPeriodHolder;
+ while (mediaPeriodHolder.getNext() != null) {
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
+ if (mediaPeriodHolder == reading) {
+ reading = playing;
+ removedReading = true;
+ }
+ mediaPeriodHolder.release();
+ length--;
+ }
+ loading.setNext(null);
+ return removedReading;
+ }
+
+ /**
+ * Clears the queue.
+ *
+ * @param keepFrontPeriodUid Whether the queue should keep the id of the media period in the front
+ * of queue (typically the playing one) for later reuse.
+ */
+ public void clear(boolean keepFrontPeriodUid) {
+ MediaPeriodHolder front = playing;
+ if (front != null) {
+ oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null;
+ oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber;
+ removeAfter(front);
+ front.release();
+ } else if (!keepFrontPeriodUid) {
+ oldFrontPeriodUid = null;
+ }
+ playing = null;
+ loading = null;
+ reading = null;
+ 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. The method assumes that the first media period in the queue is still
+ * consistent with 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) {
+ // 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.
+ MediaPeriodHolder previousPeriodHolder = null;
+ MediaPeriodHolder periodHolder = playing;
+ while (periodHolder != null) {
+ MediaPeriodInfo oldPeriodInfo = periodHolder.info;
+
+ // Get period info based on new timeline.
+ MediaPeriodInfo newPeriodInfo;
+ if (previousPeriodHolder == null) {
+ // 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);
+ } else {
+ newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
+ if (newPeriodInfo == null) {
+ // We've loaded a next media period that is not in the new timeline.
+ return !removeAfter(previousPeriodHolder);
+ }
+ if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
+ // The new media period has a different id or start position.
+ return !removeAfter(previousPeriodHolder);
+ }
+ }
+
+ // Use new period info, but keep old content position.
+ periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs);
+
+ if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) {
+ // The period duration changed. Remove all subsequent periods and check whether we read
+ // beyond the new duration.
+ long newDurationInRendererTime =
+ newPeriodInfo.durationUs == C.TIME_UNSET
+ ? Long.MAX_VALUE
+ : periodHolder.toRendererTime(newPeriodInfo.durationUs);
+ boolean isReadingAndReadBeyondNewDuration =
+ periodHolder == reading
+ && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
+ || maxRendererReadPositionUs >= newDurationInRendererTime);
+ boolean readingPeriodRemoved = removeAfter(periodHolder);
+ return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;
+ }
+
+ previousPeriodHolder = periodHolder;
+ periodHolder = periodHolder.getNext();
+ }
+ return true;
+ }
+
+ /**
+ * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into
+ * account the current timeline. This method must only be called if the period is still part of
+ * the current timeline.
+ *
+ * @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) {
+ MediaPeriodId id = info.id;
+ boolean isLastInPeriod = isLastInPeriod(id);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ timeline.getPeriodByUid(info.id.periodUid, period);
+ long durationUs =
+ id.isAd()
+ ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup)
+ : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE
+ ? period.getDurationUs()
+ : info.endPositionUs);
+ return new MediaPeriodInfo(
+ id,
+ info.startPositionUs,
+ info.contentPositionUs,
+ info.endPositionUs,
+ durationUs,
+ isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * 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 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);
+ }
+
+ // Internal methods.
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * 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 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.
+ * @return The identifier for the first media period to play, taking into account unplayed ads.
+ */
+ private MediaPeriodId resolveMediaPeriodIdForAds(
+ Object periodUid, long positionUs, long windowSequenceNumber) {
+ timeline.getPeriodByUid(periodUid, period);
+ int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
+ if (adGroupIndex == C.INDEX_UNSET) {
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
+ return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex);
+ } else {
+ int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
+ return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
+ }
+ }
+
+ /**
+ * Resolves the specified period uid to a corresponding window sequence number. Either by reusing
+ * the window sequence number of an existing matching media period or by creating a new window
+ * sequence number.
+ *
+ * @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) {
+ int windowIndex = timeline.getPeriodByUid(periodUid, period).windowIndex;
+ if (oldFrontPeriodUid != null) {
+ int oldFrontPeriodIndex = timeline.getIndexOfPeriod(oldFrontPeriodUid);
+ if (oldFrontPeriodIndex != C.INDEX_UNSET) {
+ int oldFrontWindowIndex = timeline.getPeriod(oldFrontPeriodIndex, period).windowIndex;
+ if (oldFrontWindowIndex == windowIndex) {
+ // Try to match old front uid after the queue has been cleared.
+ return oldFrontPeriodWindowSequenceNumber;
+ }
+ }
+ }
+ MediaPeriodHolder mediaPeriodHolder = playing;
+ while (mediaPeriodHolder != null) {
+ if (mediaPeriodHolder.uid.equals(periodUid)) {
+ // Reuse window sequence number of first exact period match.
+ return mediaPeriodHolder.info.id.windowSequenceNumber;
+ }
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
+ }
+ mediaPeriodHolder = playing;
+ while (mediaPeriodHolder != null) {
+ int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid);
+ if (indexOfHolderInTimeline != C.INDEX_UNSET) {
+ int holderWindowIndex = timeline.getPeriod(indexOfHolderInTimeline, period).windowIndex;
+ if (holderWindowIndex == windowIndex) {
+ // As an alternative, try to match other periods of the same window.
+ return mediaPeriodHolder.info.id.windowSequenceNumber;
+ }
+ }
+ mediaPeriodHolder = mediaPeriodHolder.getNext();
+ }
+ // If no match is found, create new sequence number.
+ long windowSequenceNumber = nextWindowSequenceNumber++;
+ if (playing == null) {
+ // If the queue is empty, save it as old front uid to allow later reuse.
+ oldFrontPeriodUid = periodUid;
+ oldFrontPeriodWindowSequenceNumber = windowSequenceNumber;
+ }
+ return windowSequenceNumber;
+ }
+
+ /**
+ * Returns whether a period described by {@code oldInfo} can be kept for playing the media period
+ * described by {@code newInfo}.
+ */
+ private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) {
+ return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id);
+ }
+
+ /**
+ * Returns whether a duration change of a period is compatible with keeping the following periods.
+ */
+ private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) {
+ return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs;
+ }
+
+ /**
+ * 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.
+ */
+ private boolean updateForPlaybackModeChange() {
+ // Find the last existing period holder that matches the new period order.
+ MediaPeriodHolder lastValidPeriodHolder = playing;
+ if (lastValidPeriodHolder == null) {
+ return true;
+ }
+ int currentPeriodIndex = timeline.getIndexOfPeriod(lastValidPeriodHolder.uid);
+ while (true) {
+ int nextPeriodIndex =
+ timeline.getNextPeriodIndex(
+ currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
+ while (lastValidPeriodHolder.getNext() != null
+ && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
+ lastValidPeriodHolder = lastValidPeriodHolder.getNext();
+ }
+
+ MediaPeriodHolder nextMediaPeriodHolder = lastValidPeriodHolder.getNext();
+ if (nextPeriodIndex == C.INDEX_UNSET || nextMediaPeriodHolder == null) {
+ break;
+ }
+ int nextPeriodHolderPeriodIndex = timeline.getIndexOfPeriod(nextMediaPeriodHolder.uid);
+ if (nextPeriodHolderPeriodIndex != nextPeriodIndex) {
+ break;
+ }
+ lastValidPeriodHolder = nextMediaPeriodHolder;
+ currentPeriodIndex = nextPeriodIndex;
+ }
+
+ // Release any period holders that don't match the new period order.
+ 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);
+
+ // If renderers may have read from a period that's been removed, it is necessary to restart.
+ return !readingPeriodRemoved;
+ }
+
+ /**
+ * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
+ */
+ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
+ return getMediaPeriodInfo(
+ playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s
+ * media period.
+ *
+ * @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
+ * next media period info.
+ */
+ private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo(
+ 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
+ // timeline is updated, to avoid repeatedly checking the same timeline.
+ MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;
+ // The expected delay until playback transitions to the new period is equal the duration of
+ // media that's currently buffered (assuming no interruptions). This is used to project forward
+ // the start position for transitions to new windows.
+ long bufferedDurationUs =
+ mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
+ if (mediaPeriodInfo.isLastInTimelinePeriod) {
+ int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid);
+ int nextPeriodIndex =
+ timeline.getNextPeriodIndex(
+ currentPeriodIndex, period, window, repeatMode, shuffleModeEnabled);
+ if (nextPeriodIndex == C.INDEX_UNSET) {
+ // We can't create a next period yet.
+ return null;
+ }
+
+ long startPositionUs;
+ long contentPositionUs;
+ int nextWindowIndex =
+ timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
+ Object nextPeriodUid = period.uid;
+ long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
+ if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
+ // We're starting to buffer a new window. When playback transitions to this window we'll
+ // 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;
+ Pair<Object, Long> defaultPosition =
+ timeline.getPeriodPosition(
+ window,
+ period,
+ nextWindowIndex,
+ /* windowPositionUs= */ C.TIME_UNSET,
+ /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ nextPeriodUid = defaultPosition.first;
+ startPositionUs = defaultPosition.second;
+ MediaPeriodHolder nextMediaPeriodHolder = mediaPeriodHolder.getNext();
+ if (nextMediaPeriodHolder != null && nextMediaPeriodHolder.uid.equals(nextPeriodUid)) {
+ windowSequenceNumber = nextMediaPeriodHolder.info.id.windowSequenceNumber;
+ } else {
+ windowSequenceNumber = nextWindowSequenceNumber++;
+ }
+ } else {
+ // We're starting to buffer a new period within the same window.
+ startPositionUs = 0;
+ contentPositionUs = 0;
+ }
+ MediaPeriodId periodId =
+ resolveMediaPeriodIdForAds(nextPeriodUid, startPositionUs, windowSequenceNumber);
+ return getMediaPeriodInfo(periodId, contentPositionUs, startPositionUs);
+ }
+
+ MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
+ timeline.getPeriodByUid(currentPeriodId.periodUid, period);
+ if (currentPeriodId.isAd()) {
+ int adGroupIndex = currentPeriodId.adGroupIndex;
+ int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex);
+ if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
+ return null;
+ }
+ int nextAdIndexInAdGroup =
+ period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup);
+ if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
+ // Play the next ad in the ad group if it's available.
+ return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup)
+ ? null
+ : getMediaPeriodInfoForAd(
+ currentPeriodId.periodUid,
+ adGroupIndex,
+ nextAdIndexInAdGroup,
+ mediaPeriodInfo.contentPositionUs,
+ currentPeriodId.windowSequenceNumber);
+ } else {
+ // Play content from the ad group position.
+ long startPositionUs = mediaPeriodInfo.contentPositionUs;
+ 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.
+ Pair<Object, Long> defaultPosition =
+ timeline.getPeriodPosition(
+ window,
+ period,
+ period.windowIndex,
+ /* windowPositionUs= */ C.TIME_UNSET,
+ /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ startPositionUs = defaultPosition.second;
+ }
+ return getMediaPeriodInfoForContent(
+ currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber);
+ }
+ } else {
+ // Play the next ad group if it's available.
+ int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
+ if (nextAdGroupIndex == C.INDEX_UNSET) {
+ // The next ad group can't be played. Play content from the previous end position instead.
+ return getMediaPeriodInfoForContent(
+ currentPeriodId.periodUid,
+ /* startPositionUs= */ mediaPeriodInfo.durationUs,
+ currentPeriodId.windowSequenceNumber);
+ }
+ int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);
+ return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup)
+ ? null
+ : getMediaPeriodInfoForAd(
+ currentPeriodId.periodUid,
+ nextAdGroupIndex,
+ adIndexInAdGroup,
+ /* contentPositionUs= */ mediaPeriodInfo.durationUs,
+ currentPeriodId.windowSequenceNumber);
+ }
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfo(
+ 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(
+ id.periodUid,
+ id.adGroupIndex,
+ id.adIndexInAdGroup,
+ contentPositionUs,
+ id.windowSequenceNumber);
+ } else {
+ return getMediaPeriodInfoForContent(id.periodUid, startPositionUs, id.windowSequenceNumber);
+ }
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForAd(
+ Object periodUid,
+ int adGroupIndex,
+ int adIndexInAdGroup,
+ long contentPositionUs,
+ long windowSequenceNumber) {
+ MediaPeriodId id =
+ new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
+ long durationUs =
+ timeline
+ .getPeriodByUid(id.periodUid, period)
+ .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
+ long startPositionUs =
+ adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex)
+ ? period.getAdResumePositionUs()
+ : 0;
+ return new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ contentPositionUs,
+ /* endPositionUs= */ C.TIME_UNSET,
+ durationUs,
+ /* isLastInTimelinePeriod= */ false,
+ /* isFinal= */ false);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForContent(
+ 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);
+ long endPositionUs =
+ nextAdGroupIndex != C.INDEX_UNSET
+ ? period.getAdGroupTimeUs(nextAdGroupIndex)
+ : C.TIME_UNSET;
+ long durationUs =
+ endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE
+ ? period.durationUs
+ : endPositionUs;
+ return new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ /* contentPositionUs= */ C.TIME_UNSET,
+ endPositionUs,
+ durationUs,
+ isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private boolean isLastInPeriod(MediaPeriodId id) {
+ return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET;
+ }
+
+ private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
+ int periodIndex = timeline.getIndexOfPeriod(id.periodUid);
+ int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex;
+ return !timeline.getWindow(windowIndex, window).isDynamic
+ && timeline.isLastPeriod(periodIndex, period, window, repeatMode, shuffleModeEnabled)
+ && isLastMediaPeriodInPeriod;
+ }
+}