diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42a6f60dd9..b7eaac93d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,7 @@ `AudioProcessors` are active, e.g. for gapless trimming ([#10847](https://github.com/google/ExoPlayer/issues/10847)). * Encapsulate Opus frames in Ogg packets in direct playbacks (offload). + * Extrapolate current position during sleep with offload scheduling. * DRM: * Reduce the visibility of several internal-only methods on `DefaultDrmSession` that aren't expected to be called from outside the diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 0e34b19fff..df6b4b12d0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -1013,6 +1013,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; @@ -1803,11 +1806,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); } } @@ -2020,10 +2030,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( @@ -2639,8 +2649,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, @@ -2762,12 +2778,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/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 411f61c325..aa950ecda3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -946,7 +946,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. @@ -1494,6 +1494,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /* bufferedPositionUs= */ startPositionUs, /* totalBufferedDurationUs= */ 0, /* positionUs= */ startPositionUs, + /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); if (releaseMediaSourceList) { mediaSourceList.release(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java index 9ea9b0e971..479c47ffa9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/PlaybackInfo.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer; +import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -23,6 +24,7 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; +import androidx.media3.common.util.Util; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; @@ -92,6 +94,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 @@ -120,6 +127,7 @@ import java.util.List; /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, + /* positionUpdateTimeMs= */ 0, /* sleepingForOffload= */ false); } @@ -142,6 +150,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( @@ -162,6 +171,7 @@ import java.util.List; long bufferedPositionUs, long totalBufferedDurationUs, long positionUs, + long positionUpdateTimeMs, boolean sleepingForOffload) { this.timeline = timeline; this.periodId = periodId; @@ -180,6 +190,7 @@ import java.util.List; this.bufferedPositionUs = bufferedPositionUs; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; + this.positionUpdateTimeMs = positionUpdateTimeMs; this.sleepingForOffload = sleepingForOffload; } @@ -231,6 +242,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + /* positionUpdateTimeMs= */ SystemClock.elapsedRealtime(), sleepingForOffload); } @@ -260,6 +272,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -289,6 +302,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -318,6 +332,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -347,6 +362,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -376,6 +392,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -409,6 +426,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -438,6 +456,7 @@ import java.util.List; bufferedPositionUs, totalBufferedDurationUs, positionUs, + positionUpdateTimeMs, sleepingForOffload); } @@ -467,6 +486,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: + * + *