diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f34b078c85..f4c93bd855 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -58,6 +58,8 @@ playback because of their higher resolution. * Fix wrong keyframe detection for TS H264 streams ([#864](https://github.com/androidx/media/pull/864)). + * Fix duration estimation of TS streams that are longer than 47721 seconds + ([#855](https://github.com/androidx/media/issues/855)). * Audio: * Fix handling of EOS for `SilenceSkippingAudioProcessor` when called multiple times ([#712](https://github.com/androidx/media/issues/712)). diff --git a/libraries/common/src/main/java/androidx/media3/common/util/TimestampAdjuster.java b/libraries/common/src/main/java/androidx/media3/common/util/TimestampAdjuster.java index 2cab4ba861..5dd6afbdf0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/TimestampAdjuster.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/TimestampAdjuster.java @@ -187,6 +187,9 @@ public final class TimestampAdjuster { /** * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. * + *
When estimating the wraparound, the method assumes that this timestamp is close to the + * previous adjusted timestamp. + * * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp. * @return The adjusted timestamp in microseconds. */ @@ -209,6 +212,30 @@ public final class TimestampAdjuster { return adjustSampleTimestamp(ptsToUs(pts90Khz)); } + /** + * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. + * + *
When estimating the wraparound, the method assumes that the timestamp is strictly greater + * than the previous adjusted timestamp. + * + * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp. + * @return The adjusted timestamp in microseconds. + */ + public synchronized long adjustTsTimestampGreaterThanPreviousTimestamp(long pts90Khz) { + if (pts90Khz == C.TIME_UNSET) { + return C.TIME_UNSET; + } + if (lastUnadjustedTimestampUs != C.TIME_UNSET) { + // The wrap count for the current PTS must be same or greater than the previous one. + long lastPts = usToNonWrappedPts(lastUnadjustedTimestampUs); + long wrapCount = lastPts / MAX_PTS_PLUS_ONE; + long ptsSameWrap = pts90Khz + (MAX_PTS_PLUS_ONE * wrapCount); + long ptsNextWrap = pts90Khz + (MAX_PTS_PLUS_ONE * (wrapCount + 1)); + pts90Khz = ptsSameWrap >= lastPts ? ptsSameWrap : ptsNextWrap; + } + return adjustSampleTimestamp(ptsToUs(pts90Khz)); + } + /** * Offsets a timestamp in microseconds. * diff --git a/libraries/common/src/test/java/androidx/media3/common/util/TimestampAdjusterTest.java b/libraries/common/src/test/java/androidx/media3/common/util/TimestampAdjusterTest.java index b46ab08bc5..8cc6a8f275 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/TimestampAdjusterTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/TimestampAdjusterTest.java @@ -82,4 +82,162 @@ public class TimestampAdjusterTest { assertThat(firstAdjustedTimestampUs).isEqualTo(5000); assertThat(secondAdjustedTimestampUs).isEqualTo(9000); } + + @Test + public void + adjustTsTimestamp_closeToWraparoundFollowedBySlightlySmallerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 180_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs - 1_000_000); + } + + @Test + public void + adjustTsTimestamp_closeToWraparoundFollowedBySlightlyLargerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 45_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 500_000); + } + + @Test + public void adjustTsTimestamp_closeToWraparoundFollowedByMuchSmallerValue_assumesWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 2_000_000); + } + + @Test + public void + adjustTsTimestamp_justBeyondWraparoundFollowedBySlightlySmallerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(45_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs - 500_000); + } + + @Test + public void + adjustTsTimestamp_justBeyondWraparoundFollowedBySlightlyLargerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(180_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 1_000_000); + } + + @Test + public void adjustTsTimestamp_justBeyondWraparoundFollowedByMuchLargerValue_assumesWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs - 2_000_000); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_closeToWraparoundFollowedBySlightlySmallerValue_assumesWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = + adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(0x200000000L - 180_000); + + assertThat(secondAdjustedTimestampUs - firstAdjustedTimestampUs).isGreaterThan(0x100000000L); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_closeToWraparoundFollowedBySlightlyLargerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = + adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(0x200000000L - 45_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 500_000); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_closeToWraparoundFollowedByMuchSmallerValue_assumesWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit) and close to the next one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L - 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(0x200000000L - 90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(90_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 2_000_000); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_justBeyondWraparoundFollowedBySlightlySmallerValue_assumesWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(45_000); + + assertThat(secondAdjustedTimestampUs - firstAdjustedTimestampUs).isGreaterThan(0x100000000L); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_justBeyondWraparoundFollowedBySlightlyLargerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = + adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(180_000); + + assertThat(secondAdjustedTimestampUs).isEqualTo(firstAdjustedTimestampUs + 1_000_000); + } + + @Test + public void + adjustTsTimestampGreaterThanPreviousTimestamp_justBeyondWraparoundFollowedByMuchLargerValue_doesNotAssumeWraparound() { + // Init timestamp with a non-zero wraparound (multiple of 33-bit), just beyond the last one. + TimestampAdjuster adjuster = + new TimestampAdjuster(TimestampAdjuster.ptsToUs(3 * 0x200000000L + 90_000)); + + long firstAdjustedTimestampUs = adjuster.adjustTsTimestamp(90_000); + long secondAdjustedTimestampUs = + adjuster.adjustTsTimestampGreaterThanPreviousTimestamp(0x200000000L - 90_000); + + assertThat(secondAdjustedTimestampUs - firstAdjustedTimestampUs).isGreaterThan(0x100000000L); + } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsDurationReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsDurationReader.java index 8a5a72bfac..93b87bd1f2 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsDurationReader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/PsDurationReader.java @@ -18,7 +18,6 @@ package androidx.media3.extractor.ts; import static java.lang.Math.min; import androidx.media3.common.C; -import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.common.util.Util; @@ -103,12 +102,9 @@ import java.io.IOException; } long minScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(firstScrValue); - long maxScrPositionUs = scrTimestampAdjuster.adjustTsTimestamp(lastScrValue); + long maxScrPositionUs = + scrTimestampAdjuster.adjustTsTimestampGreaterThanPreviousTimestamp(lastScrValue); durationUs = maxScrPositionUs - minScrPositionUs; - if (durationUs < 0) { - Log.w(TAG, "Invalid duration: " + durationUs + ". Using TIME_UNSET instead."); - durationUs = C.TIME_UNSET; - } return finishReadDuration(input); } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsDurationReader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsDurationReader.java index 475efeb298..d0085f3faf 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsDurationReader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/TsDurationReader.java @@ -18,7 +18,6 @@ package androidx.media3.extractor.ts; import static java.lang.Math.min; import androidx.media3.common.C; -import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.TimestampAdjuster; import androidx.media3.common.util.Util; @@ -99,12 +98,9 @@ import java.io.IOException; } long minPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(firstPcrValue); - long maxPcrPositionUs = pcrTimestampAdjuster.adjustTsTimestamp(lastPcrValue); + long maxPcrPositionUs = + pcrTimestampAdjuster.adjustTsTimestampGreaterThanPreviousTimestamp(lastPcrValue); durationUs = maxPcrPositionUs - minPcrPositionUs; - if (durationUs < 0) { - Log.w(TAG, "Invalid duration: " + durationUs + ". Using TIME_UNSET instead."); - durationUs = C.TIME_UNSET; - } return finishReadDuration(input); }