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 @@
consistency.
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
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
`MediaSourceEventListener.MediaLoadData` top-level classes.
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
......
......@@ -557,4 +557,23 @@ public interface ExoPlayer extends Player {
* idle state.
*/
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;
private float playbackSpeed;
private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
......@@ -436,6 +437,20 @@ import java.util.concurrent.TimeoutException;
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")
public void setPlayWhenReady(
boolean playWhenReady,
......
......@@ -845,12 +845,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|| playingPeriodDurationUs <= playbackInfo.positionUs);
if (finishedRendering && pendingPauseAtEndOfPeriod) {
pendingPauseAtEndOfPeriod = false;
// TODO: Add new change reason for timed pause requests.
setPlayWhenReadyInternal(
/* playWhenReady= */ false,
playbackInfo.playbackSuppressionReason,
/* 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) {
setState(Player.STATE_ENDED);
......@@ -1548,7 +1547,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodHolder.prepared
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs);
|| playbackInfo.positionUs < playingPeriodDurationUs
|| !shouldPlayWhenReady());
}
private void maybeThrowSourceInfoRefreshError() throws IOException {
......
......@@ -603,8 +603,9 @@ public interface Player {
* Reasons for {@link #getPlayWhenReady() playWhenReady} changes. One of {@link
* #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@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_REMOTE}.
* #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY}, {@link
* #PLAY_WHEN_READY_CHANGE_REASON_REMOTE} or {@link
* #PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
......@@ -612,10 +613,11 @@ public interface Player {
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
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 {}
/** 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;
/** Playback has been paused because of a loss of audio focus. */
int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2;
......@@ -623,6 +625,8 @@ public interface Player {
int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3;
/** Playback has been started or paused because of a remote change. */
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
......
......@@ -1350,6 +1350,18 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
verifyApplicationThread();
player.setPauseAtEndOfMediaItems(pauseAtEndOfMediaItems);
}
@Override
public boolean getPauseAtEndOfMediaItems() {
verifyApplicationThread();
return player.getPauseAtEndOfMediaItems();
}
@Override
public @RepeatMode int getRepeatMode() {
verifyApplicationThread();
return player.getRepeatMode();
......
......@@ -665,6 +665,8 @@ public class EventLogger implements AnalyticsListener {
return "REMOTE";
case Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST:
return "USER_REQUEST";
case Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM:
return "END_OF_MEDIA_ITEM";
default:
return "?";
}
......
......@@ -5990,6 +5990,85 @@ public final class ExoPlayerTest {
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.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
......@@ -93,6 +93,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private AnalyticsListener analyticsListener;
private Integer expectedPlayerEndedCount;
private boolean useLazyPreparation;
private boolean pauseAtEndOfMediaItems;
private int initialWindowIndex;
private long initialPositionMs;
private boolean skipSettingMediaSources;
......@@ -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
* {@link DefaultTrackSelector} in its initial configuration.
*
......@@ -385,6 +397,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
mediaSources,
skipSettingMediaSources,
useLazyPreparation,
pauseAtEndOfMediaItems,
renderersFactory,
trackSelector,
loadControl,
......@@ -420,6 +433,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final ArrayList<Integer> playbackStates;
private final boolean skipSettingMediaSources;
private final boolean useLazyPreparation;
private final boolean pauseAtEndOfMediaItems;
private SimpleExoPlayer player;
private Exception exception;
......@@ -434,6 +448,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
List<MediaSource> mediaSources,
boolean skipSettingMediaSources,
boolean useLazyPreparation,
boolean pauseAtEndOfMediaItems,
RenderersFactory renderersFactory,
DefaultTrackSelector trackSelector,
LoadControl loadControl,
......@@ -449,6 +464,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.mediaSources = mediaSources;
this.skipSettingMediaSources = skipSettingMediaSources;
this.useLazyPreparation = useLazyPreparation;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.renderersFactory = renderersFactory;
this.trackSelector = trackSelector;
this.loadControl = loadControl;
......@@ -509,6 +525,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
if (analyticsListener != null) {
player.addAnalyticsListener(analyticsListener);
}
if (pauseAtEndOfMediaItems) {
player.setPauseAtEndOfMediaItems(true);
}
player.play();
if (actionSchedule != null) {
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
......
......@@ -391,4 +391,14 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
public void setForegroundMode(boolean foregroundMode) {
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