diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 30bb93b482..5236184ae1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -1002,6 +1002,9 @@ import java.util.concurrent.TimeoutException; listeners.release(); playbackInfoUpdateHandler.removeCallbacksAndMessages(null); bandwidthMeter.removeEventListener(analyticsCollector); + if (playbackInfo.sleepingForOffload) { + playbackInfo = playbackInfo.copyWithEstimatedPosition(); + } playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE); playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId); playbackInfo.bufferedPositionUs = playbackInfo.positionUs; @@ -1792,11 +1795,18 @@ import java.util.concurrent.TimeoutException; private long getCurrentPositionUsInternal(PlaybackInfo playbackInfo) { if (playbackInfo.timeline.isEmpty()) { return Util.msToUs(maskingWindowPositionMs); - } else if (playbackInfo.periodId.isAd()) { - return playbackInfo.positionUs; + } + + long positionUs = + playbackInfo.sleepingForOffload + ? playbackInfo.getEstimatedPositionUs() + : playbackInfo.positionUs; + + if (playbackInfo.periodId.isAd()) { + return positionUs; } else { return periodPositionUsToWindowPositionUs( - playbackInfo.timeline, playbackInfo.periodId, playbackInfo.positionUs); + playbackInfo.timeline, playbackInfo.periodId, positionUs); } } @@ -2009,10 +2019,10 @@ import java.util.concurrent.TimeoutException; listener.onPlaybackSuppressionReasonChanged( newPlaybackInfo.playbackSuppressionReason)); } - if (isPlaying(previousPlaybackInfo) != isPlaying(newPlaybackInfo)) { + if (previousPlaybackInfo.isPlaying() != newPlaybackInfo.isPlaying()) { listeners.queueEvent( Player.EVENT_IS_PLAYING_CHANGED, - listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo))); + listener -> listener.onIsPlayingChanged(newPlaybackInfo.isPlaying())); } if (!previousPlaybackInfo.playbackParameters.equals(newPlaybackInfo.playbackParameters)) { listeners.queueEvent( @@ -2628,8 +2638,14 @@ import java.util.concurrent.TimeoutException; return; } pendingOperationAcks++; + + // Position estimation and copy must occur before changing/masking playback state. PlaybackInfo playbackInfo = - this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); + this.playbackInfo.sleepingForOffload + ? this.playbackInfo.copyWithEstimatedPosition() + : this.playbackInfo; + playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); + internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason); updatePlaybackInfo( playbackInfo, @@ -2751,12 +2767,6 @@ import java.util.concurrent.TimeoutException; : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; } - private static boolean isPlaying(PlaybackInfo playbackInfo) { - return playbackInfo.playbackState == Player.STATE_READY - && playbackInfo.playWhenReady - && playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; - } - private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder { private final Object uid; 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 e01fb0bdee..a9ca6cf18d 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 @@ -938,7 +938,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); - playbackInfo.positionUs = periodPositionUs; + playbackInfo.updatePositionUs(periodPositionUs); } // Update the buffered position and total buffered duration. @@ -1486,6 +1486,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /* bufferedPositionUs= */ startPositionUs, /* totalBufferedDurationUs= */ 0, /* positionUs= */ startPositionUs, + /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); if (releaseMediaSourceList) { mediaSourceList.release(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 5a985901d6..322df2546a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; @@ -22,6 +23,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.List; @@ -88,6 +90,11 @@ import java.util.List; * in the {@link #timeline}, in microseconds. */ public volatile long positionUs; + /** + * The value of {@link SystemClock#elapsedRealtime()} when {@link #positionUs} was updated, in + * milliseconds. + */ + public volatile long positionUpdateTimeMs; /** * Creates an empty placeholder playback info which can be used for masking as long as no real @@ -116,6 +123,7 @@ import java.util.List; /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, + /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); } @@ -138,6 +146,7 @@ import java.util.List; * @param bufferedPositionUs See {@link #bufferedPositionUs}. * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}. * @param positionUs See {@link #positionUs}. + * @param positionUpdateTimeMs See {@link #positionUpdateTimeMs}. * @param sleepingForOffload See {@link #sleepingForOffload}. */ public PlaybackInfo( @@ -158,6 +167,7 @@ import java.util.List; long bufferedPositionUs, long totalBufferedDurationUs, long positionUs, + long positionUpdateTimeMs, boolean sleepingForOffload) { this.timeline = timeline; this.periodId = periodId; @@ -176,6 +186,7 @@ import java.util.List; this.bufferedPositionUs = bufferedPositionUs; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; + this.positionUpdateTimeMs = positionUpdateTimeMs; this.sleepingForOffload = sleepingForOffload; } @@ -227,6 +238,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + /* positionUpdateTimeMs= */ SystemClock.elapsedRealtime(), sleepingForOffload); } @@ -256,6 +268,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -285,6 +298,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -314,6 +328,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -343,6 +358,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -372,6 +388,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -405,6 +422,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -434,6 +452,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -463,6 +482,99 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } + + /** + * Copies playback info with new estimated playing position. + * + *

Position is estimated with {@link #positionUs}, {@link #positionUpdateTimeMs}, and {@link + * PlaybackParameters#speed}. + * + * @return Copied playback info with new, estimated playback position. + */ + @CheckResult + public PlaybackInfo copyWithEstimatedPosition() { + return new PlaybackInfo( + timeline, + periodId, + requestedContentPositionUs, + discontinuityStartPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + staticMetadata, + loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, + playbackParameters, + bufferedPositionUs, + totalBufferedDurationUs, + getEstimatedPositionUs(), + SystemClock.elapsedRealtime(), + sleepingForOffload); + } + + /** + * Sets new playing position with update time of {@link SystemClock#elapsedRealtime()}, time + * relative to the start of the associated period in the {@link #timeline} + * + * @param positionUs The new playing position. + */ + public void updatePositionUs(long positionUs) { + // Write order of positionUs then positionUpdateTimeMs in order to be reverse of + // retrieval in getExtrapolatedPositionUs(). + this.positionUs = positionUs; + this.positionUpdateTimeMs = SystemClock.elapsedRealtime(); + } + + /** + * Retrieves estimated position based on {@link #positionUs}, {@link #positionUpdateTimeMs}, and + * {@link PlaybackParameters#speed}. + * + *

If not playing, then the estimated position is {@link #positionUs}. + * + * @return The estimated position. + */ + public long getEstimatedPositionUs() { + if (!isPlaying()) { + return this.positionUs; + } + + // Snapshot of volatile position info + long positionUs; + long positionUpdateTimeMs; + do { + // Read order of positionUpdateTimeMs then positionUs to be reverse of updatePositionUs write. + positionUpdateTimeMs = this.positionUpdateTimeMs; + positionUs = this.positionUs; + } while (positionUpdateTimeMs != this.positionUpdateTimeMs); + + long elapsedTimeMs = SystemClock.elapsedRealtime() - positionUpdateTimeMs; + long estimatedPositionMs = + Util.usToMs(positionUs) + (long) (elapsedTimeMs * playbackParameters.speed); + return Util.msToUs(estimatedPositionMs); + } + + /** + * Returns whether this object represents a playing state. + * + *

Returns true if the following conditions are met: + * + *

+ * + * @return Whether the playbackInfo represents a playing state. + */ + public boolean isPlaying() { + return playbackState == Player.STATE_READY + && playWhenReady + && playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9fb6f786da..a3b64eba86 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -9865,6 +9865,82 @@ public final class ExoPlayerTest { runUntilPlaybackState(player, Player.STATE_ENDED); } + @Test + public void enableOffloadScheduling_duringSleepGetCurrentPosition_returnsEstimatedPosition() + throws Exception { + FakeClock fakeClock = + new FakeClock(/* initialTimeMs= */ 987_654_321L, /* isAutoAdvancing= */ true); + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); + ExoPlayer player = + new TestExoPlayerBuilder(context).setClock(fakeClock).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + sleepRenderer.sleepOnNextRender(); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); + + long currentPosition = player.getCurrentPosition(); + fakeClock.advanceTime(/* timeDiffMs= */ 800); + long newPosition = player.getCurrentPosition(); + + assertThat(newPosition - currentPosition).isNotEqualTo(0); + assertThat(newPosition).isEqualTo(800); + } + + @Test + public void enableOffloadScheduling_pauseAndSeekDuringSleep_currentPositionIsSeekedPosition() + throws Exception { + FakeClock fakeClock = + new FakeClock(/* initialTimeMs= */ 987_654_321L, /* isAutoAdvancing= */ true); + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); + ExoPlayer player = + new TestExoPlayerBuilder(context).setClock(fakeClock).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + sleepRenderer.sleepOnNextRender(); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); + + // Pause, advance clock and then seek. + player.pause(); + fakeClock.advanceTime(/* timeDiffMs= */ 1000); + player.seekTo(800); + long currentPosition = player.getCurrentPosition(); + + assertThat(currentPosition).isEqualTo(800); + } + + @Test + public void enableOffloadScheduling_seekThenPauseDuringSleep_returnsEstimatePositionByPauseTime() + throws Exception { + FakeClock fakeClock = + new FakeClock(/* initialTimeMs= */ 987_654_321L, /* isAutoAdvancing= */ true); + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); + ExoPlayer player = + new TestExoPlayerBuilder(context).setClock(fakeClock).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + sleepRenderer.sleepOnNextRender(); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); + + // Seek, advance clock, then pause. + player.seekTo(800); + sleepRenderer.sleepOnNextRender(); + runUntilPlaybackState(player, Player.STATE_READY); + fakeClock.advanceTime(/* timeDiffMs= */ 1000); + player.pause(); + long currentPosition = player.getCurrentPosition(); + + assertThat(currentPosition).isEqualTo(1800); + } + @Test public void targetLiveOffsetInMedia_adjustsLiveOffsetToTargetOffset() throws Exception { long windowStartUnixTimeMs = 987_654_321_000L; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 10cbf1044a..c52a36010f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -1356,6 +1356,7 @@ public final class MediaPeriodQueueTest { /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, + /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); }