mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
HLS: Allow audio variants to initialize the timestamp adjuster
This makes HLS playback less liable to become stuck if discontinuity tags are inserted at different times across media playlists. Issue: #8700 Issue: #8372 PiperOrigin-RevId: 362903428
This commit is contained in:
parent
6b63bb2248
commit
6f8a8fbc1c
6 changed files with 94 additions and 56 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
* `DebugTextViewHelper` moved from `ui` package to `util` package.
|
* `DebugTextViewHelper` moved from `ui` package to `util` package.
|
||||||
* Spherical UI components moved from `video.spherical` package to
|
* Spherical UI components moved from `video.spherical` package to
|
||||||
`ui.spherical` package, and made package private.
|
`ui.spherical` package, and made package private.
|
||||||
* Core
|
* Core:
|
||||||
* Move `getRendererCount` and `getRendererType` methods from `Player` to
|
* Move `getRendererCount` and `getRendererType` methods from `Player` to
|
||||||
`ExoPlayer`.
|
`ExoPlayer`.
|
||||||
* Reset playback speed when live playback speed control becomes unused
|
* Reset playback speed when live playback speed control becomes unused
|
||||||
|
|
@ -34,6 +34,11 @@
|
||||||
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
|
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
|
||||||
* Add a `Listener` interface to receive all player events in a single
|
* Add a `Listener` interface to receive all player events in a single
|
||||||
object.
|
object.
|
||||||
|
* HLS:
|
||||||
|
* Fix issue that could cause playback to become stuck if corresponding
|
||||||
|
`EXT-X-DISCONTINUITY` tags in different media playlists occur at
|
||||||
|
different positions in time
|
||||||
|
([#8372](https://github.com/google/ExoPlayer/issues/8372)).
|
||||||
* Remove deprecated symbols:
|
* Remove deprecated symbols:
|
||||||
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
|
* Remove `Player.DefaultEventListener`. Use `Player.EventListener`
|
||||||
instead.
|
instead.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.util;
|
package com.google.android.exoplayer2.util;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,34 +36,73 @@ 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")
|
||||||
private long firstSampleTimestampUs;
|
private long firstSampleTimestampUs;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
private long timestampOffsetUs;
|
private long timestampOffsetUs;
|
||||||
|
|
||||||
// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
|
@GuardedBy("this")
|
||||||
private volatile long lastSampleTimestampUs;
|
private long lastSampleTimestampUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
|
* @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp in
|
||||||
|
* microseconds, or {@link #DO_NOT_OFFSET} if timestamps should not be offset.
|
||||||
*/
|
*/
|
||||||
public TimestampAdjuster(long firstSampleTimestampUs) {
|
public TimestampAdjuster(long firstSampleTimestampUs) {
|
||||||
|
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
||||||
lastSampleTimestampUs = C.TIME_UNSET;
|
lastSampleTimestampUs = C.TIME_UNSET;
|
||||||
setFirstSampleTimestampUs(firstSampleTimestampUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
|
* For shared timestamp adjusters, performs necessary initialization actions for a caller.
|
||||||
* called before any timestamps have been adjusted.
|
|
||||||
*
|
*
|
||||||
* @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
|
* <ul>
|
||||||
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
|
* <li>If the adjuster does not yet have a target {@link #getFirstSampleTimestampUs first sample
|
||||||
|
* timestamp} and if {@code canInitialize} is {@code true}, then initialization is started
|
||||||
|
* by setting the target first sample timestamp to {@code firstSampleTimestampUs}. The call
|
||||||
|
* returns, allowing the caller to proceed. Initialization completes when a caller adjusts
|
||||||
|
* the first timestamp.
|
||||||
|
* <li>If {@code canInitialize} is {@code true} and the adjuster already has a target {@link
|
||||||
|
* #getFirstSampleTimestampUs first sample timestamp}, then the call returns to allow the
|
||||||
|
* caller to proceed only if {@code firstSampleTimestampUs} is equal to the target. This
|
||||||
|
* ensures a caller that's previously started initialization can continue to proceed. It
|
||||||
|
* 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>
|
||||||
|
*
|
||||||
|
* @param canInitialize Whether the caller is able to initialize the adjuster, if needed.
|
||||||
|
* @param startTimeUs The desired first sample timestamp of the caller, 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 setFirstSampleTimestampUs(long firstSampleTimestampUs) {
|
public synchronized void sharedInitializeOrWait(boolean canInitialize, long startTimeUs)
|
||||||
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
|
throws InterruptedException {
|
||||||
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
if (canInitialize && !sharedInitializationStarted) {
|
||||||
|
firstSampleTimestampUs = startTimeUs;
|
||||||
|
sharedInitializationStarted = true;
|
||||||
|
}
|
||||||
|
if (!canInitialize || startTimeUs != firstSampleTimestampUs) {
|
||||||
|
while (lastSampleTimestampUs == C.TIME_UNSET) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
|
/**
|
||||||
public long getFirstSampleTimestampUs() {
|
* Returns the value of the first adjusted sample timestamp in microseconds, or {@link
|
||||||
|
* #DO_NOT_OFFSET} if timestamps will not be offset.
|
||||||
|
*/
|
||||||
|
public synchronized long getFirstSampleTimestampUs() {
|
||||||
return firstSampleTimestampUs;
|
return firstSampleTimestampUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,22 +112,22 @@ public final class TimestampAdjuster {
|
||||||
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
|
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
|
||||||
* C#TIME_UNSET}.
|
* C#TIME_UNSET}.
|
||||||
*/
|
*/
|
||||||
public long getLastAdjustedTimestampUs() {
|
public synchronized long getLastAdjustedTimestampUs() {
|
||||||
return lastSampleTimestampUs != C.TIME_UNSET
|
return lastSampleTimestampUs != C.TIME_UNSET
|
||||||
? (lastSampleTimestampUs + timestampOffsetUs)
|
? (lastSampleTimestampUs + timestampOffsetUs)
|
||||||
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
|
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
|
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. If
|
||||||
* If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
|
* {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
|
||||||
* adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
|
* adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
|
||||||
*
|
*
|
||||||
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
|
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. {@link
|
||||||
* {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
|
* C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not be
|
||||||
* be offset.
|
* offset.
|
||||||
*/
|
*/
|
||||||
public long getTimestampOffsetUs() {
|
public synchronized long getTimestampOffsetUs() {
|
||||||
return firstSampleTimestampUs == DO_NOT_OFFSET
|
return firstSampleTimestampUs == DO_NOT_OFFSET
|
||||||
? 0
|
? 0
|
||||||
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
|
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
|
||||||
|
|
@ -95,9 +135,14 @@ public final class TimestampAdjuster {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the instance to its initial state.
|
* Resets the instance to its initial state.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
*/
|
*/
|
||||||
public void reset() {
|
public synchronized void reset(long firstSampleTimestampUs) {
|
||||||
|
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
||||||
lastSampleTimestampUs = C.TIME_UNSET;
|
lastSampleTimestampUs = C.TIME_UNSET;
|
||||||
|
sharedInitializationStarted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,7 +151,7 @@ public final class TimestampAdjuster {
|
||||||
* @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
|
* @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
|
||||||
* @return The adjusted timestamp in microseconds.
|
* @return The adjusted timestamp in microseconds.
|
||||||
*/
|
*/
|
||||||
public long adjustTsTimestamp(long pts90Khz) {
|
public synchronized long adjustTsTimestamp(long pts90Khz) {
|
||||||
if (pts90Khz == C.TIME_UNSET) {
|
if (pts90Khz == C.TIME_UNSET) {
|
||||||
return C.TIME_UNSET;
|
return C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +176,7 @@ public final class TimestampAdjuster {
|
||||||
* @param timeUs The timestamp to adjust in microseconds.
|
* @param timeUs The timestamp to adjust in microseconds.
|
||||||
* @return The adjusted timestamp in microseconds.
|
* @return The adjusted timestamp in microseconds.
|
||||||
*/
|
*/
|
||||||
public long adjustSampleTimestamp(long timeUs) {
|
public synchronized long adjustSampleTimestamp(long timeUs) {
|
||||||
if (timeUs == C.TIME_UNSET) {
|
if (timeUs == C.TIME_UNSET) {
|
||||||
return C.TIME_UNSET;
|
return C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
@ -143,26 +188,13 @@ public final class TimestampAdjuster {
|
||||||
// Calculate the timestamp offset.
|
// Calculate the timestamp offset.
|
||||||
timestampOffsetUs = firstSampleTimestampUs - timeUs;
|
timestampOffsetUs = firstSampleTimestampUs - timeUs;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
lastSampleTimestampUs = timeUs;
|
||||||
lastSampleTimestampUs = timeUs;
|
// Notify threads waiting for this adjuster to be initialized.
|
||||||
// Notify threads waiting for this adjuster to be initialized.
|
notifyAll();
|
||||||
notifyAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return timeUs + timestampOffsetUs;
|
return timeUs + timestampOffsetUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks the calling thread until this adjuster is initialized.
|
|
||||||
*
|
|
||||||
* @throws InterruptedException If the thread was interrupted.
|
|
||||||
*/
|
|
||||||
public synchronized void waitUntilInitialized() throws InterruptedException {
|
|
||||||
while (lastSampleTimestampUs == C.TIME_UNSET) {
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a 90 kHz clock timestamp to a timestamp in microseconds.
|
* Converts a 90 kHz clock timestamp to a timestamp in microseconds.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -144,8 +144,7 @@ public final class PsExtractor implements Extractor {
|
||||||
// we have to set the first sample timestamp manually.
|
// we have to set the first sample timestamp manually.
|
||||||
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
|
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
|
||||||
// different position, we need to set the first sample timestamp manually again.
|
// different position, we need to set the first sample timestamp manually again.
|
||||||
timestampAdjuster.reset();
|
timestampAdjuster.reset(timeUs);
|
||||||
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (psBinarySearchSeeker != null) {
|
if (psBinarySearchSeeker != null) {
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,7 @@ public final class TsExtractor implements Extractor {
|
||||||
// sample timestamp for that track manually.
|
// sample timestamp for that track manually.
|
||||||
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
|
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
|
||||||
// different position, we need to set the first sample timestamp manually again.
|
// different position, we need to set the first sample timestamp manually again.
|
||||||
timestampAdjuster.reset();
|
timestampAdjuster.reset(timeUs);
|
||||||
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (timeUs != 0 && tsBinarySearchSeeker != null) {
|
if (timeUs != 0 && tsBinarySearchSeeker != null) {
|
||||||
|
|
|
||||||
|
|
@ -391,15 +391,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
@RequiresNonNull("output")
|
@RequiresNonNull("output")
|
||||||
private void loadMedia() throws IOException {
|
private void loadMedia() throws IOException {
|
||||||
if (!isMasterTimestampSource) {
|
try {
|
||||||
try {
|
timestampAdjuster.sharedInitializeOrWait(isMasterTimestampSource, startTimeUs);
|
||||||
timestampAdjuster.waitUntilInitialized();
|
} catch (InterruptedException e) {
|
||||||
} catch (InterruptedException e) {
|
throw new InterruptedIOException();
|
||||||
throw new InterruptedIOException();
|
|
||||||
}
|
|
||||||
} else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) {
|
|
||||||
// We're the master and we haven't set the desired first sample timestamp yet.
|
|
||||||
timestampAdjuster.setFirstSampleTimestampUs(startTimeUs);
|
|
||||||
}
|
}
|
||||||
feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
|
feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
||||||
// Maps sample stream wrappers to variant/rendition index by matching array positions.
|
// Maps sample stream wrappers to variant/rendition index by matching array positions.
|
||||||
private int[][] manifestUrlIndicesPerWrapper;
|
private int[][] manifestUrlIndicesPerWrapper;
|
||||||
|
private int audioVideoSampleStreamWrapperCount;
|
||||||
private SequenceableLoader compositeSequenceableLoader;
|
private SequenceableLoader compositeSequenceableLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -315,8 +316,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
if (wrapperEnabled) {
|
if (wrapperEnabled) {
|
||||||
newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
|
newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
|
||||||
if (newEnabledSampleStreamWrapperCount++ == 0) {
|
if (newEnabledSampleStreamWrapperCount++ == 0) {
|
||||||
// The first enabled wrapper is responsible for initializing timestamp adjusters. This
|
// The first enabled wrapper is always allowed to initialize timestamp adjusters. Note
|
||||||
// way, if enabled, variants are responsible. Else audio renditions. Else text renditions.
|
// that the first wrapper will correspond to a variant, or else an audio rendition, or
|
||||||
|
// else a text rendition, in that order.
|
||||||
sampleStreamWrapper.setIsTimestampMaster(true);
|
sampleStreamWrapper.setIsTimestampMaster(true);
|
||||||
if (wasReset || enabledSampleStreamWrappers.length == 0
|
if (wasReset || enabledSampleStreamWrappers.length == 0
|
||||||
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
|
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
|
||||||
|
|
@ -326,7 +328,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
forceReset = true;
|
forceReset = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sampleStreamWrapper.setIsTimestampMaster(false);
|
// Additional wrappers are also allowed to initialize timestamp adjusters if they contain
|
||||||
|
// audio or video, since they are expected to contain dense samples. Text wrappers are not
|
||||||
|
// permitted except in the case above in which no variant or audio rendition wrappers are
|
||||||
|
// enabled.
|
||||||
|
sampleStreamWrapper.setIsTimestampMaster(i < audioVideoSampleStreamWrapperCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -496,6 +502,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||||
manifestUrlIndicesPerWrapper,
|
manifestUrlIndicesPerWrapper,
|
||||||
overridingDrmInitData);
|
overridingDrmInitData);
|
||||||
|
|
||||||
|
audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size();
|
||||||
|
|
||||||
// Subtitle stream wrappers. We can always use master playlist information to prepare these.
|
// Subtitle stream wrappers. We can always use master playlist information to prepare these.
|
||||||
for (int i = 0; i < subtitleRenditions.size(); i++) {
|
for (int i = 0; i < subtitleRenditions.size(); i++) {
|
||||||
Rendition subtitleRendition = subtitleRenditions.get(i);
|
Rendition subtitleRendition = subtitleRenditions.get(i);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue