Callback when sleeping for offload and existing from it

#exo-offload

PiperOrigin-RevId: 333497538
This commit is contained in:
krocard 2020-09-24 13:50:57 +01:00 committed by kim-vde
parent 294ae10ef1
commit f37d79a4dd
7 changed files with 115 additions and 48 deletions

View File

@ -1353,6 +1353,7 @@ import java.util.concurrent.TimeoutException;
private final boolean isPlayingChanged; private final boolean isPlayingChanged;
private final boolean playbackParametersChanged; private final boolean playbackParametersChanged;
private final boolean offloadSchedulingEnabledChanged; private final boolean offloadSchedulingEnabledChanged;
private final boolean sleepingForOffloadChanged;
public PlaybackInfoUpdate( public PlaybackInfoUpdate(
PlaybackInfo playbackInfo, PlaybackInfo playbackInfo,
@ -1394,6 +1395,8 @@ import java.util.concurrent.TimeoutException;
!previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters); !previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters);
offloadSchedulingEnabledChanged = offloadSchedulingEnabledChanged =
previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled; previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled;
sleepingForOffloadChanged =
previousPlaybackInfo.sleepingForOffload != playbackInfo.sleepingForOffload;
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@ -1476,6 +1479,12 @@ import java.util.concurrent.TimeoutException;
listener.onExperimentalOffloadSchedulingEnabledChanged( listener.onExperimentalOffloadSchedulingEnabledChanged(
playbackInfo.offloadSchedulingEnabled)); playbackInfo.offloadSchedulingEnabled));
} }
if (sleepingForOffloadChanged) {
invokeAll(
listenerSnapshot,
listener ->
listener.onExperimentalSleepingForOffloadChanged(playbackInfo.sleepingForOffload));
}
} }
private static boolean isPlaying(PlaybackInfo playbackInfo) { private static boolean isPlaying(PlaybackInfo playbackInfo) {

View File

@ -960,14 +960,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled); playbackInfo = playbackInfo.copyWithOffloadSchedulingEnabled(offloadSchedulingEnabled);
} }
boolean sleepingForOffload = false;
if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY) if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
|| playbackInfo.playbackState == Player.STATE_BUFFERING) { || 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) { } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else { } else {
handler.removeMessages(MSG_DO_SOME_WORK); 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. requestForRendererSleep = false; // A sleep request is only valid for the current doSomeWork.
TraceUtil.endSection(); TraceUtil.endSection();
@ -978,12 +982,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); 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) { if (offloadSchedulingEnabled && requestForRendererSleep) {
return; return false;
} }
scheduleNextWork(operationStartTimeMs, intervalMs); scheduleNextWork(operationStartTimeMs, intervalMs);
return true;
} }
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
@ -1308,7 +1313,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
startPositionUs, startPositionUs,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
startPositionUs, startPositionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
/* sleepingForOffload= */ false);
if (releaseMediaSourceList) { if (releaseMediaSourceList) {
mediaSourceList.release(); mediaSourceList.release();
} }

View File

