diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index ab521e3733..e168505d05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -30,6 +31,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * An extensible media player exposing traditional high-level media player functionality, such as @@ -251,6 +254,17 @@ public interface ExoPlayer { */ int STATE_ENDED = 4; + /** + * Repeat modes for playback. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPEAT_MODE_OFF}) + @interface RepeatMode {} + /** + * Normal playback without repetition. + */ + int REPEAT_MODE_OFF = 0; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. 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 bf5b3f6482..2410e19f04 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 @@ -948,10 +948,7 @@ import java.io.IOException; } // The current period is in the new timeline. Update the holder and playbackInfo. - timeline.getPeriod(periodIndex, period); - boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - periodHolder.setIndex(periodIndex, isLastPeriod); + periodHolder.setIndex(periodIndex, isLastPeriod(periodIndex)); boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); @@ -962,10 +959,10 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex++; + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, + ExoPlayer.REPEAT_MODE_OFF); + boolean isLastPeriod = isLastPeriod(periodIndex); timeline.getPeriod(periodIndex, period, true); - isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; if (periodHolder.uid.equals(period.uid)) { // The holder is consistent with the new timeline. Update its index and continue. periodHolder.setIndex(periodIndex, isLastPeriod); @@ -1023,13 +1020,22 @@ import java.io.IOException; private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { int newPeriodIndex = C.INDEX_UNSET; - while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, + ExoPlayer.REPEAT_MODE_OFF); newPeriodIndex = newTimeline.getIndexOfPeriod( - oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); + oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); } return newPeriodIndex; } + private boolean isLastPeriod(int periodIndex) { + int windowIndex = timeline.getPeriod(periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(periodIndex, period, window, ExoPlayer.REPEAT_MODE_OFF); + } + /** * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the * internal timeline. @@ -1240,7 +1246,8 @@ import java.io.IOException; // We are already buffering the maximum number of periods ahead. return; } - newLoadingPeriodIndex = loadingPeriodHolder.index + 1; + newLoadingPeriodIndex = timeline.getNextPeriodIndex(loadingPeriodHolder.index, period, window, + ExoPlayer.REPEAT_MODE_OFF); } if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { @@ -1283,9 +1290,8 @@ import java.io.IOException; ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US : (loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); + boolean isLastPeriod = isLastPeriod(newLoadingPeriodIndex); timeline.getPeriod(newLoadingPeriodIndex, period, true); - boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index eb3966ae4d..8dc30b0905 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -136,6 +136,54 @@ public abstract class Timeline { */ public abstract int getWindowCount(); + /** + * Returns the index of the window after the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. + */ + public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + } + + /** + * Returns the index of the window before the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. + */ + public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + } + + /** + * Returns whether the given window is the last window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the last window of the timeline. + */ + public final boolean isLastWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + + /** + * Returns whether the given window is the first window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the first window of the timeline. + */ + public final boolean isFirstWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. @@ -180,6 +228,44 @@ public abstract class Timeline { */ public abstract int getPeriodCount(); + /** + * Returns the index of the period after the period at index {@code periodIndex} depending on the + * {@code repeatMode}. + * + * @param periodIndex Index of a period in the timeline. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. + */ + public final int getNextPeriodIndex(int periodIndex, Period period, Window window, + @ExoPlayer.RepeatMode int repeatMode) { + int windowIndex = getPeriod(periodIndex, period).windowIndex; + if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); + if (nextWindowIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return getWindow(nextWindowIndex, window).firstPeriodIndex; + } + return periodIndex + 1; + } + + /** + * Returns whether the given period is the last period of the timeline depending on the + * {@code repeatMode}. + * + * @param periodIndex A period index. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return Whether the period of the given index is the last period of the timeline. + */ + public final boolean isLastPeriod(int periodIndex, Period period, Window window, + @ExoPlayer.RepeatMode int repeatMode) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; + } + /** * Populates a {@link Period} with data for the period at the specified index. Does not populate * {@link Period#id} and {@link Period#uid}. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index ce2e81020f..baeada098a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -529,8 +529,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic; - enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic; + enablePrevious = !timeline.isFirstWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) + || isSeekable || !window.isDynamic; + enableNext = !timeline.isLastWindow(windowIndex, ExoPlayer.REPEAT_MODE_OFF) + || window.isDynamic; if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { // Always hide player controls during ads. hide(); @@ -680,9 +682,12 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, + ExoPlayer.REPEAT_MODE_OFF); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { - seekTo(windowIndex - 1, C.TIME_UNSET); + seekTo(previousWindowIndex, C.TIME_UNSET); } else { seekTo(0); } @@ -694,8 +699,9 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - if (windowIndex < timeline.getWindowCount() - 1) { - seekTo(windowIndex + 1, C.TIME_UNSET); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, ExoPlayer.REPEAT_MODE_OFF); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { seekTo(windowIndex, C.TIME_UNSET); }