From bb024fda088f67cf552017bc3ca4a9cf164a4a87 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:07:48 +0000 Subject: [PATCH] Partial support for DASH DVB Live streams. - Adds support for dash manifests that define SegmentTemplate but no SegmentTimeline. - Assumes that the device clock is correct when calculating which segments to load. The final step here is to use the Utc timing element in the DASH manifest to obtain an accurate client clock. - Doesn't yet enforce that the client shouldn't load segments that are in the future or behind the live window. --- .../exoplayer/dash/DashChunkSource.java | 35 +++++++++++++------ .../exoplayer/dash/DashSegmentIndex.java | 12 +++++-- .../MediaPresentationDescriptionParser.java | 16 ++++----- .../exoplayer/dash/mpd/SegmentBase.java | 32 ++++++++++++----- 4 files changed, 65 insertions(+), 30 deletions(-) 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);