From f37d79a4dd9de7977fce2ed10f35385fdb88bdd8 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 24 Sep 2020 13:50:57 +0100 Subject: [PATCH] Callback when sleeping for offload and existing from it #exo-offload PiperOrigin-RevId: 333497538 --- .../android/exoplayer2/ExoPlayerImpl.java | 9 +++ .../exoplayer2/ExoPlayerImplInternal.java | 14 ++-- .../android/exoplayer2/PlaybackInfo.java | 65 +++++++++++++++---- .../com/google/android/exoplayer2/Player.java | 6 ++ .../android/exoplayer2/ExoPlayerTest.java | 38 ++--------- .../exoplayer2/MediaPeriodQueueTest.java | 3 +- .../exoplayer2/testutil/TestExoPlayer.java | 28 +++++++- 7 files changed, 115 insertions(+), 48 deletions(-) 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 b1f5736465..1b0b34bd7b 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 @@ -1353,6 +1353,7 @@ import java.util.concurrent.TimeoutException; private final boolean isPlayingChanged; private final boolean playbackParametersChanged; private final boolean offloadSchedulingEnabledChanged; + private final boolean sleepingForOffloadChanged; public PlaybackInfoUpdate( PlaybackInfo playbackInfo, @@ -1394,6 +1395,8 @@ import java.util.concurrent.TimeoutException; !previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters); offloadSchedulingEnabledChanged = previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled; + sleepingForOffloadChanged = + previousPlaybackInfo.sleepingForOffload != playbackInfo.sleepingForOffload; } @SuppressWarnings("deprecation") @@ -1476,6 +1479,12 @@ import java.util.concurrent.TimeoutException; listener.onExperimentalOffloadSchedulingEnabledChanged( playbackInfo.offloadSchedulingEnabled)); } + if (sleepingForOffloadChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onExperimentalSleepingForOffloadChanged(playbackInfo.sleepingForOffload)); + } } private static boolean isPlaying(PlaybackInfo playbackInfo) { 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 e33b93ac0e..0752c08949 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 @@ -960,14 +960,18 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled); } + boolean sleepingForOffload = false; if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY) || playbackInfo.playbackState == Player.STATE_BUFFERING) { - maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS); + sleepingForOffload = !maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS); } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); } + if (playbackInfo.sleepingForOffload != sleepingForOffload) { + playbackInfo = playbackInfo.copyWithSleepingForOffload(sleepingForOffload); + } requestForRendererSleep = false; // A sleep request is only valid for the current doSomeWork. TraceUtil.endSection(); @@ -978,12 +982,13 @@ import java.util.concurrent.atomic.AtomicBoolean; handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); } - private void maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) { + private boolean maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) { if (offloadSchedulingEnabled && requestForRendererSleep) { - return; + return false; } scheduleNextWork(operationStartTimeMs, intervalMs); + return true; } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { @@ -1308,7 +1313,8 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs, /* totalBufferedDurationUs= */ 0, startPositionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + /* 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 9fb6563005..e7f200d8b7 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 @@ -67,6 +67,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final PlaybackParameters playbackParameters; /** Whether offload scheduling is enabled for the main player loop. */ public final boolean offloadSchedulingEnabled; + /** Whether the main player loop is sleeping, while using offload scheduling. */ + public final boolean sleepingForOffload; /** * Position up to which media is buffered in {@link #loadingMediaPeriodId) relative to the start @@ -109,7 +111,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, - /* offloadSchedulingEnabled= */ false); + /* offloadSchedulingEnabled= */ false, + /* sleepingForOffload= */ false); } /** @@ -131,6 +134,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}. * @param positionUs See {@link #positionUs}. * @param offloadSchedulingEnabled See {@link #offloadSchedulingEnabled}. + * @param sleepingForOffload See {@link #sleepingForOffload}. */ public PlaybackInfo( Timeline timeline, @@ -148,7 +152,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long bufferedPositionUs, long totalBufferedDurationUs, long positionUs, - boolean offloadSchedulingEnabled) { + boolean offloadSchedulingEnabled, + boolean sleepingForOffload) { this.timeline = timeline; this.periodId = periodId; this.requestedContentPositionUs = requestedContentPositionUs; @@ -165,6 +170,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.totalBufferedDurationUs = totalBufferedDurationUs; this.positionUs = positionUs; this.offloadSchedulingEnabled = offloadSchedulingEnabled; + this.sleepingForOffload = sleepingForOffload; } /** Returns a placeholder period id for an empty timeline. */ @@ -209,7 +215,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -236,7 +243,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -263,7 +271,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -290,7 +299,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -317,7 +327,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -344,7 +355,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -375,7 +387,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -402,7 +415,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); } /** @@ -430,6 +444,35 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; bufferedPositionUs, totalBufferedDurationUs, positionUs, - offloadSchedulingEnabled); + offloadSchedulingEnabled, + sleepingForOffload); + } + + /** + * Copies playback info with new sleepingForOffload. + * + * @param sleepingForOffload New main player loop sleeping state. See {@link #sleepingForOffload}. + * @return Copied playback info with new main player loop sleeping state. + */ + @CheckResult + public PlaybackInfo copyWithSleepingForOffload(boolean sleepingForOffload) { + return new PlaybackInfo( + timeline, + periodId, + requestedContentPositionUs, + playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + playWhenReady, + playbackSuppressionReason, + playbackParameters, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs, + offloadSchedulingEnabled, + sleepingForOffload); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 7a52aae738..89a00eb475 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -604,6 +604,12 @@ public interface Player { *

