Commit 82599960 by tonihei Committed by Oliver Woodman

Add public API for pauseAtEndOfMediaItem

Also adds tests covering the internal implementation.

Issue:#5660
PiperOrigin-RevId: 300513548
parent 527563da
...@@ -25,6 +25,9 @@ ...@@ -25,6 +25,9 @@
consistency. consistency.
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for * Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
consistency. consistency.
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the
end of each media item
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
* Make `MediaSourceEventListener.LoadEventInfo` and * Make `MediaSourceEventListener.LoadEventInfo` and
`MediaSourceEventListener.MediaLoadData` top-level classes. `MediaSourceEventListener.MediaLoadData` top-level classes.
* Rename `MediaCodecRenderer.onOutputFormatChanged` to * Rename `MediaCodecRenderer.onOutputFormatChanged` to
......
...@@ -557,4 +557,23 @@ public interface ExoPlayer extends Player { ...@@ -557,4 +557,23 @@ public interface ExoPlayer extends Player {
* idle state. * idle state.
*/ */
void setForegroundMode(boolean foregroundMode); void setForegroundMode(boolean foregroundMode);
/**
* Sets whether to pause playback at the end of each media item.
*
* <p>This means the player will pause at the end of each window in the current {@link
* #getCurrentTimeline() timeline}. Listeners will be informed by a call to {@link
* Player.EventListener#onPlayWhenReadyChanged(boolean, int)} with the reason {@link
* Player#PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM} when this happens.
*
* @param pauseAtEndOfMediaItems Whether to pause playback at the end of each media item.
*/
void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems);
/**
* Returns whether the player pauses playback at the end of each media item.
*
* @see #setPauseAtEndOfMediaItems(boolean)
*/
boolean getPauseAtEndOfMediaItems();
} }
...@@ -82,6 +82,7 @@ import java.util.concurrent.TimeoutException; ...@@ -82,6 +82,7 @@ import java.util.concurrent.TimeoutException;
private float playbackSpeed; private float playbackSpeed;
private SeekParameters seekParameters; private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems;
// Playback information when there is no pending seek/set source operation. // Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo; private PlaybackInfo playbackInfo;
...@@ -436,6 +437,20 @@ import java.util.concurrent.TimeoutException; ...@@ -436,6 +437,20 @@ import java.util.concurrent.TimeoutException;
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
} }
@Override
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) {
return;
}
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
internalPlayer.setPauseAtEndOfWindow(pauseAtEndOfMediaItems);
}
@Override
public boolean getPauseAtEndOfMediaItems() {
return pauseAtEndOfMediaItems;
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void setPlayWhenReady( public void setPlayWhenReady(
boolean playWhenReady, boolean playWhenReady,
......
...@@ -845,12 +845,11 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -845,12 +845,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|| playingPeriodDurationUs <= playbackInfo.positionUs); || playingPeriodDurationUs <= playbackInfo.positionUs);
if (finishedRendering && pendingPauseAtEndOfPeriod) { if (finishedRendering && pendingPauseAtEndOfPeriod) {
pendingPauseAtEndOfPeriod = false; pendingPauseAtEndOfPeriod = false;
// TODO: Add new change reason for timed pause requests.
setPlayWhenReadyInternal( setPlayWhenReadyInternal(
/* playWhenReady= */ false, /* playWhenReady= */ false,
playbackInfo.playbackSuppressionReason, playbackInfo.playbackSuppressionReason,
/* operationAck= */ false, /* operationAck= */ false,
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM);
} }
if (finishedRendering && playingPeriodHolder.info.isFinal) { if (finishedRendering && playingPeriodHolder.info.isFinal) {
setState(Player.STATE_ENDED); setState(Player.STATE_ENDED);
...@@ -1548,7 +1547,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1548,7 +1547,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodHolder.prepared return playingPeriodHolder.prepared
&& (playingPeriodDurationUs == C.TIME_UNSET && (playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs); || playbackInfo.positionUs < playingPeriodDurationUs
|| !shouldPlayWhenReady());
} }
private void maybeThrowSourceInfoRefreshError() throws IOException { private void maybeThrowSourceInfoRefreshError() throws IOException {
......
...@@ -603,8 +603,9 @@ public interface Player { ...@@ -603,8 +603,9 @@ public interface Player {
* Reasons for {@link #getPlayWhenReady() playWhenReady} changes. One of {@link * Reasons for {@link #getPlayWhenReady() playWhenReady} changes. One of {@link
* #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link * #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY} or {@link * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_REMOTE}. * #PLAY_WHEN_READY_CHANGE_REASON_REMOTE} or {@link
* #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -612,10 +613,11 @@ public interface Player { ...@@ -612,10 +613,11 @@ public interface Player {
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY,
PLAY_WHEN_READY_CHANGE_REASON_REMOTE PLAY_WHEN_READY_CHANGE_REASON_REMOTE,
PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM
}) })
@interface PlayWhenReadyChangeReason {} @interface PlayWhenReadyChangeReason {}
/** Playback has been started or paused by the user. */ /** Playback has been started or paused by a call to {@link #setPlayWhenReady(boolean)}. */
int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1;
/** Playback has been paused because of a loss of audio focus. */ /** Playback has been paused because of a loss of audio focus. */
int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2;
...@@ -623,6 +625,8 @@ public interface Player { ...@@ -623,6 +625,8 @@ public interface Player {
int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3;
/** Playback has been started or paused because of a remote change. */ /** Playback has been started or paused because of a remote change. */
int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4;
/** Playback has been paused at the end of a media item. */
int PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM = 5;
/** /**
* Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One
......
...@@ -1350,6 +1350,18 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1350,6 +1350,18 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
verifyApplicationThread();
player.setPauseAtEndOfMediaItems(pauseAtEndOfMediaItems);
}
@Override
public boolean getPauseAtEndOfMediaItems() {
verifyApplicationThread();
return player.getPauseAtEndOfMediaItems();
}
@Override
public @RepeatMode int getRepeatMode() { public @RepeatMode int getRepeatMode() {
verifyApplicationThread(); verifyApplicationThread();
return player.getRepeatMode(); return player.getRepeatMode();
......
...@@ -665,6 +665,8 @@ public class EventLogger implements AnalyticsListener { ...@@ -665,6 +665,8 @@ public class EventLogger implements AnalyticsListener {
return "REMOTE"; return "REMOTE";
case Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST: case Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST:
return "USER_REQUEST"; return "USER_REQUEST";
case Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM:
return "END_OF_MEDIA_ITEM";
default: default:
return "?"; return "?";
} }
......
...@@ -5990,6 +5990,85 @@ public final class ExoPlayerTest { ...@@ -5990,6 +5990,85 @@ public final class ExoPlayerTest {
assertThat(windowIndexAfterFinalEndedState.get()).isEqualTo(1); assertThat(windowIndexAfterFinalEndedState.get()).isEqualTo(1);
} }
@Test
public void pauseAtEndOfMediaItems_pausesPlaybackBeforeTransitioningToTheNextItem()
throws Exception {
TimelineWindowDefinition timelineWindowDefinition =
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND);
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition));
AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET);
AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET);
AtomicInteger windowIndexAfterPause = new AtomicInteger(C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlayWhenReady(true)
.waitForPlayWhenReady(false)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playbackStateAfterPause.set(player.getPlaybackState());
windowIndexAfterPause.set(player.getCurrentWindowIndex());
positionAfterPause.set(player.getContentPosition());
}
})
.play()
.build();
new Builder()
.setPauseAtEndOfMediaItems(true)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_READY);
assertThat(windowIndexAfterPause.get()).isEqualTo(0);
assertThat(positionAfterPause.get()).isEqualTo(10_000);
}
@Test
public void pauseAtEndOfMediaItems_pausesPlaybackWhenEnded() throws Exception {
TimelineWindowDefinition timelineWindowDefinition =
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND);
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition));
AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET);
AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET);
AtomicInteger windowIndexAfterPause = new AtomicInteger(C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlayWhenReady(true)
.waitForPlayWhenReady(false)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
playbackStateAfterPause.set(player.getPlaybackState());
windowIndexAfterPause.set(player.getCurrentWindowIndex());
positionAfterPause.set(player.getContentPosition());
}
})
.build();
new Builder()
.setPauseAtEndOfMediaItems(true)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_ENDED);
assertThat(windowIndexAfterPause.get()).isEqualTo(0);
assertThat(positionAfterPause.get()).isEqualTo(10_000);
}
// Internal methods. // Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
...@@ -93,6 +93,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -93,6 +93,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private AnalyticsListener analyticsListener; private AnalyticsListener analyticsListener;
private Integer expectedPlayerEndedCount; private Integer expectedPlayerEndedCount;
private boolean useLazyPreparation; private boolean useLazyPreparation;
private boolean pauseAtEndOfMediaItems;
private int initialWindowIndex; private int initialWindowIndex;
private long initialPositionMs; private long initialPositionMs;
private boolean skipSettingMediaSources; private boolean skipSettingMediaSources;
...@@ -195,6 +196,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -195,6 +196,17 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
} }
/** /**
* Sets whether to enable pausing at the end of media items.
*
* @param pauseAtEndOfMediaItems Whether to pause at the end of media items.
* @return This builder.
*/
public Builder setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
return this;
}
/**
* Sets a {@link DefaultTrackSelector} to be used by the test runner. The default value is a * Sets a {@link DefaultTrackSelector} to be used by the test runner. The default value is a
* {@link DefaultTrackSelector} in its initial configuration. * {@link DefaultTrackSelector} in its initial configuration.
* *
...@@ -385,6 +397,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -385,6 +397,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
mediaSources, mediaSources,
skipSettingMediaSources, skipSettingMediaSources,
useLazyPreparation, useLazyPreparation,
pauseAtEndOfMediaItems,
renderersFactory, renderersFactory,
trackSelector, trackSelector,
loadControl, loadControl,
...@@ -420,6 +433,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -420,6 +433,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final ArrayList<Integer> playbackStates; private final ArrayList<Integer> playbackStates;
private final boolean skipSettingMediaSources; private final boolean skipSettingMediaSources;
private final boolean useLazyPreparation; private final boolean useLazyPreparation;
private final boolean pauseAtEndOfMediaItems;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private Exception exception; private Exception exception;
...@@ -434,6 +448,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -434,6 +448,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
List<MediaSource> mediaSources, List<MediaSource> mediaSources,
boolean skipSettingMediaSources, boolean skipSettingMediaSources,
boolean useLazyPreparation, boolean useLazyPreparation,
boolean pauseAtEndOfMediaItems,
RenderersFactory renderersFactory, RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector, DefaultTrackSelector trackSelector,
LoadControl loadControl, LoadControl loadControl,
...@@ -449,6 +464,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -449,6 +464,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.mediaSources = mediaSources; this.mediaSources = mediaSources;
this.skipSettingMediaSources = skipSettingMediaSources; this.skipSettingMediaSources = skipSettingMediaSources;
this.useLazyPreparation = useLazyPreparation; this.useLazyPreparation = useLazyPreparation;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.renderersFactory = renderersFactory; this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
this.loadControl = loadControl; this.loadControl = loadControl;
...@@ -509,6 +525,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc ...@@ -509,6 +525,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (analyticsListener != null) { if (analyticsListener != null) {
player.addAnalyticsListener(analyticsListener); player.addAnalyticsListener(analyticsListener);
} }
if (pauseAtEndOfMediaItems) {
player.setPauseAtEndOfMediaItems(true);
}
player.play(); player.play();
if (actionSchedule != null) { if (actionSchedule != null) {
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
......
...@@ -391,4 +391,14 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -391,4 +391,14 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
public void setForegroundMode(boolean foregroundMode) { public void setForegroundMode(boolean foregroundMode) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
throw new UnsupportedOperationException();
}
@Override
public boolean getPauseAtEndOfMediaItems() {
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