Skip to content

Commit

Permalink
HLS: Allow audio variants to initialize the timestamp adjuster
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ojw28 authored and marcbaechinger committed Apr 9, 2021
1 parent 06e6391 commit 0d052e0
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 56 deletions.
7 changes: 6 additions & 1 deletion RELEASENOTES.md
Expand Up @@ -24,14 +24,19 @@
* `DebugTextViewHelper` moved from `ui` package to `util` package.
* Spherical UI components moved from `video.spherical` package to
`ui.spherical` package, and made package private.
* Core
* Core:
* Move `getRendererCount` and `getRendererType` methods from `Player` to
`ExoPlayer`.
* Reset playback speed when live playback speed control becomes unused
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
* Fix playback position issue when re-preparing playback after a
BehindLiveWindowException
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
* 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 `Player.DefaultEventListener`. Use `Player.EventListener`
instead.
Expand Down
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.util;

import androidx.annotation.GuardedBy;
import com.google.android.exoplayer2.C;

/**
Expand All @@ -35,34 +36,73 @@ public final class TimestampAdjuster {
*/
private static final long MAX_PTS_PLUS_ONE = 0x200000000L;

@GuardedBy("this")
private boolean sharedInitializationStarted;

@GuardedBy("this")
private long firstSampleTimestampUs;

@GuardedBy("this")
private long timestampOffsetUs;

// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
private volatile long lastSampleTimestampUs;
@GuardedBy("this")
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) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs);
}

/**
* Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
* called before any timestamps have been adjusted.
* For shared timestamp adjusters, performs necessary initialization actions for a caller.
*
* @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
* <ul>
* <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) {
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
this.firstSampleTimestampUs = firstSampleTimestampUs;
public synchronized void sharedInitializeOrWait(boolean canInitialize, long startTimeUs)
throws InterruptedException {
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;
}

Expand All @@ -72,32 +112,37 @@ public long getFirstSampleTimestampUs() {
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* C#TIME_UNSET}.
*/
public long getLastAdjustedTimestampUs() {
public synchronized long getLastAdjustedTimestampUs() {
return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
}

/**
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
* If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. If
* {@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.
*
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
* {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
* be offset.
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. {@link
* C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not be
* offset.
*/
public long getTimestampOffsetUs() {
public synchronized long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET
? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
}

/**
* 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;
sharedInitializationStarted = false;
}

/**
Expand All @@ -106,7 +151,7 @@ public void reset() {
* @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
* @return The adjusted timestamp in microseconds.
*/
public long adjustTsTimestamp(long pts90Khz) {
public synchronized long adjustTsTimestamp(long pts90Khz) {
if (pts90Khz == C.TIME_UNSET) {
return C.TIME_UNSET;
}
Expand All @@ -131,7 +176,7 @@ public long adjustTsTimestamp(long pts90Khz) {
* @param timeUs The timestamp to adjust in microseconds.
* @return The adjusted timestamp in microseconds.
*/
public long adjustSampleTimestamp(long timeUs) {
public synchronized long adjustSampleTimestamp(long timeUs) {
if (timeUs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
Expand All @@ -143,26 +188,13 @@ public long adjustSampleTimestamp(long timeUs) {
// Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs;
}
synchronized (this) {
lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized.
notifyAll();
}
lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized.
notifyAll();
}
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.
*
Expand Down
Expand Up @@ -144,8 +144,7 @@ public void seek(long position, long timeUs) {
// 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
// different position, we need to set the first sample timestamp manually again.
timestampAdjuster.reset();
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
timestampAdjuster.reset(timeUs);
}

if (psBinarySearchSeeker != null) {
Expand Down
Expand Up @@ -268,8 +268,7 @@ public void seek(long position, long timeUs) {
// sample timestamp for that track manually.
// - 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.
timestampAdjuster.reset();
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
timestampAdjuster.reset(timeUs);
}
}
if (timeUs != 0 && tsBinarySearchSeeker != null) {
Expand Down
Expand Up @@ -391,15 +391,10 @@ private void maybeLoadInitData() throws IOException {

@RequiresNonNull("output")
private void loadMedia() throws IOException {
if (!isMasterTimestampSource) {
try {
timestampAdjuster.waitUntilInitialized();
} catch (InterruptedException e) {
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);
try {
timestampAdjuster.sharedInitializeOrWait(isMasterTimestampSource, startTimeUs);
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
}
Expand Down
Expand Up @@ -88,6 +88,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
// Maps sample stream wrappers to variant/rendition index by matching array positions.
private int[][] manifestUrlIndicesPerWrapper;
private int audioVideoSampleStreamWrapperCount;
private SequenceableLoader compositeSequenceableLoader;

/**
Expand Down Expand Up @@ -315,8 +316,9 @@ public long selectTracks(
if (wrapperEnabled) {
newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
if (newEnabledSampleStreamWrapperCount++ == 0) {
// The first enabled wrapper is responsible for initializing timestamp adjusters. This
// way, if enabled, variants are responsible. Else audio renditions. Else text renditions.
// The first enabled wrapper is always allowed to initialize timestamp adjusters. Note
// 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);
if (wasReset || enabledSampleStreamWrappers.length == 0
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
Expand All @@ -326,7 +328,11 @@ public long selectTracks(
forceReset = true;
}
} 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);
}
}
}
Expand Down Expand Up @@ -496,6 +502,8 @@ private void buildAndPrepareSampleStreamWrappers(long positionUs) {
manifestUrlIndicesPerWrapper,
overridingDrmInitData);

audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size();

// Subtitle stream wrappers. We can always use master playlist information to prepare these.
for (int i = 0; i < subtitleRenditions.size(); i++) {
Rendition subtitleRendition = subtitleRenditions.get(i);
Expand Down

0 comments on commit 0d052e0

Please sign in to comment.