diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 932a8ea598..8f0dc8b71c 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -326,10 +326,13 @@ public class DashChunkSource implements ChunkSource { return; } + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + boolean indexUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + int segmentNum; if (queue.isEmpty()) { if (currentManifest.dynamic) { - seekPositionUs = getLiveSeekPosition(); + seekPositionUs = getLiveSeekPosition(indexUnbounded); } segmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { @@ -337,16 +340,18 @@ public class DashChunkSource implements ChunkSource { - representationHolder.segmentNumShift; } + // TODO: For unbounded manifests, we need to enforce that we don't try and request chunks + // behind or in front of the live window. if (currentManifest.dynamic) { if (segmentNum < segmentIndex.getFirstSegmentNum()) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; - } else if (segmentNum > segmentIndex.getLastSegmentNum()) { + } else if (!indexUnbounded && segmentNum > lastSegmentNum) { // This is beyond the last chunk in the current manifest. finishedCurrentManifest = true; return; - } else if (segmentNum == segmentIndex.getLastSegmentNum()) { + } else if (!indexUnbounded && segmentNum == lastSegmentNum) { // This is the last chunk in the current manifest. Mark the manifest as being finished, // but continue to return the final chunk. finishedCurrentManifest = true; @@ -452,16 +457,24 @@ public class DashChunkSource implements ChunkSource { * For live playbacks, determines the seek position that snaps playback to be * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest * + * @param indexUnbounded True if the segment index for this source is unbounded. False otherwise. * @return The seek position in microseconds. */ - private long getLiveSeekPosition() { - long liveEdgeTimestampUs = Long.MIN_VALUE; - for (RepresentationHolder representationHolder : representationHolders.values()) { - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) - + segmentIndex.getDurationUs(lastSegmentNum); - liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + private long getLiveSeekPosition(boolean indexUnbounded) { + long liveEdgeTimestampUs; + if (indexUnbounded) { + // TODO: Use UtcTimingElement where possible. + long nowMs = System.currentTimeMillis(); + liveEdgeTimestampUs = (nowMs - currentManifest.availabilityStartTime) * 1000; + } else { + liveEdgeTimestampUs = Long.MIN_VALUE; + for (RepresentationHolder representationHolder : representationHolders.values()) { + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) + + segmentIndex.getDurationUs(lastSegmentNum); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + } } return liveEdgeTimestampUs - liveEdgeLatencyUs; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java index 336e4c6057..e66aa38380 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer.dash.mpd.RangedUri; */ public interface DashSegmentIndex { + public static final int INDEX_UNBOUNDED = -1; + /** * Returns the segment number of the segment containing a given media time. * @@ -64,9 +66,15 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment. + * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + *

+ * An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a + * SegmentTimeline element. In this case the manifest can be used to derive information about + * segments arbitrarily far into the future. This means that the manifest does not need to be + * refreshed as frequently (if at all) during playback, however it is necessary for a player to + * manually calculate the window of currently available segments. * - * @return The segment number of the last segment. + * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. */ int getLastSegmentNum(); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index a8ed7c03f2..4aa624cca1 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -356,7 +356,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, - long periodDuration) throws XmlPullParserException, IOException { + long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -388,19 +388,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler segments = segments != null ? segments : parent.mediaSegments; } - return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, segments); } protected SegmentList buildSegmentList(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDuration, int startNumber, long duration, + long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List timeline, List segments) { - return new SegmentList(initialization, timescale, presentationTimeOffset, periodDuration, + return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, segments); } protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, - SegmentTemplate parent, long periodDuration) throws XmlPullParserException, IOException { + SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -429,15 +429,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } - return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDuration, int startNumber, long duration, + long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List timeline, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, Uri baseUrl) { - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDuration, + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index a7393865f7b..91093980c6 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.dash.DashSegmentIndex; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -127,17 +128,28 @@ public abstract class SegmentBase { this.segmentTimeline = segmentTimeline; } - public final int getSegmentNum(long timeUs) { - // TODO: Optimize this - int index = startNumber; - while (index + 1 <= getLastSegmentNum()) { - if (getSegmentTimeUs(index + 1) <= timeUs) { - index++; - } else { - return index; + public int getSegmentNum(long timeUs) { + if (segmentTimeline == null) { + // All segments are of equal duration (with the possible exception of the last one). + long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; + return startNumber + (int) (timeUs / durationUs); + } else { + // Identify the segment using binary search. + int lowIndex = getFirstSegmentNum(); + int highIndex = getLastSegmentNum(); + while (lowIndex <= highIndex) { + int midIndex = (lowIndex + highIndex) / 2; + long midTimeUs = getSegmentTimeUs(midIndex); + if (midTimeUs < timeUs) { + lowIndex = midIndex + 1; + } else if (midTimeUs > timeUs) { + highIndex = midIndex - 1; + } else { + return midIndex; + } } + return lowIndex - 1; } - return index; } public final long getSegmentDurationUs(int sequenceNumber) { @@ -285,6 +297,8 @@ public abstract class SegmentBase { public int getLastSegmentNum() { if (segmentTimeline != null) { return segmentTimeline.size() + startNumber - 1; + } else if (periodDurationMs == -1) { + return DashSegmentIndex.INDEX_UNBOUNDED; } else { long durationMs = (duration * 1000) / timescale; return startNumber + (int) (periodDurationMs / durationMs);