diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java index 9d45bc726e..3f95d8c5a1 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java @@ -64,38 +64,54 @@ public interface DashSegmentIndex { */ RangedUri getSegmentUrl(long segmentNum); - /** - * Returns the segment number of the first segment. - * - * @return The segment number of the first segment. - */ + /** Returns the segment number of the first defined segment in the index. */ long getFirstSegmentNum(); /** - * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. - *

- * 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 - * must manually determine the window of currently available segments. + * Returns the segment number of the first available segment in the index. * - * @param periodDurationUs The duration of the enclosing period in microseconds, or - * {@link C#TIME_UNSET} if the period's duration is not yet known. + * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link + * C#TIME_UNSET} if the period's duration is not yet known. + * @param nowUnixTimeUs The current time in milliseconds since the Unix epoch. + * @return The number of the first available segment. + */ + long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs); + + /** + * Returns the number of segments defined in the index, or {@link #INDEX_UNBOUNDED}. + * + *

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 + * can query the available segment using {@link #getFirstAvailableSegmentNum(long, long)} and + * {@link #getAvailableSegmentCount(long, long)}. + * + * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link + * C#TIME_UNSET} if the period's duration is not yet known. * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ int getSegmentCount(long periodDurationUs); + /** + * Returns the number of available segments in the index. + * + * @param periodDurationUs The duration of the enclosing period in microseconds, or {@link + * C#TIME_UNSET} if the period's duration is not yet known. + * @param nowUnixTimeUs The current time in milliseconds since the Unix epoch. + * @return The number of available segments in the index. + */ + int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs); + /** * Returns true if segments are defined explicitly by the index. - *

- * If true is returned, each segment is defined explicitly by the index data, and all of the + * + *

If true is returned, each segment is defined explicitly by the index data, and all of the * listed segments are guaranteed to be available at the time when the index was obtained. - *

- * If false is returned then segment information was derived from properties such as a fixed + * + *

If false is returned then segment information was derived from properties such as a fixed * segment duration. If the presentation is dynamic, it's possible that only a subset of the * segments are available. * * @return Whether segments are defined explicitly by the index. */ boolean isExplicit(); - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 3eca7892c4..723fb74739 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -41,11 +41,21 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { return 0; } + @Override + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + return 0; + } + @Override public int getSegmentCount(long periodDurationUs) { return chunkIndex.length; } + @Override + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + return chunkIndex.length; + } + @Override public long getTimeUs(long segmentNum) { return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 01e51c3f6c..d21d15bea5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.dash; -import static java.lang.Math.max; import static java.lang.Math.min; import android.net.Uri; @@ -288,9 +287,9 @@ public class DefaultDashChunkSource implements DashChunkSource { chunkIterators[i] = MediaChunkIterator.EMPTY; } else { long firstAvailableSegmentNum = - representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); + representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs); long lastAvailableSegmentNum = - representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); + representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs); long segmentNum = getSegmentNum( representationHolder, @@ -342,10 +341,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - long firstAvailableSegmentNum = - representationHolder.getFirstAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); - long lastAvailableSegmentNum = - representationHolder.getLastAvailableSegmentNum(manifest, periodIndex, nowUnixTimeUs); + long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs); + long lastAvailableSegmentNum = representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs); updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); @@ -739,6 +736,11 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } + public long getFirstAvailableSegmentNum(long nowUnixTimeUs) { + return segmentIndex.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs) + + segmentNumShift; + } + public int getSegmentCount() { return segmentIndex.getSegmentCount(periodDurationUs); } @@ -760,35 +762,10 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); } - public long getFirstAvailableSegmentNum( - DashManifest manifest, int periodIndex, long nowUnixTimeUs) { - if (getSegmentCount() == DashSegmentIndex.INDEX_UNBOUNDED - && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { - // The index is itself unbounded. We need to use the current time to calculate the range of - // available segments. - long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); - long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); - long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; - long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); - return max(getFirstSegmentNum(), getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); - } - return getFirstSegmentNum(); - } - - public long getLastAvailableSegmentNum( - DashManifest manifest, int periodIndex, long nowUnixTimeUs) { - int availableSegmentCount = getSegmentCount(); - if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { - // The index is itself unbounded. We need to use the current time to calculate the range of - // available segments. - long liveEdgeTimeUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); - long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); - long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; - // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get - // the index of the last completed segment. - return getSegmentNum(liveEdgeTimeInPeriodUs) - 1; - } - return getFirstSegmentNum() + availableSegmentCount - 1; + public long getLastAvailableSegmentNum(long nowUnixTimeUs) { + return getFirstAvailableSegmentNum(nowUnixTimeUs) + + segmentIndex.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs) + - 1; } @Nullable diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index ede5df90c4..1a24d82ddf 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -138,7 +138,13 @@ public class DashManifestParser extends DefaultHandler location = Uri.parse(xpp.nextText()); } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { Pair periodWithDurationMs = - parsePeriod(xpp, baseUrl, nextPeriodStartMs, baseUrlAvailabilityTimeOffsetUs); + parsePeriod( + xpp, + baseUrl, + nextPeriodStartMs, + baseUrlAvailabilityTimeOffsetUs, + availabilityStartTime, + timeShiftBufferDepthMs); Period period = periodWithDurationMs.first; if (period.startMs == C.TIME_UNSET) { if (dynamic) { @@ -226,10 +232,17 @@ public class DashManifestParser extends DefaultHandler } protected Pair parsePeriod( - XmlPullParser xpp, String baseUrl, long defaultStartMs, long baseUrlAvailabilityTimeOffsetUs) + XmlPullParser xpp, + String baseUrl, + long defaultStartMs, + long baseUrlAvailabilityTimeOffsetUs, + long availabilityStartTimeMs, + long timeShiftBufferDepthMs) throws XmlPullParserException, IOException { @Nullable String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", defaultStartMs); + long periodStartUnixTimeMs = + availabilityStartTimeMs != C.TIME_UNSET ? availabilityStartTimeMs + startMs : C.TIME_UNSET; long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); @Nullable SegmentBase segmentBase = null; @Nullable Descriptor assetIdentifier = null; @@ -254,7 +267,9 @@ public class DashManifestParser extends DefaultHandler segmentBase, durationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs)); + segmentBaseAvailabilityTimeOffsetUs, + periodStartUnixTimeMs, + timeShiftBufferDepthMs)); } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { eventStreams.add(parseEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { @@ -308,7 +323,9 @@ public class DashManifestParser extends DefaultHandler @Nullable SegmentBase segmentBase, long periodDurationMs, long baseUrlAvailabilityTimeOffsetUs, - long segmentBaseAvailabilityTimeOffsetUs) + long segmentBaseAvailabilityTimeOffsetUs, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -428,7 +445,9 @@ public class DashManifestParser extends DefaultHandler label, drmSchemeType, drmSchemeDatas, - inbandEventStreams)); + inbandEventStreams, + periodStartUnixTimeMs, + timeShiftBufferDepthMs)); } return buildAdaptationSet( @@ -725,7 +744,9 @@ public class DashManifestParser extends DefaultHandler @Nullable String label, @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, - ArrayList extraInbandEventStreams) { + ArrayList extraInbandEventStreams, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) { Format.Builder formatBuilder = representationInfo.format.buildUpon(); if (label != null) { formatBuilder.setLabel(label); @@ -747,7 +768,9 @@ public class DashManifestParser extends DefaultHandler formatBuilder.build(), representationInfo.baseUrl, representationInfo.segmentBase, - inbandEventStreams); + inbandEventStreams, + periodStartUnixTimeMs, + timeShiftBufferDepthMs); } // SegmentBase, SegmentList and SegmentTemplate parsing. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 03151631d3..2438835be6 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import static java.lang.Math.max; + import android.net.Uri; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -71,7 +73,14 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { - return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); + return newInstance( + revisionId, + format, + baseUrl, + segmentBase, + /* inbandEventStreams= */ null, + /* periodStartUnixTimeMs= */ C.TIME_UNSET, + /* timeShiftBufferDepthMs= */ C.TIME_UNSET); } /** @@ -82,6 +91,9 @@ public abstract class Representation { * @param baseUrl The base URL. * @param segmentBase A segment base element for the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds + * since the Unix epoch, or {@link C#TIME_UNSET} is not applicable. + * @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}. * @return The constructed instance. */ public static Representation newInstance( @@ -89,9 +101,18 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - @Nullable List inbandEventStreams) { + @Nullable List inbandEventStreams, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) { return newInstance( - revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null); + revisionId, + format, + baseUrl, + segmentBase, + inbandEventStreams, + periodStartUnixTimeMs, + timeShiftBufferDepthMs, + /* cacheKey= */ null); } /** @@ -102,6 +123,9 @@ public abstract class Representation { * @param baseUrl The base URL of the representation. * @param segmentBase A segment base element for the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds + * since the Unix epoch, or {@link C#TIME_UNSET} is not applicable. + * @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}. * @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. @@ -112,6 +136,8 @@ public abstract class Representation { String baseUrl, SegmentBase segmentBase, @Nullable List inbandEventStreams, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs, @Nullable String cacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation( @@ -124,7 +150,13 @@ public abstract class Representation { C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation( - revisionId, format, baseUrl, (MultiSegmentBase) segmentBase, inbandEventStreams); + revisionId, + format, + baseUrl, + (MultiSegmentBase) segmentBase, + inbandEventStreams, + periodStartUnixTimeMs, + timeShiftBufferDepthMs); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -277,22 +309,33 @@ public abstract class Representation { implements DashSegmentIndex { @VisibleForTesting /* package */ final MultiSegmentBase segmentBase; + private final long periodStartUnixTimeUs; + private final long timeShiftBufferDepthUs; /** + * Creates the multi-segment Representation. + * * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. * @param inbandEventStreams The in-band event streams in the representation. May be null. + * @param periodStartUnixTimeMs The start time of the enclosing {@link Period} in milliseconds + * since the Unix epoch, or {@link C#TIME_UNSET} is not applicable. + * @param timeShiftBufferDepthMs The {@link DashManifest#timeShiftBufferDepthMs}. */ public MultiSegmentRepresentation( long revisionId, Format format, String baseUrl, MultiSegmentBase segmentBase, - @Nullable List inbandEventStreams) { + @Nullable List inbandEventStreams, + long periodStartUnixTimeMs, + long timeShiftBufferDepthMs) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; + this.periodStartUnixTimeUs = C.msToUs(periodStartUnixTimeMs); + this.timeShiftBufferDepthUs = C.msToUs(timeShiftBufferDepthMs); } @Override @@ -339,11 +382,41 @@ public abstract class Representation { return segmentBase.getFirstSegmentNum(); } + @Override + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + long segmentCount = segmentBase.getSegmentCount(periodDurationUs); + if (segmentCount != INDEX_UNBOUNDED || timeShiftBufferDepthUs == C.TIME_UNSET) { + return segmentBase.getFirstSegmentNum(); + } + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs; + long timeShiftBufferStartInPeriodUs = liveEdgeTimeInPeriodUs - timeShiftBufferDepthUs; + long timeShiftBufferStartSegmentNum = + getSegmentNum(timeShiftBufferStartInPeriodUs, periodDurationUs); + return max(getFirstSegmentNum(), timeShiftBufferStartSegmentNum); + } + @Override public int getSegmentCount(long periodDurationUs) { return segmentBase.getSegmentCount(periodDurationUs); } + @Override + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + int segmentCount = segmentBase.getSegmentCount(periodDurationUs); + if (segmentCount != INDEX_UNBOUNDED) { + return segmentCount; + } + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimeInPeriodUs = nowUnixTimeUs - periodStartUnixTimeUs; + // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet. + long firstIncompleteSegmentNum = getSegmentNum(liveEdgeTimeInPeriodUs, periodDurationUs); + long firstAvailableSegmentNum = getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); + return (int) (firstIncompleteSegmentNum - firstAvailableSegmentNum); + } + @Override public boolean isExplicit() { return segmentBase.isExplicit(); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index a56a11fe50..7c6c8a7aa9 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -56,11 +56,21 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; return 0; } + @Override + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + return 0; + } + @Override public int getSegmentCount(long periodDurationUs) { return 1; } + @Override + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + return 1; + } + @Override public boolean isExplicit() { return true;