Commit 9a367836 by olly Committed by Oliver Woodman

DASH: Avoid rounding error in getSegmentCount

Issue: #8804
PiperOrigin-RevId: 369484117
parent 7a2eaa96
...@@ -142,6 +142,10 @@ ...@@ -142,6 +142,10 @@
* DASH: * DASH:
* Parse `forced_subtitle` role from DASH manifests * Parse `forced_subtitle` role from DASH manifests
([#8781](https://github.com/google/ExoPlayer/issues/8781)). ([#8781](https://github.com/google/ExoPlayer/issues/8781)).
* DASH:
* Fix rounding error that could cause `SegmentTemplate.getSegmentCount()`
to return incorrect values
([#8804](https://github.com/google/ExoPlayer/issues/8804)).
* HLS: * HLS:
* Fix bug of ignoring `EXT-X-START` when setting the live target offset * Fix bug of ignoring `EXT-X-START` when setting the live target offset
([#8764](https://github.com/google/ExoPlayer/pull/8764)). ([#8764](https://github.com/google/ExoPlayer/pull/8764)).
......
...@@ -1139,7 +1139,7 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -1139,7 +1139,7 @@ public final class DashMediaSource extends BaseMediaSource {
if (index == null) { if (index == null) {
return periodStartTimeInManifestUs; return periodStartTimeInManifestUs;
} }
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); long availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
if (availableSegmentCount == 0) { if (availableSegmentCount == 0) {
return periodStartTimeInManifestUs; return periodStartTimeInManifestUs;
} }
...@@ -1171,7 +1171,7 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -1171,7 +1171,7 @@ public final class DashMediaSource extends BaseMediaSource {
if (index == null) { if (index == null) {
return periodStartTimeInManifestUs + periodDurationUs; return periodStartTimeInManifestUs + periodDurationUs;
} }
int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); long availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
if (availableSegmentCount == 0) { if (availableSegmentCount == 0) {
return periodStartTimeInManifestUs; return periodStartTimeInManifestUs;
} }
......
...@@ -89,7 +89,7 @@ public interface DashSegmentIndex { ...@@ -89,7 +89,7 @@ public interface DashSegmentIndex {
* C#TIME_UNSET} if the period's duration is not yet known. * C#TIME_UNSET} if the period's duration is not yet known.
* @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}.
*/ */
int getSegmentCount(long periodDurationUs); long getSegmentCount(long periodDurationUs);
/** /**
* Returns the number of available segments in the index. * Returns the number of available segments in the index.
...@@ -99,7 +99,7 @@ public interface DashSegmentIndex { ...@@ -99,7 +99,7 @@ public interface DashSegmentIndex {
* @param nowUnixTimeUs The current time in milliseconds since the Unix epoch. * @param nowUnixTimeUs The current time in milliseconds since the Unix epoch.
* @return The number of available segments in the index. * @return The number of available segments in the index.
*/ */
int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs); long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs);
/** /**
* Returns the time, in microseconds, at which a new segment becomes available, or {@link * Returns the time, in microseconds, at which a new segment becomes available, or {@link
......
...@@ -48,12 +48,12 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { ...@@ -48,12 +48,12 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex {
} }
@Override @Override
public int getSegmentCount(long periodDurationUs) { public long getSegmentCount(long periodDurationUs) {
return chunkIndex.length; return chunkIndex.length;
} }
@Override @Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
return chunkIndex.length; return chunkIndex.length;
} }
......
...@@ -222,7 +222,7 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -222,7 +222,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
if (representationHolder.segmentIndex != null) { if (representationHolder.segmentIndex != null) {
long segmentNum = representationHolder.getSegmentNum(positionUs); long segmentNum = representationHolder.getSegmentNum(positionUs);
long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum); long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);
int segmentCount = representationHolder.getSegmentCount(); long segmentCount = representationHolder.getSegmentCount();
long secondSyncUs = long secondSyncUs =
firstSyncUs < positionUs firstSyncUs < positionUs
&& (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED && (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED
...@@ -465,7 +465,7 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -465,7 +465,7 @@ 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 segmentCount = representationHolder.getSegmentCount(); long segmentCount = representationHolder.getSegmentCount();
if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) {
long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1;
if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {
...@@ -723,7 +723,7 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -723,7 +723,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
newPeriodDurationUs, newRepresentation, chunkExtractor, segmentNumShift, newIndex); newPeriodDurationUs, newRepresentation, chunkExtractor, segmentNumShift, newIndex);
} }
int oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs); long oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs);
if (oldIndexSegmentCount == 0) { if (oldIndexSegmentCount == 0) {
// Segment numbers cannot shift if the old index was empty. // Segment numbers cannot shift if the old index was empty.
return new RepresentationHolder( return new RepresentationHolder(
...@@ -777,7 +777,7 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -777,7 +777,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
+ segmentNumShift; + segmentNumShift;
} }
public int getSegmentCount() { public long getSegmentCount() {
return segmentIndex.getSegmentCount(periodDurationUs); return segmentIndex.getSegmentCount(periodDurationUs);
} }
......
...@@ -352,12 +352,12 @@ public abstract class Representation { ...@@ -352,12 +352,12 @@ public abstract class Representation {
} }
@Override @Override
public int getSegmentCount(long periodDurationUs) { public long getSegmentCount(long periodDurationUs) {
return segmentBase.getSegmentCount(periodDurationUs); return segmentBase.getSegmentCount(periodDurationUs);
} }
@Override @Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
return segmentBase.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); return segmentBase.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs);
} }
......
...@@ -24,6 +24,9 @@ import androidx.annotation.VisibleForTesting; ...@@ -24,6 +24,9 @@ import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.math.BigIntegerMath;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.List; import java.util.List;
/** /**
...@@ -214,7 +217,7 @@ public abstract class SegmentBase { ...@@ -214,7 +217,7 @@ public abstract class SegmentBase {
long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration; long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration;
return (duration * C.MICROS_PER_SECOND) / timescale; return (duration * C.MICROS_PER_SECOND) / timescale;
} else { } else {
int segmentCount = getSegmentCount(periodDurationUs); long segmentCount = getSegmentCount(periodDurationUs);
return segmentCount != INDEX_UNBOUNDED return segmentCount != INDEX_UNBOUNDED
&& sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1)
? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))
...@@ -264,8 +267,8 @@ public abstract class SegmentBase { ...@@ -264,8 +267,8 @@ public abstract class SegmentBase {
} }
/** See {@link DashSegmentIndex#getAvailableSegmentCount(long, long)}. */ /** See {@link DashSegmentIndex#getAvailableSegmentCount(long, long)}. */
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
int segmentCount = getSegmentCount(periodDurationUs); long segmentCount = getSegmentCount(periodDurationUs);
if (segmentCount != INDEX_UNBOUNDED) { if (segmentCount != INDEX_UNBOUNDED) {
return segmentCount; return segmentCount;
} }
...@@ -298,7 +301,7 @@ public abstract class SegmentBase { ...@@ -298,7 +301,7 @@ public abstract class SegmentBase {
} }
/** See {@link DashSegmentIndex#getSegmentCount(long)}. */ /** See {@link DashSegmentIndex#getSegmentCount(long)}. */
public abstract int getSegmentCount(long periodDurationUs); public abstract long getSegmentCount(long periodDurationUs);
} }
/** A {@link MultiSegmentBase} that uses a SegmentList to define its segments. */ /** A {@link MultiSegmentBase} that uses a SegmentList to define its segments. */
...@@ -356,7 +359,7 @@ public abstract class SegmentBase { ...@@ -356,7 +359,7 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getSegmentCount(long periodDurationUs) { public long getSegmentCount(long periodDurationUs) {
return mediaSegments.size(); return mediaSegments.size();
} }
...@@ -455,14 +458,17 @@ public abstract class SegmentBase { ...@@ -455,14 +458,17 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getSegmentCount(long periodDurationUs) { public long getSegmentCount(long periodDurationUs) {
if (segmentTimeline != null) { if (segmentTimeline != null) {
return segmentTimeline.size(); return segmentTimeline.size();
} else if (endNumber != C.INDEX_UNSET) { } else if (endNumber != C.INDEX_UNSET) {
return (int) (endNumber - startNumber + 1); return endNumber - startNumber + 1;
} else if (periodDurationUs != C.TIME_UNSET) { } else if (periodDurationUs != C.TIME_UNSET) {
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; BigInteger numerator =
return (int) Util.ceilDivide(periodDurationUs, durationUs); BigInteger.valueOf(periodDurationUs).multiply(BigInteger.valueOf(timescale));
BigInteger denominator =
BigInteger.valueOf(duration).multiply(BigInteger.valueOf(C.MICROS_PER_SECOND));
return BigIntegerMath.divide(numerator, denominator, RoundingMode.CEILING).longValue();
} else { } else {
return INDEX_UNBOUNDED; return INDEX_UNBOUNDED;
} }
......
...@@ -63,12 +63,12 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; ...@@ -63,12 +63,12 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
} }
@Override @Override
public int getSegmentCount(long periodDurationUs) { public long getSegmentCount(long periodDurationUs) {
return 1; return 1;
} }
@Override @Override
public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) {
return 1; return 1;
} }
......
...@@ -183,7 +183,7 @@ public final class DashDownloader extends SegmentDownloader<DashManifest> { ...@@ -183,7 +183,7 @@ public final class DashDownloader extends SegmentDownloader<DashManifest> {
continue; continue;
} }
int segmentCount = index.getSegmentCount(periodDurationUs); long segmentCount = index.getSegmentCount(periodDurationUs);
if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) {
throw new DownloadException("Unbounded segment index"); throw new DownloadException("Unbounded segment index");
} }
......
...@@ -182,4 +182,43 @@ public final class SegmentBaseTest { ...@@ -182,4 +182,43 @@ public final class SegmentBaseTest {
/* nowUnixTimeUs= */ periodStartUnixTimeUs + 17_500_000)) /* nowUnixTimeUs= */ periodStartUnixTimeUs + 17_500_000))
.isEqualTo(19_500_000); .isEqualTo(19_500_000);
} }
/** Regression test for https://github.com/google/ExoPlayer/issues/8804. */
@Test
public void getSegmentCount_withSegmentTemplate_avoidsIncorrectRounding() {
SegmentBase.SegmentTemplate segmentTemplate =
new SegmentBase.SegmentTemplate(
/* initialization= */ null,
/* timescale= */ 90000,
/* presentationTimeOffset= */ 0,
/* startNumber= */ 0,
/* endNumber= */ C.INDEX_UNSET,
/* duration= */ 179989,
/* segmentTimeline= */ null,
/* availabilityTimeOffsetUs= */ C.TIME_UNSET,
/* initializationTemplate= */ null,
/* mediaTemplate= */ null,
/* timeShiftBufferDepthUs= */ C.TIME_UNSET,
/* periodStartUnixTimeUs= */ C.TIME_UNSET);
assertThat(segmentTemplate.getSegmentCount(2931820000L)).isEqualTo(1466);
}
@Test
public void getSegmentCount_withSegmentTemplate_avoidsOverflow() {
SegmentBase.SegmentTemplate segmentTemplate =
new SegmentBase.SegmentTemplate(
/* initialization= */ null,
/* timescale= */ 1000000,
/* presentationTimeOffset= */ 0,
/* startNumber= */ 0,
/* endNumber= */ C.INDEX_UNSET,
/* duration= */ 179989,
/* segmentTimeline= */ null,
/* availabilityTimeOffsetUs= */ C.TIME_UNSET,
/* initializationTemplate= */ null,
/* mediaTemplate= */ null,
/* timeShiftBufferDepthUs= */ C.TIME_UNSET,
/* periodStartUnixTimeUs= */ C.TIME_UNSET);
assertThat(segmentTemplate.getSegmentCount(1618875028000000L)).isEqualTo(8994299808L);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment