diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 71620e7a3f..a46b64dda8 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -87,6 +88,14 @@ public class EventLogger implements ExoPlayer.EventListener, SimpleExoPlayer.Deb // Do nothing. } + @Override + public void onTimelineChanged(Timeline timeline) { + boolean isFinal = timeline.isFinal(); + int periodCount = timeline.getPeriodCount(); + Log.d(TAG, "timelineChanged [" + isFinal + ", " + + (periodCount == Timeline.UNKNOWN_PERIOD_COUNT ? "?" : periodCount) + "]"); + } + @Override public void onPlayerError(ExoPlaybackException e) { Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index af91e0e775..3a762aae73 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.metadata.id3.TxxxFrame; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator; @@ -457,6 +458,11 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, } } + @Override + public void onTimelineChanged(Timeline timeline) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException e) { String errorString = null; diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 8c99d1b895..aa7df3c14b 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -100,6 +101,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTimelineChanged(Timeline timeline) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 2b7c49ab20..950b11f2fa 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -100,6 +101,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTimelineChanged(Timeline timeline) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index e0928b1253..f3d6d6289d 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -119,6 +120,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onTimelineChanged(Timeline timeline) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b198f1df02..922e932282 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.Timeline; /** * An extensible media player exposing traditional high-level media player functionality, such as @@ -138,6 +139,13 @@ public interface ExoPlayer { */ void onPositionDiscontinuity(int periodIndex, long positionMs); + /** + * Invoked when the timeline changes. + * + * @param timeline The new timeline. + */ + void onTimelineChanged(Timeline timeline); + /** * Invoked when an error occurs. The playback state will transition to * {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance @@ -329,10 +337,10 @@ public interface ExoPlayer { void blockingSendMessages(ExoPlayerMessage... messages); /** - * Gets the duration of the track in milliseconds. + * Gets the duration of the current period in milliseconds. * - * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the - * duration is not known. + * @return The duration of the current period in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} + * if the duration is not known. */ long getDuration(); @@ -350,6 +358,13 @@ public interface ExoPlayer { */ int getCurrentPeriodIndex(); + /** + * Gets the current {@link Timeline}, or {@code null} if there is no timeline. + * + * @return The current {@link Timeline}, or {@code null} if there is no timeline. + */ + Timeline getCurrentTimeline(); + /** * Gets an estimate of the absolute position in milliseconds up to which data is buffered. * diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ccfde62649..441400152a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Assertions; @@ -44,6 +45,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private int pendingPlayWhenReadyAcks; private int pendingSeekAcks; private boolean isLoading; + private Timeline timeline; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -75,9 +77,9 @@ import java.util.concurrent.CopyOnWriteArraySet; ExoPlayerImpl.this.handleEvent(msg); } }; - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - eventHandler); playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0); + internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, + eventHandler, playbackInfo); } @Override @@ -97,6 +99,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void setMediaSource(MediaSource mediaSource) { + timeline = null; internalPlayer.setMediaSource(mediaSource); } @@ -188,6 +191,11 @@ import java.util.concurrent.CopyOnWriteArraySet; return pendingSeekAcks == 0 ? playbackInfo.periodIndex : maskingPeriodIndex; } + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + @Override public long getBufferedPosition() { if (pendingSeekAcks == 0) { @@ -245,6 +253,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; } + case ExoPlayerImplInternal.MSG_TIMELINE_CHANGED: { + timeline = (Timeline) msg.obj; + for (EventListener listener : listeners) { + listener.onTimelineChanged(timeline); + } + break; + } case ExoPlayerImplInternal.MSG_ERROR: { ExoPlaybackException exception = (ExoPlaybackException) msg.obj; for (EventListener listener : listeners) { diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index aef3ebee04..a72ac33206 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -19,10 +19,11 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.PriorityHandlerThread; import com.google.android.exoplayer2.util.StandaloneMediaClock; @@ -44,7 +45,7 @@ import java.util.ArrayList; * Implements the internal behavior of {@link ExoPlayerImpl}. */ /* package */ final class ExoPlayerImplInternal implements Handler.Callback, MediaPeriod.Callback, - InvalidationListener { + TrackSelector.InvalidationListener, MediaSource.InvalidationListener { /** * Playback position information which is read on the application's thread by @@ -73,7 +74,8 @@ import java.util.ArrayList; 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_ERROR = 6; + public static final int MSG_TIMELINE_CHANGED = 6; + public static final int MSG_ERROR = 7; // Internal messages private static final int MSG_SET_MEDIA_SOURCE = 0; @@ -85,18 +87,19 @@ import java.util.ArrayList; private static final int MSG_PERIOD_PREPARED = 6; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 7; private static final int MSG_TRACK_SELECTION_INVALIDATED = 8; - private static final int MSG_CUSTOM = 9; + private static final int MSG_SOURCE_INVALIDATED = 9; + private static final int MSG_CUSTOM = 10; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; /** - * Limits the maximum number of sources to buffer ahead of the current source in the timeline. The - * source buffering policy normally prevents buffering too far ahead, but the policy could allow - * too many very small sources to be buffered if the buffered source count were not limited. + * Limits the maximum number of periods to buffer ahead of the current playing period. The + * buffering policy normally prevents buffering too far ahead, but the policy could allow too many + * small periods to be buffered if the period count were not limited. */ - private static final int MAXIMUM_BUFFER_AHEAD_SOURCES = 100; + private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; private final TrackSelector trackSelector; private final LoadControl loadControl; @@ -104,7 +107,7 @@ import java.util.ArrayList; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; - private final Timeline timeline; + private final InternalTimeline internalTimeline; private PlaybackInfo playbackInfo; private Renderer rendererMediaClockSource; @@ -123,12 +126,14 @@ import java.util.ArrayList; private long internalPositionUs; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, Handler eventHandler) { + LoadControl loadControl, boolean playWhenReady, Handler eventHandler, + PlaybackInfo playbackInfo) { this.trackSelector = trackSelector; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; + this.playbackInfo = playbackInfo; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -136,8 +141,7 @@ import java.util.ArrayList; standaloneMediaClock = new StandaloneMediaClock(); enabledRenderers = new Renderer[0]; - timeline = new Timeline(renderers); - playbackInfo = new PlaybackInfo(0); + internalTimeline = new InternalTimeline(renderers); trackSelector.init(this); @@ -205,7 +209,7 @@ import java.util.ArrayList; internalPlaybackThread.quit(); } - // InvalidationListener implementation. + // TrackSelector.InvalidationListener implementation. @Override public void onTrackSelectionsInvalidated() { @@ -255,17 +259,21 @@ import java.util.ArrayList; return true; } case MSG_PERIOD_PREPARED: { - timeline.handlePeriodPrepared((MediaPeriod) msg.obj); + internalTimeline.handlePeriodPrepared((MediaPeriod) msg.obj); return true; } case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { - timeline.handleContinueLoadingRequested((MediaPeriod) msg.obj); + internalTimeline.handleContinueLoadingRequested((MediaPeriod) msg.obj); return true; } case MSG_TRACK_SELECTION_INVALIDATED: { reselectTracksInternal(); return true; } + case MSG_SOURCE_INVALIDATED: { + internalTimeline.invalidate((Timeline) msg.obj); + return true; + } case MSG_CUSTOM: { sendMessagesInternal((ExoPlayerMessage[]) msg.obj); return true; @@ -292,6 +300,19 @@ import java.util.ArrayList; } } + // MediaSource.InvalidationListener implementation. + + @Override + public void onTimelineChanged(Timeline timeline) { + try { + internalTimeline.invalidate(timeline); + } catch (ExoPlaybackException | IOException e) { + Log.e(TAG, "Error handling timeline change.", e); + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + stopInternal(); + } + } + // Private methods. private void setState(int state) { @@ -311,7 +332,7 @@ import java.util.ArrayList; private void setMediaSourceInternal(MediaSource mediaSource) { resetInternal(); this.mediaSource = mediaSource; - mediaSource.prepareSource(); + mediaSource.prepareSource(this); setState(ExoPlayer.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -352,7 +373,7 @@ import java.util.ArrayList; } private void updatePlaybackPositions() throws ExoPlaybackException { - MediaPeriod mediaPeriod = timeline.getPeriod(); + MediaPeriod mediaPeriod = internalTimeline.getPeriod(); if (mediaPeriod == null) { return; } @@ -373,7 +394,7 @@ import java.util.ArrayList; } else { internalPositionUs = standaloneMediaClock.getPositionUs(); } - positionUs = internalPositionUs - timeline.playingPeriod.offsetUs; + positionUs = internalPositionUs - internalTimeline.playingPeriod.offsetUs; } playbackInfo.positionUs = positionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; @@ -391,10 +412,10 @@ import java.util.ArrayList; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = SystemClock.elapsedRealtime(); - timeline.updatePeriods(); - if (timeline.getPeriod() == null) { + internalTimeline.updatePeriods(); + if (internalTimeline.getPeriod() == null) { // We're still waiting for the first source to be prepared. - timeline.maybeThrowPeriodPrepareError(); + internalTimeline.maybeThrowPeriodPrepareError(); scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); return; } @@ -420,23 +441,23 @@ import java.util.ArrayList; } if (!allRenderersReadyOrEnded) { - timeline.maybeThrowPeriodPrepareError(); + internalTimeline.maybeThrowPeriodPrepareError(); } if (allRenderersEnded && (playbackInfo.durationUs == C.UNSET_TIME_US - || playbackInfo.durationUs <= playbackInfo.positionUs) && timeline.isEnded) { + || playbackInfo.durationUs <= playbackInfo.positionUs) && internalTimeline.isEnded) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { - if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady) - && timeline.haveSufficientBuffer(rebuffering)) { + if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : internalTimeline.isReady) + && internalTimeline.haveSufficientBuffer(rebuffering)) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); } } } else if (state == ExoPlayer.STATE_READY) { - if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !timeline.isReady) { + if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !internalTimeline.isReady) { rebuffering = playWhenReady; setState(ExoPlayer.STATE_BUFFERING); stopRenderers(); @@ -464,38 +485,42 @@ import java.util.ArrayList; } } - private void seekToInternal(int periodIndex, long seekPositionUs) throws ExoPlaybackException { + private void seekToInternal(int periodIndex, long positionUs) throws ExoPlaybackException { try { if (periodIndex == playbackInfo.periodIndex - && (seekPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + && (positionUs / 1000) == (playbackInfo.positionUs / 1000)) { // Seek position equals the current position to the nearest millisecond. Do nothing. return; } - - stopRenderers(); - rebuffering = false; - - seekPositionUs = timeline.seekTo(periodIndex, seekPositionUs); - if (periodIndex != playbackInfo.periodIndex) { - playbackInfo = new PlaybackInfo(periodIndex); - playbackInfo.positionUs = seekPositionUs; - eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget(); - } else { - playbackInfo.positionUs = seekPositionUs; - } - - updatePlaybackPositions(); - if (mediaSource != null) { - setState(ExoPlayer.STATE_BUFFERING); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } + seekToPeriodPosition(periodIndex, positionUs); } finally { eventHandler.sendEmptyMessage(MSG_SEEK_ACK); } } + private void seekToPeriodPosition(int periodIndex, long positionUs) throws ExoPlaybackException { + stopRenderers(); + rebuffering = false; + + positionUs = internalTimeline.seekTo(periodIndex, positionUs); + if (periodIndex != playbackInfo.periodIndex) { + playbackInfo = new PlaybackInfo(periodIndex); + playbackInfo.positionUs = positionUs; + eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget(); + } else { + playbackInfo.positionUs = positionUs; + } + + updatePlaybackPositions(); + if (mediaSource != null) { + setState(ExoPlayer.STATE_BUFFERING); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + private void resetInternalPosition(long periodPositionUs) throws ExoPlaybackException { - long sourceOffsetUs = timeline.playingPeriod == null ? 0 : timeline.playingPeriod.offsetUs; + long sourceOffsetUs = + internalTimeline.playingPeriod == null ? 0 : internalTimeline.playingPeriod.offsetUs; internalPositionUs = sourceOffsetUs + periodPositionUs; standaloneMediaClock.setPositionUs(internalPositionUs); for (Renderer renderer : enabledRenderers) { @@ -537,7 +562,7 @@ import java.util.ArrayList; mediaSource.releaseSource(); mediaSource = null; } - timeline.reset(); + internalTimeline.reset(); loadControl.reset(); setIsLoading(false); } @@ -566,19 +591,20 @@ import java.util.ArrayList; } private void reselectTracksInternal() throws ExoPlaybackException { - if (timeline.getPeriod() == null) { + if (internalTimeline.getPeriod() == null) { // We don't have tracks yet, so we don't care. return; } - timeline.reselectTracks(); + internalTimeline.reselectTracks(); updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } + // TODO[playlists]: Merge this into the outer class. /** * Keeps track of the {@link Period}s of media being played in the timeline. */ - private final class Timeline { + private final class InternalTimeline { private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; @@ -592,7 +618,9 @@ import java.util.ArrayList; private long playingPeriodEndPositionUs; - public Timeline(Renderer[] renderers) { + private Timeline timeline; + + public InternalTimeline(Renderer[] renderers) { this.renderers = renderers; rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -613,9 +641,7 @@ import java.util.ArrayList; long bufferedPositionUs = !loadingPeriod.prepared ? 0 : loadingPeriod.mediaPeriod.getBufferedPositionUs(); if (bufferedPositionUs == C.END_OF_SOURCE_US) { - int periodCount = mediaSource.getPeriodCount(); - if (periodCount != MediaSource.UNKNOWN_PERIOD_COUNT - && loadingPeriod.index == periodCount - 1) { + if (loadingPeriod.isLast) { return true; } bufferedPositionUs = loadingPeriod.mediaPeriod.getDurationUs(); @@ -635,32 +661,122 @@ import java.util.ArrayList; } } + public void invalidate(Timeline timeline) throws ExoPlaybackException, IOException { + Timeline oldTimeline = this.timeline; + this.timeline = timeline; + eventHandler.obtainMessage(MSG_TIMELINE_CHANGED, timeline).sendToTarget(); + + // Update the loaded periods to take into account the new timeline. + if (playingPeriod != null) { + int index = timeline.getIndexOfPeriod(playingPeriod.id); + if (index == Timeline.NO_PERIOD_INDEX) { + int newPlayingPeriodIndex = + mediaSource.getNewPlayingPeriodIndex(playingPeriod.index, oldTimeline); + if (newPlayingPeriodIndex == Timeline.NO_PERIOD_INDEX) { + // There is no period to play, so stop the player. + stopInternal(); + return; + } + + // Release all loaded periods and seek to the new playing period index. + releasePeriodsFrom(playingPeriod); + playingPeriod = null; + seekToPeriodPosition(newPlayingPeriodIndex, 0); + return; + } + + // The playing period is also in the new timeline. Update index and isLast on each loaded + // period until a period is found that has changed. + int periodCount = timeline.getPeriodCount(); + playingPeriod.index = index; + playingPeriod.isLast = timeline.isFinal() && index == periodCount - 1; + + Period previousPeriod = playingPeriod; + boolean seenReadingPeriod = false; + while (previousPeriod != null) { + Period period = previousPeriod.nextPeriod; + index++; + if (!period.id.equals(timeline.getPeriodId(index))) { + if (!seenReadingPeriod) { + // Renderers may have read a period that has been removed, so release all loaded + // periods and seek to the playing period index. + index = playingPeriod.index; + releasePeriodsFrom(playingPeriod); + playingPeriod = null; + seekToPeriodPosition(index, 0); + return; + } + + // Update the loading period to be the latest period that is still valid. + loadingPeriod = previousPeriod; + loadingPeriod.nextPeriod = null; + + // Release the rest of the timeline. + releasePeriodsFrom(period); + break; + } + + period.index = index; + period.isLast = timeline.isFinal() && index == periodCount - 1; + if (period == readingPeriod) { + seenReadingPeriod = true; + } + previousPeriod = period; + } + } else if (loadingPeriod != null) { + Object id = loadingPeriod.id; + int index = timeline.getIndexOfPeriod(id); + if (index == Timeline.NO_PERIOD_INDEX) { + loadingPeriod.release(); + loadingPeriod = null; + } else { + int periodCount = timeline.getPeriodCount(); + loadingPeriod.index = index; + loadingPeriod.isLast = timeline.isFinal() && index == periodCount - 1; + } + } + + // TODO[playlists]: Signal the identifier discontinuity, even if the index hasn't changed. + if (oldTimeline != null) { + int newPlayingIndex = playingPeriod != null ? playingPeriod.index + : loadingPeriod != null ? loadingPeriod.index + : mediaSource.getNewPlayingPeriodIndex(playbackInfo.periodIndex, oldTimeline); + if (newPlayingIndex != Timeline.NO_PERIOD_INDEX + && newPlayingIndex != playbackInfo.periodIndex) { + playbackInfo = new PlaybackInfo(newPlayingIndex); + updatePlaybackPositions(); + eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget(); + } + } + } + public void updatePeriods() throws ExoPlaybackException, IOException { - // TODO[playlists]: Let MediaSource invalidate periods that are already loaded. + if (timeline == null) { + // We're waiting to get information about periods. + return; + } // Update the loading period. - int periodCount = mediaSource.getPeriodCount(); - if (loadingPeriod == null - || (loadingPeriod.isFullyBuffered() && loadingPeriod.index - - (playingPeriod != null ? playingPeriod.index : 0) < MAXIMUM_BUFFER_AHEAD_SOURCES)) { - // Try and obtain the next period to start loading. + if (loadingPeriod == null || (loadingPeriod.isFullyBuffered() && !loadingPeriod.isLast + && (playingPeriod == null || loadingPeriod.index - playingPeriod.index + < MAXIMUM_BUFFER_AHEAD_PERIODS))) { + // Try to obtain the next period to start loading. int periodIndex = loadingPeriod == null ? playbackInfo.periodIndex : loadingPeriod.index + 1; - if (periodCount == MediaSource.UNKNOWN_PERIOD_COUNT || periodIndex < periodCount) { - // Attempt to create the next period. - MediaPeriod mediaPeriod = mediaSource.createPeriod(periodIndex); - if (mediaPeriod != null) { - Period newPeriod = new Period(renderers, rendererCapabilities, trackSelector, - mediaPeriod, periodIndex); - if (loadingPeriod != null) { - loadingPeriod.setNextPeriod(newPeriod); - } - loadingPeriod = newPeriod; - long startPositionUs = playingPeriod == null ? playbackInfo.positionUs : 0; - setIsLoading(true); - loadingPeriod.mediaPeriod.preparePeriod(ExoPlayerImplInternal.this, - loadControl.getAllocator(), startPositionUs); + // Attempt to create the next period. + MediaPeriod mediaPeriod = mediaSource.createPeriod(periodIndex); + if (mediaPeriod != null) { + Period newPeriod = new Period(renderers, rendererCapabilities, trackSelector, mediaPeriod, + timeline.getPeriodId(periodIndex), periodIndex); + newPeriod.isLast = timeline.isFinal() && periodIndex == timeline.getPeriodCount() - 1; + if (loadingPeriod != null) { + loadingPeriod.setNextPeriod(newPeriod); } + loadingPeriod = newPeriod; + long startPositionUs = playingPeriod == null ? playbackInfo.positionUs : 0; + setIsLoading(true); + loadingPeriod.mediaPeriod.preparePeriod(ExoPlayerImplInternal.this, + loadControl.getAllocator(), startPositionUs); } } @@ -725,10 +841,8 @@ import java.util.ArrayList; } } } - } else if (periodCount != MediaSource.UNKNOWN_PERIOD_COUNT - && readingPeriod.index == periodCount - 1) { + } else if (readingPeriod.isLast) { readingPeriod = null; - // This is the last period, so signal the renderers to read the end of the stream. for (Renderer renderer : enabledRenderers) { renderer.setCurrentStreamIsFinal(); } @@ -825,6 +939,7 @@ import java.util.ArrayList; return; } if (period.selectTracks()) { + // Selected tracks have changed for this period. break; } if (period == readingPeriod) { @@ -837,11 +952,7 @@ import java.util.ArrayList; if (selectionsChangedForReadPeriod) { // Release everything after the playing period because a renderer may have read data from a // track whose selection has now changed. - period = playingPeriod.nextPeriod; - while (period != null) { - period.release(); - period = period.nextPeriod; - } + releasePeriodsFrom(playingPeriod.nextPeriod); playingPeriod.nextPeriod = null; readingPeriod = playingPeriod; loadingPeriod = playingPeriod; @@ -898,18 +1009,21 @@ import java.util.ArrayList; } public void reset() { - Period period = playingPeriod != null ? playingPeriod : loadingPeriod; - while (period != null) { - period.release(); - period = period.nextPeriod; - } + releasePeriodsFrom(playingPeriod != null ? playingPeriod : loadingPeriod); playingPeriodEndPositionUs = C.UNSET_TIME_US; isReady = false; isEnded = false; playingPeriod = null; readingPeriod = null; loadingPeriod = null; - eventHandler.obtainMessage(MSG_PERIOD_CHANGED, playbackInfo).sendToTarget(); + timeline = null; + } + + private void releasePeriodsFrom(Period period) { + while (period != null) { + period.release(); + period = period.nextPeriod; + } } private void setPlayingPeriod(Period period) throws ExoPlaybackException { @@ -945,9 +1059,7 @@ import java.util.ArrayList; isReady = playingPeriodEndPositionUs == C.UNSET_TIME_US || internalPositionUs < playingPeriodEndPositionUs || (playingPeriod.nextPeriod != null && playingPeriod.nextPeriod.prepared); - int periodCount = mediaSource.getPeriodCount(); - isEnded = periodCount != MediaSource.UNKNOWN_PERIOD_COUNT - && playingPeriod.index == periodCount - 1; + isEnded = playingPeriod.isLast; } private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) @@ -998,9 +1110,11 @@ import java.util.ArrayList; private static final class Period { public final MediaPeriod mediaPeriod; - public final int index; + public final Object id; public final SampleStream[] sampleStreams; + public int index; + public boolean isLast; public boolean prepared; public boolean hasEnabledTracks; public long offsetUs; @@ -1016,13 +1130,14 @@ import java.util.ArrayList; private TrackSelectionArray periodTrackSelections; public Period(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - TrackSelector trackSelector, MediaPeriod mediaPeriod, int index) { + TrackSelector trackSelector, MediaPeriod mediaPeriod, Object id, int index) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.trackSelector = trackSelector; this.mediaPeriod = mediaPeriod; - this.index = index; + this.id = Assertions.checkNotNull(id); sampleStreams = new SampleStream[renderers.length]; + this.index = index; } public void setNextPeriod(Period nextPeriod) { diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 3857db65f4..27b1059d28 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -26,11 +26,13 @@ import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; + import android.annotation.TargetApi; import android.content.Context; import android.media.AudioManager; @@ -389,6 +391,11 @@ public final class SimpleExoPlayer implements ExoPlayer { return player.getCurrentPeriodIndex(); } + @Override + public Timeline getCurrentTimeline() { + return player.getCurrentTimeline(); + } + @Override public long getBufferedPosition() { return player.getBufferedPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index a45473de16..3f42b902ea 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,7 +15,10 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.util.Util; + import java.io.IOException; +import java.util.Arrays; /** * Concatenates multiple {@link MediaSource}s. @@ -23,45 +26,50 @@ import java.io.IOException; public final class ConcatenatingMediaSource implements MediaSource { private final MediaSource[] mediaSources; + private final Timeline[] timelines; + + private ConcatenatedTimeline timeline; /** * @param mediaSources The {@link MediaSource}s to concatenate. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { this.mediaSources = mediaSources; + timelines = new Timeline[mediaSources.length]; } @Override - public void prepareSource() { - for (MediaSource mediaSource : mediaSources) { - mediaSource.prepareSource(); + public void prepareSource(final InvalidationListener listener) { + for (int i = 0; i < mediaSources.length; i++) { + final int index = i; + mediaSources[i].prepareSource(new InvalidationListener() { + @Override + public void onTimelineChanged(Timeline timeline) { + timelines[index] = timeline; + ConcatenatingMediaSource.this.timeline = new ConcatenatedTimeline(timelines.clone()); + listener.onTimelineChanged(ConcatenatingMediaSource.this.timeline); + } + }); } } @Override - public int getPeriodCount() { - int sourceCount = 0; - for (MediaSource mediaSource : mediaSources) { - int count = mediaSource.getPeriodCount(); - if (count == MediaSource.UNKNOWN_PERIOD_COUNT) { - return UNKNOWN_PERIOD_COUNT; - } - sourceCount += count; - } - return sourceCount; + public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) + 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]); } @Override public MediaPeriod createPeriod(int index) throws IOException { - int sourceCount = 0; - for (MediaSource mediaSource : mediaSources) { - int count = mediaSource.getPeriodCount(); - if (count == MediaSource.UNKNOWN_PERIOD_COUNT || index < sourceCount + count) { - return mediaSource.createPeriod(index - sourceCount); - } - sourceCount += count; - } - throw new IndexOutOfBoundsException(); + int sourceIndex = timeline.getSourceIndexForPeriod(index); + int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex); + return mediaSources[sourceIndex].createPeriod(periodIndexInSource); } @Override @@ -71,4 +79,99 @@ public final class ConcatenatingMediaSource implements MediaSource { } } + /** + * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. + */ + private static final class ConcatenatedTimeline implements Timeline { + + private final Timeline[] timelines; + private final Object[] manifests; + private final int count; + private final boolean isFinal; + private int[] sourceOffsets; + + public ConcatenatedTimeline(Timeline[] timelines) { + this.timelines = timelines; + + int[] sourceOffsets = new int[timelines.length]; + int sourceIndexOffset = 0; + for (int i = 0; i < timelines.length; i++) { + Timeline manifest = timelines[i]; + int periodCount; + if (manifest == null + || (periodCount = manifest.getPeriodCount()) == Timeline.UNKNOWN_PERIOD_COUNT) { + sourceOffsets = Arrays.copyOf(sourceOffsets, i); + break; + } + sourceIndexOffset += periodCount; + sourceOffsets[i] = sourceIndexOffset; + } + this.sourceOffsets = sourceOffsets; + count = sourceOffsets.length == timelines.length ? sourceOffsets[sourceOffsets.length - 1] + : UNKNOWN_PERIOD_COUNT; + boolean isFinal = true; + manifests = new Object[timelines.length]; + for (int i = 0; i < timelines.length; i++) { + Timeline timeline = timelines[i]; + if (timeline != null) { + manifests[i] = timeline.getManifest(); + if (!timeline.isFinal()) { + isFinal = false; + } + } + } + this.isFinal = isFinal; + } + + @Override + public int getPeriodCount() { + return count; + } + + @Override + public boolean isFinal() { + return isFinal; + } + + @Override + public long getPeriodDuration(int index) { + int sourceIndex = getSourceIndexForPeriod(index); + return timelines[sourceIndex].getPeriodDuration(sourceIndex); + } + + @Override + public Object getPeriodId(int index) { + int sourceIndex = getSourceIndexForPeriod(index); + int firstPeriodIndexInSource = getFirstPeriodIndexInSource(index); + return timelines[sourceIndex].getPeriodId(index - firstPeriodIndexInSource); + } + + @Override + public int getIndexOfPeriod(Object id) { + for (int sourceIndex = 0; sourceIndex < timelines.length; sourceIndex++) { + int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(id); + if (periodIndexInSource != NO_PERIOD_INDEX) { + int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); + return firstPeriodIndexInSource + periodIndexInSource; + } + } + return NO_PERIOD_INDEX; + } + + @Override + public Object getManifest() { + return manifests; + } + + private int getSourceIndexForPeriod(int periodIndex) { + return Util.binarySearchFloor(sourceOffsets, periodIndex, true, false) + 1; + } + + private int getFirstPeriodIndexInSource(int sourceIndex) { + return sourceIndex == 0 ? 0 : sourceIndex > sourceOffsets.length + ? Timeline.NO_PERIOD_INDEX : sourceOffsets[sourceIndex - 1]; + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 9628aa6ff8..f9889a76e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -180,13 +180,13 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, // MediaSource implementation. @Override - public void prepareSource() { - // do nothing + public void prepareSource(InvalidationListener listener) { + listener.onTimelineChanged(new SinglePeriodTimeline(this)); } @Override - public int getPeriodCount() { - return 1; + public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) { + return oldPlayingPeriodIndex; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 67edb80896..489128b964 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -23,28 +23,44 @@ import java.io.IOException; public interface MediaSource { /** - * Returned by {@link #getPeriodCount()} if the number of periods is not known. + * Listener for invalidation events. */ - int UNKNOWN_PERIOD_COUNT = -1; + interface InvalidationListener { + + /** + * Invoked when the timeline is invalidated. + *
+ * May only be called on the player's thread. + * + * @param timeline The new timeline. + */ + void onTimelineChanged(Timeline timeline); + + } /** * Starts preparation of the source. + * + * @param listener The listener for source invalidation events. */ - void prepareSource(); + void prepareSource(InvalidationListener listener); /** - * Returns the number of periods in the source, or {@link #UNKNOWN_PERIOD_COUNT} if the number - * of periods is not yet known. + * Returns the period index to play in this source's new timeline. + * + * @param oldPlayingPeriodIndex The period index that was being played in the old timeline. + * @param oldTimeline The old timeline. + * @return The period index to play in this source's new timeline. + * @throws IOException Thrown if the required period can't be loaded. */ - int getPeriodCount(); + int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) throws IOException; /** * 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. * - * @param index The index of the period. Must be less than {@link #getPeriodCount()} unless the - * period count is {@link #UNKNOWN_PERIOD_COUNT}. - * @return A {@link MediaPeriod}, or {@code null} if the source at the specified index is not yet + * @param index The index of the period. + * @return A {@link MediaPeriod}, or {@code null} if the source at the specified index is not * available. * @throws IOException If there is an error that's preventing the source from becoming prepared or * creating periods. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 97a92e27f5..3adebe2ebc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -22,38 +22,47 @@ import java.io.IOException; /** * Merges multiple {@link MediaPeriod} instances. *
- * The {@link MediaSource}s being merged must have known and equal period counts, and may not return - * {@code null} from {@link #createPeriod(int)}. + * The {@link MediaSource}s being merged must have final timelines and equal period counts. */ public final class MergingMediaSource implements MediaSource { private final MediaSource[] mediaSources; - private final int periodCount; + + private int periodCount; /** * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { this.mediaSources = mediaSources; - periodCount = mediaSources[0].getPeriodCount(); - Assertions.checkState(periodCount != UNKNOWN_PERIOD_COUNT, - "Child sources must have known period counts"); - for (MediaSource mediaSource : mediaSources) { - Assertions.checkState(mediaSource.getPeriodCount() == periodCount, - "Child sources must have equal period counts"); + periodCount = -1; + } + + @Override + public void prepareSource(final InvalidationListener listener) { + mediaSources[0].prepareSource(new InvalidationListener() { + @Override + public void onTimelineChanged(Timeline timeline) { + checkConsistentTimeline(timeline); + + // All source timelines must match. + listener.onTimelineChanged(timeline); + } + }); + for (int i = 1; i < mediaSources.length; i++) { + mediaSources[i].prepareSource(new InvalidationListener() { + @Override + public void onTimelineChanged(Timeline timeline) { + checkConsistentTimeline(timeline); + } + }); } } @Override - public void prepareSource() { - for (MediaSource mediaSource : mediaSources) { - mediaSource.prepareSource(); - } - } - - @Override - public int getPeriodCount() { - return periodCount; + public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) + throws IOException { + return mediaSources[0].getNewPlayingPeriodIndex(oldPlayingPeriodIndex, oldTimeline); } @Override @@ -73,4 +82,14 @@ public final class MergingMediaSource implements MediaSource { } } + private void checkConsistentTimeline(Timeline timeline) { + Assertions.checkArgument(timeline.isFinal()); + int periodCount = timeline.getPeriodCount(); + if (this.periodCount == -1) { + this.periodCount = periodCount; + } else { + Assertions.checkState(this.periodCount == periodCount); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java new file mode 100644 index 0000000000..56dff03614 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link Timeline} consisting of a single period. + */ +public final class SinglePeriodTimeline implements Timeline { + + private final Object id; + private final Object manifest; + private final long duration; + + /** + * Creates a new timeline with one period of unknown duration. + * + * @param id The identifier for the period. + */ + public SinglePeriodTimeline(Object id) { + this(id, null); + } + + /** + * Creates a new timeline with one period of unknown duration providing an optional manifest. + * + * @param id The identifier for the period. + * @param manifest The source-specific manifest that defined the period, or {@code null}. + */ + public SinglePeriodTimeline(Object id, Object manifest) { + this(id, manifest, ExoPlayer.UNKNOWN_TIME); + } + + /** + * Creates a new timeline with one period of the specified duration providing an optional + * manifest. + * + * @param id The identifier for the period. + * @param manifest The source-specific manifest that defined the period, or {@code null}. + * @param duration The duration of the period in milliseconds. + */ + public SinglePeriodTimeline(Object id, Object manifest, long duration) { + this.id = Assertions.checkNotNull(id); + this.manifest = manifest; + this.duration = duration; + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public boolean isFinal() { + return true; + } + + @Override + public long getPeriodDuration(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException("Index " + index + " out of bounds"); + } + return duration; + } + + @Override + public Object getPeriodId(int index) { + return index == 0 ? id : null; + } + + @Override + public int getIndexOfPeriod(Object id) { + return id.equals(this.id) ? 0 : Timeline.NO_PERIOD_INDEX; + } + + @Override + public Object getManifest() { + return manifest; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index ca9526c4f3..da556f2b67 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -115,13 +115,13 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, // MediaSource implementation. @Override - public void prepareSource() { - // do nothing + public void prepareSource(InvalidationListener listener) { + listener.onTimelineChanged(new SinglePeriodTimeline(this)); } @Override - public int getPeriodCount() { - return 1; + public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) { + return oldPlayingPeriodIndex; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/Timeline.java b/library/src/main/java/com/google/android/exoplayer2/source/Timeline.java new file mode 100644 index 0000000000..12ce6a6ba3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/Timeline.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.ExoPlayer; + +/** + * The player's timeline consisting of one or more periods. Instances are immutable. + */ +public interface Timeline { + + /** + * Returned by {@link #getPeriodCount()} when the number of periods is not known. + */ + int UNKNOWN_PERIOD_COUNT = -1; + + /** + * Returned by {@link #getIndexOfPeriod(Object)} if no period index corresponds to the specified + * identifier. + */ + int NO_PERIOD_INDEX = -1; + + /** + * Returns the number of periods in the timeline, or {@link #UNKNOWN_PERIOD_COUNT} if not known. + * If {@link #isFinal()} returns {@code true}, the number of periods must be known. + */ + int getPeriodCount(); + + /** + * Returns whether the timeline is final, which means it will not be invalidated again. + */ + boolean isFinal(); + + /** + * Returns the duration of the period at {@code index} in the timeline, in milliseconds, or + * {@link ExoPlayer#UNKNOWN_TIME} if not known. + * + * @param index The index of the period. + * @return The duration of the period in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME}. + */ + long getPeriodDuration(int index); + + /** + * Returns a unique identifier for the period at {@code index}, or {@code null} if the period at + * {@code index} is not known. The identifier is stable across {@link Timeline} instances. + *
+ * When a source is invalidated the player uses period identifiers to determine what periods are
+ * unchanged. Implementations that associate an object with each period can return the object for
+ * the provided index to guarantee uniqueness. Other implementations must be careful to return
+ * identifiers that can't clash with (for example) identifiers used by other timelines that may be
+ * concatenated with this one.
+ *
+ * @param index A period index.
+ * @return An identifier for the period, or {@code null} if the period is not known.
+ */
+ Object getPeriodId(int index);
+
+ /**
+ * Returns the index of the period identified by {@code id}, or {@link #NO_PERIOD_INDEX} if the
+ * period is not in the timeline.
+ *
+ * @param id An identifier for a period.
+ * @return The index of the period, or {@link #NO_PERIOD_INDEX} if the period was not found.
+ */
+ int getIndexOfPeriod(Object id);
+
+ /**
+ * Returns the immutable manifest corresponding to this timeline.
+ */
+ Object getManifest();
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index e2c5905a40..a1a20decd7 100644
--- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -21,6 +21,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.Timeline;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
@@ -62,6 +63,7 @@ public final class DashMediaSource implements MediaSource {
private final DashManifestParser manifestParser;
private final ManifestCallback manifestCallback;
+ private MediaSource.InvalidationListener invalidationListener;
private DataSource dataSource;
private Loader loader;
@@ -95,7 +97,8 @@ public final class DashMediaSource implements MediaSource {
// MediaSource implementation.
@Override
- public void prepareSource() {
+ public void prepareSource(InvalidationListener listener) {
+ invalidationListener = listener;
dataSource = manifestDataSourceFactory.createDataSource();
loader = new Loader("Loader:DashMediaSource");
manifestRefreshHandler = new Handler();
@@ -103,11 +106,22 @@ public final class DashMediaSource implements MediaSource {
}
@Override
- public int getPeriodCount() {
- if (manifest == null) {
- return UNKNOWN_PERIOD_COUNT;
+ public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) {
+ int periodIndex = oldPlayingPeriodIndex;
+ int oldPeriodCount = oldTimeline.getPeriodCount();
+ while (oldPeriodCount == Timeline.UNKNOWN_PERIOD_COUNT || periodIndex < oldPeriodCount) {
+ Object id = oldTimeline.getPeriodId(periodIndex);
+ if (id == null) {
+ break;
+ }
+ for (int i = 0; i < periods.length; i++) {
+ if (periods[i] == id) {
+ return i;
+ }
+ }
+ periodIndex++;
}
- return manifest.getPeriodCount();
+ return Timeline.NO_PERIOD_INDEX;
}
@Override
@@ -161,6 +175,8 @@ public final class DashMediaSource implements MediaSource {
}
scheduleManifestRefresh();
}
+
+ invalidationListener.onTimelineChanged(new DashTimeline(manifest, periods));
}
/* package */ int onManifestLoadError(ParsingLoadable
+ * The search is performed using a binary search algorithm, and so the array must be sorted.
+ *
+ * @param a The array to search.
+ * @param key The key being searched for.
+ * @param inclusive If the key is present in the array, whether to return the corresponding index.
+ * If false then the returned index corresponds to the largest value in the array that is
+ * strictly less than the key.
+ * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than
+ * the smallest value in the array. If false then -1 will be returned.
+ */
+ public static int binarySearchFloor(int[] a, int key, boolean inclusive, boolean stayInBounds) {
+ int index = Arrays.binarySearch(a, key);
+ index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1));
+ return stayInBounds ? Math.max(0, index) : index;
+ }
+
/**
* Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key.
diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
index f038c0a5e7..52fcbee20a 100644
--- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
+++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java
@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.Timeline;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.util.Util;
@@ -34,6 +35,7 @@ import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;
+
import junit.framework.Assert;
@@ -212,6 +214,11 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen
// Do nothing.
}
+ @Override
+ public final void onTimelineChanged(Timeline timeline) {
+ // Do nothing.
+ }
+
// SimpleExoPlayer.DebugListener
@Override