Commit f46cb907 by tonihei Committed by Oliver Woodman

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
parent d8439878
...@@ -18,11 +18,12 @@ ...@@ -18,11 +18,12 @@
use this with `FfmpegAudioRenderer`. use this with `FfmpegAudioRenderer`.
* Support extraction and decoding of Dolby Atmos * Support extraction and decoding of Dolby Atmos
([#2465](https://github.com/google/ExoPlayer/issues/2465)). ([#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. initial preparation, reset and dynamic updates.
* DefaultTrackSelector: Support undefined language text track selection when the * DefaultTrackSelector: Support undefined language text track selection when the
preferred language is not available preferred language is not available
([#2980](https://github.com/google/ExoPlayer/issues/2980)). ([#2980](https://github.com/google/ExoPlayer/issues/2980)).
* Add optional parameter to `Player.stop` to reset the player when stopping.
### 2.6.0 ### ### 2.6.0 ###
......
...@@ -359,7 +359,13 @@ public final class CastPlayer implements Player { ...@@ -359,7 +359,13 @@ public final class CastPlayer implements Player {
@Override @Override
public void stop() { public void stop() {
stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
// TODO(b/69792021): Support or emulate stop without position reset.
remoteMediaClient.stop(); remoteMediaClient.stop();
} }
} }
......
...@@ -621,4 +621,158 @@ public final class ExoPlayerTest extends TestCase { ...@@ -621,4 +621,158 @@ public final class ExoPlayerTest extends TestCase {
new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS); .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();
}
} }
...@@ -55,7 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -55,7 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private int playbackState; private int playbackState;
private int pendingSeekAcks; private int pendingSeekAcks;
private int pendingPrepareAcks; private int pendingPrepareOrStopAcks;
private boolean waitingForInitialTimeline; private boolean waitingForInitialTimeline;
private boolean isLoading; private boolean isLoading;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
...@@ -134,35 +134,9 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -134,35 +134,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { 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; waitingForInitialTimeline = true;
pendingPrepareAcks++; pendingPrepareOrStopAcks++;
reset(resetPosition, resetState);
internalPlayer.prepare(mediaSource, resetPosition); internalPlayer.prepare(mediaSource, resetPosition);
} }
...@@ -286,7 +260,14 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -286,7 +260,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override @Override
public void stop() { public void stop() {
internalPlayer.stop(); stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
pendingPrepareOrStopAcks++;
reset(/* resetPosition= */ reset, /* resetState= */ reset);
internalPlayer.stop(reset);
} }
@Override @Override
...@@ -468,14 +449,14 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -468,14 +449,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
break; break;
} }
case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
int prepareAcks = msg.arg1; int prepareOrStopAcks = msg.arg1;
int seekAcks = msg.arg2; int seekAcks = msg.arg2;
handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL); /* ignored */ DISCONTINUITY_REASON_INTERNAL);
break; break;
} }
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
if (pendingPrepareAcks == 0) { if (pendingPrepareOrStopAcks == 0) {
TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
tracksSelected = true; tracksSelected = true;
trackGroups = trackSelectorResult.groups; trackGroups = trackSelectorResult.groups;
...@@ -520,12 +501,12 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -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) { boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) {
Assertions.checkNotNull(playbackInfo.timeline); Assertions.checkNotNull(playbackInfo.timeline);
pendingPrepareAcks -= prepareAcks; pendingPrepareOrStopAcks -= prepareOrStopAcks;
pendingSeekAcks -= seekAcks; pendingSeekAcks -= seekAcks;
if (pendingPrepareAcks == 0 && pendingSeekAcks == 0) { if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) {
boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline
|| this.playbackInfo.manifest != playbackInfo.manifest; || this.playbackInfo.manifest != playbackInfo.manifest;
this.playbackInfo = playbackInfo; this.playbackInfo = playbackInfo;
...@@ -556,6 +537,36 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -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) { private long playbackInfoPositionUsToWindowPositionMs(long positionUs) {
long positionMs = C.usToMs(positionUs); long positionMs = C.usToMs(positionUs);
if (!playbackInfo.periodId.isAd()) { if (!playbackInfo.periodId.isAd()) {
...@@ -566,7 +577,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -566,7 +577,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
private boolean shouldMaskPosition() { private boolean shouldMaskPosition() {
return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareAcks > 0; return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0;
} }
} }
...@@ -196,8 +196,8 @@ import java.io.IOException; ...@@ -196,8 +196,8 @@ import java.io.IOException;
handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
} }
public void stop() { public void stop(boolean reset) {
handler.sendEmptyMessage(MSG_STOP); handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
} }
public void sendMessages(ExoPlayerMessage... messages) { public void sendMessages(ExoPlayerMessage... messages) {
...@@ -324,7 +324,7 @@ import java.io.IOException; ...@@ -324,7 +324,7 @@ import java.io.IOException;
return true; return true;
} }
case MSG_STOP: { case MSG_STOP: {
stopInternal(); stopInternal(/* reset= */ msg.arg1 != 0);
return true; return true;
} }
case MSG_RELEASE: { case MSG_RELEASE: {
...@@ -357,18 +357,18 @@ import java.io.IOException; ...@@ -357,18 +357,18 @@ import java.io.IOException;
} catch (ExoPlaybackException e) { } catch (ExoPlaybackException e) {
Log.e(TAG, "Renderer error.", e); Log.e(TAG, "Renderer error.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal(); stopInternal(/* reset= */ false);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Source error.", e); Log.e(TAG, "Source error.", e);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
stopInternal(); stopInternal(/* reset= */ false);
return true; return true;
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e); Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
.sendToTarget(); .sendToTarget();
stopInternal(); stopInternal(/* reset= */ false);
return true; return true;
} }
} }
...@@ -394,8 +394,8 @@ import java.io.IOException; ...@@ -394,8 +394,8 @@ import java.io.IOException;
resetInternal(/* releaseMediaSource= */ true, resetPosition); resetInternal(/* releaseMediaSource= */ true, resetPosition);
loadControl.onPrepared(); loadControl.onPrepared();
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this);
setState(Player.STATE_BUFFERING); setState(Player.STATE_BUFFERING);
mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this);
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} }
...@@ -765,8 +765,23 @@ import java.io.IOException; ...@@ -765,8 +765,23 @@ import java.io.IOException;
mediaClock.setPlaybackParameters(playbackParameters); mediaClock.setPlaybackParameters(playbackParameters);
} }
private void stopInternal() { private void stopInternal(boolean reset) {
resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); // 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(); loadControl.onStopped();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
} }
...@@ -1170,13 +1185,14 @@ import java.io.IOException; ...@@ -1170,13 +1185,14 @@ import java.io.IOException;
notifySourceInfoRefresh(0, 0); notifySourceInfoRefresh(0, 0);
} }
private void notifySourceInfoRefresh(int prepareAcks, int seekAcks) { private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) {
notifySourceInfoRefresh(prepareAcks, seekAcks, playbackInfo); notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo);
} }
private void notifySourceInfoRefresh(int prepareAcks, int seekAcks, PlaybackInfo playbackInfo) { private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks,
eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareAcks, seekAcks, playbackInfo) PlaybackInfo playbackInfo) {
.sendToTarget(); eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks,
playbackInfo).sendToTarget();
} }
/** /**
......
...@@ -429,18 +429,30 @@ public interface Player { ...@@ -429,18 +429,30 @@ public interface Player {
PlaybackParameters getPlaybackParameters(); PlaybackParameters getPlaybackParameters();
/** /**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than
* is to pause playback. * this method if the intention is to pause playback.
* <p> *
* Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * <p>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 * player instance can still be used, and {@link #release()} must still be called on the player if
* it's no longer required. * it's no longer required.
* <p> *
* Calling this method does not reset the playback position. * <p>Calling this method does not reset the playback position.
*/ */
void stop(); void stop();
/** /**
* Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather
* than this method if the intention is to pause playback.
*
* <p>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 * 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. * player must not be used after calling this method.
*/ */
......
...@@ -133,6 +133,9 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -133,6 +133,9 @@ public class SimpleExoPlayer implements ExoPlayer {
case C.TRACK_TYPE_AUDIO: case C.TRACK_TYPE_AUDIO:
audioRendererCount++; audioRendererCount++;
break; break;
default:
// Don't count other track types.
break;
} }
} }
this.videoRendererCount = videoRendererCount; this.videoRendererCount = videoRendererCount;
...@@ -693,6 +696,11 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -693,6 +696,11 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public void stop(boolean reset) {
player.stop(reset);
}
@Override
public void release() { public void release() {
player.release(); player.release();
removeSurfaceCallbacks(); removeSurfaceCallbacks();
......
...@@ -89,45 +89,89 @@ public abstract class Action { ...@@ -89,45 +89,89 @@ public abstract class Action {
Surface surface); 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 { public static final class Seek extends Action {
private final Integer windowIndex;
private final long positionMs; private final long positionMs;
/** /**
* Action calls {@link Player#seekTo(long)}.
*
* @param tag A tag to use for logging. * @param tag A tag to use for logging.
* @param positionMs The seek position. * @param positionMs The seek position.
*/ */
public Seek(String tag, long positionMs) { public Seek(String tag, long positionMs) {
super(tag, "Seek:" + 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; this.positionMs = positionMs;
} }
@Override @Override
protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector,
Surface surface) { 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 { 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. * @param tag A tag to use for logging.
*/ */
public Stop(String tag) { 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 @Override
protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector,
Surface surface) { Surface surface) {
player.stop(); if (reset == null) {
player.stop();
} else {
player.stop(reset);
}
} }
} }
......
...@@ -161,6 +161,17 @@ public final class ActionSchedule { ...@@ -161,6 +161,17 @@ public final class ActionSchedule {
} }
/** /**
* 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. * Schedules a seek action to be executed and waits until playback resumes after the seek.
* *
* @param positionMs The seek position. * @param positionMs The seek position.
...@@ -193,6 +204,16 @@ public final class ActionSchedule { ...@@ -193,6 +204,16 @@ public final class ActionSchedule {
} }
/** /**
* 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. * Schedules a play action to be executed.
* *
* @return The builder, for convenience. * @return The builder, for convenience.
......
...@@ -166,13 +166,18 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { ...@@ -166,13 +166,18 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer {
@Override @Override
public void stop() { public void stop() {
stop(/* quitPlaybackThread= */ false); stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
stopPlayback(/* quitPlaybackThread= */ false);
} }
@Override @Override
@SuppressWarnings("ThreadJoinLoop") @SuppressWarnings("ThreadJoinLoop")
public void release() { public void release() {
stop(/* quitPlaybackThread= */ true); stopPlayback(/* quitPlaybackThread= */ true);
while (playbackThread.isAlive()) { while (playbackThread.isAlive()) {
try { try {
playbackThread.join(); playbackThread.join();
...@@ -513,7 +518,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { ...@@ -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() { playbackHandler.post(new Runnable() {
@Override @Override
public void run () { public void run () {
......
...@@ -131,6 +131,11 @@ public abstract class StubExoPlayer implements ExoPlayer { ...@@ -131,6 +131,11 @@ public abstract class StubExoPlayer implements ExoPlayer {
} }
@Override @Override
public void stop(boolean resetStateAndPosition) {
throw new UnsupportedOperationException();
}
@Override
public void release() { public void release() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment