Fix some EventListener logic in ExoPlayerImpl

We currently have multiple places in ExoPlayerImpl that assign PlaybackInfo
instances and then inform listeners of all current changes. This is not ideal
because it causes multiple issues:
 1. Some changes may easily be forgotten, e.g. there are clearly some checks
    missing to see if isPlaying changed (e.g. in seekTo or setMediaSources)
 2. Some callbacks didn't check if the value actually changed before sending
    the callback (e.g. for the timeline change in setMediaSources - if the
    timeline is still the same, we shouldn't send a onTimelineChanged event).
 3. Having multiple callbacks in a single Runnable changes the order of
    listener invocations slightly: Currently all events for one listener will
    be send first before moving to the next listener. It should however send
    a single event to all listeners first before moving to the next event.

All these issues can be solved by always using updatePlaybackInfo and never
assigning playbackInfo directly in another place.

Some tests needed to be updated as well because of issues (2) and (3). Also
added a new test to cover issue (1).

PiperOrigin-RevId: 302844981
This commit is contained in:
tonihei 2020-03-25 09:13:04 +00:00 committed by Oliver Woodman
parent c3cbccbbf8
commit 46a10ec01a
2 changed files with 180 additions and 114 deletions

View File

@ -432,11 +432,16 @@ import java.util.concurrent.TimeoutException;
Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++;
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
Timeline timeline =
PlaybackInfo playbackInfo =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
internalPlayer.addMediaSources(index, holders, shuffleOrder);
notifyListeners(
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
@Override
@ -469,11 +474,16 @@ import java.util.concurrent.TimeoutException;
pendingOperationAcks++;
newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex));
Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
Timeline timeline =
PlaybackInfo playbackInfo =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder);
notifyListeners(
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
@Override
@ -486,13 +496,18 @@ import java.util.concurrent.TimeoutException;
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
Timeline timeline = maskTimeline();
PlaybackInfo playbackInfo = maskTimeline();
maskWithCurrentPosition();
pendingOperationAcks++;
this.shuffleOrder = shuffleOrder;
internalPlayer.setShuffleOrder(shuffleOrder);
notifyListeners(
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
@Override
@ -517,38 +532,26 @@ import java.util.concurrent.TimeoutException;
return pauseAtEndOfMediaItems;
}
@SuppressWarnings("deprecation")
public void setPlayWhenReady(
boolean playWhenReady,
@PlaybackSuppressionReason int playbackSuppressionReason,
@PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
boolean oldIsPlaying = isPlaying();
boolean playWhenReadyChanged = playbackInfo.playWhenReady != playWhenReady;
boolean suppressionReasonChanged =
playbackInfo.playbackSuppressionReason != playbackSuppressionReason;
if (!playWhenReadyChanged && !suppressionReasonChanged) {
if (playbackInfo.playWhenReady == playWhenReady
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
return;
}
maskWithCurrentPosition();
pendingOperationAcks++;
playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
PlaybackInfo playbackInfo =
this.playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
boolean isPlaying = isPlaying();
boolean isPlayingChanged = oldIsPlaying != isPlaying;
int playbackState = playbackInfo.playbackState;
notifyListeners(
listener -> {
if (playWhenReadyChanged) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
}
if (suppressionReasonChanged) {
listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
}
if (isPlayingChanged) {
listener.onIsPlayingChanged(isPlaying);
}
});
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
playWhenReadyChangeReason,
/* seekProcessed= */ false);
}
@Override
@ -589,7 +592,6 @@ import java.util.concurrent.TimeoutException;
return playbackInfo.isLoading;
}
@SuppressWarnings("deprecation")
@Override
public void seekTo(int windowIndex, long positionMs) {
Timeline timeline = playbackInfo.timeline;
@ -597,8 +599,6 @@ import java.util.concurrent.TimeoutException;
throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
}
hasPendingSeek = true;
boolean playWhenReady = getPlayWhenReady();
@Player.State int playbackState = getPlaybackState();
pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
@ -617,18 +617,16 @@ import java.util.concurrent.TimeoutException;
maskWindowIndexAndPositionForSeek(timeline, windowIndex, positionMs);
@Player.State
int newPlaybackState =
playbackState == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
boolean playbackStateChanged = playbackState != newPlaybackState;
playbackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState);
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
notifyListeners(
listener -> {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK);
if (playbackStateChanged) {
listener.onPlayerStateChanged(playWhenReady, newPlaybackState);
listener.onPlaybackStateChanged(newPlaybackState);
}
});
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@ -1011,7 +1009,6 @@ import java.util.concurrent.TimeoutException;
seekProcessed));
}
@SuppressWarnings("deprecation")
private void setMediaSourcesInternal(
List<MediaSource> mediaItems,
int startWindowIndex,
@ -1019,14 +1016,14 @@ import java.util.concurrent.TimeoutException;
boolean resetToDefaultPosition) {
int currentWindowIndex = getCurrentWindowIndexInternal();
long currentPositionMs = getCurrentPosition();
boolean currentPlayWhenReady = getPlayWhenReady();
pendingOperationAcks++;
if (!mediaSourceHolders.isEmpty()) {
removeMediaSourceHolders(
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
}
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(/* index= */ 0, mediaItems);
Timeline timeline = maskTimeline();
PlaybackInfo playbackInfo = maskTimeline();
Timeline timeline = playbackInfo.timeline;
if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) {
throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs);
}
@ -1040,9 +1037,9 @@ import java.util.concurrent.TimeoutException;
}
maskWindowIndexAndPositionForSeek(
timeline, startWindowIndex == C.INDEX_UNSET ? 0 : startWindowIndex, startPositionMs);
// mask the playback state
// Mask the playback state.
int maskingPlaybackState = playbackInfo.playbackState;
if (startWindowIndex != C.INDEX_UNSET) {
if (startWindowIndex != C.INDEX_UNSET && playbackInfo.playbackState != STATE_IDLE) {
// Position reset to startWindowIndex (results in pending initial seek).
if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) {
// Setting an empty timeline or invalid seek transitions to ended.
@ -1051,23 +1048,16 @@ import java.util.concurrent.TimeoutException;
maskingPlaybackState = STATE_BUFFERING;
}
}
boolean playbackStateChanged =
playbackInfo.playbackState != STATE_IDLE
&& playbackInfo.playbackState != maskingPlaybackState;
int finalMaskingPlaybackState = maskingPlaybackState;
if (playbackStateChanged) {
playbackInfo = playbackInfo.copyWithPlaybackState(finalMaskingPlaybackState);
}
playbackInfo = playbackInfo.copyWithPlaybackState(maskingPlaybackState);
internalPlayer.setMediaSources(
holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder);
notifyListeners(
listener -> {
listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
if (playbackStateChanged) {
listener.onPlayerStateChanged(currentPlayWhenReady, finalMaskingPlaybackState);
listener.onPlaybackStateChanged(finalMaskingPlaybackState);
}
});
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ Player.DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
private List<Playlist.MediaSourceHolder> addMediaSourceHolders(
@ -1085,18 +1075,16 @@ import java.util.concurrent.TimeoutException;
return holders;
}
@SuppressWarnings("deprecation")
private void removeMediaItemsInternal(int fromIndex, int toIndex) {
Assertions.checkArgument(
fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size());
int currentWindowIndex = getCurrentWindowIndex();
long currentPositionMs = getCurrentPosition();
boolean currentPlayWhenReady = getPlayWhenReady();
Timeline oldTimeline = getCurrentTimeline();
int currentMediaSourceCount = mediaSourceHolders.size();
pendingOperationAcks++;
removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex);
Timeline timeline =
PlaybackInfo playbackInfo =
maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline);
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
final boolean transitionsToEnded =
@ -1104,19 +1092,18 @@ import java.util.concurrent.TimeoutException;
&& playbackInfo.playbackState != STATE_ENDED
&& fromIndex < toIndex
&& toIndex == currentMediaSourceCount
&& currentWindowIndex >= timeline.getWindowCount();
&& currentWindowIndex >= playbackInfo.timeline.getWindowCount();
if (transitionsToEnded) {
playbackInfo = playbackInfo.copyWithPlaybackState(STATE_ENDED);
}
internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder);
notifyListeners(
listener -> {
listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
if (transitionsToEnded) {
listener.onPlayerStateChanged(currentPlayWhenReady, STATE_ENDED);
listener.onPlaybackStateChanged(STATE_ENDED);
}
});
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ Player.DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
}
private List<Playlist.MediaSourceHolder> removeMediaSourceHolders(
@ -1129,18 +1116,17 @@ import java.util.concurrent.TimeoutException;
return removed;
}
private Timeline maskTimeline() {
playbackInfo =
playbackInfo.copyWithTimeline(
private PlaybackInfo maskTimeline() {
return playbackInfo.copyWithTimeline(
mediaSourceHolders.isEmpty()
? Timeline.EMPTY
: new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder));
return playbackInfo.timeline;
}
private Timeline maskTimelineAndWindowIndex(
private PlaybackInfo maskTimelineAndWindowIndex(
int currentWindowIndex, long currentPositionMs, Timeline oldTimeline) {
Timeline maskingTimeline = maskTimeline();
PlaybackInfo playbackInfo = maskTimeline();
Timeline maskingTimeline = playbackInfo.timeline;
if (oldTimeline.isEmpty()) {
// The index is the default index or was set by a seek in the empty old timeline.
maskingWindowIndex = currentWindowIndex;
@ -1148,7 +1134,7 @@ import java.util.concurrent.TimeoutException;
// The seek is not valid in the new timeline.
maskWithDefaultPosition(maskingTimeline);
}
return maskingTimeline;
return playbackInfo;
}
@Nullable
Pair<Object, Long> periodPosition =
@ -1186,7 +1172,7 @@ import java.util.concurrent.TimeoutException;
maskWithDefaultPosition(maskingTimeline);
}
}
return maskingTimeline;
return playbackInfo;
}
private void maskWindowIndexAndPositionForSeek(

View File

@ -3391,7 +3391,6 @@ public final class ExoPlayerTest {
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertArrayEquals(new long[] {2}, windowCounts);
assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices);
@ -4157,7 +4156,7 @@ public final class ExoPlayerTest {
int seekToWindowIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
@ -4168,12 +4167,11 @@ public final class ExoPlayerTest {
}
})
.executeRunnable(
() -> {
concatenatingMediaSource.addMediaSource(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT));
concatenatingMediaSource.addMediaSource(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT));
})
() ->
concatenatingMediaSource.addMediaSources(
Arrays.asList(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT))))
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@ -4203,7 +4201,7 @@ public final class ExoPlayerTest {
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ false);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG).waitForTimelineChanged().build();
new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED).build();
ExoPlayerTestRunner exoPlayerTestRunner =
new Builder()
.setMediaSources(concatenatingMediaSource)
@ -4229,7 +4227,7 @@ public final class ExoPlayerTest {
int seekToWindowIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
@ -4240,12 +4238,11 @@ public final class ExoPlayerTest {
}
})
.executeRunnable(
() -> {
concatenatingMediaSource.addMediaSource(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT));
concatenatingMediaSource.addMediaSource(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT));
})
() ->
concatenatingMediaSource.addMediaSources(
Arrays.asList(
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, Builder.VIDEO_FORMAT))))
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@ -4727,7 +4724,6 @@ public final class ExoPlayerTest {
new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE},
maskingPlaybackStates);
exoPlayerTestRunner.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);
@ -4768,7 +4764,6 @@ public final class ExoPlayerTest {
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@ -4841,7 +4836,6 @@ public final class ExoPlayerTest {
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@ -4924,9 +4918,6 @@ public final class ExoPlayerTest {
},
maskingPlaybackStates);
exoPlayerTestRunner.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,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
@ -4969,7 +4960,6 @@ public final class ExoPlayerTest {
exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@ -5050,7 +5040,6 @@ public final class ExoPlayerTest {
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}
@ -5213,7 +5202,6 @@ public final class ExoPlayerTest {
Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
@ -6161,6 +6149,7 @@ public final class ExoPlayerTest {
public void loading_withLargeAllocationCausingOom_playsRemainingMediaAndThenThrows() {
Loader.Loadable loadable =
new Loader.Loadable() {
@SuppressWarnings("UnusedVariable")
@Override
public void load() throws IOException {
@SuppressWarnings("unused") // This test needs the allocation to cause an OOM.
@ -6229,6 +6218,97 @@ public final class ExoPlayerTest {
assertThat(renderer.sampleBufferReadCount).isEqualTo(3);
}
@Test
public void seekTo_whileReady_callsOnIsPlayingChanged() throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.seek(/* positionMs= */ 0)
.waitForPlaybackState(Player.STATE_ENDED)
.build();
List<Boolean> onIsPlayingChanges = new ArrayList<>();
Player.EventListener eventListener =
new Player.EventListener() {
@Override
public void onIsPlayingChanged(boolean isPlaying) {
onIsPlayingChanges.add(isPlaying);
}
};
new ExoPlayerTestRunner.Builder()
.setEventListener(eventListener)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(onIsPlayingChanges).containsExactly(true, false, true, false).inOrder();
}
@Test
public void multipleListenersAndMultipleCallbacks_callbacksAreOrderedByType() throws Exception {
String playWhenReadyChange1 = "playWhenReadyChange1";
String playWhenReadyChange2 = "playWhenReadyChange2";
String isPlayingChange1 = "isPlayingChange1";
String isPlayingChange2 = "isPlayingChange2";
ArrayList<String> events = new ArrayList<>();
Player.EventListener eventListener1 =
new Player.EventListener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
events.add(playWhenReadyChange1);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.add(isPlayingChange1);
}
};
Player.EventListener eventListener2 =
new Player.EventListener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
events.add(playWhenReadyChange2);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.add(isPlayingChange2);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.addListener(eventListener1);
player.addListener(eventListener2);
}
})
.waitForPlaybackState(Player.STATE_READY)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
new ExoPlayerTestRunner.Builder()
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(events)
.containsExactly(
playWhenReadyChange1,
playWhenReadyChange2,
isPlayingChange1,
isPlayingChange2,
isPlayingChange1,
isPlayingChange2)
.inOrder();
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {