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();