diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 2f40985171..35f5f393be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1014,6 +1014,15 @@ import java.io.IOException; return getPeriodPosition(timeline, windowIndex, windowPositionUs); } + /** + * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position + * projection. + */ + private Pair getPeriodPosition(Timeline timeline, int windowIndex, + long windowPositionUs) { + return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); + } + /** * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). * @@ -1021,14 +1030,23 @@ import java.io.IOException; * @param windowIndex The window index. * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default * start position. - * @return The corresponding (periodIndex, periodPositionUs). + * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the + * duration into the future by which the window's position should be projected. + * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} + * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's + * position could not be projected by {@code defaultPositionProjectionUs}. */ private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs) { - timeline.getWindow(windowIndex, window); + long windowPositionUs, long defaultPositionProjectionUs) { + timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); + if (windowPositionUs == C.TIME_UNSET) { + windowPositionUs = window.getDefaultPositionUs(); + if (windowPositionUs == C.TIME_UNSET) { + return null; + } + } int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() - + (windowPositionUs == C.TIME_UNSET ? window.getDefaultPositionUs() : windowPositionUs); + long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { @@ -1062,30 +1080,44 @@ import java.io.IOException; : (isFirstPeriodInWindow ? C.TIME_UNSET : 0); if (periodStartPositionUs == C.TIME_UNSET) { // This is the first period of a new window or we don't have a start position, so seek to - // the default position for the window. - Pair defaultPosition = getPeriodPosition(windowIndex, C.TIME_UNSET); - newLoadingPeriodIndex = defaultPosition.first; - periodStartPositionUs = defaultPosition.second; + // the default position for the window. If we're buffering ahead we also project the + // default position so that it's correct for starting playing the buffered duration of + // time in the future. + long defaultPositionProjectionUs = loadingPeriodHolder == null ? 0 + : (loadingPeriodHolder.rendererPositionOffsetUs + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs - rendererPositionUs); + Pair defaultPosition = getPeriodPosition(timeline, windowIndex, + C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + newLoadingPeriodIndex = C.INDEX_UNSET; + periodStartPositionUs = C.TIME_UNSET; + } else { + newLoadingPeriodIndex = defaultPosition.first; + periodStartPositionUs = defaultPosition.second; + } } - Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid; - MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, - loadControl.getAllocator(), periodStartPositionUs); - newMediaPeriod.prepare(this); - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); - timeline.getWindow(windowIndex, window); - newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.setNext(newPeriodHolder); - newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - loadingPeriodHolder.startPositionUs; - } else { - newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; + if (newLoadingPeriodIndex != C.INDEX_UNSET) { + Object newPeriodUid = timeline.getPeriod(newLoadingPeriodIndex, period, true).uid; + MediaPeriod newMediaPeriod = mediaSource.createPeriod(newLoadingPeriodIndex, + loadControl.getAllocator(), periodStartPositionUs); + newMediaPeriod.prepare(this); + MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, + trackSelector, mediaSource, newMediaPeriod, newPeriodUid, periodStartPositionUs); + timeline.getWindow(windowIndex, window); + newPeriodHolder.setIndex(timeline, window, newLoadingPeriodIndex); + if (loadingPeriodHolder != null) { + loadingPeriodHolder.setNext(newPeriodHolder); + newPeriodHolder.rendererPositionOffsetUs = loadingPeriodHolder.rendererPositionOffsetUs + + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() + - loadingPeriodHolder.startPositionUs; + } else { + newPeriodHolder.rendererPositionOffsetUs = periodStartPositionUs; + } + bufferAheadPeriodCount++; + loadingPeriodHolder = newPeriodHolder; + setIsLoading(true); } - bufferAheadPeriodCount++; - loadingPeriodHolder = newPeriodHolder; - setIsLoading(true); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/Timeline.java index b394ecabf8..7d3ad1feae 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -114,10 +114,26 @@ public abstract class Timeline { * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. + * null. The caller should pass false for efficiency reasons unless the field is required. * @return The populated {@link Window}, for convenience. */ - public abstract Window getWindow(int windowIndex, Window window, boolean setIds); + public Window getWindow(int windowIndex, Window window, boolean setIds) { + return getWindow(windowIndex, window, setIds, 0); + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to + * null. The caller should pass false for efficiency reasons unless the field is required. + * @param defaultPositionProjectionUs A duration into the future that the populated window's + * default start position should be projected. + * @return The populated {@link Window}, for convenience. + */ + public abstract Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs); /** * Returns the number of periods in the timeline. @@ -231,7 +247,9 @@ public abstract class Timeline { /** * Returns the default position relative to the start of the window at which to begin playback, - * in milliseconds. + * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. */ public long getDefaultPositionMs() { return C.usToMs(defaultPositionUs); @@ -239,7 +257,9 @@ public abstract class Timeline { /** * Returns the default position relative to the start of the window at which to begin playback, - * in microseconds. + * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a + * non-zero default position projection, and if the specified projection cannot be performed + * whilst remaining within the bounds of the window. */ public long getDefaultPositionUs() { return defaultPositionUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 3b743c5fda..678ae8b06c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -171,11 +171,13 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { int sourceIndex = getSourceIndexForWindow(windowIndex); int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds); + timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds, + defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInSource; window.lastPeriodIndex += firstPeriodIndexInSource; return window; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 21455ed89d..938051843d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -118,8 +118,10 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { - childTimeline.getWindow(windowIndex % childWindowCount, window, setIds); + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + childTimeline.getWindow(windowIndex % childWindowCount, window, setIds, + defaultPositionProjectionUs); int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; window.firstPeriodIndex += periodIndexOffset; window.lastPeriodIndex += periodIndexOffset; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index f298d04432..ae367ef14c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -74,9 +74,18 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; + long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; + if (isDynamic) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } + } return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic, windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index ec794a534d..3fde4f9c8f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -400,39 +399,13 @@ public final class DashMediaSource implements MediaSource { ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; } // Snap the default position to the start of the segment containing it. - long defaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); - if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { + windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); + if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { // The default start position is too close to the start of the live window. Set it to the // minimum default start position provided the window is at least twice as big. Else set // it to the middle of the window. - defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); - } - - int periodIndex = 0; - long defaultStartPositionInPeriodUs = currentStartTimeUs + defaultStartPositionUs; - long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - while (periodIndex < manifest.getPeriodCount() - 1 - && defaultStartPositionInPeriodUs >= periodDurationUs) { - defaultStartPositionInPeriodUs -= periodDurationUs; - periodIndex++; - periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - } - Period period = manifest.getPeriod(periodIndex); - int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); - if (videoAdaptationSetIndex != C.INDEX_UNSET) { - // If there are multiple video adaptation sets with unaligned segments, the initial time may - // not correspond to the start of a segment in both, but this is an edge case. - DashSegmentIndex index = - period.adaptationSets.get(videoAdaptationSetIndex).representations.get(0).getIndex(); - if (index != null) { - int segmentNum = index.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); - windowDefaultStartPositionUs = - defaultStartPositionUs - defaultStartPositionInPeriodUs + index.getTimeUs(segmentNum); - } else { - windowDefaultStartPositionUs = defaultStartPositionUs; - } - } else { - windowDefaultStartPositionUs = defaultStartPositionUs; + windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, + windowDurationUs / 2); } } long windowStartTimeMs = manifest.availabilityStartTime @@ -561,8 +534,11 @@ public final class DashMediaSource implements MediaSource { } @Override - public Window getWindow(int windowIndex, Window window, boolean setIdentifier) { + public Window getWindow(int windowIndex, Window window, boolean setIdentifier, + long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); + long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( + defaultPositionProjectionUs); return window.set(null, presentationStartTimeMs, windowStartTimeMs, true /* isSeekable */, manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, 0, manifest.getPeriodCount() - 1, offsetInFirstPeriodUs); @@ -578,6 +554,48 @@ public final class DashMediaSource implements MediaSource { ? C.INDEX_UNSET : (periodId - firstPeriodId); } + private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { + long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; + if (!manifest.dynamic) { + return windowDefaultStartPositionUs; + } + if (defaultPositionProjectionUs > 0) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. + return C.TIME_UNSET; + } + } + // Attempt to snap to the start of the corresponding video segment. + int periodIndex = 0; + long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs; + long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + while (periodIndex < manifest.getPeriodCount() - 1 + && defaultStartPositionInPeriodUs >= periodDurationUs) { + defaultStartPositionInPeriodUs -= periodDurationUs; + periodIndex++; + periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + } + com.google.android.exoplayer2.source.dash.manifest.Period period = + manifest.getPeriod(periodIndex); + int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); + if (videoAdaptationSetIndex == C.INDEX_UNSET) { + // No video adaptation set for snapping. + return windowDefaultStartPositionUs; + } + // If there are multiple video adaptation sets with unaligned segments, the initial time may + // not correspond to the start of a segment in both, but this is an edge case. + DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) + .representations.get(0).getIndex(); + if (snapIndex == null) { + // Video adaptation set does not include an index for snapping. + return windowDefaultStartPositionUs; + } + int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); + return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum) + - defaultStartPositionInPeriodUs; + } + } private final class ManifestCallback implements