diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 95eda228ea..9875333dad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,12 @@ sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, `SsMediaSource.Factory`, and `MergingMediaSource`. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. + * Note: `SeekParameters` are only currently effective when playing + `ExtractorMediaSource`s (i.e. progressive streams). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can 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 7b52f79be5..09b3231467 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 @@ -678,20 +678,20 @@ import java.io.IOException; periodPositionUs = 0; } try { + long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { - long adjustedPeriodPositionUs = periodPositionUs; - if (playingPeriodHolder != null) { - adjustedPeriodPositionUs = + if (playingPeriodHolder != null && newPeriodPositionUs != 0) { + newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( - adjustedPeriodPositionUs, SeekParameters.DEFAULT); + newPeriodPositionUs, seekParameters); } - if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + if ((newPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { // Seek will be performed to the current position. Do nothing. periodPositionUs = playbackInfo.positionUs; return; } } - long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); + newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index b1c12d6192..5685b8b70b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -165,16 +165,23 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sampleStream.clearSentEos(); } } - long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs - || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); + long offsetPositionUs = positionUs + startUs; + long seekUs = mediaPeriod.seekToUs(offsetPositionUs); + Assertions.checkState( + seekUs == offsetPositionUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); return seekUs - startUs; } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs( - positionUs + startUs, adjustSeekParameters(positionUs + startUs, seekParameters)); + if (positionUs == startUs) { + // Never adjust seeks to the start of the clipped view. + return 0; + } + long offsetPositionUs = positionUs + startUs; + SeekParameters clippedSeekParameters = clipSeekParameters(offsetPositionUs, seekParameters); + return mediaPeriod.getAdjustedSeekPositionUs(offsetPositionUs, clippedSeekParameters) - startUs; } @Override @@ -209,12 +216,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } - private SeekParameters adjustSeekParameters(long positionUs, SeekParameters seekParameters) { - long toleranceBeforeMs = Math.min(positionUs - startUs, seekParameters.toleranceBeforeUs); + private SeekParameters clipSeekParameters(long offsetPositionUs, SeekParameters seekParameters) { + long toleranceBeforeMs = Math.min(offsetPositionUs - startUs, seekParameters.toleranceBeforeUs); long toleranceAfterMs = endUs == C.TIME_END_OF_SOURCE ? seekParameters.toleranceAfterUs - : Math.min(endUs - positionUs, seekParameters.toleranceAfterUs); + : Math.min(endUs - offsetPositionUs, seekParameters.toleranceAfterUs); if (toleranceBeforeMs == seekParameters.toleranceBeforeUs && toleranceAfterMs == seekParameters.toleranceAfterUs) { return seekParameters; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 4773ac53a1..e5d1fae7bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -372,8 +373,33 @@ import java.util.Arrays; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - // Treat all seeks into non-seekable media as being to t=0. - return seekMap.isSeekable() ? positionUs : 0; + if (!seekMap.isSeekable()) { + // Treat all seeks into non-seekable media as being to t=0. + return 0; + } + SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); + long minPositionUs = + Util.subtractWithOverflowDefault( + positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + long firstPointUs = seekPoints.first.timeUs; + boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs; + long secondPointUs = seekPoints.second.timeUs; + boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs; + if (firstPointValid && secondPointValid) { + if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) { + return firstPointUs; + } else { + return secondPointUs; + } + } else if (firstPointValid) { + return firstPointUs; + } else if (secondPointValid) { + return secondPointUs; + } else { + return minPositionUs; + } } // SampleStream methods. @@ -657,7 +683,7 @@ import java.util.Arrays; return pendingResetPositionUs != C.TIME_UNSET; } - private boolean isLoadableExceptionFatal(IOException e) { + private static boolean isLoadableExceptionFatal(IOException e) { return e instanceof UnrecognizedInputFormatException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 0594f52288..d796e6936f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -362,6 +362,40 @@ public final class Util { return Math.max(min, Math.min(value, max)); } + /** + * Returns the sum of two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x + y} overflows. + * @return {@code x + y}, or {@code overflowResult} if the result overflows. + */ + public static long addWithOverflowDefault(long x, long y, long overflowResult) { + long result = x + y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ result) & (y ^ result)) < 0) { + return overflowResult; + } + return result; + } + + /** + * Returns the difference between two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x - y} overflows. + * @return {@code x - y}, or {@code overflowResult} if the result overflows. + */ + public static long subtractWithOverflowDefault(long x, long y, long overflowResult) { + long result = x - y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ y) & (x ^ result)) < 0) { + return overflowResult; + } + return result; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 68ed686c62..ca7a3b199d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -41,6 +41,42 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class UtilTest { + @Test + public void testAddWithOverflowDefault() { + long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(15); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void testSubtrackWithOverflowDefault() { + long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(-5); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + @Test public void testInferContentType() { assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS);