From f46cb907b7f88eb753497ff4bb92ceb694025329 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 01:53:15 -0800 Subject: [PATCH] Add stop with position reset to Player interface. The ExoPlayerImpl implementation forwards the stop request with this optional parameter. To ensure correct masking (e.g. when timeline updates arrive after calling reset in ExoPlayerImpl but before resetInternal in ExoPlayerImplInternal), we use the existing prepareAck counter and extend it also count stop operations. For this to work, we also return the updated empty timeline after finishing the reset. The CastPlayer doesn't support the two reset options so far. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177132107 --- RELEASENOTES.md | 3 +- .../exoplayer2/ext/cast/CastPlayer.java | 6 + .../android/exoplayer2/ExoPlayerTest.java | 154 ++++++++++++++++++ .../android/exoplayer2/ExoPlayerImpl.java | 85 +++++----- .../exoplayer2/ExoPlayerImplInternal.java | 44 +++-- .../com/google/android/exoplayer2/Player.java | 24 ++- .../android/exoplayer2/SimpleExoPlayer.java | 8 + .../android/exoplayer2/testutil/Action.java | 54 +++++- .../exoplayer2/testutil/ActionSchedule.java | 21 +++ .../testutil/FakeSimpleExoPlayer.java | 11 +- .../exoplayer2/testutil/StubExoPlayer.java | 5 + 11 files changed, 349 insertions(+), 66 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dd4a6ce655..2c07ad6118 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,11 +18,12 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Added a reason to `EventListener.onTimelineChanged` to distinguish between +* Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add optional parameter to `Player.stop` to reset the player when stopping. ### 2.6.0 ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 32e064e834..92e36c7f2d 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -359,7 +359,13 @@ public final class CastPlayer implements Player { @Override public void stop() { + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { if (remoteMediaClient != null) { + // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 27e4a97ac5..4c5ac1ac0f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -621,4 +621,158 @@ public final class ExoPlayerTest extends TestCase { new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); } + + public void testStopDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopDoesNotResetPosition") + .waitForPlaybackState(Player.STATE_READY) + .stop() + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithoutResetDoesNotReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithResetDoesResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithResetDoesReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testStopWithResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + // If we were still using the first timeline, this would throw. + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + testRunner.assertPlayedPeriodIndices(0, 1); + } + + public void testStopDuringPreparationOverwritesPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .stop(true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + } 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 77131f5ded..34dffd0e73 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 @@ -55,7 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean shuffleModeEnabled; private int playbackState; private int pendingSeekAcks; - private int pendingPrepareAcks; + private int pendingPrepareOrStopAcks; private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; @@ -134,35 +134,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (!resetPosition) { - maskingWindowIndex = getCurrentWindowIndex(); - maskingPeriodIndex = getCurrentPeriodIndex(); - maskingWindowPositionMs = getCurrentPosition(); - } else { - maskingWindowIndex = 0; - maskingPeriodIndex = 0; - maskingWindowPositionMs = 0; - } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } - } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - } waitingForInitialTimeline = true; - pendingPrepareAcks++; + pendingPrepareOrStopAcks++; + reset(resetPosition, resetState); internalPlayer.prepare(mediaSource, resetPosition); } @@ -286,7 +260,14 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop() { - internalPlayer.stop(); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + pendingPrepareOrStopAcks++; + reset(/* resetPosition= */ reset, /* resetState= */ reset); + internalPlayer.stop(reset); } @Override @@ -468,14 +449,14 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareAcks = msg.arg1; + int prepareOrStopAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false, /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareAcks == 0) { + if (pendingPrepareOrStopAcks == 0) { TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; tracksSelected = true; trackGroups = trackSelectorResult.groups; @@ -520,12 +501,12 @@ import java.util.concurrent.CopyOnWriteArraySet; } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, + private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); - pendingPrepareAcks -= prepareAcks; + pendingPrepareOrStopAcks -= prepareOrStopAcks; pendingSeekAcks -= seekAcks; - if (pendingPrepareAcks == 0 && pendingSeekAcks == 0) { + if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; @@ -556,6 +537,36 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private void reset(boolean resetPosition, boolean resetState) { + if (resetPosition) { + maskingWindowIndex = 0; + maskingPeriodIndex = 0; + maskingWindowPositionMs = 0; + } else { + maskingWindowIndex = getCurrentWindowIndex(); + maskingPeriodIndex = getCurrentPeriodIndex(); + maskingWindowPositionMs = getCurrentPosition(); + } + if (resetState) { + if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { + playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); + } + } + if (tracksSelected) { + tracksSelected = false; + trackGroups = TrackGroupArray.EMPTY; + trackSelections = emptyTrackSelections; + trackSelector.onSelectionActivated(null); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } + } + } + } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { long positionMs = C.usToMs(positionUs); if (!playbackInfo.periodId.isAd()) { @@ -566,7 +577,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; } } 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 4e37211e80..f62d36e48b 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 @@ -196,8 +196,8 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } - public void stop() { - handler.sendEmptyMessage(MSG_STOP); + public void stop(boolean reset) { + handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } public void sendMessages(ExoPlayerMessage... messages) { @@ -324,7 +324,7 @@ import java.io.IOException; return true; } case MSG_STOP: { - stopInternal(); + stopInternal(/* reset= */ msg.arg1 != 0); return true; } case MSG_RELEASE: { @@ -357,18 +357,18 @@ import java.io.IOException; } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } } @@ -394,8 +394,8 @@ import java.io.IOException; resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); this.mediaSource = mediaSource; - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -765,8 +765,23 @@ import java.io.IOException; mediaClock.setPlaybackParameters(playbackParameters); } - private void stopInternal() { - resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); + private void stopInternal(boolean reset) { + // Releasing the internal player sets the timeline to null. Use the current timeline or + // Timeline.EMPTY for notifying the eventHandler. + Timeline publicTimeline = reset || playbackInfo.timeline == null + ? Timeline.EMPTY : playbackInfo.timeline; + Object publicManifest = reset ? null : playbackInfo.manifest; + resetInternal(/* releaseMediaSource= */ true, reset); + PlaybackInfo publicPlaybackInfo = playbackInfo.copyWithTimeline(publicTimeline, publicManifest); + if (reset) { + // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for + // notifying the eventHandler. + publicPlaybackInfo = + publicPlaybackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET); + } + int prepareOrStopAcks = pendingPrepareCount + 1; + pendingPrepareCount = 0; + notifySourceInfoRefresh(prepareOrStopAcks, 0, publicPlaybackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1170,13 +1185,14 @@ import java.io.IOException; notifySourceInfoRefresh(0, 0); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks) { - notifySourceInfoRefresh(prepareAcks, seekAcks, playbackInfo); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) { + notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks, PlaybackInfo playbackInfo) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareAcks, seekAcks, playbackInfo) - .sendToTarget(); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks, + PlaybackInfo playbackInfo) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks, + playbackInfo).sendToTarget(); } /** 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 a036a2021d..b3ae4c28c6 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 @@ -429,17 +429,29 @@ public interface Player { PlaybackParameters getPlaybackParameters(); /** - * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention - * is to pause playback. - *

- * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than + * this method if the intention is to pause playback. + * + *

Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * player instance can still be used, and {@link #release()} must still be called on the player if * it's no longer required. - *

- * Calling this method does not reset the playback position. + * + *

Calling this method does not reset the playback position. */ void stop(); + /** + * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather + * than this method if the intention is to pause playback. + * + *

Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * player instance can still be used, and {@link #release()} must still be called on the player if + * it's no longer required. + * + * @param reset Whether the player should be reset. + */ + void stop(boolean reset); + /** * Releases the player. This method must be called when the player is no longer required. The * player must not be used after calling this method. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5a5a948d58..a153e4ed43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -133,6 +133,9 @@ public class SimpleExoPlayer implements ExoPlayer { case C.TRACK_TYPE_AUDIO: audioRendererCount++; break; + default: + // Don't count other track types. + break; } } this.videoRendererCount = videoRendererCount; @@ -692,6 +695,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.stop(); } + @Override + public void stop(boolean reset) { + player.stop(reset); + } + @Override public void release() { player.release(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 003d08cd59..ff0b8a6bc0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -89,45 +89,89 @@ public abstract class Action { Surface surface); /** - * Calls {@link Player#seekTo(long)}. + * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. */ public static final class Seek extends Action { + private final Integer windowIndex; private final long positionMs; /** + * Action calls {@link Player#seekTo(long)}. + * * @param tag A tag to use for logging. * @param positionMs The seek position. */ public Seek(String tag, long positionMs) { super(tag, "Seek:" + positionMs); + this.windowIndex = null; + this.positionMs = positionMs; + } + + /** + * Action calls {@link Player#seekTo(int, long)}. + * + * @param tag A tag to use for logging. + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + */ + public Seek(String tag, int windowIndex, long positionMs) { + super(tag, "Seek:" + positionMs); + this.windowIndex = windowIndex; this.positionMs = positionMs; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.seekTo(positionMs); + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); + } } } /** - * Calls {@link Player#stop()}. + * Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { + private static final String STOP_ACTION_TAG = "Stop"; + + private final Boolean reset; + /** + * Action will call {@link Player#stop()}. + * * @param tag A tag to use for logging. */ public Stop(String tag) { - super(tag, "Stop"); + super(tag, STOP_ACTION_TAG); + this.reset = null; + } + + /** + * Action will call {@link Player#stop(boolean)}. + * + * @param tag A tag to use for logging. + * @param reset The value to pass to {@link Player#stop(boolean)}. + */ + public Stop(String tag, boolean reset) { + super(tag, STOP_ACTION_TAG); + this.reset = reset; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.stop(); + if (reset == null) { + player.stop(); + } else { + player.stop(reset); + } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 5e3d6bcb9a..abca2cafdb 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -160,6 +160,17 @@ public final class ActionSchedule { return apply(new Seek(tag, positionMs)); } + /** + * Schedules a seek action to be executed. + * + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + * @return The builder, for convenience. + */ + public Builder seek(int windowIndex, long positionMs) { + return apply(new Seek(tag, windowIndex, positionMs)); + } + /** * Schedules a seek action to be executed and waits until playback resumes after the seek. * @@ -192,6 +203,16 @@ public final class ActionSchedule { return apply(new Stop(tag)); } + /** + * Schedules a stop action to be executed. + * + * @param reset Whether the player should be reset. + * @return The builder, for convenience. + */ + public Builder stop(boolean reset) { + return apply(new Stop(tag, reset)); + } + /** * Schedules a play action to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 58f19ace1e..0358e5d980 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,13 +166,18 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - stop(/* quitPlaybackThread= */ false); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + stopPlayback(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(/* quitPlaybackThread= */ true); + stopPlayback(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -513,7 +518,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(final boolean quitPlaybackThread) { + private void stopPlayback(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index e03f6fbad9..0d94b8fa03 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -130,6 +130,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void stop(boolean resetStateAndPosition) { + throw new UnsupportedOperationException(); + } + @Override public void release() { throw new UnsupportedOperationException();