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 699851b37b..8dcaa01d90 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 @@ -91,11 +91,17 @@ import java.util.Locale; } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { boolean isFinal = timeline.isFinal(); int periodCount = timeline.getPeriodCount(); - Log.d(TAG, "timelineChanged [" + isFinal + ", " - + (periodCount == Timeline.UNKNOWN_PERIOD_COUNT ? "?" : periodCount) + "]"); + int seekWindowCount = timeline.getSeekWindowCount(); + Log.d(TAG, "sourceInfo[isFinal=" + isFinal + ", periodCount=" + + (periodCount == Timeline.UNKNOWN_PERIOD_COUNT ? "?" : periodCount) + ", seekWindows: " + + seekWindowCount); + for (int seekWindowIndex = 0; seekWindowIndex < seekWindowCount; seekWindowIndex++) { + Log.d(TAG, " " + timeline.getSeekWindow(seekWindowIndex)); + } + Log.d(TAG, "]"); } @Override 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 aad3ead6db..87098c5705 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 @@ -410,7 +410,7 @@ public class PlayerActivity extends Activity implements OnKeyListener, OnTouchLi } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. } 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 aa7a6a2630..328f60e116 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 @@ -101,7 +101,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. } 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 59dbf3de06..dd210b9268 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 @@ -101,7 +101,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. } 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 5ca5953925..08f43b43b9 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 @@ -120,7 +120,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. } 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 3a23c00699..9739e4048d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -140,11 +140,12 @@ public interface ExoPlayer { void onPositionDiscontinuity(int periodIndex, long positionMs); /** - * Called when the timeline changes. + * Called when manifest and/or timeline has been refreshed. * - * @param timeline The new timeline. + * @param timeline The source's timeline. + * @param manifest The loaded manifest. */ - void onTimelineChanged(Timeline timeline); + void onSourceInfoRefreshed(Timeline timeline, Object manifest); /** * Called when an error occurs. The playback state will transition to @@ -375,6 +376,12 @@ public interface ExoPlayer { */ Timeline getCurrentTimeline(); + /** + * Returns the current manifest. The type depends on the {@link MediaSource} passed to + * {@link #setMediaSource(MediaSource)} or {@link #setMediaSource(MediaSource, boolean)}. + */ + Object getCurrentManifest(); + /** * Returns an estimate of the absolute position in milliseconds up to which data is buffered, * or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available. 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 e39efd0f11..0461465672 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.Log; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.Timeline; @@ -44,6 +45,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private int pendingSeekAcks; private boolean isLoading; private Timeline timeline; + private Object manifest; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -206,6 +208,11 @@ import java.util.concurrent.CopyOnWriteArraySet; return timeline; } + @Override + public Object getCurrentManifest() { + return manifest; + } + @Override public long getBufferedPosition() { if (pendingSeekAcks == 0) { @@ -272,10 +279,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } break; } - case ExoPlayerImplInternal.MSG_TIMELINE_CHANGED: { - timeline = (Timeline) msg.obj; + case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { + @SuppressWarnings("unchecked") + Pair timelineAndManifest = (Pair) msg.obj; + timeline = timelineAndManifest.first; + manifest = timelineAndManifest.second; for (EventListener listener : listeners) { - listener.onTimelineChanged(timeline); + listener.onSourceInfoRefreshed(timeline, manifest); } break; } 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 298abdaf8c..aa59325860 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -43,7 +43,7 @@ import java.util.ArrayList; * Implements the internal behavior of {@link ExoPlayerImpl}. */ /* package */ final class ExoPlayerImplInternal implements Handler.Callback, MediaPeriod.Callback, - TrackSelector.InvalidationListener, MediaSource.InvalidationListener { + TrackSelector.InvalidationListener, MediaSource.Listener { /** * Playback position information which is read on the application's thread by @@ -73,7 +73,7 @@ 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_POSITION_DISCONTINUITY = 5; - public static final int MSG_TIMELINE_CHANGED = 6; + public static final int MSG_SOURCE_INFO_REFRESHED = 6; public static final int MSG_ERROR = 7; // Internal messages @@ -309,12 +309,14 @@ import java.util.ArrayList; } } - // MediaSource.InvalidationListener implementation. + // MediaSource.Listener implementation. @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { try { - handleSourceInvalidated(timeline); + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, Pair.create(timeline, manifest)) + .sendToTarget(); + handleTimelineRefreshed(timeline); } catch (ExoPlaybackException | IOException e) { Log.e(TAG, "Error handling timeline change.", e); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); @@ -802,10 +804,9 @@ import java.util.ArrayList; } } - public void handleSourceInvalidated(Timeline timeline) throws ExoPlaybackException, IOException { + public void handleTimelineRefreshed(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) { 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 2c6cb16e93..27b51d0e40 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -404,6 +404,11 @@ public final class SimpleExoPlayer implements ExoPlayer { return player.getCurrentTimeline(); } + @Override + public Object getCurrentManifest() { + return player.getCurrentManifest(); + } + @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 f8bacd8bf6..9521267154 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; /** @@ -26,6 +27,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final Timeline[] timelines; + private final Object[] manifests; private ConcatenatedTimeline timeline; @@ -35,25 +37,29 @@ public final class ConcatenatingMediaSource implements MediaSource { public ConcatenatingMediaSource(MediaSource... mediaSources) { this.mediaSources = mediaSources; timelines = new Timeline[mediaSources.length]; + manifests = new Object[mediaSources.length]; } @Override - public void prepareSource(final InvalidationListener listener) { + public void prepareSource(final Listener listener) { for (int i = 0; i < mediaSources.length; i++) { final int index = i; - mediaSources[i].prepareSource(new InvalidationListener() { + mediaSources[i].prepareSource(new Listener() { + @Override - public void onTimelineChanged(Timeline timeline) { - timelines[index] = timeline; + public void onSourceInfoRefreshed(Timeline sourceTimeline, Object manifest) { + timelines[index] = sourceTimeline; + manifests[index] = manifest; for (int i = 0; i < timelines.length; i++) { if (timelines[i] == null) { // Don't invoke the listener until all sources have timelines. return; } } - ConcatenatingMediaSource.this.timeline = new ConcatenatedTimeline(timelines.clone()); - listener.onTimelineChanged(ConcatenatingMediaSource.this.timeline); + timeline = new ConcatenatedTimeline(timelines.clone()); + listener.onSourceInfoRefreshed(timeline, manifests.clone()); } + }); } } @@ -100,45 +106,44 @@ public final class ConcatenatingMediaSource implements MediaSource { 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; + private final int[] sourceOffsets; + private final SeekWindow[] seekWindows; public ConcatenatedTimeline(Timeline[] timelines) { - this.timelines = timelines; - - int[] sourceOffsets = new int[timelines.length]; - int sourceIndexOffset = 0; - for (int i = 0; i < timelines.length; i++) { - int periodCount = timelines[i].getPeriodCount(); - if (periodCount == 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]; + int[] sourceOffsets = new int[timelines.length]; + int sourceOffset = 0; + ArrayList concatenatedSeekWindows = new ArrayList<>(); for (int i = 0; i < timelines.length; i++) { Timeline timeline = timelines[i]; - if (timeline != null) { - manifests[i] = timeline.getManifest(); - if (!timeline.isFinal()) { - isFinal = false; - } + isFinal &= timeline.isFinal(); + // Offset the seek windows so they are relative to the source. + int seekWindowCount = timeline.getSeekWindowCount(); + for (int j = 0; j < seekWindowCount; j++) { + SeekWindow sourceSeekWindow = timeline.getSeekWindow(j); + concatenatedSeekWindows.add(sourceSeekWindow.copyOffsetByPeriodCount(sourceOffset)); } + int periodCount = timeline.getPeriodCount(); + if (periodCount == Timeline.UNKNOWN_PERIOD_COUNT) { + sourceOffsets = Arrays.copyOf(sourceOffsets, i); + isFinal = false; + break; + } + sourceOffset += periodCount; + sourceOffsets[i] = sourceOffset; } + this.timelines = timelines; this.isFinal = isFinal; + this.sourceOffsets = sourceOffsets; + seekWindows = + concatenatedSeekWindows.toArray(new SeekWindow[concatenatedSeekWindows.size()]); } @Override public int getPeriodCount() { - return count; + return sourceOffsets.length == timelines.length ? sourceOffsets[sourceOffsets.length - 1] + : UNKNOWN_PERIOD_COUNT; } @Override @@ -172,8 +177,13 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public Object getManifest() { - return manifests; + public int getSeekWindowCount() { + return seekWindows.length; + } + + @Override + public SeekWindow getSeekWindow(int index) { + return seekWindows[index]; } private int getSourceIndexForPeriod(int periodIndex) { 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 152b5dcd14..7577b41cdf 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 @@ -114,6 +114,7 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, private final Handler eventHandler; private final EventListener eventListener; + private MediaSource.Listener sourceListener; private DataSource dataSource; private Loader loader; private ExtractorHolder extractorHolder; @@ -178,8 +179,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, // MediaSource implementation. @Override - public void prepareSource(InvalidationListener listener) { - listener.onTimelineChanged(new SinglePeriodTimeline(this)); + public void prepareSource(MediaSource.Listener listener) { + sourceListener = listener; + listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null); } @Override @@ -200,7 +202,7 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, @Override public void releaseSource() { - // do nothing + sourceListener = null; } // MediaPeriod implementation. @@ -500,6 +502,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, tracks = new TrackGroupArray(trackArray); prepared = true; callback.onPeriodPrepared(this); + sourceListener.onSourceInfoRefreshed(seekMap.isSeekable() + ? SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs) + : SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs), null); } private void copyLengthFromLoader(ExtractingLoadable loadable) { 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 8e8682d11c..2617f0488a 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,18 +23,17 @@ import java.io.IOException; public interface MediaSource { /** - * Listener for invalidation events. + * Listener for source events. */ - interface InvalidationListener { + interface Listener { /** - * Called when the timeline is invalidated. - *

- * May only be called on the player's thread. + * Called when manifest and/or timeline has been refreshed. * - * @param timeline The new timeline. + * @param timeline The source's timeline. + * @param manifest The loaded manifest. */ - void onTimelineChanged(Timeline timeline); + void onSourceInfoRefreshed(Timeline timeline, Object manifest); } @@ -68,14 +67,15 @@ public interface MediaSource { this.periodIndex = periodIndex; this.positionUs = positionUs; } + } /** * Starts preparation of the source. * - * @param listener The listener for source invalidation events. + * @param listener The listener for source events. */ - void prepareSource(InvalidationListener listener); + void prepareSource(Listener listener); /** * Returns the period index to play in this source's new timeline. 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 cfffafbd14..3b32348445 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 @@ -38,22 +38,26 @@ public final class MergingMediaSource implements MediaSource { } @Override - public void prepareSource(final InvalidationListener listener) { - mediaSources[0].prepareSource(new InvalidationListener() { + public void prepareSource(final Listener listener) { + mediaSources[0].prepareSource(new Listener() { + @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { checkConsistentTimeline(timeline); // All source timelines must match. - listener.onTimelineChanged(timeline); + listener.onSourceInfoRefreshed(timeline, manifest); } + }); for (int i = 1; i < mediaSources.length; i++) { - mediaSources[i].prepareSource(new InvalidationListener() { + mediaSources[i].prepareSource(new Listener() { + @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { checkConsistentTimeline(timeline); } + }); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SeekWindow.java b/library/src/main/java/com/google/android/exoplayer2/source/SeekWindow.java new file mode 100644 index 0000000000..14ce0f9529 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/SeekWindow.java @@ -0,0 +1,114 @@ +/* + * 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; + +/** + * A window of times the player can seek to. + */ +public final class SeekWindow { + + public static final SeekWindow UNSEEKABLE = new SeekWindow(0); + + /** + * The period index at the start of the window. + */ + public final int startPeriodIndex; + /** + * The time at the start of the window relative to the start of the period at + * {@link #startPeriodIndex}, in microseconds. + */ + public final long startTimeUs; + /** + * The period index at the end of the window. + */ + public final int endPeriodIndex; + /** + * The time at the end of the window relative to the start of the period at + * {@link #endPeriodIndex}, in microseconds. + */ + public final long endTimeUs; + + /** + * Constructs a new {@link SeekWindow} containing times from zero up to {@code durationUs} in the + * first period. + * + * @param durationUs The duration of the window, in microseconds. + */ + public SeekWindow(long durationUs) { + this(0, 0, 0, durationUs); + } + + /** + * Constructs a new {@link SeekWindow} representing the specified time range. + * + * @param startPeriodIndex The index of the period containing the start of the window. + * @param startTimeUs The start time of the window in microseconds, relative to the start of the + * specified start period. + * @param endPeriodIndex The index of the period containing the end of the window. + * @param endTimeUs The end time of the window in microseconds, relative to the start of the + * specified end period. + */ + public SeekWindow(int startPeriodIndex, long startTimeUs, int endPeriodIndex, long endTimeUs) { + this.startPeriodIndex = startPeriodIndex; + this.startTimeUs = startTimeUs; + this.endPeriodIndex = endPeriodIndex; + this.endTimeUs = endTimeUs; + } + + /** + * Returns a new seek window that is offset by the specified number of periods. + * + * @param periodCount The number of periods to add to {@link #startPeriodIndex} and + * {@link #endPeriodIndex} when constructing the copy. + * @return A new seek window that is offset by the specified number of periods. + */ + public SeekWindow copyOffsetByPeriodCount(int periodCount) { + return new SeekWindow(startPeriodIndex + periodCount, startTimeUs, endPeriodIndex + periodCount, + endTimeUs); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + startPeriodIndex; + result = 31 * result + (int) startTimeUs; + result = 31 * result + endPeriodIndex; + result = 31 * result + (int) endTimeUs; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekWindow other = (SeekWindow) obj; + return other.startPeriodIndex == startPeriodIndex + && other.startTimeUs == startTimeUs + && other.endPeriodIndex == endPeriodIndex + && other.endTimeUs == endTimeUs; + } + + @Override + public String toString() { + return "SeekWindow[" + startPeriodIndex + ", " + startTimeUs + ", " + endPeriodIndex + ", " + + endTimeUs + "]"; + } + +} 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 index 56dff03614..d1a368ccb9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.source; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; /** @@ -23,41 +23,50 @@ import com.google.android.exoplayer2.util.Assertions; */ public final class SinglePeriodTimeline implements Timeline { + /** + * Returns a new timeline with one period of unknown duration and no seek window. + * + * @param id The identifier for the period. + * @return A new timeline with one period of unknown duration. + */ + public static Timeline createNonFinalTimeline(Object id) { + return new SinglePeriodTimeline(id, false, C.UNSET_TIME_US); + } + + /** + * Creates a final timeline with one period of known duration and an empty seek window. + * + * @param id The identifier for the period. + * @param durationUs The duration of the period, in microseconds. + * @return A new, unseekable, final timeline with one period. + */ + public static Timeline createUnseekableFinalTimeline(Object id, long durationUs) { + return new SinglePeriodTimeline(id, true, durationUs, SeekWindow.UNSEEKABLE); + } + + /** + * Creates a final timeline with one period of known duration and a seek window extending from + * zero to its duration. + * + * @param id The identifier for the period. + * @param durationUs The duration of the period, in microseconds. + * @return A new, seekable, final timeline with one period. + */ + public static Timeline createSeekableFinalTimeline(Object id, long durationUs) { + return new SinglePeriodTimeline(id, true, durationUs, new SeekWindow(durationUs)); + } + private final Object id; - private final Object manifest; + private final boolean isFinal; private final long duration; + private final SeekWindow[] seekWindows; - /** - * 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) { + private SinglePeriodTimeline(Object id, boolean isFinal, long duration, + SeekWindow... seekWindows) { this.id = Assertions.checkNotNull(id); - this.manifest = manifest; + this.isFinal = isFinal; this.duration = duration; + this.seekWindows = seekWindows; } @Override @@ -67,7 +76,7 @@ public final class SinglePeriodTimeline implements Timeline { @Override public boolean isFinal() { - return true; + return isFinal; } @Override @@ -89,8 +98,13 @@ public final class SinglePeriodTimeline implements Timeline { } @Override - public Object getManifest() { - return manifest; + public int getSeekWindowCount() { + return seekWindows.length; + } + + @Override + public SeekWindow getSeekWindow(int index) { + return seekWindows[index]; } } 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 54d1ef5dc1..583c1b389e 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 @@ -113,8 +113,9 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, // MediaSource implementation. @Override - public void prepareSource(InvalidationListener listener) { - listener.onTimelineChanged(new SinglePeriodTimeline(this)); + public void prepareSource(MediaSource.Listener listener) { + Timeline timeline = SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs); + listener.onSourceInfoRefreshed(timeline, null); } @Override @@ -135,7 +136,7 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, @Override public void releaseSource() { - // do nothing + // Do nothing. } // MediaPeriod implementation. 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 index 12ce6a6ba3..b2c6ce2031 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/Timeline.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/Timeline.java @@ -40,7 +40,7 @@ public interface Timeline { int getPeriodCount(); /** - * Returns whether the timeline is final, which means it will not be invalidated again. + * Returns whether the timeline is final, which means it will not change. */ boolean isFinal(); @@ -57,7 +57,7 @@ public interface Timeline { * 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 + * When the timeline changes 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 @@ -78,8 +78,15 @@ public interface Timeline { int getIndexOfPeriod(Object id); /** - * Returns the immutable manifest corresponding to this timeline. + * Returns the number of seek windows that can be accessed via {@link #getSeekWindow(int)}. */ - Object getManifest(); + int getSeekWindowCount(); + + /** + * Returns the {@link SeekWindow} at {@code index}, which represents positions that can be seeked + * to in the timeline. The seek windows may change when + * {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} is called. + */ + SeekWindow getSeekWindow(int index); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index bd171a41c9..4e5d5a3b71 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -78,7 +78,6 @@ public class ChunkSampleStream implements SampleStream, S mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(allocator); - pendingResetPositionUs = C.UNSET_TIME_US; lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index f111b5539f..c960d7e22f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -124,8 +124,7 @@ import java.util.List; newSampleStreamArray(newEnabledSourceCount); int newEnabledSourceIndex = 0; - // Iterate over currently enabled streams, either releasing them or adding them to the new - // list. + // Iterate over currently enabled streams, either releasing them or adding them to the new list. for (ChunkSampleStream sampleStream : sampleStreams) { if (oldStreams.contains(sampleStream)) { sampleStream.release(); @@ -137,8 +136,7 @@ import java.util.List; // Instantiate and return new streams. SampleStream[] streamsToReturn = new SampleStream[newSelections.size()]; for (int i = 0; i < newSelections.size(); i++) { - newSampleStreams[newEnabledSourceIndex] = - buildSampleStream(newSelections.get(i), positionUs); + newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs); streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex]; newEnabledSourceIndex++; } 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 e5f41e1194..04c5b5dbd9 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 @@ -25,9 +25,11 @@ 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.SeekWindow; 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.Period; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; @@ -52,6 +54,17 @@ public final class DashMediaSource implements MediaSource { * The default minimum number of times to retry loading data prior to failing. */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * The interval in milliseconds between invocations of + * {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} when the source's + * {@link SeekWindow} is changing dynamically (for example, for incomplete live streams). + */ + private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; + /** + * The offset in microseconds subtracted from the live edge position when calculating the default + * position returned by {@link #getDefaultStartPosition(int)}. + */ + private static final long LIVE_EDGE_OFFSET_US = 30000000; private static final String TAG = "DashMediaSource"; @@ -62,8 +75,9 @@ public final class DashMediaSource implements MediaSource { private final DashManifestParser manifestParser; private final ManifestCallback manifestCallback; private final Object manifestUriLock; + private final Runnable refreshSourceInfoRunnable; - private MediaSource.InvalidationListener invalidationListener; + private MediaSource.Listener sourceListener; private DataSource dataSource; private Loader loader; @@ -71,9 +85,10 @@ public final class DashMediaSource implements MediaSource { private long manifestLoadStartTimestamp; private long manifestLoadEndTimestamp; private DashManifest manifest; - private Handler manifestRefreshHandler; + private Handler handler; private ArrayList periods; - private long elapsedRealtimeOffset; + private SeekWindow seekWindow; + private long elapsedRealtimeOffsetMs; public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, @@ -93,6 +108,12 @@ public final class DashMediaSource implements MediaSource { manifestParser = new DashManifestParser(); manifestCallback = new ManifestCallback(); manifestUriLock = new Object(); + refreshSourceInfoRunnable = new Runnable() { + @Override + public void run() { + refreshSourceInfo(); + } + }; } /** @@ -109,11 +130,11 @@ public final class DashMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(InvalidationListener listener) { - invalidationListener = listener; + public void prepareSource(MediaSource.Listener listener) { + sourceListener = listener; dataSource = manifestDataSourceFactory.createDataSource(); loader = new Loader("Loader:DashMediaSource"); - manifestRefreshHandler = new Handler(); + handler = new Handler(); startLoadingManifest(); } @@ -137,10 +158,21 @@ public final class DashMediaSource implements MediaSource { @Override public Position getDefaultStartPosition(int index) { + if (seekWindow == null) { + return null; + } + 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); + // The stream is live, so return a position a position offset from the live edge. + int periodIndex = seekWindow.endPeriodIndex; + long positionUs = seekWindow.endTimeUs - LIVE_EDGE_OFFSET_US; + while (positionUs < 0 && periodIndex > seekWindow.startPeriodIndex) { + periodIndex--; + positionUs += manifest.getPeriodDuration(periodIndex) * 1000; + } + positionUs = Math.max(positionUs, + periodIndex == seekWindow.startPeriodIndex ? seekWindow.startTimeUs : 0); + return new Position(periodIndex, positionUs); } return new Position(index, 0); } @@ -164,11 +196,11 @@ public final class DashMediaSource implements MediaSource { manifestLoadStartTimestamp = 0; manifestLoadEndTimestamp = 0; manifest = null; - if (manifestRefreshHandler != null) { - manifestRefreshHandler.removeCallbacksAndMessages(null); - manifestRefreshHandler = null; + if (handler != null) { + handler.removeCallbacksAndMessages(null); + handler = null; } - elapsedRealtimeOffset = 0; + elapsedRealtimeOffsetMs = 0; } // Loadable callbacks. @@ -312,7 +344,7 @@ public final class DashMediaSource implements MediaSource { } private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { - this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs; + this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; finishManifestProcessing(); } @@ -329,13 +361,49 @@ public final class DashMediaSource implements MediaSource { int periodCount = manifest.getPeriodCount(); for (int i = periods.size(); i < periodCount; i++) { periods.add(new DashMediaPeriod(manifest, i, chunkSourceFactory, minLoadableRetryCount, - eventDispatcher, elapsedRealtimeOffset, loader)); + eventDispatcher, elapsedRealtimeOffsetMs, loader)); } - invalidationListener.onTimelineChanged(new DashTimeline(manifest, - periods.toArray(new DashMediaPeriod[periods.size()]))); + + handler.removeCallbacks(refreshSourceInfoRunnable); + refreshSourceInfo(); scheduleManifestRefresh(); } + private void refreshSourceInfo() { + // Update the seek window. + int periodCount = manifest.getPeriodCount(); + int lastPeriodIndex = periodCount - 1; + PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0), + manifest.getPeriodDuration(0) * 1000); + PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo( + manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDuration(lastPeriodIndex) * 1000); + long currentStartTimeUs; + long currentEndTimeUs; + if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { + // The seek window is changing so post a Runnable to update it. + handler.postDelayed(refreshSourceInfoRunnable, NOTIFY_MANIFEST_INTERVAL_MS); + + long minStartPositionUs = firstPeriodSeekInfo.availableStartTimeUs; + long maxEndPositionUs = lastPeriodSeekInfo.availableEndTimeUs; + long timeShiftBufferDepthUs = manifest.timeShiftBufferDepth == -1 ? -1 + : manifest.timeShiftBufferDepth * 1000; + currentEndTimeUs = Math.min(maxEndPositionUs, + getNowUnixTimeUs() - manifest.availabilityStartTime * 1000); + currentStartTimeUs = timeShiftBufferDepthUs == -1 ? minStartPositionUs + : Math.max(minStartPositionUs, currentEndTimeUs - timeShiftBufferDepthUs); + } else { + handler.removeCallbacks(refreshSourceInfoRunnable); + currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs; + currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs; + } + seekWindow = new SeekWindow(0, currentStartTimeUs, lastPeriodIndex, currentEndTimeUs); + + DashMediaPeriod[] mediaPeriods = + periods.toArray(new DashMediaPeriod[manifest.getPeriodCount()]); + sourceListener.onSourceInfoRefreshed(new DashTimeline(manifest, mediaPeriods, seekWindow), + manifest); + } + private void scheduleManifestRefresh() { if (!manifest.dynamic) { return; @@ -350,7 +418,7 @@ public final class DashMediaSource implements MediaSource { } long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); - manifestRefreshHandler.postDelayed(new Runnable() { + handler.postDelayed(new Runnable() { @Override public void run() { startLoadingManifest(); @@ -364,14 +432,65 @@ public final class DashMediaSource implements MediaSource { eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } + private long getNowUnixTimeUs() { + if (elapsedRealtimeOffsetMs != 0) { + return SystemClock.elapsedRealtime() * 1000 + elapsedRealtimeOffsetMs; + } else { + return System.currentTimeMillis() * 1000; + } + } + + private static final class PeriodSeekInfo { + + public static PeriodSeekInfo createPeriodSeekInfo(Period period, long durationUs) { + int adaptationSetCount = period.adaptationSets.size(); + long availableStartTimeUs = 0; + long availableEndTimeUs = Long.MAX_VALUE; + boolean isIndexExplicit = false; + for (int i = 0; i < adaptationSetCount; i++) { + DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); + if (index == null) { + return new PeriodSeekInfo(true, 0, durationUs); + } + int firstSegmentNum = index.getFirstSegmentNum(); + int lastSegmentNum = index.getLastSegmentNum(durationUs); + isIndexExplicit |= index.isExplicit(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } else { + // The available end time is unmodified, because this index is unbounded. + } + } + return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); + } + + public final boolean isIndexExplicit; + public final long availableStartTimeUs; + public final long availableEndTimeUs; + + private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs, + long availableEndTimeUs) { + this.isIndexExplicit = isIndexExplicit; + this.availableStartTimeUs = availableStartTimeUs; + this.availableEndTimeUs = availableEndTimeUs; + } + + } + private static final class DashTimeline implements Timeline { private final DashManifest manifest; private final DashMediaPeriod[] periods; + private final SeekWindow[] seekWindows; - public DashTimeline(DashManifest manifest, DashMediaPeriod[] periods) { + public DashTimeline(DashManifest manifest, DashMediaPeriod[] periods, SeekWindow seekWindow) { this.manifest = manifest; this.periods = periods; + seekWindows = new SeekWindow[] {seekWindow}; } @Override @@ -405,8 +524,13 @@ public final class DashMediaSource implements MediaSource { } @Override - public Object getManifest() { - return manifest; + public int getSeekWindowCount() { + return seekWindows.length; + } + + @Override + public SeekWindow getSeekWindow(int index) { + return seekWindows[index]; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index ea2e308f93..eff4149d5b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -516,7 +516,6 @@ public class DashManifestParser extends DefaultHandler protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl, SegmentTemplate parent) throws XmlPullParserException, IOException { - long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", parent != null ? parent.presentationTimeOffset : 0); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index d065aae01c..6d6b53a451 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -67,6 +67,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final HlsPlaylistParser manifestParser; + private MediaSource.Listener sourceListener; private DataSource manifestDataSource; private Loader manifestFetcher; @@ -75,6 +76,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private long preparePositionUs; private int pendingPrepareCount; + private HlsPlaylist playlist; private boolean seenFirstTrackSelection; private long durationUs; private long pendingDiscontinuityPositionUs; @@ -108,9 +110,10 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, // MediaSource implementation. @Override - public void prepareSource(InvalidationListener listener) { + public void prepareSource(MediaSource.Listener listener) { + sourceListener = listener; // TODO: Defer until the playlist has been loaded. - listener.onTimelineChanged(new SinglePeriodTimeline(this)); + listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null); } @Override @@ -132,7 +135,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, @Override public void releaseSource() { - // do nothing + sourceListener = null; } // MediaPeriod implementation. @@ -255,6 +258,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, allocator = null; preparePositionUs = 0; pendingPrepareCount = 0; + playlist = null; seenFirstTrackSelection = false; durationUs = 0; isLive = false; @@ -277,8 +281,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, long loadDurationMs) { eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); - HlsPlaylist playlist = loadable.getResult(); - List sampleStreamWrapperList = buildSampleStreamWrappers(playlist); + playlist = loadable.getResult(); + List sampleStreamWrapperList = buildSampleStreamWrappers(); sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()]; sampleStreamWrapperList.toArray(sampleStreamWrappers); selectedTrackCounts = new int[sampleStreamWrappers.length]; @@ -330,6 +334,12 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, } trackGroups = new TrackGroupArray(trackGroupArray); callback.onPeriodPrepared(this); + + // TODO[playlists]: Calculate the seek window. + Timeline timeline = + isLive ? SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs) + : SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs); + sourceListener.onSourceInfoRefreshed(timeline, playlist); } @Override @@ -343,7 +353,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, // Internal methods. - private List buildSampleStreamWrappers(HlsPlaylist playlist) { + private List buildSampleStreamWrappers() { ArrayList sampleStreamWrappers = new ArrayList<>(); String baseUri = playlist.baseUri; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index ed032a4ff4..a04a007a08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -55,7 +55,7 @@ public final class SsMediaSource implements MediaSource, private final EventDispatcher eventDispatcher; private final SsManifestParser manifestParser; - private MediaSource.InvalidationListener invalidationListener; + private MediaSource.Listener sourceListener; private DataSource manifestDataSource; private Loader manifestLoader; @@ -87,8 +87,8 @@ public final class SsMediaSource implements MediaSource, // MediaSource implementation. @Override - public void prepareSource(InvalidationListener listener) { - this.invalidationListener = listener; + public void prepareSource(MediaSource.Listener listener) { + sourceListener = listener; manifestDataSource = dataSourceFactory.createDataSource(); manifestLoader = new Loader("Loader:Manifest"); manifestRefreshHandler = new Handler(); @@ -114,6 +114,7 @@ public final class SsMediaSource implements MediaSource, @Override public void releaseSource() { + sourceListener = null; period = null; manifest = null; manifestDataSource = null; @@ -140,13 +141,13 @@ public final class SsMediaSource implements MediaSource, if (period == null) { period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, eventDispatcher, manifestLoader); - Timeline timeline = manifest.durationUs == C.UNSET_TIME_US - ? new SinglePeriodTimeline(this, manifest) - : new SinglePeriodTimeline(this, manifest, manifest.durationUs / 1000); - invalidationListener.onTimelineChanged(timeline); } else { period.updateManifest(manifest); } + Timeline timeline = manifest.isLive || manifest.durationUs == C.UNSET_TIME_US + ? SinglePeriodTimeline.createUnseekableFinalTimeline(this, C.UNSET_TIME_US) + : SinglePeriodTimeline.createSeekableFinalTimeline(this, manifest.durationUs); + sourceListener.onSourceInfoRefreshed(timeline, manifest); scheduleManifestRefresh(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 3eb4d2a04b..a58f4bef42 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -162,7 +162,7 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe } @Override - public void onTimelineChanged(Timeline timeline) { + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. } 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 9d2baa04c5..02e95a6de8 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 @@ -217,7 +217,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen } @Override - public final void onTimelineChanged(Timeline timeline) { + public final void onSourceInfoRefreshed(Timeline timeline, Object manifest) { // Do nothing. }