mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
HLS: Avoid stuck-buffering issues
Issue: #8850 Issue: #9153 #minor-release PiperOrigin-RevId: 388257563
This commit is contained in:
parent
e95c42ef98
commit
8732f2f030
4 changed files with 80 additions and 52 deletions
|
|
@ -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 @@
|
||||||
* 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;
|
||||||
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 DO_NOT_OFFSET = Long.MAX_VALUE;
|
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 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
|
||||||
|
|
@ -36,9 +52,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")
|
|
||||||
private boolean sharedInitializationStarted;
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private long firstSampleTimestampUs;
|
private long firstSampleTimestampUs;
|
||||||
|
|
||||||
|
|
@ -48,11 +61,19 @@ public final class TimestampAdjuster {
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
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 {
|
||||||
* 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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
* 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 {
|
||||||
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 @@
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 @@
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue