Call onSeekProcessed immediately after seek command.

OnSeekProcessed is documented to be called as soon as all neccessary state changes
as a result of the seek have been made. As we now mask the state changes directly
when calling seekTo, we can also call this callback immediately without changing
the semantics of the method.

Our tests often use this callback as a way to wait for the internal player
to receive all pending commands and then report back. This is a special test
requirement for cases where we want to make sure no further updates happen as
a result of the player handling commands. To facilitate that, a new action is
added with a more descriptive name that achieves the same goal.

PiperOrigin-RevId: 303296210
This commit is contained in:
tonihei 2020-03-27 10:28:01 +00:00 committed by Oliver Woodman
parent 09be441e3d
commit af05ceac61
5 changed files with 85 additions and 143 deletions

View File

@ -75,7 +75,6 @@ import java.util.concurrent.TimeoutException;
@RepeatMode private int repeatMode; @RepeatMode private int repeatMode;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private int pendingOperationAcks; private int pendingOperationAcks;
private boolean hasPendingSeek;
private boolean hasPendingDiscontinuity; private boolean hasPendingDiscontinuity;
@DiscontinuityReason private int pendingDiscontinuityReason; @DiscontinuityReason private int pendingDiscontinuityReason;
@PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason; @PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason;
@ -552,7 +551,6 @@ import java.util.concurrent.TimeoutException;
if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
} }
hasPendingSeek = true;
pendingOperationAcks++; pendingOperationAcks++;
if (isPlayingAd()) { if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in // TODO: Investigate adding support for seeking during ads. This is complicated to do in
@ -580,7 +578,7 @@ import java.util.concurrent.TimeoutException;
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK, /* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, /* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, /* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false); /* seekProcessed= */ true);
} }
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */ /** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@ -889,9 +887,7 @@ import java.util.concurrent.TimeoutException;
// Update the masking variables, which are used when the timeline becomes empty. // Update the masking variables, which are used when the timeline becomes empty.
resetMaskingPosition(); resetMaskingPosition();
} }
boolean seekProcessed = hasPendingSeek;
boolean positionDiscontinuity = hasPendingDiscontinuity; boolean positionDiscontinuity = hasPendingDiscontinuity;
hasPendingSeek = false;
hasPendingDiscontinuity = false; hasPendingDiscontinuity = false;
updatePlaybackInfo( updatePlaybackInfo(
playbackInfoUpdate.playbackInfo, playbackInfoUpdate.playbackInfo,
@ -899,7 +895,7 @@ import java.util.concurrent.TimeoutException;
pendingDiscontinuityReason, pendingDiscontinuityReason,
TIMELINE_CHANGE_REASON_SOURCE_UPDATE, TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
pendingPlayWhenReadyChangeReason, pendingPlayWhenReadyChangeReason,
seekProcessed); /* seekProcessed= */ false);
} }
} }

View File

