DASH: Correctly handle empty segment indices

Issue: #1865

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=147613244
This commit is contained in:
olly 2017-02-15 10:27:26 -08:00 committed by Oliver Woodman
parent 5c571e6e9d
commit d6e15b7953
7 changed files with 82 additions and 60 deletions

View File

@ -572,22 +572,28 @@ public final class DashMediaSource implements MediaSource {
long availableStartTimeUs = 0; long availableStartTimeUs = 0;
long availableEndTimeUs = Long.MAX_VALUE; long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false; boolean isIndexExplicit = false;
boolean seenEmptyIndex = false;
for (int i = 0; i < adaptationSetCount; i++) { for (int i = 0; i < adaptationSetCount; i++) {
DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex();
if (index == null) { if (index == null) {
return new PeriodSeekInfo(true, 0, durationUs); return new PeriodSeekInfo(true, 0, durationUs);
} }
int firstSegmentNum = index.getFirstSegmentNum();
int lastSegmentNum = index.getLastSegmentNum(durationUs);
isIndexExplicit |= index.isExplicit(); isIndexExplicit |= index.isExplicit();
int segmentCount = index.getSegmentCount(durationUs);
if (segmentCount == 0) {
seenEmptyIndex = true;
availableStartTimeUs = 0;
availableEndTimeUs = 0;
} else if (!seenEmptyIndex) {
int firstSegmentNum = index.getFirstSegmentNum();
long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum);
availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);
if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) {
int lastSegmentNum = firstSegmentNum + segmentCount - 1;
long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum)
+ index.getDurationUs(lastSegmentNum, durationUs); + index.getDurationUs(lastSegmentNum, durationUs);
availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);
} else { }
// The available end time is unmodified, because this index is unbounded.
} }
} }
return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs);
@ -704,8 +710,8 @@ public final class DashMediaSource implements MediaSource {
// not correspond to the start of a segment in both, but this is an edge case. // not correspond to the start of a segment in both, but this is an edge case.
DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)
.representations.get(0).getIndex(); .representations.get(0).getIndex();
if (snapIndex == null) { if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {
// Video adaptation set does not include an index for snapping. // Video adaptation set does not include a non-empty index for snapping.
return windowDefaultStartPositionUs; return windowDefaultStartPositionUs;
} }
int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs);

View File

