diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 541132726f4..e5cb1a9e2b2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -142,6 +142,10 @@ * DASH: * Parse `forced_subtitle` role from DASH manifests ([#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: * Fix bug of ignoring `EXT-X-START` when setting the live target offset ([#8764](https://github.com/google/ExoPlayer/pull/8764)). 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 3e83c32ecdf..4136fab1bfa 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 @@ -1139,7 +1139,7 @@ private static long getAvailableStartTimeInManifestUs( if (index == null) { return periodStartTimeInManifestUs; } - int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + long availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); if (availableSegmentCount == 0) { return periodStartTimeInManifestUs; } @@ -1171,7 +1171,7 @@ private static long getAvailableEndTimeInManifestUs( if (index == null) { return periodStartTimeInManifestUs + periodDurationUs; } - int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); + long availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); if (availableSegmentCount == 0) { return periodStartTimeInManifestUs; } 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 527ed6ce82b..9e7b339eee5 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 @@ -89,7 +89,7 @@ public interface DashSegmentIndex { * 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); + long getSegmentCount(long periodDurationUs); /** * Returns the number of available segments in the index. @@ -99,7 +99,7 @@ public interface DashSegmentIndex { * @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); + long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs); /** * Returns the time, in microseconds, at which a new segment becomes available, or {@link 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 4c771cdcbf8..6f325b9e749 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 @@ -48,12 +48,12 @@ public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeU } @Override - public int getSegmentCount(long periodDurationUs) { + public long getSegmentCount(long periodDurationUs) { return chunkIndex.length; } @Override - public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { return chunkIndex.length; } 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 829d13ac5b3..66af0c5edea 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 @@ -222,7 +222,7 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame if (representationHolder.segmentIndex != null) { long segmentNum = representationHolder.getSegmentNum(positionUs); long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum); - int segmentCount = representationHolder.getSegmentCount(); + long segmentCount = representationHolder.getSegmentCount(); long secondSyncUs = firstSyncUs < positionUs && (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED @@ -465,7 +465,7 @@ public boolean onChunkLoadError( && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int segmentCount = representationHolder.getSegmentCount(); + long segmentCount = representationHolder.getSegmentCount(); if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { @@ -723,7 +723,7 @@ protected static final class RepresentationHolder { newPeriodDurationUs, newRepresentation, chunkExtractor, segmentNumShift, newIndex); } - int oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs); + long oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs); if (oldIndexSegmentCount == 0) { // Segment numbers cannot shift if the old index was empty. return new RepresentationHolder( @@ -777,7 +777,7 @@ public long getFirstAvailableSegmentNum(long nowUnixTimeUs) { + segmentNumShift; } - public int getSegmentCount() { + public long getSegmentCount() { return segmentIndex.getSegmentCount(periodDurationUs); } 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 c0b1dceec53..018c12f9e2c 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 @@ -352,12 +352,12 @@ public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeU } @Override - public int getSegmentCount(long periodDurationUs) { + public long getSegmentCount(long periodDurationUs) { return segmentBase.getSegmentCount(periodDurationUs); } @Override - public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { return segmentBase.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); } 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 495f288805c..8851e9a3d60 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 @@ -24,6 +24,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; 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; /** @@ -214,7 +217,7 @@ public final long getSegmentDurationUs(long sequenceNumber, long periodDurationU long duration = segmentTimeline.get((int) (sequenceNumber - startNumber)).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - int segmentCount = getSegmentCount(periodDurationUs); + long segmentCount = getSegmentCount(periodDurationUs); return segmentCount != INDEX_UNBOUNDED && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) @@ -264,8 +267,8 @@ public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeU } /** See {@link DashSegmentIndex#getAvailableSegmentCount(long, long)}. */ - public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { - int segmentCount = getSegmentCount(periodDurationUs); + public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + long segmentCount = getSegmentCount(periodDurationUs); if (segmentCount != INDEX_UNBOUNDED) { return segmentCount; } @@ -298,7 +301,7 @@ public boolean isExplicit() { } /** 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. */ @@ -356,7 +359,7 @@ public RangedUri getSegmentUrl(Representation representation, long sequenceNumbe } @Override - public int getSegmentCount(long periodDurationUs) { + public long getSegmentCount(long periodDurationUs) { return mediaSegments.size(); } @@ -455,14 +458,17 @@ public RangedUri getSegmentUrl(Representation representation, long sequenceNumbe } @Override - public int getSegmentCount(long periodDurationUs) { + public long getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { return segmentTimeline.size(); } else if (endNumber != C.INDEX_UNSET) { - return (int) (endNumber - startNumber + 1); + return endNumber - startNumber + 1; } else if (periodDurationUs != C.TIME_UNSET) { - long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return (int) Util.ceilDivide(periodDurationUs, durationUs); + BigInteger numerator = + 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 { 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 523bc2d0719..a5806450a31 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 @@ -63,12 +63,12 @@ public long getFirstAvailableSegmentNum(long periodDurationUs, long nowUnixTimeU } @Override - public int getSegmentCount(long periodDurationUs) { + public long getSegmentCount(long periodDurationUs) { return 1; } @Override - public int getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { + public long getAvailableSegmentCount(long periodDurationUs, long nowUnixTimeUs) { return 1; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 7b99d55fd93..ed195ec417f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -183,7 +183,7 @@ private void addSegmentsForAdaptationSet( continue; } - int segmentCount = index.getSegmentCount(periodDurationUs); + long segmentCount = index.getSegmentCount(periodDurationUs); if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { throw new DownloadException("Unbounded segment index"); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java index dd442a91f45..91ddfbbde9a 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBaseTest.java @@ -182,4 +182,43 @@ public void getNextSegmentShiftTimeUse_unboundedSegmentTemplate() { /* nowUnixTimeUs= */ periodStartUnixTimeUs + 17_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); + } }