Commit 8732f2f0 by olly Committed by Christos Tsilopoulos

HLS: Avoid stuck-buffering issues

Issue: #8850
Issue: #9153
#minor-release
PiperOrigin-RevId: 388257563
parent e95c42ef
...@@ -106,9 +106,13 @@ ...@@ -106,9 +106,13 @@
* Deprecate `setControlDispatcher` in `PlayerView`, `StyledPlayerView`, * Deprecate `setControlDispatcher` in `PlayerView`, `StyledPlayerView`,
`PlayerControlView`, `StyledPlayerControlView` and `PlayerControlView`, `StyledPlayerControlView` and
`PlayerNotificationManager`. `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: * Extractors:
* Add support for DTS-UHD in MP4 * Add support for DTS-UHD in MP4
([#9163](https://github.com/google/ExoPlayer/issues/9163). ([#9163](https://github.com/google/ExoPlayer/issues/9163)).
* Text: * Text:
* TTML: Inherit the `rubyPosition` value from a containing `<span * TTML: Inherit the `rubyPosition` value from a containing `<span
ruby="container">` element. ruby="container">` element.
...@@ -167,7 +171,7 @@ ...@@ -167,7 +171,7 @@
* Add support for multiple base URLs and DVB attributes in the manifest. * Add support for multiple base URLs and DVB attributes in the manifest.
Apps that are using `DefaultLoadErrorHandlingPolicy` with such manifests Apps that are using `DefaultLoadErrorHandlingPolicy` with such manifests
have base URL fallback automatically enabled 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)). [#7654](https://github.com/google/ExoPlayer/issues/7654)).
* HLS: * HLS:
* Fix issue where playback of a live event could become stuck rather than * Fix issue where playback of a live event could become stuck rather than
......
...@@ -19,16 +19,32 @@ import androidx.annotation.GuardedBy; ...@@ -19,16 +19,32 @@ import androidx.annotation.GuardedBy;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
/** /**
* Offsets timestamps according to an initial sample timestamp offset. MPEG-2 TS timestamps scaling * Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported,
* and adjustment is supported, taking into account timestamp rollover. * taking into account timestamp rollover.
*/ */
public final class TimestampAdjuster { public final class TimestampAdjuster {
/** /**
* A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should
* not be offset. * not be offset. In this mode:
*
* <ul>
* <li>{@link #getFirstSampleTimestampUs()} will always return {@link C#TIME_UNSET}.
* <li>The only timestamp adjustment performed is to account for MPEG-2 TS timestamp rollover.
* </ul>
*/
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:
*
* <ul>
* <li>{@link #getFirstSampleTimestampUs()} will always return {@link C#TIME_UNSET}.
* <li>Calling threads must call {@link #sharedInitializeOrWait} prior to adjusting timestamps.
* </ul>
*/ */
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 * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock
...@@ -37,9 +53,6 @@ public final class TimestampAdjuster { ...@@ -37,9 +53,6 @@ public final class TimestampAdjuster {
private static final long MAX_PTS_PLUS_ONE = 0x200000000L; private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
@GuardedBy("this") @GuardedBy("this")
private boolean sharedInitializationStarted;
@GuardedBy("this")
private long firstSampleTimestampUs; private long firstSampleTimestampUs;
@GuardedBy("this") @GuardedBy("this")
...@@ -49,10 +62,18 @@ public final class TimestampAdjuster { ...@@ -49,10 +62,18 @@ public final class TimestampAdjuster {
private long lastUnadjustedTimestampUs; private long lastUnadjustedTimestampUs;
/** /**
* Next sample timestamps for calling threads in shared mode when {@link #timestampOffsetUs} has
* not yet been set.
*/
private final ThreadLocal<Long> nextSampleTimestampUs;
/**
* @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp in * @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) { public TimestampAdjuster(long firstSampleTimestampUs) {
nextSampleTimestampUs = new ThreadLocal<>();
reset(firstSampleTimestampUs); reset(firstSampleTimestampUs);
} }
...@@ -60,37 +81,33 @@ public final class TimestampAdjuster { ...@@ -60,37 +81,33 @@ public final class TimestampAdjuster {
* For shared timestamp adjusters, performs necessary initialization actions for a caller. * For shared timestamp adjusters, performs necessary initialization actions for a caller.
* *
* <ul> * <ul>
* <li>If the adjuster does not yet have a target {@link #getFirstSampleTimestampUs first sample * <li>If the adjuster has already established a {@link #getTimestampOffsetUs timestamp offset}
* timestamp} and if {@code canInitialize} is {@code true}, then initialization is started * then this method is a no-op.
* by setting the target first sample timestamp to {@code firstSampleTimestampUs}. The call * <li>If {@code canInitialize} is {@code true} and the adjuster has not yet established a
* returns, allowing the caller to proceed. Initialization completes when a caller adjusts * timestamp offset, then the adjuster records the desired first sample timestamp for the
* the first timestamp. * calling thread and returns to allow the caller to proceed. If the timestamp offset has
* <li>If {@code canInitialize} is {@code true} and the adjuster already has a target {@link * still not been established when the caller attempts to adjust its first timestamp, then
* #getFirstSampleTimestampUs first sample timestamp}, then the call returns to allow the * the recorded timestamp is used to set it.
* caller to proceed only if {@code firstSampleTimestampUs} is equal to the target. This * <li>If {@code canInitialize} is {@code false} and the adjuster has not yet established a
* ensures a caller that's previously started initialization can continue to proceed. It * timestamp offset, then the call blocks until the timestamp offset is set.
* also allows other callers with the same {@code firstSampleTimestampUs} to proceed, since
* in this case it doesn't matter which caller adjusts the first timestamp to complete
* initialization.
* <li>If {@code canInitialize} is {@code false} or if {@code firstSampleTimestampUs} differs
* from the target {@link #getFirstSampleTimestampUs first sample timestamp}, then the call
* blocks until initialization completes. If initialization has already been completed the
* call returns immediately.
* </ul> * </ul>
* *
* @param canInitialize Whether the caller is able to initialize the adjuster, if needed. * @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 * @param nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling
* microseconds. Only used if {@code canInitialize} is {@code true}. * thread, in microseconds. Only used if {@code canInitialize} is {@code true}.
* @throws InterruptedException If the thread is interrupted whilst blocked waiting for * @throws InterruptedException If the thread is interrupted whilst blocked waiting for
* initialization to complete. * initialization to complete.
*/ */
public synchronized void sharedInitializeOrWait( public synchronized void sharedInitializeOrWait(boolean canInitialize, long nextSampleTimestampUs)
boolean canInitialize, long firstSampleTimestampUs) throws InterruptedException { throws InterruptedException {
if (canInitialize && !sharedInitializationStarted) { Assertions.checkState(firstSampleTimestampUs == MODE_SHARED);
reset(firstSampleTimestampUs); if (timestampOffsetUs != C.TIME_UNSET) {
sharedInitializationStarted = true; // Already initialized.
} return;
if (!canInitialize || this.firstSampleTimestampUs != firstSampleTimestampUs) { } else if (canInitialize) {
this.nextSampleTimestampUs.set(nextSampleTimestampUs);
} else {
// Wait for another calling thread to complete initialization.
while (timestampOffsetUs == C.TIME_UNSET) { while (timestampOffsetUs == C.TIME_UNSET) {
wait(); wait();
} }
...@@ -99,22 +116,22 @@ public final class TimestampAdjuster { ...@@ -99,22 +116,22 @@ public final class TimestampAdjuster {
/** /**
* Returns the value of the first adjusted sample timestamp in microseconds, or {@link * 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() { 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 * Returns the last adjusted timestamp, in microseconds. If no timestamps have been adjusted yet
* #adjustSampleTimestamp} has not been called, returns the result of calling {@link * then the result of {@link #getFirstSampleTimestampUs()} is returned.
* #getFirstSampleTimestampUs()} unless that value is {@link #DO_NOT_OFFSET}, in which case {@link
* C#TIME_UNSET} is returned.
*/ */
public synchronized long getLastAdjustedTimestampUs() { public synchronized long getLastAdjustedTimestampUs() {
return lastUnadjustedTimestampUs != C.TIME_UNSET return lastUnadjustedTimestampUs != C.TIME_UNSET
? lastUnadjustedTimestampUs + timestampOffsetUs ? lastUnadjustedTimestampUs + timestampOffsetUs
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; : getFirstSampleTimestampUs();
} }
/** /**
...@@ -129,13 +146,13 @@ public final class TimestampAdjuster { ...@@ -129,13 +146,13 @@ public final class TimestampAdjuster {
* Resets the instance. * Resets the instance.
* *
* @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp after * @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) { public synchronized void reset(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = 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; lastUnadjustedTimestampUs = C.TIME_UNSET;
sharedInitializationStarted = false;
} }
/** /**
...@@ -174,7 +191,11 @@ public final class TimestampAdjuster { ...@@ -174,7 +191,11 @@ public final class TimestampAdjuster {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
if (timestampOffsetUs == 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. // Notify threads waiting for the timestamp offset to be determined.
notifyAll(); notifyAll();
} }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.util; 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 static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
...@@ -47,8 +47,9 @@ public class TimestampAdjusterTest { ...@@ -47,8 +47,9 @@ public class TimestampAdjusterTest {
} }
@Test @Test
public void adjustSampleTimestamp_doNotOffset() { public void adjustSampleTimestamp_noOffset() {
TimestampAdjuster adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ DO_NOT_OFFSET); TimestampAdjuster adjuster =
new TimestampAdjuster(/* firstSampleTimestampUs= */ MODE_NO_OFFSET);
long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000); long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000);
long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000); long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000);
...@@ -57,11 +58,11 @@ public class TimestampAdjusterTest { ...@@ -57,11 +58,11 @@ public class TimestampAdjusterTest {
} }
@Test @Test
public void adjustSampleTimestamp_afterResetToNotOffset() { public void adjustSampleTimestamp_afterResetToNoOffset() {
TimestampAdjuster adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); TimestampAdjuster adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
// Let the adjuster establish an offset, to make sure that reset really clears it. // Let the adjuster establish an offset, to make sure that reset really clears it.
adjuster.adjustSampleTimestamp(/* timeUs= */ 1000); adjuster.adjustSampleTimestamp(/* timeUs= */ 1000);
adjuster.reset(/* firstSampleTimestampUs= */ DO_NOT_OFFSET); adjuster.reset(/* firstSampleTimestampUs= */ MODE_NO_OFFSET);
long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000); long firstAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 2000);
long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000); long secondAdjustedTimestampUs = adjuster.adjustSampleTimestamp(/* timeUs= */ 6000);
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import static com.google.android.exoplayer2.util.TimestampAdjuster.MODE_SHARED;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
...@@ -40,7 +42,7 @@ public final class TimestampAdjusterProvider { ...@@ -40,7 +42,7 @@ public final class TimestampAdjusterProvider {
public TimestampAdjuster getAdjuster(int discontinuitySequence) { public TimestampAdjuster getAdjuster(int discontinuitySequence) {
@Nullable TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence); @Nullable TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
if (adjuster == null) { if (adjuster == null) {
adjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); adjuster = new TimestampAdjuster(MODE_SHARED);
timestampAdjusters.put(discontinuitySequence, adjuster); timestampAdjusters.put(discontinuitySequence, adjuster);
} }
return adjuster; return adjuster;
......
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