This method is experimental, and will be renamed or removed in a future release. */ default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {} + /** + * Called when the player has started or finished sleeping for offload. + * + *

This method is experimental, and will be renamed or removed in a future release. + */ + default void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) {} } /** 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 7934298df0..a1b3b9014d 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 @@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSample import static com.google.android.exoplayer2.testutil.TestExoPlayer.playUntilStartOfWindow; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilPlaybackState; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilReceiveOffloadSchedulingEnabledNewState; +import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilSleepingForOffload; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged; import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; @@ -111,7 +112,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -8249,16 +8249,14 @@ public final class ExoPlayerTest { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); player.experimentalSetOffloadSchedulingEnabled(true); - runUntilReceiveOffloadSchedulingEnabledNewState(player); player.prepare(); player.play(); - runMainLooperUntil(sleepRenderer::isSleeping); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); player.experimentalSetOffloadSchedulingEnabled(false); assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse(); } - @Test public void enableOffloadScheduling_isEnable_playerSleeps() throws Exception { FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); @@ -8271,14 +8269,7 @@ public final class ExoPlayerTest { sleepRenderer.sleepOnNextRender(); - runMainLooperUntil(sleepRenderer::isSleeping); - // TODO(b/163303129): There is currently no way to check that the player is sleeping for - // offload, for now use a timeout to check that the renderer is never woken up. - final int renderTimeoutMs = 500; - assertThrows( - TimeoutException.class, - () -> - runMainLooperUntil(() -> !sleepRenderer.isSleeping(), renderTimeoutMs, Clock.DEFAULT)); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); } @Test @@ -8292,11 +8283,11 @@ public final class ExoPlayerTest { player.experimentalSetOffloadSchedulingEnabled(true); player.prepare(); player.play(); - runMainLooperUntil(sleepRenderer::isSleeping); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); player.experimentalSetOffloadSchedulingEnabled(false); // Force the player to exit offload sleep - runMainLooperUntil(() -> !sleepRenderer.isSleeping()); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false); runUntilPlaybackState(player, Player.STATE_ENDED); } @@ -8309,11 +8300,11 @@ public final class ExoPlayerTest { player.experimentalSetOffloadSchedulingEnabled(true); player.prepare(); player.play(); - runMainLooperUntil(sleepRenderer::isSleeping); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true); sleepRenderer.wakeup(); - runMainLooperUntil(() -> !sleepRenderer.isSleeping()); + runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false); runUntilPlaybackState(player, Player.STATE_ENDED); } @@ -8350,13 +8341,11 @@ public final class ExoPlayerTest { private static class FakeSleepRenderer extends FakeRenderer { private static final long WAKEUP_DEADLINE_MS = 60 * C.MICROS_PER_SECOND; private final AtomicBoolean sleepOnNextRender; - private final AtomicBoolean isSleeping; private final AtomicReference wakeupListenerReceiver; public FakeSleepRenderer(int trackType) { super(trackType); sleepOnNextRender = new AtomicBoolean(false); - isSleeping = new AtomicBoolean(false); wakeupListenerReceiver = new AtomicReference<>(); } @@ -8372,14 +8361,6 @@ public final class ExoPlayerTest { return this; } - /** - * Returns whether {@link Renderer.WakeupListener#onSleep(long)} was called on the last {@link - * #render(long, long)} - */ - public boolean isSleeping() { - return isSleeping.get(); - } - @Override public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { if (what == MSG_SET_WAKEUP_LISTENER) { @@ -8394,11 +8375,6 @@ public final class ExoPlayerTest { super.render(positionUs, elapsedRealtimeUs); if (sleepOnNextRender.compareAndSet(/* expect= */ true, /* update= */ false)) { wakeupListenerReceiver.get().onSleep(WAKEUP_DEADLINE_MS); - // TODO(b/163303129): Use an actual message from the player instead of guessing that the - // player will always sleep for offload after calling `onSleep`. - isSleeping.set(true); - } else { - isSleeping.set(false); } } } 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 20be8fe12b..ff4cdb7340 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 @@ -439,7 +439,8 @@ public final class MediaPeriodQueueTest { /* bufferedPositionUs= */ 0, /* totalBufferedDurationUs= */ 0, /* positionUs= */ 0, - /* offloadSchedulingEnabled= */ false); + /* offloadSchedulingEnabled= */ false, + /* sleepingForOffload= */ false); } private void advance() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java index 6b8f32ef01..1fe606faca 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java @@ -466,6 +466,32 @@ public class TestExoPlayer { return Assertions.checkNotNull(offloadSchedulingEnabledReceiver.get()); } + /** + * Runs tasks of the main {@link Looper} until a {@link + * Player.EventListener#onExperimentalSleepingForOffloadChanged(boolean)} callback occurred. + * + * @param player The {@link Player}. + * @param expectedSleepForOffload The expected sleep of offload state. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public static void runUntilSleepingForOffload(Player player, boolean expectedSleepForOffload) + throws TimeoutException { + verifyMainTestThread(player); + AtomicBoolean receiverCallback = new AtomicBoolean(false); + Player.EventListener listener = + new Player.EventListener() { + @Override + public void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) { + if (sleepingForOffload == expectedSleepForOffload) { + receiverCallback.set(true); + } + } + }; + player.addListener(listener); + runMainLooperUntil(receiverCallback::get); + } + /** * Runs tasks of the main {@link Looper} until the {@link VideoListener#onRenderedFirstFrame} * callback has been called. @@ -504,7 +530,7 @@ public class TestExoPlayer { verifyMainTestThread(player); Handler testHandler = Util.createHandlerForCurrentOrMainLooper(); - AtomicBoolean messageHandled = new AtomicBoolean(); + AtomicBoolean messageHandled = new AtomicBoolean(false); player .createMessage( (messageType, payload) -> {