diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a0dbfe8c154..e59c861854f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -106,9 +106,13 @@ * Deprecate `setControlDispatcher` in `PlayerView`, `StyledPlayerView`, `PlayerControlView`, `StyledPlayerControlView` and `PlayerNotificationManager`. +* HLS: + * Fix issue that could cause some playbacks to be stuck buffering + ([#8850](https://github.com/google/ExoPlayer/issues/8850), + [#9153](https://github.com/google/ExoPlayer/issues/9153)). * Extractors: * Add support for DTS-UHD in MP4 - ([#9163](https://github.com/google/ExoPlayer/issues/9163). + ([#9163](https://github.com/google/ExoPlayer/issues/9163)). * Text: * TTML: Inherit the `rubyPosition` value from a containing `` element. @@ -167,7 +171,7 @@ * Add support for multiple base URLs and DVB attributes in the manifest. Apps that are using `DefaultLoadErrorHandlingPolicy` with such manifests have base URL fallback automatically enabled - ([#771](https://github.com/google/ExoPlayer/issues/771) and + ([#771](https://github.com/google/ExoPlayer/issues/771), [#7654](https://github.com/google/ExoPlayer/issues/7654)). * HLS: * Fix issue where playback of a live event could become stuck rather than diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index cb7edd527b1..1f5da87a597 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -19,16 +19,32 @@ import com.google.android.exoplayer2.C; /** - * Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling - * and adjustment is supported, taking into account timestamp rollover. + * Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported, + * taking into account timestamp rollover. */ public final class TimestampAdjuster { /** * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should - * not be offset. + * not be offset. In this mode: + * + * + */ + public static final long MODE_NO_OFFSET = Long.MAX_VALUE; + + /** + * A special {@code firstSampleTimestampUs} value indicating that the adjuster will be shared by + * multiple threads. In this mode: + * + * */ - public static final long DO_NOT_OFFSET = Long.MAX_VALUE; + public static final long MODE_SHARED = Long.MAX_VALUE - 1; /** * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock @@ -36,9 +52,6 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - @GuardedBy("this") - private boolean sharedInitializationStarted; - @GuardedBy("this") private long firstSampleTimestampUs; @@ -48,11 +61,19 @@ public final class TimestampAdjuster { @GuardedBy("this") private long lastUnadjustedTimestampUs; + /** + * Next sample timestamps for calling threads in shared mode when {@link #timestampOffsetUs} has + * not yet been set. + */ + private final ThreadLocal nextSampleTimestampUs; + /** * @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp in - * microseconds, or {@link #DO_NOT_OFFSET} if timestamps should not be offset. + * microseconds, or {@link #MODE_NO_OFFSET} if timestamps should not be offset, or {@link + * #MODE_SHARED} if the adjuster will be used in shared mode. */ public TimestampAdjuster(long firstSampleTimestampUs) { + nextSampleTimestampUs = new ThreadLocal<>(); reset(firstSampleTimestampUs); } @@ -60,37 +81,33 @@ public TimestampAdjuster(long firstSampleTimestampUs) { * For shared timestamp adjusters, performs necessary initialization actions for a caller. * * * * @param canInitialize Whether the caller is able to initialize the adjuster, if needed. - * @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp in - * microseconds. Only used if {@code canInitialize} is {@code true}. + * @param nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling + * thread, in microseconds. Only used if {@code canInitialize} is {@code true}. * @throws InterruptedException If the thread is interrupted whilst blocked waiting for * initialization to complete. */ - public synchronized void sharedInitializeOrWait( - boolean canInitialize, long firstSampleTimestampUs) throws InterruptedException { - if (canInitialize && !sharedInitializationStarted) { - reset(firstSampleTimestampUs); - sharedInitializationStarted = true; - } - if (!canInitialize || this.firstSampleTimestampUs != firstSampleTimestampUs) { + public synchronized void sharedInitializeOrWait(boolean canInitialize, long nextSampleTimestampUs) + throws InterruptedException { + Assertions.checkState(firstSampleTimestampUs == MODE_SHARED); + if (timestampOffsetUs != C.TIME_UNSET) { + // Already initialized. + return; + } else if (canInitialize) { + this.nextSampleTimestampUs.set(nextSampleTimestampUs); + } else { + // Wait for another calling thread to complete initialization. while (timestampOffsetUs == C.TIME_UNSET) { wait(); } @@ -99,22 +116,22 @@ public synchronized void sharedInitializeOrWait( /** * Returns the value of the first adjusted sample timestamp in microseconds, or {@link - * #DO_NOT_OFFSET} if timestamps will not be offset. + * C#TIME_UNSET} if timestamps will not be offset or if the adjuster is in shared mode. */ public synchronized long getFirstSampleTimestampUs() { - return firstSampleTimestampUs; + return firstSampleTimestampUs == MODE_NO_OFFSET || firstSampleTimestampUs == MODE_SHARED + ? C.TIME_UNSET + : firstSampleTimestampUs; } /** - * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link - * #adjustSampleTimestamp} has not been called, returns the result of calling {@link - * #getFirstSampleTimestampUs()} unless that value is {@link #DO_NOT_OFFSET}, in which case {@link - * C#TIME_UNSET} is returned. + * Returns the last adjusted timestamp, in microseconds. If no timestamps have been adjusted yet + * then the result of {@link #getFirstSampleTimestampUs()} is returned. */ public synchronized long getLastAdjustedTimestampUs() { return lastUnadjustedTimestampUs != C.TIME_UNSET ? lastUnadjustedTimestampUs + timestampOffsetUs - : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; + : getFirstSampleTimestampUs(); } /** @@ -129,13 +146,13 @@ public synchronized long getTimestampOffsetUs() { * Resets the instance. * * @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp after - * this reset, in microseconds, or {@link #DO_NOT_OFFSET} if timestamps should not be offset. + * this reset in microseconds, or {@link #MODE_NO_OFFSET} if timestamps should not be offset, + * or {@link #MODE_SHARED} if the adjuster will be used in shared mode. */ public synchronized void reset(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; - timestampOffsetUs = firstSampleTimestampUs == DO_NOT_OFFSET ? 0 : C.TIME_UNSET; + timestampOffsetUs = firstSampleTimestampUs == MODE_NO_OFFSET ? 0 : C.TIME_UNSET; lastUnadjustedTimestampUs = C.TIME_UNSET; - sharedInitializationStarted = false; } /** @@ -174,7 +191,11 @@ public synchronized long adjustSampleTimestamp(long timeUs) { return C.TIME_UNSET; } if (timestampOffsetUs == C.TIME_UNSET) { - timestampOffsetUs = firstSampleTimestampUs - timeUs; + long desiredSampleTimestampUs = + firstSampleTimestampUs == MODE_SHARED + ? Assertions.checkNotNull(nextSampleTimestampUs.get()) + : firstSampleTimestampUs; + timestampOffsetUs = desiredSampleTimestampUs - timeUs; // Notify threads waiting for the timestamp offset to be determined. notifyAll(); } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/TimestampAdjusterTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/TimestampAdjusterTest.java index f509e294ecc..d7466362bee 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/TimestampAdjusterTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/TimestampAdjusterTest.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.util; -import static com.google.android.exoplayer2.util.TimestampAdjuster.DO_NOT_OFFSET; +import static com.google.android.exoplayer2.util.TimestampAdjuster.MODE_NO_OFFSET; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -47,8 +47,9 @@ public void adjustSampleTimestamp_fromNonZero() { } @Test - public void adjustSampleTimestamp_doNotOffset() { - TimestampAdjuster adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ DO_NOT_OFFSET); + public void adjustSampleTimestamp_noOffset() { + TimestampAdjuster adjuster = + new TimestampAdjuster(/* firstSampleTimestampUs= */ MODE_NO_OFFSET); long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000); long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000); @@ -57,11 +58,11 @@ public void adjustSampleTimestamp_doNotOffset() { } @Test - public void adjustSampleTimestamp_afterResetToNotOffset() { + public void adjustSampleTimestamp_afterResetToNoOffset() { TimestampAdjuster adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); // Let the adjuster establish an offset, to make sure that reset really clears it. adjuster.adjustSampleTimestamp(/* timeUs= */ 1000); - adjuster.reset(/* firstSampleTimestampUs= */ DO_NOT_OFFSET); + adjuster.reset(/* firstSampleTimestampUs= */ MODE_NO_OFFSET); long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000); long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java index 0509376f722..54de6425e6f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source.hls; +import static com.google.android.exoplayer2.util.TimestampAdjuster.MODE_SHARED; + import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.TimestampAdjuster; @@ -40,7 +42,7 @@ public TimestampAdjusterProvider() { public TimestampAdjuster getAdjuster(int discontinuitySequence) { @Nullable TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence); if (adjuster == null) { - adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); + adjuster = new TimestampAdjuster(MODE_SHARED); timestampAdjusters.put(discontinuitySequence, adjuster); } return adjuster;