From 01578780a6bbd69d206f4f7e815c9f5e9d3c5fa4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2023 07:44:32 -0800 Subject: [PATCH] Use different wraparound assumptions for duration readers The timestamp adjuster also estimates the number of wraparounds of the 90Khz TS timestamp. It does that by assuming that a new timestamp is always close to the previous one (in either direction). This logic doesn't always work for duration estimates because the timestamp at the end of the media is not close to the one at the beginning and it may also never be less than the one at the beginning. This can be fixed by introducing a new estimation model that assumes the new timestamp is strictly greater than the previous one without making the assumption that it has to be close to it. Issue: androidx/media#855 #minor-release PiperOrigin-RevId: 590936953 --- RELEASENOTES.md | 2 + .../media3/common/util/TimestampAdjuster.java | 27 +++ .../common/util/TimestampAdjusterTest.java | 158 ++++++++++++++++++ .../media3/extractor/ts/PsDurationReader.java | 8 +- .../media3/extractor/ts/TsDurationReader.java | 8 +- 5 files changed, 191 insertions(+), 12 deletions(-) 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); }