@ -26,12 +26,10 @@ public interface DashSegmentIndex {
int INDEX_UNBOUNDED = -1; int INDEX_UNBOUNDED = -1;
/** /**
* Returns the segment number of the segment containing a given media time. * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is
* <p> * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() +
* If the given media time is outside the range of the index, then the returned segment number is * getSegmentCount() - 1} if the given media time is later than the end of the last segment.
* clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the * Otherwise, returns the segment number of the segment containing the given media time.
* first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the
* end of the last segment).
* *
* @param timeUs The time in microseconds. * @param timeUs The time in microseconds.
* @param periodDurationUs The duration of the enclosing period in microseconds, or * @param periodDurationUs The duration of the enclosing period in microseconds, or
@ -74,7 +72,7 @@ public interface DashSegmentIndex {
int getFirstSegmentNum(); int getFirstSegmentNum();
/** /**
* Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}.
* <p> * <p>
* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
* SegmentTimeline element, and if the period duration is not yet known. In this case the caller * SegmentTimeline element, and if the period duration is not yet known. In this case the caller
@ -82,9 +80,9 @@ public interface DashSegmentIndex {
* *
* @param periodDurationUs The duration of the enclosing period in microseconds, or * @param periodDurationUs The duration of the enclosing period in microseconds, or
* {@link C#TIME_UNSET} if the period's duration is not yet known. * {@link C#TIME_UNSET} if the period's duration is not yet known.
* @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}.
*/ */
int getLastSegmentNum(long periodDurationUs); int getSegmentCount(long periodDurationUs);
/** /**
* Returns true if segments are defined explicitly by the index. * Returns true if segments are defined explicitly by the index.

View File

@ -39,8 +39,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
} }
@Override @Override
public int getLastSegmentNum(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return chunkIndex.length - 1; return chunkIndex.length;
} }
@Override @Override

View File

@ -194,10 +194,16 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
long nowUs = getNowUnixTimeUs(); long nowUs = getNowUnixTimeUs();
int availableSegmentCount = representationHolder.getSegmentCount();
if (availableSegmentCount == 0) {
// The index doesn't define any segments.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
return;
}
int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum();
int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); int lastAvailableSegmentNum;
boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {
if (indexUnbounded) {
// The index is itself unbounded. We need to use the current time to calculate the range of // The index is itself unbounded. We need to use the current time to calculate the range of
// available segments. // available segments.
long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000;
@ -211,6 +217,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
// getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the
// index of the last completed segment. // index of the last completed segment.
lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1;
} else {
lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1;
} }
int segmentNum; int segmentNum;
@ -268,12 +276,15 @@ public class DefaultDashChunkSource implements DashChunkSource {
&& ((InvalidResponseCodeException) e).responseCode == 404) { && ((InvalidResponseCodeException) e).responseCode == 404) {
RepresentationHolder representationHolder = RepresentationHolder representationHolder =
representationHolders[trackSelection.indexOf(chunk.trackFormat)]; representationHolders[trackSelection.indexOf(chunk.trackFormat)];
int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); int segmentCount = representationHolder.getSegmentCount();
if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) {
int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1;
if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {
missingLastSegment = true; missingLastSegment = true;
return true; return true;
} }
} }
}
// Blacklist if appropriate. // Blacklist if appropriate.
return ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, return ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
trackSelection.indexOf(chunk.trackFormat), e); trackSelection.indexOf(chunk.trackFormat), e);
@ -405,15 +416,20 @@ public class DefaultDashChunkSource implements DashChunkSource {
return; return;
} }
int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs);
if (oldIndexSegmentCount == 0) {
// Segment numbers cannot shift if the old index was empty.
return;
}
int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1;
long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
+ oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs);
int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
if (oldIndexEndTimeUs == newIndexStartTimeUs) { if (oldIndexEndTimeUs == newIndexStartTimeUs) {
// The new index continues where the old one ended, with no overlap. // The new index continues where the old one ended, with no overlap.
segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum;
- newIndexFirstSegmentNum;
} else if (oldIndexEndTimeUs < newIndexStartTimeUs) { } else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
// There's a gap between the old index and the new one which means we've slipped behind the // There's a gap between the old index and the new one which means we've slipped behind the
// live window and can't proceed. // live window and can't proceed.
@ -429,12 +445,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
return segmentIndex.getFirstSegmentNum() + segmentNumShift; return segmentIndex.getFirstSegmentNum() + segmentNumShift;
} }
public int getLastSegmentNum() { public int getSegmentCount() {
int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); return segmentIndex.getSegmentCount(periodDurationUs);
if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
return DashSegmentIndex.INDEX_UNBOUNDED;
}
return lastSegmentNum + segmentNumShift;
} }
public long getSegmentStartTimeUs(int segmentNum) { public long getSegmentStartTimeUs(int segmentNum) {

View File

@ -318,8 +318,8 @@ public abstract class Representation {
} }
@Override @Override
public int getLastSegmentNum(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return segmentBase.getLastSegmentNum(periodDurationUs); return segmentBase.getSegmentCount(periodDurationUs);
} }
@Override @Override

View File

@ -130,18 +130,22 @@ public abstract class SegmentBase {
*/ */
public int getSegmentNum(long timeUs, long periodDurationUs) { public int getSegmentNum(long timeUs, long periodDurationUs) {
final int firstSegmentNum = getFirstSegmentNum(); final int firstSegmentNum = getFirstSegmentNum();
int lowIndex = firstSegmentNum; final int segmentCount = getSegmentCount(periodDurationUs);
int highIndex = getLastSegmentNum(periodDurationUs); if (segmentCount == 0) {
return firstSegmentNum;
}
if (segmentTimeline == null) { if (segmentTimeline == null) {
// All segments are of equal duration (with the possible exception of the last one). // All segments are of equal duration (with the possible exception of the last one).
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
int segmentNum = startNumber + (int) (timeUs / durationUs); int segmentNum = startNumber + (int) (timeUs / durationUs);
// Ensure we stay within bounds. // Ensure we stay within bounds.
return segmentNum < lowIndex ? lowIndex return segmentNum < firstSegmentNum ? firstSegmentNum
: highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum
: segmentNum; : Math.min(segmentNum, firstSegmentNum + segmentCount - 1);
} else { } else {
// The high index cannot be unbounded. Identify the segment using binary search. // The index cannot be unbounded. Identify the segment using binary search.
int lowIndex = firstSegmentNum;
int highIndex = firstSegmentNum + segmentCount - 1;
while (lowIndex <= highIndex) { while (lowIndex <= highIndex) {
int midIndex = lowIndex + (highIndex - lowIndex) / 2; int midIndex = lowIndex + (highIndex - lowIndex) / 2;
long midTimeUs = getSegmentTimeUs(midIndex); long midTimeUs = getSegmentTimeUs(midIndex);
@ -165,7 +169,9 @@ public abstract class SegmentBase {
long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
return (duration * C.MICROS_PER_SECOND) / timescale; return (duration * C.MICROS_PER_SECOND) / timescale;
} else { } else {
return sequenceNumber == getLastSegmentNum(periodDurationUs) int segmentCount = getSegmentCount(periodDurationUs);
return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED
&& sequenceNumber == (getFirstSegmentNum() + segmentCount - 1)
? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))
: ((duration * C.MICROS_PER_SECOND) / timescale); : ((duration * C.MICROS_PER_SECOND) / timescale);
} }
@ -201,9 +207,9 @@ public abstract class SegmentBase {
} }
/** /**
* @see DashSegmentIndex#getLastSegmentNum(long) * @see DashSegmentIndex#getSegmentCount(long)
*/ */
public abstract int getLastSegmentNum(long periodDurationUs); public abstract int getSegmentCount(long periodDurationUs);
/** /**
* @see DashSegmentIndex#isExplicit() * @see DashSegmentIndex#isExplicit()
@ -250,8 +256,8 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getLastSegmentNum(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return startNumber + mediaSegments.size() - 1; return mediaSegments.size();
} }
@Override @Override
@ -322,14 +328,14 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getLastSegmentNum(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
if (segmentTimeline != null) { if (segmentTimeline != null) {
return segmentTimeline.size() + startNumber - 1; return segmentTimeline.size();
} else if (periodDurationUs == C.TIME_UNSET) { } else if (periodDurationUs != C.TIME_UNSET) {
return DashSegmentIndex.INDEX_UNBOUNDED;
} else {
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; return (int) Util.ceilDivide(periodDurationUs, durationUs);
} else {
return DashSegmentIndex.INDEX_UNBOUNDED;
} }
} }

View File

@ -57,8 +57,8 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
} }
@Override @Override
public int getLastSegmentNum(long periodDurationUs) { public int getSegmentCount(long periodDurationUs) {
return 0; return 1;
} }
@Override @Override