@ -67,6 +67,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
public final PlaybackParameters playbackParameters; public final PlaybackParameters playbackParameters;
/** Whether offload scheduling is enabled for the main player loop. */ /** Whether offload scheduling is enabled for the main player loop. */
public final boolean offloadSchedulingEnabled; 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 * 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, /* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
/* positionUs= */ 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 totalBufferedDurationUs See {@link #totalBufferedDurationUs}.
* @param positionUs See {@link #positionUs}. * @param positionUs See {@link #positionUs}.
* @param offloadSchedulingEnabled See {@link #offloadSchedulingEnabled}. * @param offloadSchedulingEnabled See {@link #offloadSchedulingEnabled}.
* @param sleepingForOffload See {@link #sleepingForOffload}.
*/ */
public PlaybackInfo( public PlaybackInfo(
Timeline timeline, Timeline timeline,
@ -148,7 +152,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
long bufferedPositionUs, long bufferedPositionUs,
long totalBufferedDurationUs, long totalBufferedDurationUs,
long positionUs, long positionUs,
boolean offloadSchedulingEnabled) { boolean offloadSchedulingEnabled,
boolean sleepingForOffload) {
this.timeline = timeline; this.timeline = timeline;
this.periodId = periodId; this.periodId = periodId;
this.requestedContentPositionUs = requestedContentPositionUs; this.requestedContentPositionUs = requestedContentPositionUs;
@ -165,6 +170,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
this.totalBufferedDurationUs = totalBufferedDurationUs; this.totalBufferedDurationUs = totalBufferedDurationUs;
this.positionUs = positionUs; this.positionUs = positionUs;
this.offloadSchedulingEnabled = offloadSchedulingEnabled; this.offloadSchedulingEnabled = offloadSchedulingEnabled;
this.sleepingForOffload = sleepingForOffload;
} }
/** Returns a placeholder period id for an empty timeline. */ /** Returns a placeholder period id for an empty timeline. */
@ -209,7 +215,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -236,7 +243,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -263,7 +271,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -290,7 +299,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -317,7 +327,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -344,7 +355,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -375,7 +387,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -402,7 +415,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, positionUs,
offloadSchedulingEnabled); offloadSchedulingEnabled,
sleepingForOffload);
} }
/** /**
@ -430,6 +444,35 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
bufferedPositionUs, bufferedPositionUs,
totalBufferedDurationUs, totalBufferedDurationUs,
positionUs, 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);
} }
} }

View File

@ -604,6 +604,12 @@ public interface Player {
* <p>This method is experimental, and will be renamed or removed in a future release. * <p>This method is experimental, and will be renamed or removed in a future release.
*/ */
default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {} default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {}
/**
* Called when the player has started or finished sleeping for offload.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
default void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) {}
} }
/** /**

View File

@ -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.playUntilStartOfWindow;
import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilPlaybackState; 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.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.TestExoPlayer.runUntilTimelineChanged;
import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@ -111,7 +112,6 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -8249,16 +8249,14 @@ public final class ExoPlayerTest {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true); player.experimentalSetOffloadSchedulingEnabled(true);
runUntilReceiveOffloadSchedulingEnabledNewState(player);
player.prepare(); player.prepare();
player.play(); player.play();
runMainLooperUntil(sleepRenderer::isSleeping); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
player.experimentalSetOffloadSchedulingEnabled(false); player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse(); assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
} }
@Test @Test
public void enableOffloadScheduling_isEnable_playerSleeps() throws Exception { public void enableOffloadScheduling_isEnable_playerSleeps() throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO);
@ -8271,14 +8269,7 @@ public final class ExoPlayerTest {
sleepRenderer.sleepOnNextRender(); sleepRenderer.sleepOnNextRender();
runMainLooperUntil(sleepRenderer::isSleeping); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
// 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));
} }
@Test @Test
@ -8292,11 +8283,11 @@ public final class ExoPlayerTest {
player.experimentalSetOffloadSchedulingEnabled(true); player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare(); player.prepare();
player.play(); player.play();
runMainLooperUntil(sleepRenderer::isSleeping); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
player.experimentalSetOffloadSchedulingEnabled(false); // Force the player to exit offload sleep player.experimentalSetOffloadSchedulingEnabled(false); // Force the player to exit offload sleep
runMainLooperUntil(() -> !sleepRenderer.isSleeping()); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false);
runUntilPlaybackState(player, Player.STATE_ENDED); runUntilPlaybackState(player, Player.STATE_ENDED);
} }
@ -8309,11 +8300,11 @@ public final class ExoPlayerTest {
player.experimentalSetOffloadSchedulingEnabled(true); player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare(); player.prepare();
player.play(); player.play();
runMainLooperUntil(sleepRenderer::isSleeping); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
sleepRenderer.wakeup(); sleepRenderer.wakeup();
runMainLooperUntil(() -> !sleepRenderer.isSleeping()); runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false);
runUntilPlaybackState(player, Player.STATE_ENDED); runUntilPlaybackState(player, Player.STATE_ENDED);
} }
@ -8350,13 +8341,11 @@ public final class ExoPlayerTest {
private static class FakeSleepRenderer extends FakeRenderer { private static class FakeSleepRenderer extends FakeRenderer {
private static final long WAKEUP_DEADLINE_MS = 60 * C.MICROS_PER_SECOND; private static final long WAKEUP_DEADLINE_MS = 60 * C.MICROS_PER_SECOND;
private final AtomicBoolean sleepOnNextRender; private final AtomicBoolean sleepOnNextRender;
private final AtomicBoolean isSleeping;
private final AtomicReference<Renderer.WakeupListener> wakeupListenerReceiver; private final AtomicReference<Renderer.WakeupListener> wakeupListenerReceiver;
public FakeSleepRenderer(int trackType) { public FakeSleepRenderer(int trackType) {
super(trackType); super(trackType);
sleepOnNextRender = new AtomicBoolean(false); sleepOnNextRender = new AtomicBoolean(false);
isSleeping = new AtomicBoolean(false);
wakeupListenerReceiver = new AtomicReference<>(); wakeupListenerReceiver = new AtomicReference<>();
} }
@ -8372,14 +8361,6 @@ public final class ExoPlayerTest {
return this; 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 @Override
public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException {
if (what == MSG_SET_WAKEUP_LISTENER) { if (what == MSG_SET_WAKEUP_LISTENER) {
@ -8394,11 +8375,6 @@ public final class ExoPlayerTest {
super.render(positionUs, elapsedRealtimeUs); super.render(positionUs, elapsedRealtimeUs);
if (sleepOnNextRender.compareAndSet(/* expect= */ true, /* update= */ false)) { if (sleepOnNextRender.compareAndSet(/* expect= */ true, /* update= */ false)) {
wakeupListenerReceiver.get().onSleep(WAKEUP_DEADLINE_MS); 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);
} }
} }
} }

View File

@ -439,7 +439,8 @@ public final class MediaPeriodQueueTest {
/* bufferedPositionUs= */ 0, /* bufferedPositionUs= */ 0,
/* totalBufferedDurationUs= */ 0, /* totalBufferedDurationUs= */ 0,
/* positionUs= */ 0, /* positionUs= */ 0,
/* offloadSchedulingEnabled= */ false); /* offloadSchedulingEnabled= */ false,
/* sleepingForOffload= */ false);
} }
private void advance() { private void advance() {

View File

@ -466,6 +466,32 @@ public class TestExoPlayer {
return Assertions.checkNotNull(offloadSchedulingEnabledReceiver.get()); 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} * Runs tasks of the main {@link Looper} until the {@link VideoListener#onRenderedFirstFrame}
* callback has been called. * callback has been called.
@ -504,7 +530,7 @@ public class TestExoPlayer {
verifyMainTestThread(player); verifyMainTestThread(player);
Handler testHandler = Util.createHandlerForCurrentOrMainLooper(); Handler testHandler = Util.createHandlerForCurrentOrMainLooper();
AtomicBoolean messageHandled = new AtomicBoolean(); AtomicBoolean messageHandled = new AtomicBoolean(false);
player player
.createMessage( .createMessage(
(messageType, payload) -> { (messageType, payload) -> {