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.
This commit is contained in:
Oliver Woodman 2014-12-12 14:07:48 +00:00
parent ae55b12bd8
commit bb024fda08
4 changed files with 65 additions and 30 deletions

View File

@ -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;
}

View File

@ -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}.
* <p>
* 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();

View File

@ -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<SegmentTimelineElement> timeline, List<RangedUri> 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<SegmentTimelineElement> 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);
}

View File

@ -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);