diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 4cbcc00886..56bc633c9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -174,13 +174,14 @@ public class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { + public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, + boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; } @Override - public boolean shouldContinueLoading(long bufferedDurationUs) { + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; 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 668d52425e..71da7043be 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 @@ -284,10 +284,8 @@ import java.io.IOException; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // TODO(b/37237846): Make LoadControl, period transition position projection, adaptive track - // selection and potentially any time-related code in renderers take into account the playback - // speed. eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); } // Handler.Callback implementation. @@ -573,9 +571,10 @@ import java.io.IOException; setState(Player.STATE_ENDED); stopRenderers(); } else if (state == Player.STATE_BUFFERING) { + float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded - && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) + ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( + rendererPositionUs, playbackSpeed, rebuffering)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(Player.STATE_READY); @@ -853,6 +852,7 @@ import java.io.IOException; // We don't have tracks yet, so we don't care. return; } + float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = playingPeriodHolder; boolean selectionsChangedForReadPeriod = true; @@ -861,7 +861,7 @@ import java.io.IOException; // The reselection did not change any prepared periods. return; } - if (periodHolder.selectTracks()) { + if (periodHolder.selectTracks(playbackSpeed)) { // Selected tracks have changed for this period. break; } @@ -935,6 +935,18 @@ import java.io.IOException; } } + private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { + MediaPeriodHolder periodHolder = + playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; + while (periodHolder != null) { + TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + periodHolder = periodHolder.next; + } + } + private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs @@ -1391,7 +1403,7 @@ import java.io.IOException; // Stale event. return; } - loadingPeriodHolder.handlePrepared(); + loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; @@ -1410,7 +1422,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs); + boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( + rendererPositionUs, mediaClock.getPlaybackParameters().speed); setIsLoading(continueLoading); if (continueLoading) { loadingPeriodHolder.continueLoading(rendererPositionUs); @@ -1572,7 +1585,8 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } - public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) { + public boolean haveSufficientBuffer(long rendererPositionUs, float playbackSpeed, + boolean rebuffering) { long bufferedPositionUs = !prepared ? info.startPositionUs : mediaPeriod.getBufferedPositionUs(); if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { @@ -1582,24 +1596,24 @@ import java.io.IOException; bufferedPositionUs = info.durationUs; } return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs), - rebuffering); + playbackSpeed, rebuffering); } - public void handlePrepared() throws ExoPlaybackException { + public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; - selectTracks(); + selectTracks(playbackSpeed); long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); info = info.copyWithStartPositionUs(newStartPositionUs); } - public boolean shouldContinueLoading(long rendererPositionUs) { + public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; } else { long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - return loadControl.shouldContinueLoading(bufferedDurationUs); + return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } } @@ -1608,13 +1622,18 @@ import java.io.IOException; mediaPeriod.continueLoading(loadingPeriodPositionUs); } - public boolean selectTracks() throws ExoPlaybackException { + public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups()); if (selectorResult.isEquivalent(periodTrackSelectorResult)) { return false; } trackSelectorResult = selectorResult; + for (TrackSelection trackSelection : trackSelectorResult.selections.getAll()) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + } return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index 44b16b0cf6..ee4775d048 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -92,19 +92,21 @@ public interface LoadControl { * started or resumed. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by * buffer depletion rather than a user action. Hence this parameter is false during initial * buffering and when buffering as a result of a seek operation. * @return Whether playback should be allowed to start or resume. */ - boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering); + boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); /** * Called by the player to determine whether it should continue to load the source. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @return Whether the loading should continue. */ - boolean shouldContinueLoading(long bufferedDurationUs); + boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java index 90aded7660..47d5bc88b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.util.Assertions; + /** * The parameters that apply to playback. */ @@ -40,23 +42,25 @@ public final class PlaybackParameters { /** * Creates new playback parameters. * - * @param speed The factor by which playback will be sped up. - * @param pitch The factor by which the audio pitch will be scaled. + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. */ public PlaybackParameters(float speed, float pitch) { + Assertions.checkArgument(speed > 0); + Assertions.checkArgument(pitch > 0); this.speed = speed; this.pitch = pitch; scaledUsPerMs = Math.round(speed * 1000f); } /** - * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in - * microseconds. + * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of + * wallclock time. * * @param timeMs The time to scale, in milliseconds. * @return The scaled time, in microseconds. */ - public long getSpeedAdjustedDurationUs(long timeMs) { + public long getMediaTimeUsForPlayoutTimeMs(long timeMs) { return timeMs * scaledUsPerMs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ba62ac126e..3b14b69916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1012,7 +1012,8 @@ public final class DefaultAudioSink implements AudioSink { } // We are playing data at a previous playback speed, so fall back to multiplying by the speed. return playbackParametersOffsetUs - + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); + + Util.getMediaDurationForPlayoutDuration( + positionUs - playbackParametersPositionUs, playbackParameters.speed); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 6bc6afb88b..9a58ac07aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -138,6 +138,11 @@ public abstract class BaseTrackSelection implements TrackSelection { return tracks[getSelectedIndex()]; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + // Do nothing. + } + @Override public int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue) { return queue.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 027b2abde9..55e6050622 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -136,6 +136,14 @@ public interface TrackSelection { // Adaptation. + /** + * Called to notify the selection of the current playback speed. The playback speed may affect + * adaptive track selection. + * + * @param speed The playback speed. + */ + void onPlaybackSpeed(float speed); + /** * Updates the selected track. *
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index fad3a00f10..3c0ec2a854 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -88,7 +88,7 @@ public final class StandaloneMediaClock implements MediaClock { if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { - positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; 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 3b402ec59d..4582ab7c86 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 @@ -672,6 +672,20 @@ public final class Util { } } + /** + * Returns the duration of media that will elapse in {@code playoutDuration}. + * + * @param playoutDuration The duration to scale. + * @param speed The playback speed. + * @return The scaled duration, in the same units as {@code playoutDuration}. + */ + public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) { + if (speed == 1f) { + return playoutDuration; + } + return Math.round((double) playoutDuration * speed); + } + /** * Converts a list of integers to a primitive array. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index 9db4d57a65..9edb84eaa9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -368,7 +368,7 @@ public class DefaultMediaClockTest { long clockStartUs = mediaClock.syncAndGetPositionUs(); fakeClock.advanceTime(SLEEP_TIME_MS); assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs - + mediaClock.getPlaybackParameters().getSpeedAdjustedDurationUs(SLEEP_TIME_MS)); + + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); } private void assertClockIsStopped() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 6dc9cf7fd8..094aaa5273 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -465,7 +465,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; - if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) { newIsLoading = true; mediaPeriod.continueLoading(rendererPositionUs); } @@ -488,7 +488,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { return true; } - return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + return + loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 1f, rebuffering); } private void handlePlayerError(final ExoPlaybackException e) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java index 20346a0355..717dcda7b1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -111,6 +111,11 @@ public final class FakeTrackSelection implements TrackSelection { return null; } + @Override + public void onPlaybackSpeed(float speed) { + // Do nothing. + } + @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) {