Allow seeking to a default position in a period.

When seeking to the default position in a period, the containing source may
actually return a position in another period. Multi-period live sources can do
this to seek the player to the live edge.

ExoPlayerImplInternal uses the same functionality when the playback position
reaches the end of a period to determine what period/position to play next.
This means that when playback transitions to a multi-period live source from
some other source (playing a concatenation of those two sources), the player
will play the live edge rather than the beginning of the earliest period.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=128984355
This commit is contained in:
andrewlewis 2016-08-01 07:43:05 -07:00 committed by Oliver Woodman
parent abd5653dc4
commit c1729b640c
12 changed files with 216 additions and 37 deletions

View File

@ -300,6 +300,17 @@ public interface ExoPlayer {
*/
void seekTo(int periodIndex, long positionMs);
/**
* Seeks to the default position associated with the specified period. The position can depend on
* the type of source passed to {@link #setMediaSource(MediaSource)}. For live streams it will
* typically be the live edge. For other types of streams it will typically be the start of the
* stream.
*
* @param periodIndex The index of the period whose associated default position should be seeked
* to.
*/
void seekToDefaultPosition(int periodIndex);
/**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
* is to pause playback.

View File

@ -136,17 +136,25 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void seekTo(int periodIndex, long positionMs) {
boolean periodChanging = periodIndex != getCurrentPeriodIndex();
boolean seekToDefaultPosition = positionMs == ExoPlayer.UNKNOWN_TIME;
maskingPeriodIndex = periodIndex;
maskingPositionMs = positionMs;
maskingPositionMs = seekToDefaultPosition ? 0 : positionMs;
maskingDurationMs = periodChanging ? ExoPlayer.UNKNOWN_TIME : getDuration();
pendingSeekAcks++;
internalPlayer.seekTo(periodIndex, positionMs * 1000);
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(periodIndex, positionMs);
internalPlayer.seekTo(periodIndex, seekToDefaultPosition ? C.UNSET_TIME_US : positionMs * 1000);
if (!seekToDefaultPosition) {
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(periodIndex, positionMs);
}
}
}
@Override
public void seekToDefaultPosition(int periodIndex) {
seekTo(periodIndex, ExoPlayer.UNKNOWN_TIME);
}
@Override
public void stop() {
internalPlayer.stop();
@ -180,8 +188,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public long getCurrentPosition() {
return pendingSeekAcks == 0 ? playbackInfo.positionUs / 1000
: maskingPositionMs;
return pendingSeekAcks > 0 ? maskingPositionMs
: playbackInfo.positionUs == C.UNSET_TIME_US ? 0 : (playbackInfo.positionUs / 1000);
}
@Override
@ -239,14 +247,23 @@ import java.util.concurrent.CopyOnWriteArraySet;
break;
}
case ExoPlayerImplInternal.MSG_SEEK_ACK: {
pendingSeekAcks--;
if (--pendingSeekAcks == 0) {
long positionMs = playbackInfo.startPositionUs == C.UNSET_TIME_US ? 0
: playbackInfo.startPositionUs / 1000;
if (playbackInfo.periodIndex != maskingPeriodIndex || positionMs != maskingPositionMs) {
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(playbackInfo.periodIndex, positionMs);
}
}
}
break;
}
case ExoPlayerImplInternal.MSG_PERIOD_CHANGED: {
case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
if (pendingSeekAcks == 0) {
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(playbackInfo.periodIndex, 0);
listener.onPositionDiscontinuity(playbackInfo.periodIndex,
playbackInfo.startPositionUs / 1000);
}
}
break;

View File

@ -56,6 +56,7 @@ import java.util.ArrayList;
public volatile long positionUs;
public volatile long bufferedPositionUs;
public volatile long durationUs;
public volatile long startPositionUs;
public PlaybackInfo(int periodIndex) {
this.periodIndex = periodIndex;
@ -71,7 +72,7 @@ import java.util.ArrayList;
public static final int MSG_LOADING_CHANGED = 2;
public static final int MSG_SET_PLAY_WHEN_READY_ACK = 3;
public static final int MSG_SEEK_ACK = 4;
public static final int MSG_PERIOD_CHANGED = 5;
public static final int MSG_POSITION_DISCONTINUITY = 5;
public static final int MSG_TIMELINE_CHANGED = 6;
public static final int MSG_ERROR = 7;
@ -485,9 +486,20 @@ import java.util.ArrayList;
private void seekToInternal(int periodIndex, long positionUs) throws ExoPlaybackException {
try {
if (positionUs == C.UNSET_TIME_US && mediaSource != null) {
MediaSource.Position defaultStartPosition =
mediaSource.getDefaultStartPosition(periodIndex);
if (defaultStartPosition != null) {
// We know the default position so seek to it now.
periodIndex = defaultStartPosition.periodIndex;
positionUs = defaultStartPosition.positionUs;
}
}
if (periodIndex == playbackInfo.periodIndex
&& (positionUs / 1000) == (playbackInfo.positionUs / 1000)) {
// Seek position equals the current position to the nearest millisecond. Do nothing.
&& ((positionUs == C.UNSET_TIME_US && playbackInfo.positionUs == C.UNSET_TIME_US)
|| ((positionUs / 1000) == (playbackInfo.positionUs / 1000)))) {
// Seek position equals the current position. Do nothing.
return;
}
seekToPeriodPosition(periodIndex, positionUs);
@ -503,13 +515,15 @@ import java.util.ArrayList;
positionUs = internalTimeline.seekTo(periodIndex, positionUs);
if (periodIndex != playbackInfo.periodIndex) {
playbackInfo = new PlaybackInfo(periodIndex);
playbackInfo.startPositionUs = positionUs;
playbackInfo.positionUs = positionUs;
eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
} else {
playbackInfo.startPositionUs = positionUs;
playbackInfo.positionUs = positionUs;
}
updatePlaybackPositions();
if (mediaSource != null) {
setState(ExoPlayer.STATE_BUFFERING);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
@ -681,7 +695,14 @@ import java.util.ArrayList;
// Release all loaded periods and seek to the new playing period index.
releasePeriodsFrom(playingPeriod);
playingPeriod = null;
seekToPeriodPosition(newPlayingPeriodIndex, 0);
MediaSource.Position defaultStartPosition =
mediaSource.getDefaultStartPosition(newPlayingPeriodIndex);
if (defaultStartPosition != null) {
seekToPeriodPosition(defaultStartPosition.periodIndex, defaultStartPosition.positionUs);
} else {
seekToPeriodPosition(newPlayingPeriodIndex, C.UNSET_TIME_US);
}
return;
}
@ -746,9 +767,11 @@ import java.util.ArrayList;
: mediaSource.getNewPlayingPeriodIndex(playbackInfo.periodIndex, oldTimeline);
if (newPlayingIndex != Timeline.NO_PERIOD_INDEX
&& newPlayingIndex != playbackInfo.periodIndex) {
long oldPositionUs = playbackInfo.positionUs;
playbackInfo = new PlaybackInfo(newPlayingIndex);
playbackInfo.startPositionUs = oldPositionUs;
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
}
}
@ -762,21 +785,33 @@ import java.util.ArrayList;
// Update the loading period.
if (loadingPeriod == null || (loadingPeriod.isFullyBuffered() && !loadingPeriod.isLast
&& bufferAheadPeriodCount < MAXIMUM_BUFFER_AHEAD_PERIODS)) {
// Try to obtain the next period to start loading.
int periodIndex = loadingPeriod == null ? playbackInfo.periodIndex
: loadingPeriod.index + 1;
// Attempt to create the next period.
MediaPeriod mediaPeriod = mediaSource.createPeriod(periodIndex);
if (mediaPeriod != null) {
int periodIndex =
loadingPeriod == null ? playbackInfo.periodIndex : loadingPeriod.index + 1;
long startPositionUs = playbackInfo.positionUs;
if (loadingPeriod != null || startPositionUs == C.UNSET_TIME_US) {
// We are starting to load the next period or seeking to the default position, so request
// a period and position from the source.
MediaSource.Position defaultStartPosition =
mediaSource.getDefaultStartPosition(periodIndex);
if (defaultStartPosition != null) {
periodIndex = defaultStartPosition.periodIndex;
startPositionUs = defaultStartPosition.positionUs;
} else {
startPositionUs = C.UNSET_TIME_US;
}
}
MediaPeriod mediaPeriod;
if (startPositionUs != C.UNSET_TIME_US
&& (mediaPeriod = mediaSource.createPeriod(periodIndex)) != null) {
Period newPeriod = new Period(renderers, rendererCapabilities, trackSelector, mediaPeriod,
timeline.getPeriodId(periodIndex), periodIndex);
timeline.getPeriodId(periodIndex), periodIndex, startPositionUs);
newPeriod.isLast = timeline.isFinal() && periodIndex == timeline.getPeriodCount() - 1;
if (loadingPeriod != null) {
loadingPeriod.setNextPeriod(newPeriod);
}
bufferAheadPeriodCount++;
loadingPeriod = newPeriod;
long startPositionUs = playingPeriod == null ? playbackInfo.positionUs : 0;
setIsLoading(true);
loadingPeriod.mediaPeriod.preparePeriod(ExoPlayerImplInternal.this,
loadControl.getAllocator(), startPositionUs);
@ -807,8 +842,9 @@ import java.util.ArrayList;
setPlayingPeriod(playingPeriod.nextPeriod);
bufferAheadPeriodCount--;
playbackInfo = new PlaybackInfo(playingPeriod.index);
playbackInfo.startPositionUs = playingPeriod.startPositionUs;
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
updateTimelineState();
if (readingPeriod == null) {
@ -858,12 +894,19 @@ import java.util.ArrayList;
// Stale event.
return;
}
long startPositionUs = playingPeriod == null ? playbackInfo.positionUs : 0;
loadingPeriod.handlePrepared(startPositionUs, loadControl);
loadingPeriod.handlePrepared(loadingPeriod.startPositionUs, loadControl);
if (playingPeriod == null) {
// This is the first prepared period, so start playing it.
readingPeriod = loadingPeriod;
setPlayingPeriod(readingPeriod);
if (playbackInfo.startPositionUs == C.UNSET_TIME_US) {
// Update the playback info when seeking to a default position.
playbackInfo = new PlaybackInfo(playingPeriod.index);
playbackInfo.startPositionUs = playingPeriod.startPositionUs;
resetInternalPosition(playbackInfo.startPositionUs);
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
}
updateTimelineState();
}
maybeContinueLoading();
@ -895,6 +938,11 @@ import java.util.ArrayList;
}
public long seekTo(int periodIndex, long seekPositionUs) throws ExoPlaybackException {
if (seekPositionUs == C.UNSET_TIME_US) {
// We don't know where to seek to yet, so clear the whole timeline.
periodIndex = Timeline.NO_PERIOD_INDEX;
}
// Clear the timeline, but keep the requested period if it is already prepared.
Period period = playingPeriod;
Period newPlayingPeriod = null;
@ -929,7 +977,9 @@ import java.util.ArrayList;
playingPeriod = null;
readingPeriod = null;
loadingPeriod = null;
resetInternalPosition(seekPositionUs);
if (seekPositionUs != C.UNSET_TIME_US) {
resetInternalPosition(seekPositionUs);
}
}
return seekPositionUs;
}
@ -1120,6 +1170,7 @@ import java.util.ArrayList;
public final MediaPeriod mediaPeriod;
public final Object id;
public final SampleStream[] sampleStreams;
public final long startPositionUs;
public int index;
public boolean isLast;
@ -1138,13 +1189,15 @@ import java.util.ArrayList;
private TrackSelectionArray periodTrackSelections;
public Period(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
TrackSelector trackSelector, MediaPeriod mediaPeriod, Object id, int index) {
TrackSelector trackSelector, MediaPeriod mediaPeriod, Object id, int index,
long positionUs) {
this.renderers = renderers;
this.rendererCapabilities = rendererCapabilities;
this.trackSelector = trackSelector;
this.mediaPeriod = mediaPeriod;
this.id = Assertions.checkNotNull(id);
sampleStreams = new SampleStream[renderers.length];
startPositionUs = positionUs;
this.index = index;
}

View File

@ -359,6 +359,11 @@ public final class SimpleExoPlayer implements ExoPlayer {
player.seekTo(periodIndex, positionMs);
}
@Override
public void seekToDefaultPosition(int periodIndex) {
player.seekToDefaultPosition(periodIndex);
}
@Override
public void stop() {
player.stop();

View File

@ -53,15 +53,25 @@ public final class ConcatenatingMediaSource implements MediaSource {
}
@Override
public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline)
public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldConcatenatedTimeline)
throws IOException {
ConcatenatedTimeline oldConcatenatedTimeline = (ConcatenatedTimeline) oldTimeline;
int sourceIndex = oldConcatenatedTimeline.getSourceIndexForPeriod(oldPlayingPeriodIndex);
int sourceFirstPeriodIndex = oldConcatenatedTimeline.getFirstPeriodIndexInSource(sourceIndex);
return sourceFirstPeriodIndex == Timeline.NO_PERIOD_INDEX ? Timeline.NO_PERIOD_INDEX
: sourceFirstPeriodIndex + mediaSources[sourceIndex].getNewPlayingPeriodIndex(
oldPlayingPeriodIndex - sourceFirstPeriodIndex,
oldConcatenatedTimeline.timelines[sourceIndex]);
ConcatenatedTimeline oldTimeline = (ConcatenatedTimeline) oldConcatenatedTimeline;
int sourceIndex = oldTimeline.getSourceIndexForPeriod(oldPlayingPeriodIndex);
int oldFirstPeriodIndex = oldTimeline.getFirstPeriodIndexInSource(sourceIndex);
int firstPeriodIndex = timeline.getFirstPeriodIndexInSource(sourceIndex);
return firstPeriodIndex == Timeline.NO_PERIOD_INDEX ? Timeline.NO_PERIOD_INDEX
: firstPeriodIndex + mediaSources[sourceIndex].getNewPlayingPeriodIndex(
oldPlayingPeriodIndex - oldFirstPeriodIndex, oldTimeline.timelines[sourceIndex]);
}
@Override
public Position getDefaultStartPosition(int index) {
int sourceIndex = timeline.getSourceIndexForPeriod(index);
int sourceFirstPeriodIndex = timeline.getFirstPeriodIndexInSource(sourceIndex);
Position defaultStartPosition =
mediaSources[sourceIndex].getDefaultStartPosition(index - sourceFirstPeriodIndex);
return new Position(defaultStartPosition.periodIndex + sourceFirstPeriodIndex,
defaultStartPosition.positionUs);
}
@Override

View File

@ -187,6 +187,11 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
return oldPlayingPeriodIndex;
}
@Override
public Position getDefaultStartPosition(int index) {
return Position.DEFAULT;
}
@Override
public MediaPeriod createPeriod(int index) {
Assertions.checkArgument(index == 0);

View File

@ -38,6 +38,38 @@ public interface MediaSource {
}
/**
* A position in the timeline.
*/
final class Position {
/**
* A start position at the earliest time in the first period.
*/
public static final Position DEFAULT = new Position(0, 0);
/**
* The index of the period containing the timeline position.
*/
public final int periodIndex;
/**
* The position in microseconds within the period.
*/
public final long positionUs;
/**
* Creates a new timeline position.
*
* @param periodIndex The index of the period containing the timeline position.
* @param positionUs The position in microseconds within the period.
*/
public Position(int periodIndex, long positionUs) {
this.periodIndex = periodIndex;
this.positionUs = positionUs;
}
}
/**
* Starts preparation of the source.
*
@ -55,6 +87,20 @@ public interface MediaSource {
*/
int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) throws IOException;
/**
* Returns the default {@link Position} that the player should play when it reaches the period at
* {@code index}, or {@code null} if the default start period and position are not yet known.
* <p>
* For example, sources can return a {@link Position} with the passed period {@code index} to play
* the period at {@code index} immediately after the period at {@code index - 1}. Or, sources
* providing multi-period live streams may return the index and position of the live edge when
* passed {@code index == 0} so that the playback position jumps to the live edge.
*
* @param index The index of the period the player has just reached.
* @return The default start position.
*/
Position getDefaultStartPosition(int index);
/**
* Returns a {@link MediaPeriod} corresponding to the period at the specified index, or
* {@code null} if the period at the specified index is not yet available.

View File

@ -64,6 +64,11 @@ public final class MergingMediaSource implements MediaSource {
return mediaSources[0].getNewPlayingPeriodIndex(oldPlayingPeriodIndex, oldTimeline);
}
@Override
public Position getDefaultStartPosition(int index) {
return mediaSources[0].getDefaultStartPosition(index);
}
@Override
public MediaPeriod createPeriod(int index) throws IOException {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];

View File

@ -122,6 +122,11 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource,
return oldPlayingPeriodIndex;
}
@Override
public Position getDefaultStartPosition(int index) {
return Position.DEFAULT;
}
@Override
public MediaPeriod createPeriod(int index) {
Assertions.checkArgument(index == 0);

View File

@ -122,6 +122,16 @@ public final class DashMediaSource implements MediaSource {
return Timeline.NO_PERIOD_INDEX;
}
@Override
public Position getDefaultStartPosition(int index) {
if (index == 0 && manifest.dynamic) {
// The stream is live, so jump to the live edge.
// TODO[playlists]: Actually jump to the live edge, rather than the start of the last period.
return new Position(periods.size() - 1, 0);
}
return new Position(index, 0);
}
@Override
public MediaPeriod createPeriod(int index) throws IOException {
if (periods == null || periods.size() <= index) {

View File

@ -118,6 +118,12 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
return oldPlayingPeriodIndex;
}
@Override
public Position getDefaultStartPosition(int index) {
// TODO: Return the position of the live edge, if applicable.
return Position.DEFAULT;
}
@Override
public MediaPeriod createPeriod(int index) {
Assertions.checkArgument(index == 0);

View File

@ -100,6 +100,12 @@ public final class SsMediaSource implements MediaSource,
return oldPlayingPeriodIndex;
}
@Override
public Position getDefaultStartPosition(int index) {
// TODO: Return the position of the live edge, if applicable.
return Position.DEFAULT;
}
@Override
public MediaPeriod createPeriod(int index) {
Assertions.checkArgument(index == 0);