@ -531,72 +531,6 @@ public final class ExoPlayerTest {
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();
} }
@Test
public void seekProcessedCallback() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Initial seek. Expect immediate seek processed.
.pause()
.seek(5)
.waitForSeekProcessed()
// Multiple overlapping seeks while the player is still preparing. Expect only one seek
// processed.
.seek(2)
.seek(10)
// Wait until media source prepared and re-seek to same position. Expect a seek
// processed while still being in STATE_READY.
.waitForPlaybackState(Player.STATE_READY)
.seek(10)
// Start playback and wait until playback reaches second window.
.playUntilStartOfWindow(/* windowIndex= */ 1)
// Seek twice in concession, expecting the first seek to be replaced (and thus except
// only on seek processed callback).
.seek(5)
.seek(60)
.waitForSeekProcessed()
.play()
.build();
final List<Integer> playbackStatesWhenSeekProcessed = new ArrayList<>();
EventListener eventListener =
new EventListener() {
private int currentPlaybackState = Player.STATE_IDLE;
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
currentPlaybackState = playbackState;
}
@Override
public void onSeekProcessed() {
playbackStatesWhenSeekProcessed.add(currentPlaybackState);
}
};
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setEventListener(eventListener)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_SEEK);
assertThat(playbackStatesWhenSeekProcessed)
.containsExactly(
Player.STATE_BUFFERING,
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_BUFFERING)
.inOrder();
}
@Test @Test
public void illegalSeekPositionDoesThrow() throws Exception { public void illegalSeekPositionDoesThrow() throws Exception {
final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1];
@ -1407,9 +1341,8 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.seek(0)
.stop(true) .stop(true)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
@ -1423,21 +1356,19 @@ public final class ExoPlayerTest {
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
} }
@Test @Test
public void stopAndSeekAfterStopDoesNotResetTimeline() throws Exception { public void stopAndSeekAfterStopDoesNotResetTimeline() throws Exception {
// Combining additional stop and seek after initial stop in one test to get the seek processed
// callback which ensures that all operations have been processed by the player.
Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.stop(false) .stop(false)
.stop(false) .stop(false)
.seek(0) // Wait until the player fully processed the second stop to see that no further
.waitForSeekProcessed() // callbacks are triggered.
.waitForPendingPlayerCommands()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder() new ExoPlayerTestRunner.Builder()
@ -1451,7 +1382,6 @@ public final class ExoPlayerTest {
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
} }
@Test @Test
@ -1493,7 +1423,7 @@ public final class ExoPlayerTest {
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE) .waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 50) .seek(/* positionMs= */ 50)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -1517,12 +1447,14 @@ public final class ExoPlayerTest {
.setTimeline(timeline) .setTimeline(timeline)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context); .build(context);
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); assertThrows(
fail(); ExoPlaybackException.class,
} catch (ExoPlaybackException e) { () ->
// Expected exception. testRunner
} .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS));
testRunner.assertTimelinesSame(dummyTimeline, timeline); testRunner.assertTimelinesSame(dummyTimeline, timeline);
testRunner.assertTimelineChangeReasonsEqual( testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
@ -1690,7 +1622,7 @@ public final class ExoPlayerTest {
} }
}) })
.seek(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET) .seek(/* windowIndex= */ 0, /* positionMs= */ C.TIME_UNSET)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -1717,12 +1649,14 @@ public final class ExoPlayerTest {
.setMediaSources(firstMediaSource) .setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule) .setActionSchedule(actionSchedule)
.build(context); .build(context);
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); assertThrows(
fail(); ExoPlaybackException.class,
} catch (ExoPlaybackException e) { () ->
// Expected exception. testRunner
} .start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS));
assertThat(positionHolder[0]).isAtLeast(500L); assertThat(positionHolder[0]).isAtLeast(500L);
assertThat(positionHolder[1]).isEqualTo(0L); assertThat(positionHolder[1]).isEqualTo(0L);
assertThat(positionHolder[2]).isEqualTo(0L); assertThat(positionHolder[2]).isEqualTo(0L);
@ -2459,11 +2393,12 @@ public final class ExoPlayerTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999) .seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.waitForSeekProcessed() // Wait after each seek until the internal player has updated its state.
.waitForPendingPlayerCommands()
.seek(/* windowIndex= */ 0, /* positionMs= */ 1) .seek(/* windowIndex= */ 0, /* positionMs= */ 1)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999) .seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.play() .play()
.build(); .build();
ExoPlayerTestRunner testRunner = ExoPlayerTestRunner testRunner =
@ -2526,7 +2461,7 @@ public final class ExoPlayerTest {
player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1000L); player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1000L);
} }
}) })
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -2795,7 +2730,7 @@ public final class ExoPlayerTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.seek(/* windowIndex= */ 1, /* positionMs= */ 0) .seek(/* windowIndex= */ 1, /* positionMs= */ 0)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.play() .play()
.build(); .build();
List<TrackGroupArray> trackGroupsList = new ArrayList<>(); List<TrackGroupArray> trackGroupsList = new ArrayList<>();
@ -2949,8 +2884,10 @@ public final class ExoPlayerTest {
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.pause() .pause()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
// Seek while unprepared and wait until the player handled all updates.
.seek(/* positionMs= */ 10) .seek(/* positionMs= */ 10)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
// Finish preparation.
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline))
.waitForTimelineChanged() .waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
@ -2995,7 +2932,6 @@ public final class ExoPlayerTest {
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
// Seek 10ms into the second period. // Seek 10ms into the second period.
.seek(/* positionMs= */ periodDurationMs + 10) .seek(/* positionMs= */ periodDurationMs + 10)
.waitForSeekProcessed()
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline)) .executeRunnable(() -> mediaSource.setNewSourceInfo(timeline))
.waitForTimelineChanged() .waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
@ -3369,7 +3305,6 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.seek(/* windowIndex= */ 1, /* positionMs= */ 5000) .seek(/* windowIndex= */ 1, /* positionMs= */ 5000)
.waitForSeekProcessed()
.waitForTimelineChanged( .waitForTimelineChanged(
/* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) /* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable( .executeRunnable(
@ -3882,7 +3817,6 @@ public final class ExoPlayerTest {
.executeRunnable( .executeRunnable(
new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts)) new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts))
.seek(/* windowIndex= */ 1, /* positionMs= */ 2000) .seek(/* windowIndex= */ 1, /* positionMs= */ 2000)
.waitForSeekProcessed()
.prepare() .prepare()
// The first expected buffering state arrives after prepare but not before. // The first expected buffering state arrives after prepare but not before.
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
@ -3956,7 +3890,6 @@ public final class ExoPlayerTest {
.addMediaSources(secondMediaSource) // add again to be able to test the seek .addMediaSources(secondMediaSource) // add again to be able to test the seek
.waitForTimelineChanged() .waitForTimelineChanged()
.seek(/* positionMs= */ 2_000) // seek must transition to buffering .seek(/* positionMs= */ 2_000) // seek must transition to buffering
.waitForSeekProcessed()
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
@ -4120,7 +4053,8 @@ public final class ExoPlayerTest {
int seekToWindowIndex = 1; int seekToWindowIndex = 1;
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() .waitForPlaybackState(Player.STATE_BUFFERING)
.waitForTimelineChanged()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4280,9 +4214,8 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING) .waitForPlaybackState(Player.STATE_BUFFERING)
// Do something and wait so that the player can create its unprepared MaskingMediaPeriod // Wait so that the player can create its unprepared MaskingMediaPeriod.
.seek(/* positionMs= */ 0) .waitForPendingPlayerCommands()
.waitForSeekProcessed()
// Let the player assign the unprepared period to window1. // Let the player assign the unprepared period to window1.
.executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window1, window2))) .executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window1, window2)))
.waitForTimelineChanged() .waitForTimelineChanged()
@ -4398,7 +4331,9 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4440,7 +4375,9 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4547,7 +4484,9 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4589,7 +4528,9 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4735,7 +4676,9 @@ public final class ExoPlayerTest {
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -4931,7 +4874,6 @@ public final class ExoPlayerTest {
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed()
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@ -5215,7 +5157,9 @@ public final class ExoPlayerTest {
Arrays.fill(currentWindowIndices, C.INDEX_UNSET); Arrays.fill(currentWindowIndices, C.INDEX_UNSET);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5262,7 +5206,9 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5324,7 +5270,6 @@ public final class ExoPlayerTest {
} }
}) })
.seek(/* windowIndex= */ 2, C.TIME_UNSET) .seek(/* windowIndex= */ 2, C.TIME_UNSET)
.waitForSeekProcessed()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5354,7 +5299,6 @@ public final class ExoPlayerTest {
} }
}) })
.seek(/* windowIndex= */ 0, C.TIME_UNSET) .seek(/* windowIndex= */ 0, C.TIME_UNSET)
.waitForSeekProcessed()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5425,7 +5369,7 @@ public final class ExoPlayerTest {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() .waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5458,7 +5402,7 @@ public final class ExoPlayerTest {
final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET}; final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() .waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5501,7 +5445,6 @@ public final class ExoPlayerTest {
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET); Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@ -5610,7 +5553,7 @@ public final class ExoPlayerTest {
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET) .seek(/* windowIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.waitForSeekProcessed() .waitForPendingPlayerCommands()
.removeMediaItem(/* index= */ 1) .removeMediaItem(/* index= */ 1)
.prepare() .prepare()
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)
@ -5637,7 +5580,7 @@ public final class ExoPlayerTest {
final int[] maskingPlaybackState = {C.INDEX_UNSET}; final int[] maskingPlaybackState = {C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() .waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override
@ -5671,7 +5614,9 @@ public final class ExoPlayerTest {
final int[] currentStates = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET}; final int[] currentStates = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule = ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.waitForSeekProcessed() // Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable( .executeRunnable(
new PlayerRunnable() { new PlayerRunnable() {
@Override @Override

View File

@ -319,7 +319,6 @@ public final class AnalyticsCollectorTest {
.waitForIsLoading(true) .waitForIsLoading(true)
.waitForIsLoading(false) .waitForIsLoading(false)
.seek(/* windowIndex= */ 1, /* positionMs= */ 0) .seek(/* windowIndex= */ 1, /* positionMs= */ 0)
.waitForSeekProcessed()
.play() .play()
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
@ -1088,7 +1087,7 @@ public final class AnalyticsCollectorTest {
midrollAd /* seek adjustment */, midrollAd /* seek adjustment */,
contentAfterMidroll /* ad transition */); contentAfterMidroll /* ad transition */);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(contentBeforeMidroll); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(contentBeforeMidroll);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(midrollAd); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(contentAfterMidroll);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly( .containsExactly(
contentBeforeMidroll, contentBeforeMidroll,

View File

@ -1024,12 +1024,12 @@ public abstract class Action {
} }
} }
/** Waits for {@link Player.EventListener#onSeekProcessed()}. */ /** Waits until the player acknowledged all pending player commands. */
public static final class WaitForSeekProcessed extends Action { public static final class WaitForPendingPlayerCommands extends Action {
/** @param tag A tag to use for logging. */ /** @param tag A tag to use for logging. */
public WaitForSeekProcessed(String tag) { public WaitForPendingPlayerCommands(String tag) {
super(tag, "WaitForSeekProcessed"); super(tag, "WaitForPendingPlayerCommands");
} }
@Override @Override
@ -1042,14 +1042,14 @@ public abstract class Action {
if (nextAction == null) { if (nextAction == null) {
return; return;
} }
player.addListener( // Send message to player that will arrive after all other pending commands. Thus, the message
new Player.EventListener() { // execution on the app thread will also happen after all other pending command
@Override // acknowledgements have arrived back on the app thread.
public void onSeekProcessed() { player
player.removeListener(this); .createMessage(
nextAction.schedule(player, trackSelector, surface, handler); (type, data) -> nextAction.schedule(player, trackSelector, surface, handler))
} .setHandler(Util.createHandler())
}); .send();
} }
@Override @Override

View File

@ -45,10 +45,10 @@ import com.google.android.exoplayer2.testutil.Action.Stop;
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading;
import com.google.android.exoplayer2.testutil.Action.WaitForMessage; import com.google.android.exoplayer2.testutil.Action.WaitForMessage;
import com.google.android.exoplayer2.testutil.Action.WaitForPendingPlayerCommands;
import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady;
import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState;
import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;
import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed;
import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -198,17 +198,19 @@ public final class ActionSchedule {
*/ */
public Builder seekAndWait(long positionMs) { public Builder seekAndWait(long positionMs) {
return apply(new Seek(tag, positionMs)) return apply(new Seek(tag, positionMs))
.apply(new WaitForSeekProcessed(tag))
.apply(new WaitForPlaybackState(tag, Player.STATE_READY)); .apply(new WaitForPlaybackState(tag, Player.STATE_READY));
} }
/** /**
* Schedules a delay until the player indicates that a seek has been processed. * Schedules a delay until all pending player commands have been handled.
*
* <p>A command is considered as having been handled if it arrived on the playback thread and
* the player acknowledged that it received the command back to the app thread.
* *
* @return The builder, for convenience. * @return The builder, for convenience.
*/ */
public Builder waitForSeekProcessed() { public Builder waitForPendingPlayerCommands() {
return apply(new WaitForSeekProcessed(tag)); return apply(new WaitForPendingPlayerCommands(tag));
} }
/** /**