diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 92763704e5..550c3c4a43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -26,7 +26,6 @@ import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -82,13 +81,6 @@ import java.util.Collections; private static final int RENDERING_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; - /** - * 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; - /** * Offset added to all sample timestamps read by renderers to make them non-negative. This is * provided for convenience of sources that may return negative timestamps due to prerolling @@ -108,14 +100,13 @@ import java.util.Collections; private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; - private final MediaPeriodInfoSequence mediaPeriodInfoSequence; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList customMessageInfos; private final Clock clock; - private final MediaPeriodHolderQueue queue; + private final MediaPeriodQueue queue; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -156,7 +147,7 @@ import java.util.Collections; this.eventHandler = eventHandler; this.player = player; this.clock = clock; - this.queue = new MediaPeriodHolderQueue(); + this.queue = new MediaPeriodQueue(); backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -176,7 +167,6 @@ import java.util.Collections; enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); - mediaPeriodInfoSequence = new MediaPeriodInfoSequence(); trackSelector.init(this); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can @@ -427,14 +417,14 @@ import java.util.Collections; private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; - mediaPeriodInfoSequence.setRepeatMode(repeatMode); + queue.setRepeatMode(repeatMode); validateExistingPeriodHolders(); } private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) throws ExoPlaybackException { this.shuffleModeEnabled = shuffleModeEnabled; - mediaPeriodInfoSequence.setShuffleModeEnabled(shuffleModeEnabled); + queue.setShuffleModeEnabled(shuffleModeEnabled); validateExistingPeriodHolders(); } @@ -463,8 +453,7 @@ import java.util.Collections; boolean readingPeriodRemoved = queue.removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. - lastValidPeriodHolder.info = - mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); + lastValidPeriodHolder.info = queue.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); if (readingPeriodRemoved && queue.hasPlayingPeriod()) { // Renderers may have read from a period that's been removed. Seek back to the current @@ -644,10 +633,9 @@ import java.util.Collections; seekPositionAdjusted = true; } else { // Update the resolved seek position to take ads into account. - periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds( - resolvedSeekPosition.first, resolvedSeekPosition.second); + int periodIndex = resolvedSeekPosition.first; contentPositionUs = resolvedSeekPosition.second; + periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs); if (periodId.isAd()) { periodPositionUs = 0; seekPositionAdjusted = true; @@ -832,7 +820,7 @@ import java.util.Collections; pendingInitialSeekPosition = null; } if (resetState) { - mediaPeriodInfoSequence.setTimeline(null); + queue.setTimeline(null); for (CustomMessageInfo customMessageInfo : customMessageInfos) { customMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } @@ -1058,7 +1046,7 @@ import java.util.Collections; long periodPositionUs = playingPeriodHolder.applyTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - updateLoadControlTrackSelection(playingPeriodHolder); + updateLoadControlTrackSelection(playingPeriodHolder.trackSelectorResult); if (playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, @@ -1097,7 +1085,7 @@ import java.util.Collections; Math.max( periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); periodHolder.applyTrackSelection(loadingPeriodPositionUs, false); - updateLoadControlTrackSelection(periodHolder); + updateLoadControlTrackSelection(periodHolder.trackSelectorResult); } } if (playbackInfo.playbackState != Player.STATE_ENDED) { @@ -1107,8 +1095,7 @@ import java.util.Collections; } } - private void updateLoadControlTrackSelection(MediaPeriodHolder periodHolder) { - TrackSelectorResult trackSelectorResult = periodHolder.trackSelectorResult; + private void updateLoadControlTrackSelection(TrackSelectorResult trackSelectorResult) { loadControl.onTracksSelected( renderers, trackSelectorResult.groups, trackSelectorResult.selections); } @@ -1165,7 +1152,7 @@ import java.util.Collections; Timeline oldTimeline = playbackInfo.timeline; Timeline timeline = sourceRefreshInfo.timeline; Object manifest = sourceRefreshInfo.manifest; - mediaPeriodInfoSequence.setTimeline(timeline); + queue.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); resolveCustomMessagePositions(); @@ -1183,8 +1170,7 @@ import java.util.Collections; } else { int periodIndex = periodPosition.first; long positionUs = periodPosition.second; - MediaPeriodId periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); } @@ -1196,8 +1182,7 @@ import java.util.Collections; timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); int periodIndex = defaultPosition.first; long startPositionUs = defaultPosition.second; - MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, - startPositionUs); + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); } @@ -1236,8 +1221,7 @@ import java.util.Collections; while (periodHolder.next != null) { periodHolder = periodHolder.next; if (periodHolder.uid.equals(newPeriodUid)) { - periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, - newPeriodIndex); + periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, newPeriodIndex); } else { periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); } @@ -1257,8 +1241,8 @@ import java.util.Collections; if (playbackInfo.periodId.isAd()) { // Check that the playing ad hasn't been marked as played. If it has, skip forward. - MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, - playbackInfo.contentPositionUs); + MediaPeriodId periodId = + queue.resolveMediaPeriodIdForAds(periodIndex, playbackInfo.contentPositionUs); if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; @@ -1303,8 +1287,7 @@ import java.util.Collections; private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { while (true) { - periodHolder.info = - mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) { return periodHolder; } @@ -1538,55 +1521,36 @@ import java.util.Collections; } private void maybeUpdateLoadingPeriod() throws IOException { - MediaPeriodInfo info; - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - if (loadingPeriodHolder == null) { - info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); - } else { - loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); - if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered() - || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { - return; + queue.reevaluateBuffer(rendererPositionUs); + if (queue.shouldLoadNextMediaPeriod()) { + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } else { + Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; + MediaPeriod mediaPeriod = + queue.enqueueNextMediaPeriod( + rendererCapabilities, + RENDERER_TIMESTAMP_OFFSET_US, + trackSelector, + loadControl.getAllocator(), + mediaSource, + uid, + info); + mediaPeriod.prepare(this, info.startPositionUs); + setIsLoading(true); } - if (queue.getLength() == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; - } - info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info, - loadingPeriodHolder.getRendererOffset(), rendererPositionUs); } - if (info == null) { - mediaSource.maybeThrowSourceInfoRefreshError(); - return; - } - - long rendererPositionOffsetUs = - loadingPeriodHolder == null - ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) - : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); - Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; - MediaPeriodHolder newPeriodHolder = - new MediaPeriodHolder( - rendererCapabilities, - rendererPositionOffsetUs, - trackSelector, - loadControl.getAllocator(), - mediaSource, - uid, - info); - queue.enqueueLoadingPeriod(newPeriodHolder); - newPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); - setIsLoading(true); } - private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { + private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { + if (!queue.isLoading(mediaPeriod)) { // Stale event. return; } - loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); - updateLoadControlTrackSelection(loadingPeriodHolder); + TrackSelectorResult trackSelectorResult = + queue.handleLoadingPeriodPrepared(mediaClock.getPlaybackParameters().speed); + updateLoadControlTrackSelection(trackSelectorResult); if (!queue.hasPlayingPeriod()) { // This is the first prepared period, so start playing it. MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); @@ -1596,13 +1560,12 @@ import java.util.Collections; maybeContinueLoading(); } - private void handleContinueLoadingRequested(MediaPeriod period) { - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { + private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { + if (!queue.isLoading(mediaPeriod)) { // Stale event. return; } - loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); + queue.reevaluateBuffer(rendererPositionUs); maybeContinueLoading(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index b46155a6d3..43036b154b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import android.util.Log; -import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -52,19 +51,31 @@ import com.google.android.exoplayer2.util.Assertions; private TrackSelectorResult periodTrackSelectorResult; + /** + * Creates a new holder with information required to play it as part of a timeline. + * + * @param rendererCapabilities The renderer capabilities. + * @param rendererPositionOffsetUs The time offset of the start of the media period to provide to + * renderers. + * @param trackSelector The track selector. + * @param allocator The allocator. + * @param mediaSource The media source that produced the media period. + * @param uid The unique identifier for the containing timeline period. + * @param info Information used to identify this media period in its timeline period. + */ public MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - Object periodUid, + Object uid, MediaPeriodInfo info) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; this.trackSelector = trackSelector; this.mediaSource = mediaSource; - this.uid = Assertions.checkNotNull(periodUid); + this.uid = Assertions.checkNotNull(uid); this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; @@ -121,12 +132,13 @@ import com.google.android.exoplayer2.util.Assertions; return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); } - public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { + public TrackSelectorResult handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; selectTracks(playbackSpeed); long newStartPositionUs = applyTrackSelection(info.startPositionUs, false); rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); + return trackSelectorResult; } public void reevaluateBuffer(long rendererPositionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolderQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolderQueue.java deleted file mode 100644 index 3504a22d6a..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolderQueue.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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 com.google.android.exoplayer2; - -import com.google.android.exoplayer2.util.Assertions; - -/** - * Holds a queue of {@link MediaPeriodHolder}s from the currently playing period holder at the front - * to the loading period holder at the end of the queue. Also has a reference to the reading period - * holder. - */ -/* package */ final class MediaPeriodHolderQueue { - - private MediaPeriodHolder playing; - private MediaPeriodHolder reading; - private MediaPeriodHolder loading; - private int length; - - /** - * Returns the loading period holder which is at the end of the queue, or null if the queue is - * empty. - */ - 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 or hasn't started playing. - */ - public MediaPeriodHolder getPlayingPeriod() { - return playing; - } - - /** - * Returns the reading period holder, or null if the queue is empty or the player hasn't started - * reading. - */ - public MediaPeriodHolder getReadingPeriod() { - return reading; - } - - /** - * Returns the period holder in the front of the queue which is the playing period holder when - * playing, or null if the queue is empty. - */ - public MediaPeriodHolder getFrontPeriod() { - return hasPlayingPeriod() ? playing : loading; - } - - /** Returns the current length of the queue. */ - public int getLength() { - return length; - } - - /** Returns whether the reading and playing period holders are set. */ - public boolean hasPlayingPeriod() { - return playing != null; - } - - /** - * Continues reading from the next period holder in the queue. - * - * @return The updated reading period holder. - */ - public MediaPeriodHolder advanceReadingPeriod() { - Assertions.checkState(reading != null && reading.next != null); - reading = reading.next; - return reading; - } - - /** Enqueues a new period holder at the end, which becomes the new loading period holder. */ - public void enqueueLoadingPeriod(MediaPeriodHolder mediaPeriodHolder) { - Assertions.checkState(mediaPeriodHolder != null); - if (loading != null) { - Assertions.checkState(hasPlayingPeriod()); - loading.next = mediaPeriodHolder; - } - loading = mediaPeriodHolder; - length++; - } - - /** - * 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. If the playing period holder is unset, set it to the - * item in the front of the queue. - * - * @return The updated playing period holder, or null if the queue is or becomes empty. - */ - public MediaPeriodHolder advancePlayingPeriod() { - if (playing != null) { - if (playing == reading) { - reading = playing.next; - } - playing.release(); - playing = playing.next; - length--; - if (length == 0) { - loading = null; - } - } else { - playing = loading; - reading = loading; - } - 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.next != null) { - mediaPeriodHolder = mediaPeriodHolder.next; - if (mediaPeriodHolder == reading) { - reading = playing; - removedReading = true; - } - mediaPeriodHolder.release(); - length--; - } - loading.next = null; - return removedReading; - } - - /** Clears the queue. */ - public void clear() { - MediaPeriodHolder front = getFrontPeriod(); - if (front != null) { - front.release(); - removeAfter(front); - } - playing = null; - loading = null; - reading = null; - length = 0; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java new file mode 100644 index 0000000000..a415f9f0a7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -0,0 +1,97 @@ +/* + * 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 com.google.android.exoplayer2; + +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** Stores the information required to load and play a {@link MediaPeriod}. */ +/* package */ final class MediaPeriodInfo { + + /** The media period's identifier. */ + public final MediaPeriodId id; + /** The start position of the media to play within the media period, in microseconds. */ + public final long startPositionUs; + /** + * The end position of the media to play within the media period, in microseconds, or {@link + * C#TIME_END_OF_SOURCE} if the end position is the end of the media period. + */ + public final long endPositionUs; + /** + * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} + * otherwise. + */ + public final long contentPositionUs; + /** + * The duration of the media to play within the media period, in microseconds, or {@link + * C#TIME_UNSET} if not known. + */ + public final long durationUs; + /** + * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media + * period corresponding to a timeline period without ads). + */ + public final boolean isLastInTimelinePeriod; + /** + * Whether this is the last media period in the entire timeline. If true, {@link + * #isLastInTimelinePeriod} will also be true. + */ + public final boolean isFinal; + + MediaPeriodInfo( + MediaPeriodId id, + long startPositionUs, + long endPositionUs, + long contentPositionUs, + long durationUs, + boolean isLastInTimelinePeriod, + boolean isFinal) { + this.id = id; + this.startPositionUs = startPositionUs; + this.endPositionUs = endPositionUs; + this.contentPositionUs = contentPositionUs; + this.durationUs = durationUs; + this.isLastInTimelinePeriod = isLastInTimelinePeriod; + this.isFinal = isFinal; + } + + /** + * Returns a copy of this instance with the period identifier's period index set to the specified + * value. + */ + public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { + return new MediaPeriodInfo( + id.copyWithPeriodIndex(periodIndex), + startPositionUs, + endPositionUs, + contentPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); + } + + /** Returns a copy of this instance with the start position set to the specified value. */ + public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { + return new MediaPeriodInfo( + id, + startPositionUs, + endPositionUs, + contentPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java deleted file mode 100644 index 6cb76e5471..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2017 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 com.google.android.exoplayer2; - -import android.util.Pair; -import com.google.android.exoplayer2.Player.RepeatMode; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; - -/** - * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and - * start/end positions for {@link MediaPeriod}s to load and play. - */ -/* package */ final class MediaPeriodInfoSequence { - - // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal. - - /** - * Stores the information required to load and play a {@link MediaPeriod}. - */ - public static final class MediaPeriodInfo { - - /** - * The media period's identifier. - */ - public final MediaPeriodId id; - /** - * The start position of the media to play within the media period, in microseconds. - */ - public final long startPositionUs; - /** - * The end position of the media to play within the media period, in microseconds, or - * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period. - */ - public final long endPositionUs; - /** - * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} - * otherwise. - */ - public final long contentPositionUs; - /** - * The duration of the media to play within the media period, in microseconds, or - * {@link C#TIME_UNSET} if not known. - */ - public final long durationUs; - /** - * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media - * period corresponding to a timeline period without ads). - */ - public final boolean isLastInTimelinePeriod; - /** - * Whether this is the last media period in the entire timeline. If true, - * {@link #isLastInTimelinePeriod} will also be true. - */ - public final boolean isFinal; - - private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs, - long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { - this.id = id; - this.startPositionUs = startPositionUs; - this.endPositionUs = endPositionUs; - this.contentPositionUs = contentPositionUs; - this.durationUs = durationUs; - this.isLastInTimelinePeriod = isLastInTimelinePeriod; - this.isFinal = isFinal; - } - - /** - * Returns a copy of this instance with the period identifier's period index set to the - * specified value. - */ - public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { - return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs, - endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); - } - - /** - * Returns a copy of this instance with the start position set to the specified value. - */ - public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { - return new MediaPeriodInfo(id, startPositionUs, endPositionUs, contentPositionUs, durationUs, - isLastInTimelinePeriod, isFinal); - } - - } - - private final Timeline.Period period; - private final Timeline.Window window; - - private Timeline timeline; - private @RepeatMode int repeatMode; - private boolean shuffleModeEnabled; - - /** - * Creates a new media period info sequence. - */ - public MediaPeriodInfoSequence() { - period = new Timeline.Period(); - window = new Timeline.Window(); - } - - /** - * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information - * taking into account the new timeline. - */ - public void setTimeline(Timeline timeline) { - this.timeline = timeline; - } - - /** - * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period - * information taking into account the new repeat mode. - */ - public void setRepeatMode(@RepeatMode int repeatMode) { - this.repeatMode = repeatMode; - } - - /** - * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period - * information taking into account the shuffle mode. - */ - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - this.shuffleModeEnabled = shuffleModeEnabled; - } - - /** - * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. - */ - public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { - return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.contentPositionUs, - playbackInfo.startPositionUs); - } - - /** - * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}. - * - * @param currentMediaPeriodInfo The current media period info. - * @param rendererOffsetUs The current renderer offset 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 - * next media period info. - */ - public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo, - long rendererOffsetUs, 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. - if (currentMediaPeriodInfo.isLastInTimelinePeriod) { - int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode, shuffleModeEnabled); - if (nextPeriodIndex == C.INDEX_UNSET) { - // We can't create a next period yet. - return null; - } - - long startPositionUs; - int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex; - 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. The expected delay until playback - // transitions is equal the duration of media that's currently buffered (assuming no - // interruptions). Hence we project the default start position forward by the duration of - // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = - rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; - Pair defaultPosition = timeline.getPeriodPosition(window, period, - nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); - if (defaultPosition == null) { - return null; - } - nextPeriodIndex = defaultPosition.first; - startPositionUs = defaultPosition.second; - } else { - startPositionUs = 0; - } - MediaPeriodId periodId = resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs); - return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); - } - - MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; - if (currentPeriodId.isAd()) { - int currentAdGroupIndex = currentPeriodId.adGroupIndex; - timeline.getPeriod(currentPeriodId.periodIndex, period); - int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); - if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { - return null; - } - int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1; - if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { - // Play the next ad in the ad group if it's available. - return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null - : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex, - nextAdIndexInAdGroup, currentMediaPeriodInfo.contentPositionUs); - } else { - // 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(currentPeriodId.periodIndex, - currentMediaPeriodInfo.contentPositionUs, endUs); - } - } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { - // Play the next ad group if it's available. - int nextAdGroupIndex = - period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); - return !period.isAdAvailable(nextAdGroupIndex, 0) ? null - : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0, - currentMediaPeriodInfo.endPositionUs); - } else { - // Check if the postroll ad should be played. - int adGroupCount = period.getAdGroupCount(); - if (adGroupCount == 0 - || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE - || period.hasPlayedAdGroup(adGroupCount - 1) - || !period.isAdAvailable(adGroupCount - 1, 0)) { - return null; - } - long contentDurationUs = period.getDurationUs(); - return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0, - contentDurationUs); - } - } - - /** - * 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. - */ - public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) { - timeline.getPeriod(periodIndex, period); - int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); - if (adGroupIndex == C.INDEX_UNSET) { - return new MediaPeriodId(periodIndex); - } else { - int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); - return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); - } - } - - /** - * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline. - */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) { - return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id); - } - - /** - * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline, - * resetting the identifier of the media period to the specified {@code newPeriodIndex}. - */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo, - int newPeriodIndex) { - return getUpdatedMediaPeriodInfo(mediaPeriodInfo, - mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex)); - } - - // Internal methods. - - private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { - long startPositionUs = info.startPositionUs; - long endPositionUs = info.endPositionUs; - boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs); - boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); - timeline.getPeriod(newId.periodIndex, period); - long durationUs = newId.isAd() - ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) - : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); - return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, info.contentPositionUs, - durationUs, isLastInPeriod, isLastInTimeline); - } - - private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long contentPositionUs, - long startPositionUs) { - timeline.getPeriod(id.periodIndex, period); - if (id.isAd()) { - if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { - return null; - } - return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, - contentPositionUs); - } else { - int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); - long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE - : period.getAdGroupTimeUs(nextAdGroupIndex); - return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs); - } - } - - private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex, - int adIndexInAdGroup, long contentPositionUs) { - MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); - boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); - long durationUs = timeline.getPeriod(id.periodIndex, period) - .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); - long startPositionUs = adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex) - ? period.getAdResumePositionUs() : 0; - return new MediaPeriodInfo(id, startPositionUs, C.TIME_END_OF_SOURCE, contentPositionUs, - durationUs, isLastInPeriod, isLastInTimeline); - } - - private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs, - long endUs) { - MediaPeriodId id = new MediaPeriodId(periodIndex); - boolean isLastInPeriod = isLastInPeriod(id, endUs); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); - timeline.getPeriod(id.periodIndex, period); - long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; - return new MediaPeriodInfo(id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, - isLastInTimeline); - } - - private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) { - int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); - if (adGroupCount == 0) { - return true; - } - - int lastAdGroupIndex = adGroupCount - 1; - boolean isAd = id.isAd(); - if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) { - // There's no postroll ad. - return !isAd && endPositionUs == C.TIME_END_OF_SOURCE; - } - - int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex); - if (postrollAdCount == C.LENGTH_UNSET) { - // We won't know if this is the last ad until we know how many postroll ads there are. - return false; - } - - boolean isLastAd = isAd && id.adGroupIndex == lastAdGroupIndex - && id.adIndexInAdGroup == postrollAdCount - 1; - return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount); - } - - private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { - int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; - return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) - && isLastMediaPeriodInPeriod; - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java new file mode 100644 index 0000000000..34547a1c25 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -0,0 +1,559 @@ +/* + * 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 com.google.android.exoplayer2; + +import android.support.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.Allocator; +import 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 Timeline timeline; + private @RepeatMode int repeatMode; + private boolean shuffleModeEnabled; + private MediaPeriodHolder playing; + private MediaPeriodHolder reading; + private MediaPeriodHolder loading; + private int length; + + /** Creates a new media period queue. */ + public MediaPeriodQueue() { + period = new Timeline.Period(); + window = new Timeline.Window(); + } + + /** + * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information + * taking into account the new timeline. + */ + public void setTimeline(Timeline timeline) { + this.timeline = timeline; + } + + /** + * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period + * information taking into account the new repeat mode. + */ + public void setRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + } + + /** + * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period + * information taking into account the shuffle mode. + */ + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + } + + /** 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.info, loading.getRendererOffset(), rendererPositionUs); + } + + /** + * Enqueues a new media period based on the specified information as the new loading media period, + * and returns it. + * + * @param rendererCapabilities The renderer capabilities. + * @param rendererTimestampOffsetUs The base time offset added to for renderers. + * @param trackSelector The track selector. + * @param allocator The allocator. + * @param mediaSource The media source that produced the media period. + * @param uid The unique identifier for the containing timeline period. + * @param info Information used to identify this media period in its timeline period. + */ + public MediaPeriod enqueueNextMediaPeriod( + RendererCapabilities[] rendererCapabilities, + long rendererTimestampOffsetUs, + TrackSelector trackSelector, + Allocator allocator, + MediaSource mediaSource, + Object uid, + MediaPeriodInfo info) { + long rendererPositionOffsetUs = + loading == null + ? (info.startPositionUs + rendererTimestampOffsetUs) + : (loading.getRendererOffset() + loading.info.durationUs); + MediaPeriodHolder newPeriodHolder = + new MediaPeriodHolder( + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + allocator, + mediaSource, + uid, + info); + if (loading != null) { + Assertions.checkState(hasPlayingPeriod()); + loading.next = newPeriodHolder; + } + loading = newPeriodHolder; + length++; + return newPeriodHolder.mediaPeriod; + } + + /** + * Handles the loading media period being prepared. + * + * @param playbackSpeed The current playback speed. + * @return The result of selecting tracks on the newly prepared loading media period. + */ + public TrackSelectorResult handleLoadingPeriodPrepared(float playbackSpeed) + throws ExoPlaybackException { + return loading.handlePrepared(playbackSpeed); + } + + /** + * Returns the loading period holder which is at the end of the queue, or null if the queue is + * empty. + */ + 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 or hasn't started playing. + */ + public MediaPeriodHolder getPlayingPeriod() { + return playing; + } + + /** + * Returns the reading period holder, or null if the queue is empty or the player hasn't started + * reading. + */ + public MediaPeriodHolder getReadingPeriod() { + return reading; + } + + /** + * Returns the period holder in the front of the queue which is the playing period holder when + * playing, or null if the queue is empty. + */ + public MediaPeriodHolder getFrontPeriod() { + return hasPlayingPeriod() ? playing : loading; + } + + /** Returns whether the reading and playing period holders are set. */ + public boolean hasPlayingPeriod() { + return playing != null; + } + + /** + * Continues reading from the next period holder in the queue. + * + * @return The updated reading period holder. + */ + public MediaPeriodHolder advanceReadingPeriod() { + Assertions.checkState(reading != null && reading.next != null); + reading = reading.next; + 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. If the playing period holder is unset, set it to the + * item in the front of the queue. + * + * @return The updated playing period holder, or null if the queue is or becomes empty. + */ + public MediaPeriodHolder advancePlayingPeriod() { + if (playing != null) { + if (playing == reading) { + reading = playing.next; + } + playing.release(); + playing = playing.next; + length--; + if (length == 0) { + loading = null; + } + } else { + playing = loading; + reading = loading; + } + 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.next != null) { + mediaPeriodHolder = mediaPeriodHolder.next; + if (mediaPeriodHolder == reading) { + reading = playing; + removedReading = true; + } + mediaPeriodHolder.release(); + length--; + } + loading.next = null; + return removedReading; + } + + /** Clears the queue. */ + public void clear() { + MediaPeriodHolder front = getFrontPeriod(); + if (front != null) { + front.release(); + removeAfter(front); + } + playing = null; + loading = null; + reading = null; + length = 0; + } + + /** + * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into + * account the current timeline. + * + * @param mediaPeriodInfo 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 mediaPeriodInfo) { + return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id); + } + + /** + * 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}. + * + * @param mediaPeriodInfo Media period info for a media period based on an old timeline. + * @param newPeriodIndex The new period index in the new timeline for the existing media period. + * @return The updated media period info for the current timeline. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo( + MediaPeriodInfo mediaPeriodInfo, int newPeriodIndex) { + return getUpdatedMediaPeriodInfo( + mediaPeriodInfo, mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex)); + } + + /** + * 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 periodIndex The index 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(int periodIndex, long positionUs) { + timeline.getPeriod(periodIndex, period); + int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex == C.INDEX_UNSET) { + return new MediaPeriodId(periodIndex); + } else { + int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); + return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + } + } + + // Internal methods. + + /** + * 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} following {@code currentMediaPeriodInfo}. + * + * @param currentMediaPeriodInfo The current media period info. + * @param rendererOffsetUs The current renderer offset 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 + * next media period info. + */ + private MediaPeriodInfo getFollowingMediaPeriodInfo( + MediaPeriodInfo currentMediaPeriodInfo, long rendererOffsetUs, 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. + if (currentMediaPeriodInfo.isLastInTimelinePeriod) { + int nextPeriodIndex = + timeline.getNextPeriodIndex( + currentMediaPeriodInfo.id.periodIndex, + period, + window, + repeatMode, + shuffleModeEnabled); + if (nextPeriodIndex == C.INDEX_UNSET) { + // We can't create a next period yet. + return null; + } + + long startPositionUs; + int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex; + 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. The expected delay until playback + // transitions is equal the duration of media that's currently buffered (assuming no + // interruptions). Hence we project the default start position forward by the duration of + // the buffer, and start buffering from this point. + long defaultPositionProjectionUs = + rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + nextWindowIndex, + C.TIME_UNSET, + Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + return null; + } + nextPeriodIndex = defaultPosition.first; + startPositionUs = defaultPosition.second; + } else { + startPositionUs = 0; + } + MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodIndex, startPositionUs); + return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); + } + + MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; + if (currentPeriodId.isAd()) { + int currentAdGroupIndex = currentPeriodId.adGroupIndex; + timeline.getPeriod(currentPeriodId.periodIndex, period); + int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); + if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { + return null; + } + int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1; + if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { + // Play the next ad in the ad group if it's available. + return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) + ? null + : getMediaPeriodInfoForAd( + currentPeriodId.periodIndex, + currentAdGroupIndex, + nextAdIndexInAdGroup, + currentMediaPeriodInfo.contentPositionUs); + } else { + // 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( + currentPeriodId.periodIndex, currentMediaPeriodInfo.contentPositionUs, endUs); + } + } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { + // Play the next ad group if it's available. + int nextAdGroupIndex = + period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); + return !period.isAdAvailable(nextAdGroupIndex, 0) + ? null + : getMediaPeriodInfoForAd( + currentPeriodId.periodIndex, + nextAdGroupIndex, + 0, + currentMediaPeriodInfo.endPositionUs); + } else { + // Check if the postroll ad should be played. + int adGroupCount = period.getAdGroupCount(); + if (adGroupCount == 0 + || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE + || period.hasPlayedAdGroup(adGroupCount - 1) + || !period.isAdAvailable(adGroupCount - 1, 0)) { + return null; + } + long contentDurationUs = period.getDurationUs(); + return getMediaPeriodInfoForAd( + currentPeriodId.periodIndex, adGroupCount - 1, 0, contentDurationUs); + } + } + + private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { + long startPositionUs = info.startPositionUs; + long endPositionUs = info.endPositionUs; + boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs); + boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); + timeline.getPeriod(newId.periodIndex, period); + long durationUs = + newId.isAd() + ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) + : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); + return new MediaPeriodInfo( + newId, + startPositionUs, + endPositionUs, + info.contentPositionUs, + durationUs, + isLastInPeriod, + isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfo( + MediaPeriodId id, long contentPositionUs, long startPositionUs) { + timeline.getPeriod(id.periodIndex, period); + if (id.isAd()) { + if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { + return null; + } + return getMediaPeriodInfoForAd( + id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs); + } else { + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + long endUs = + nextAdGroupIndex == C.INDEX_UNSET + ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs); + } + } + + private MediaPeriodInfo getMediaPeriodInfoForAd( + int periodIndex, int adGroupIndex, int adIndexInAdGroup, long contentPositionUs) { + MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + long durationUs = + timeline + .getPeriod(id.periodIndex, period) + .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); + long startPositionUs = + adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex) + ? period.getAdResumePositionUs() + : 0; + return new MediaPeriodInfo( + id, + startPositionUs, + C.TIME_END_OF_SOURCE, + contentPositionUs, + durationUs, + isLastInPeriod, + isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfoForContent( + int periodIndex, long startPositionUs, long endUs) { + MediaPeriodId id = new MediaPeriodId(periodIndex); + boolean isLastInPeriod = isLastInPeriod(id, endUs); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + timeline.getPeriod(id.periodIndex, period); + long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; + return new MediaPeriodInfo( + id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); + } + + private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) { + int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); + if (adGroupCount == 0) { + return true; + } + + int lastAdGroupIndex = adGroupCount - 1; + boolean isAd = id.isAd(); + if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) { + // There's no postroll ad. + return !isAd && endPositionUs == C.TIME_END_OF_SOURCE; + } + + int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex); + if (postrollAdCount == C.LENGTH_UNSET) { + // We won't know if this is the last ad until we know how many postroll ads there are. + return false; + } + + boolean isLastAd = + isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1; + return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount); + } + + private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) + && isLastMediaPeriodInPeriod; + } +}