From de8d402aa616597f858602e41032c0032663e41b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 26 Jan 2018 08:08:19 -0800 Subject: [PATCH] Merge MediaPeriodInfoSequence into MediaPeriodQueue MediaPeriodInfoSequence has functionality for determining what MediaPeriod should be loaded next. Move this into the queue as an initial step towards moving logic concerning updating the queue of media periods out of ExoPlayerImplInternal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=183391114 --- .../exoplayer2/ExoPlayerImplInternal.java | 125 ++-- .../android/exoplayer2/MediaPeriodHolder.java | 20 +- .../exoplayer2/MediaPeriodHolderQueue.java | 158 ----- .../android/exoplayer2/MediaPeriodInfo.java | 97 +++ .../exoplayer2/MediaPeriodInfoSequence.java | 359 ----------- .../android/exoplayer2/MediaPeriodQueue.java | 559 ++++++++++++++++++ 6 files changed, 716 insertions(+), 602 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolderQueue.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java 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; + } +}