From c107017a4bd623aece55bfe959fddcb297911c83 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 25 Sep 2020 13:30:36 +0100 Subject: [PATCH] Ensure implicit manifest updates arrives asap PiperOrigin-RevId: 333714978 --- .../source/dash/DashMediaSource.java | 43 ++++++- .../source/dash/DashSegmentIndex.java | 12 ++ .../source/dash/DashWrappingSegmentIndex.java | 6 + .../dash/manifest/DashManifestParser.java | 76 ++++++++---- .../source/dash/manifest/Representation.java | 74 ++---------- .../source/dash/manifest/SegmentBase.java | 109 +++++++++++++---- .../dash/manifest/SingleSegmentIndex.java | 6 + ...entationTest.java => SegmentBaseTest.java} | 112 ++++++++++++------ 8 files changed, 287 insertions(+), 151 deletions(-) rename library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/{RepresentationTest.java => SegmentBaseTest.java} (55%) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 2f5b169e30..1473547ab3 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -50,6 +50,8 @@ import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCal import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -68,10 +70,12 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.SntpClient; import com.google.android.exoplayer2.util.Util; import com.google.common.base.Charsets; +import com.google.common.math.LongMath; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collections; @@ -441,7 +445,7 @@ public final class DashMediaSource extends BaseMediaSource { * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link * Timeline} is changing dynamically (for example, for incomplete live streams). */ - private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; + private static final long DEFAULT_NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** * The minimum default start position for live streams, relative to the start of the live window. */ @@ -1106,7 +1110,10 @@ public final class DashMediaSource extends BaseMediaSource { handler.removeCallbacks(simulateManifestRefreshRunnable); // If the window is changing implicitly, post a simulated manifest refresh to update it. if (windowChangingImplicitly) { - handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); + handler.postDelayed( + simulateManifestRefreshRunnable, + getIntervalUntilNextManifestRefreshMs( + manifest, Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs))); } if (manifestLoadPending) { startLoadingManifest(); @@ -1165,6 +1172,38 @@ public final class DashMediaSource extends BaseMediaSource { loadable.type); } + private static long getIntervalUntilNextManifestRefreshMs( + DashManifest manifest, long nowUnixTimeMs) { + int periodIndex = manifest.getPeriodCount() - 1; + Period period = manifest.getPeriod(periodIndex); + long periodStartUs = C.msToUs(period.startMs); + long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + long nowUnixTimeUs = C.msToUs(nowUnixTimeMs); + long availabilityStartTimeUs = C.msToUs(manifest.availabilityStartTimeMs); + long intervalUs = C.msToUs(DEFAULT_NOTIFY_MANIFEST_INTERVAL_MS); + for (int i = 0; i < period.adaptationSets.size(); i++) { + List representations = period.adaptationSets.get(i).representations; + if (representations.isEmpty()) { + continue; + } + @Nullable DashSegmentIndex index = representations.get(0).getIndex(); + if (index != null) { + long nextSegmentShiftUnixTimeUs = + availabilityStartTimeUs + + periodStartUs + + index.getNextSegmentAvailableTimeUs(periodDurationUs, nowUnixTimeUs); + long requiredIntervalUs = nextSegmentShiftUnixTimeUs - nowUnixTimeUs; + // Avoid multiple refreshes within a very small amount of time. + if (requiredIntervalUs < intervalUs - 100_000 + || (requiredIntervalUs > intervalUs && requiredIntervalUs < intervalUs + 100_000)) { + intervalUs = requiredIntervalUs; + } + } + } + // Round up to compensate for a potential loss in the us to ms conversion. + return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING); + } + private static final class PeriodSeekInfo { public static PeriodSeekInfo createPeriodSeekInfo( 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 3f95d8c5a1..527ed6ce82 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 @@ -101,6 +101,18 @@ public interface DashSegmentIndex { */ int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs); + /** + * Returns the time, in microseconds, at which a new segment becomes available, or {@link + * C#TIME_UNSET} if not applicable. + * + * @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 time, in microseconds, at which a new segment becomes available, or {@link + * C#TIME_UNSET} if not applicable. + */ + long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs); + /** * Returns true if segments are defined explicitly by the index. * 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 723fb74739..4c771cdcbf 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; @@ -56,6 +57,11 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { return chunkIndex.length; } + @Override + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + return C.TIME_UNSET; + } + @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/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index e784c7d489..19fcc321cb 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 @@ -291,9 +291,11 @@ public class DashManifestParser extends DefaultHandler parseSegmentList( xpp, /* parent= */ null, + periodStartUnixTimeMs, durationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBaseAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); @@ -302,9 +304,11 @@ public class DashManifestParser extends DefaultHandler xpp, /* parent= */ null, ImmutableList.of(), + periodStartUnixTimeMs, durationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); } else { @@ -407,9 +411,11 @@ public class DashManifestParser extends DefaultHandler essentialProperties, supplementalProperties, segmentBase, + periodStartUnixTimeMs, periodDurationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); contentType = checkContentTypeConsistency( contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType)); @@ -423,9 +429,11 @@ public class DashManifestParser extends DefaultHandler parseSegmentList( xpp, (SegmentList) segmentBase, + periodStartUnixTimeMs, periodDurationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBaseAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); @@ -434,9 +442,11 @@ public class DashManifestParser extends DefaultHandler xpp, (SegmentTemplate) segmentBase, supplementalProperties, + periodStartUnixTimeMs, periodDurationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { @@ -455,9 +465,7 @@ public class DashManifestParser extends DefaultHandler label, drmSchemeType, drmSchemeDatas, - inbandEventStreams, - periodStartUnixTimeMs, - timeShiftBufferDepthMs)); + inbandEventStreams)); } return buildAdaptationSet( @@ -599,9 +607,11 @@ public class DashManifestParser extends DefaultHandler List adaptationSetEssentialProperties, List adaptationSetSupplementalProperties, @Nullable SegmentBase segmentBase, + long periodStartUnixTimeMs, long periodDurationMs, long baseUrlAvailabilityTimeOffsetUs, - long segmentBaseAvailabilityTimeOffsetUs) + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -641,9 +651,11 @@ public class DashManifestParser extends DefaultHandler parseSegmentList( xpp, (SegmentList) segmentBase, + periodStartUnixTimeMs, periodDurationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBaseAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); @@ -652,9 +664,11 @@ public class DashManifestParser extends DefaultHandler xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties, + periodStartUnixTimeMs, periodDurationMs, baseUrlAvailabilityTimeOffsetUs, - segmentBaseAvailabilityTimeOffsetUs); + segmentBaseAvailabilityTimeOffsetUs, + timeShiftBufferDepthMs); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -754,9 +768,7 @@ public class DashManifestParser extends DefaultHandler @Nullable String label, @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, - ArrayList extraInbandEventStreams, - long periodStartUnixTimeMs, - long timeShiftBufferDepthMs) { + ArrayList extraInbandEventStreams) { Format.Builder formatBuilder = representationInfo.format.buildUpon(); if (label != null) { formatBuilder.setLabel(label); @@ -778,9 +790,7 @@ public class DashManifestParser extends DefaultHandler formatBuilder.build(), representationInfo.baseUrl, representationInfo.segmentBase, - inbandEventStreams, - periodStartUnixTimeMs, - timeShiftBufferDepthMs); + inbandEventStreams); } // SegmentBase, SegmentList and SegmentTemplate parsing. @@ -825,9 +835,11 @@ public class DashManifestParser extends DefaultHandler protected SegmentList parseSegmentList( XmlPullParser xpp, @Nullable SegmentList parent, + long periodStartUnixTimeMs, long periodDurationMs, long baseUrlAvailabilityTimeOffsetUs, - long segmentBaseAvailabilityTimeOffsetUs) + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -873,7 +885,9 @@ public class DashManifestParser extends DefaultHandler duration, timeline, availabilityTimeOffsetUs, - segments); + segments, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); } protected SegmentList buildSegmentList( @@ -884,7 +898,9 @@ public class DashManifestParser extends DefaultHandler long duration, @Nullable List timeline, long availabilityTimeOffsetUs, - @Nullable List segments) { + @Nullable List segments, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { return new SegmentList( initialization, timescale, @@ -893,16 +909,20 @@ public class DashManifestParser extends DefaultHandler duration, timeline, availabilityTimeOffsetUs, - segments); + segments, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); } protected SegmentTemplate parseSegmentTemplate( XmlPullParser xpp, @Nullable SegmentTemplate parent, List adaptationSetSupplementalProperties, + long periodStartUnixTimeMs, long periodDurationMs, long baseUrlAvailabilityTimeOffsetUs, - long segmentBaseAvailabilityTimeOffsetUs) + long segmentBaseAvailabilityTimeOffsetUs, + long timeShiftBufferDepthMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -949,7 +969,9 @@ public class DashManifestParser extends DefaultHandler timeline, availabilityTimeOffsetUs, initializationTemplate, - mediaTemplate); + mediaTemplate, + timeShiftBufferDepthMs, + periodStartUnixTimeMs); } protected SegmentTemplate buildSegmentTemplate( @@ -962,7 +984,9 @@ public class DashManifestParser extends DefaultHandler List timeline, long availabilityTimeOffsetUs, @Nullable UrlTemplate initializationTemplate, - @Nullable UrlTemplate mediaTemplate) { + @Nullable UrlTemplate mediaTemplate, + long timeShiftBufferDepthMs, + long periodStartUnixTimeMs) { return new SegmentTemplate( initialization, timescale, @@ -973,7 +997,9 @@ public class DashManifestParser extends DefaultHandler timeline, availabilityTimeOffsetUs, initializationTemplate, - mediaTemplate); + mediaTemplate, + C.msToUs(timeShiftBufferDepthMs), + C.msToUs(periodStartUnixTimeMs)); } /** 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 b384562376..c0b1dceec5 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,8 +15,6 @@ */ 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; @@ -73,14 +71,7 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { - return newInstance( - revisionId, - format, - baseUrl, - segmentBase, - /* inbandEventStreams= */ null, - /* periodStartUnixTimeMs= */ C.TIME_UNSET, - /* timeShiftBufferDepthMs= */ C.TIME_UNSET); + return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); } /** @@ -91,9 +82,6 @@ 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( @@ -101,17 +89,13 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - @Nullable List inbandEventStreams, - long periodStartUnixTimeMs, - long timeShiftBufferDepthMs) { + @Nullable List inbandEventStreams) { return newInstance( revisionId, format, baseUrl, segmentBase, inbandEventStreams, - periodStartUnixTimeMs, - timeShiftBufferDepthMs, /* cacheKey= */ null); } @@ -123,9 +107,6 @@ 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. @@ -136,8 +117,6 @@ 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( @@ -150,13 +129,7 @@ public abstract class Representation { C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation( - revisionId, - format, - baseUrl, - (MultiSegmentBase) segmentBase, - inbandEventStreams, - periodStartUnixTimeMs, - timeShiftBufferDepthMs); + revisionId, format, baseUrl, (MultiSegmentBase) segmentBase, inbandEventStreams); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -309,8 +282,6 @@ public abstract class Representation { implements DashSegmentIndex { @VisibleForTesting /* package */ final MultiSegmentBase segmentBase; - private final long periodStartUnixTimeUs; - private final long timeShiftBufferDepthUs; /** * Creates the multi-segment Representation. @@ -320,22 +291,15 @@ public abstract class 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, - long periodStartUnixTimeMs, - long timeShiftBufferDepthMs) { + @Nullable List inbandEventStreams) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; - this.periodStartUnixTimeUs = C.msToUs(periodStartUnixTimeMs); - this.timeShiftBufferDepthUs = C.msToUs(timeShiftBufferDepthMs); } @Override @@ -384,17 +348,7 @@ public abstract class Representation { @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); + return segmentBase.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); } @Override @@ -404,18 +358,12 @@ public abstract class Representation { @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; - long availabilityEndTimeUs = liveEdgeTimeInPeriodUs + segmentBase.availabilityTimeOffsetUs; - // getSegmentNum(availabilityEndTimeUs) will not be completed yet. - long firstIncompleteSegmentNum = getSegmentNum(availabilityEndTimeUs, periodDurationUs); - long firstAvailableSegmentNum = getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); - return (int) (firstIncompleteSegmentNum - firstAvailableSegmentNum); + return segmentBase.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + } + + @Override + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + return segmentBase.getNextSegmentAvailableTimeUs(periodDurationUs, nowUnixTimeUs); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 5de2814b29..495f288805 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -15,9 +15,12 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import static com.google.android.exoplayer2.source.dash.DashSegmentIndex.INDEX_UNBOUNDED; +import static java.lang.Math.max; import static java.lang.Math.min; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.util.Util; @@ -119,6 +122,8 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; @Nullable /* package */ final List segmentTimeline; + private final long timeShiftBufferDepthUs; + private final long periodStartUnixTimeUs; /** * Offset to the current realtime at which segments become available, in microseconds, or {@link @@ -127,7 +132,7 @@ public abstract class SegmentBase { *

Segments will be available once their end time ≤ currentRealTime + * availabilityTimeOffset. */ - /* package */ final long availabilityTimeOffsetUs; + @VisibleForTesting /* package */ final long availabilityTimeOffsetUs; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -144,6 +149,9 @@ public abstract class SegmentBase { * parameter. * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become * available in microseconds, or {@link C#TIME_UNSET} if not applicable. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. */ public MultiSegmentBase( @Nullable RangedUri initialization, @@ -152,15 +160,19 @@ public abstract class SegmentBase { long startNumber, long duration, @Nullable List segmentTimeline, - long availabilityTimeOffsetUs) { + long availabilityTimeOffsetUs, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { super(initialization, timescale, presentationTimeOffset); this.startNumber = startNumber; this.duration = duration; this.segmentTimeline = segmentTimeline; this.availabilityTimeOffsetUs = availabilityTimeOffsetUs; + this.timeShiftBufferDepthUs = timeShiftBufferDepthUs; + this.periodStartUnixTimeUs = periodStartUnixTimeUs; } - /** @see DashSegmentIndex#getSegmentNum(long, long) */ + /** See {@link DashSegmentIndex#getSegmentNum(long, long)}. */ public long getSegmentNum(long timeUs, long periodDurationUs) { final long firstSegmentNum = getFirstSegmentNum(); final long segmentCount = getSegmentCount(periodDurationUs); @@ -174,7 +186,7 @@ public abstract class SegmentBase { // Ensure we stay within bounds. return segmentNum < firstSegmentNum ? firstSegmentNum - : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED + : segmentCount == INDEX_UNBOUNDED ? segmentNum : min(segmentNum, firstSegmentNum + segmentCount - 1); } else { @@ -196,21 +208,21 @@ public abstract class SegmentBase { } } - /** @see DashSegmentIndex#getDurationUs(long, long) */ + /** See {@link DashSegmentIndex#getDurationUs(long, long)}. */ public final long getSegmentDurationUs(long sequenceNumber, long periodDurationUs) { if (segmentTimeline != null) { long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { int segmentCount = getSegmentCount(periodDurationUs); - return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED - && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) + return segmentCount != INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } } - /** @see DashSegmentIndex#getTimeUs(long) */ + /** See {@link DashSegmentIndex#getTimeUs(long)}. */ public final long getSegmentTimeUs(long sequenceNumber) { long unscaledSegmentTime; if (segmentTimeline != null) { @@ -227,27 +239,66 @@ public abstract class SegmentBase { * Returns a {@link RangedUri} defining the location of a segment for the given index in the * given representation. * - * @see DashSegmentIndex#getSegmentUrl(long) + *

See {@link DashSegmentIndex#getSegmentUrl(long)}. */ public abstract RangedUri getSegmentUrl(Representation representation, long index); - /** @see DashSegmentIndex#getFirstSegmentNum() */ + /** See {@link DashSegmentIndex#getFirstSegmentNum()}. */ public long getFirstSegmentNum() { return startNumber; } - /** - * @see DashSegmentIndex#getSegmentCount(long) - */ - public abstract int getSegmentCount(long periodDurationUs); + /** See {@link DashSegmentIndex#getFirstAvailableSegmentNum(long, long)}. */ + public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeUs) { + long segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount != INDEX_UNBOUNDED || timeShiftBufferDepthUs == C.TIME_UNSET) { + return 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); + } - /** - * @see DashSegmentIndex#isExplicit() - */ + /** See {@link DashSegmentIndex#getAvailableSegmentCount(long, long)}. */ + public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + int segmentCount = 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; + long availabilityTimeOffsetUs = liveEdgeTimeInPeriodUs + this.availabilityTimeOffsetUs; + // getSegmentNum(availabilityTimeOffsetUs) will not be completed yet. + long firstIncompleteSegmentNum = getSegmentNum(availabilityTimeOffsetUs, periodDurationUs); + long firstAvailableSegmentNum = getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); + return (int) (firstIncompleteSegmentNum - firstAvailableSegmentNum); + } + + /** See {@link DashSegmentIndex#getNextSegmentAvailableTimeUs(long, long)}. */ + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + if (segmentTimeline != null) { + return C.TIME_UNSET; + } + long firstIncompleteSegmentNum = + getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs) + + getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + return getSegmentTimeUs(firstIncompleteSegmentNum) + + getSegmentDurationUs(firstIncompleteSegmentNum, periodDurationUs) + - availabilityTimeOffsetUs; + } + + /** See {@link DashSegmentIndex#isExplicit()} */ public boolean isExplicit() { return segmentTimeline != null; } + /** See {@link DashSegmentIndex#getSegmentCount(long)}. */ + public abstract int getSegmentCount(long periodDurationUs); } /** A {@link MultiSegmentBase} that uses a SegmentList to define its segments. */ @@ -271,6 +322,9 @@ public abstract class SegmentBase { * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become * available in microseconds, or {@link C#TIME_UNSET} if not applicable. * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. */ public SegmentList( RangedUri initialization, @@ -280,7 +334,9 @@ public abstract class SegmentBase { long duration, @Nullable List segmentTimeline, long availabilityTimeOffsetUs, - @Nullable List mediaSegments) { + @Nullable List mediaSegments, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { super( initialization, timescale, @@ -288,7 +344,9 @@ public abstract class SegmentBase { startNumber, duration, segmentTimeline, - availabilityTimeOffsetUs); + availabilityTimeOffsetUs, + timeShiftBufferDepthUs, + periodStartUnixTimeUs); this.mediaSegments = mediaSegments; } @@ -339,6 +397,9 @@ public abstract class SegmentBase { * such data exists. If non-null then the {@code initialization} parameter is ignored. If * null then {@code initialization} will be used. * @param mediaTemplate A template defining the location of each media segment. + * @param timeShiftBufferDepthUs The time shift buffer depth in microseconds. + * @param periodStartUnixTimeUs The start of the enclosing period in microseconds since the Unix + * epoch. */ public SegmentTemplate( RangedUri initialization, @@ -350,7 +411,9 @@ public abstract class SegmentBase { @Nullable List segmentTimeline, long availabilityTimeOffsetUs, @Nullable UrlTemplate initializationTemplate, - @Nullable UrlTemplate mediaTemplate) { + @Nullable UrlTemplate mediaTemplate, + long timeShiftBufferDepthUs, + long periodStartUnixTimeUs) { super( initialization, timescale, @@ -358,7 +421,9 @@ public abstract class SegmentBase { startNumber, duration, segmentTimeline, - availabilityTimeOffsetUs); + availabilityTimeOffsetUs, + timeShiftBufferDepthUs, + periodStartUnixTimeUs); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; this.endNumber = endNumber; @@ -399,7 +464,7 @@ public abstract class SegmentBase { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; return (int) Util.ceilDivide(periodDurationUs, durationUs); } else { - return DashSegmentIndex.INDEX_UNBOUNDED; + return INDEX_UNBOUNDED; } } } 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 7c6c8a7aa9..523bc2d071 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; /** @@ -71,6 +72,11 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; return 1; } + @Override + public long getNextSegmentAvailableTimeUs(long periodDurationUs, long nowUnixTimeUs) { + return C.TIME_UNSET; + } + @Override public boolean isExplicit() { return true; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java similarity index 55% rename from library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java index d22071cefa..dd442a91f4 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java @@ -19,16 +19,15 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link Representation}. */ +/** Unit test for {@link SegmentBase}. */ @RunWith(AndroidJUnit4.class) -public final class RepresentationTest { +public final class SegmentBaseTest { @Test - public void getFirstAvailableSegmentNum_multiSegmentRepresentationWithUnboundedTemplate() { + public void getFirstAvailableSegmentNum_unboundedSegmentTemplate() { long periodStartUnixTimeUs = 123_000_000_000_000L; SegmentBase.SegmentTemplate segmentTemplate = new SegmentBase.SegmentTemplate( @@ -41,50 +40,43 @@ public final class RepresentationTest { /* segmentTimeline= */ null, /* availabilityTimeOffsetUs= */ 500_000, /* initializationTemplate= */ null, - /* mediaTemplate= */ null); - Representation.MultiSegmentRepresentation representation = - new Representation.MultiSegmentRepresentation( - /* revisionId= */ 0, - new Format.Builder().build(), - /* baseUrl= */ "https://baseUrl/", - segmentTemplate, - /* inbandEventStreams= */ null, - /* periodStartUnixTimeMs= */ C.usToMs(periodStartUnixTimeUs), - /* timeShiftBufferDepthMs= */ 6_000); + /* mediaTemplate= */ null, + /* timeShiftBufferDepthUs= */ 6_000_000, + /* periodStartUnixTimeUs= */ periodStartUnixTimeUs); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs - 10_000_000)) .isEqualTo(42); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs)) .isEqualTo(42); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 7_999_999)) .isEqualTo(42); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 8_000_000)) .isEqualTo(43); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 9_999_999)) .isEqualTo(43); assertThat( - representation.getFirstAvailableSegmentNum( + segmentTemplate.getFirstAvailableSegmentNum( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 10_000_000)) .isEqualTo(44); } @Test - public void getAvailableSegmentCount_multiSegmentRepresentationWithUnboundedTemplate() { + public void getAvailableSegmentCount_unboundedSegmentTemplate() { long periodStartUnixTimeUs = 123_000_000_000_000L; SegmentBase.SegmentTemplate segmentTemplate = new SegmentBase.SegmentTemplate( @@ -97,55 +89,97 @@ public final class RepresentationTest { /* segmentTimeline= */ null, /* availabilityTimeOffsetUs= */ 500_000, /* initializationTemplate= */ null, - /* mediaTemplate= */ null); - Representation.MultiSegmentRepresentation representation = - new Representation.MultiSegmentRepresentation( - /* revisionId= */ 0, - new Format.Builder().build(), - /* baseUrl= */ "https://baseUrl/", - segmentTemplate, - /* inbandEventStreams= */ null, - /* periodStartUnixTimeMs= */ C.usToMs(periodStartUnixTimeUs), - /* timeShiftBufferDepthMs= */ 6_000); + /* mediaTemplate= */ null, + /* timeShiftBufferDepthUs= */ 6_000_000, + /* periodStartUnixTimeUs= */ periodStartUnixTimeUs); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs - 10_000_000)) .isEqualTo(0); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs)) .isEqualTo(0); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 1_499_999)) .isEqualTo(0); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 1_500_000)) .isEqualTo(1); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 7_499_999)) .isEqualTo(3); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 7_500_000)) .isEqualTo(4); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 7_999_999)) .isEqualTo(4); assertThat( - representation.getAvailableSegmentCount( + segmentTemplate.getAvailableSegmentCount( /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs + 8_000_000)) .isEqualTo(3); } + + @Test + public void getNextSegmentShiftTimeUse_unboundedSegmentTemplate() { + long periodStartUnixTimeUs = 123_000_000_000_000L; + SegmentBase.SegmentTemplate segmentTemplate = + new SegmentBase.SegmentTemplate( + /* initialization= */ null, + /* timescale= */ 1000, + /* presentationTimeOffset= */ 0, + /* startNumber= */ 42, + /* endNumber= */ C.INDEX_UNSET, + /* duration= */ 2000, + /* segmentTimeline= */ null, + /* availabilityTimeOffsetUs= */ 500_000, + /* initializationTemplate= */ null, + /* mediaTemplate= */ null, + /* timeShiftBufferDepthUs= */ 6_000_000, + /* periodStartUnixTimeUs= */ periodStartUnixTimeUs); + + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, + /* nowUnixTimeUs= */ periodStartUnixTimeUs - 10_000_000)) + .isEqualTo(1_500_000); + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, /* nowUnixTimeUs= */ periodStartUnixTimeUs)) + .isEqualTo(1_500_000); + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, + /* nowUnixTimeUs= */ periodStartUnixTimeUs + 1_499_999)) + .isEqualTo(1_500_000); + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, + /* nowUnixTimeUs= */ periodStartUnixTimeUs + 1_500_000)) + .isEqualTo(3_500_000); + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, + /* nowUnixTimeUs= */ periodStartUnixTimeUs + 17_499_999)) + .isEqualTo(17_500_000); + assertThat( + segmentTemplate.getNextSegmentAvailableTimeUs( + /* periodDurationUs= */ C.TIME_UNSET, + /* nowUnixTimeUs= */ periodStartUnixTimeUs + 17_500_000)) + .isEqualTo(19_500_000); + } }