Skip to content

Commit

Permalink
Throw exception when TimestampAdjuster initialization hits timeout
Browse files Browse the repository at this point in the history
Add `HlsMediaSource.Factory.setTimestampAdjusterInitializationTimeoutMs(long)` to set the timeout for the loading thread to wait for the `TimestampAdjuster` to initialize. If the initialization doesn't complete before the timeout, a `PlaybackException` is thrown to avoid the playback endless stalling. The timeout is set to zero by default.

This can avoid HLS playback endlessly stalls when manifest has missing discontinuities. According to the HLS spec, all variants and renditions have discontinuities at the same points in time. If not, the one with discontinuities will have a new `TimestampAdjuster` not shared by the others. When the loading thread of that variant is waiting for the other threads to initialize the timestamp and hits the timeout, the playback will stall.

Issue: #323

#minor-release

PiperOrigin-RevId: 539108886
  • Loading branch information
tianyif authored and tof-tof committed Jun 10, 2023
1 parent cd604e7 commit db3e662
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 11 deletions.
7 changes: 7 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@
produced a `IndexOutOfBoundsException`
([#10838](https://github.com/google/ExoPlayer/issues/10838)).
* HLS Extension:
* Add
`HlsMediaSource.Factory.setTimestampAdjusterInitializationTimeoutMs(long)`
to set a timeout for the loading thread to wait for the
`TimestampAdjuster` to initialize. If the initialization doesn't
complete before the timeout, a `PlaybackException` is thrown to avoid
the playback endless stalling. The timeout is set to zero by default
([#323](https://github.com/androidx/media/issues//323)).
* Smooth Streaming Extension:
* RTSP Extension:
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
*/
package androidx.media3.common.util;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;

import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.media3.common.C;
import java.util.concurrent.TimeoutException;

/**
* Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported,
Expand Down Expand Up @@ -100,20 +105,40 @@ public TimestampAdjuster(long firstSampleTimestampUs) {
* @param canInitialize Whether the caller is able to initialize the adjuster, if needed.
* @param nextSampleTimestampUs The desired timestamp for the next sample loaded by the calling
* thread, in microseconds. Only used if {@code canInitialize} is {@code true}.
* @param timeoutMs The timeout for the thread to wait for the timestamp adjuster to initialize,
* in milliseconds. A timeout of zero is interpreted as an infinite timeout.
* @throws InterruptedException If the thread is interrupted whilst blocked waiting for
* initialization to complete.
* @throws TimeoutException If the thread is timeout whilst blocked waiting for initialization to
* complete.
*/
public synchronized void sharedInitializeOrWait(boolean canInitialize, long nextSampleTimestampUs)
throws InterruptedException {
Assertions.checkState(firstSampleTimestampUs == MODE_SHARED);
public synchronized void sharedInitializeOrWait(
boolean canInitialize, long nextSampleTimestampUs, long timeoutMs)
throws InterruptedException, TimeoutException {
checkState(firstSampleTimestampUs == MODE_SHARED);
if (isInitialized()) {
return;
} else if (canInitialize) {
this.nextSampleTimestampUs.set(nextSampleTimestampUs);
} else {
// Wait for another calling thread to complete initialization.
long totalWaitDurationMs = 0;
long remainingTimeoutMs = timeoutMs;
while (!isInitialized()) {
wait();
if (timeoutMs == 0) {
wait();
} else {
checkState(remainingTimeoutMs > 0);
long waitStartingTimeMs = SystemClock.elapsedRealtime();
wait(remainingTimeoutMs);
totalWaitDurationMs += SystemClock.elapsedRealtime() - waitStartingTimeMs;
if (totalWaitDurationMs >= timeoutMs && !isInitialized()) {
String message =
"TimestampAdjuster failed to initialize in " + timeoutMs + " milliseconds";
throw new TimeoutException(message);
}
remainingTimeoutMs = timeoutMs - totalWaitDurationMs;
}
}
}
}
Expand Down Expand Up @@ -197,7 +222,7 @@ public synchronized long adjustSampleTimestamp(long timeUs) {
if (!isInitialized()) {
long desiredSampleTimestampUs =
firstSampleTimestampUs == MODE_SHARED
? Assertions.checkNotNull(nextSampleTimestampUs.get())
? checkNotNull(nextSampleTimestampUs.get())
: firstSampleTimestampUs;
timestampOffsetUs = desiredSampleTimestampUs - timeUs;
// Notify threads waiting for the timestamp offset to be determined.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public void clear() {
private final FullSegmentEncryptionKeyCache keyCache;
private final PlayerId playerId;
@Nullable private final CmcdConfiguration cmcdConfiguration;
private final long timestampAdjusterInitializationTimeoutMs;

private boolean isPrimaryTimestampSource;
private byte[] scratchSpace;
Expand Down Expand Up @@ -161,6 +162,9 @@ public void clear() {
* @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If multiple
* {@link HlsChunkSource}s are used for a single playback, they should all share the same
* provider.
* @param timestampAdjusterInitializationTimeoutMs The timeout for the loading thread to wait for
* the timestamp adjuster to initialize, in milliseconds. A timeout of zero is interpreted as
* an infinite timeout.
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
* information is available in the multivariant playlist.
*/
Expand All @@ -172,6 +176,7 @@ public HlsChunkSource(
HlsDataSourceFactory dataSourceFactory,
@Nullable TransferListener mediaTransferListener,
TimestampAdjusterProvider timestampAdjusterProvider,
long timestampAdjusterInitializationTimeoutMs,
@Nullable List<Format> muxedCaptionFormats,
PlayerId playerId,
@Nullable CmcdConfiguration cmcdConfiguration) {
Expand All @@ -180,6 +185,7 @@ public HlsChunkSource(
this.playlistUrls = playlistUrls;
this.playlistFormats = playlistFormats;
this.timestampAdjusterProvider = timestampAdjusterProvider;
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
this.muxedCaptionFormats = muxedCaptionFormats;
this.playerId = playerId;
this.cmcdConfiguration = cmcdConfiguration;
Expand Down Expand Up @@ -519,6 +525,7 @@ public void getNextChunk(
trackSelection.getSelectionData(),
isPrimaryTimestampSource,
timestampAdjusterProvider,
timestampAdjusterInitializationTimeoutMs,
previous,
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import java.io.InterruptedIOException;
import java.math.BigInteger;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Expand All @@ -73,6 +74,9 @@
* @param isPrimaryTimestampSource True if the chunk can initialize the timestamp adjuster.
* @param timestampAdjusterProvider The provider from which to obtain the {@link
* TimestampAdjuster}.
* @param timestampAdjusterInitializationTimeoutMs The timeout for the loading thread to wait for
* the timestamp adjuster to initialize, in milliseconds. A timeout of zero is interpreted as
* an infinite timeout.
* @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
* @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
Expand All @@ -92,6 +96,7 @@ public static HlsMediaChunk createInstance(
@Nullable Object trackSelectionData,
boolean isPrimaryTimestampSource,
TimestampAdjusterProvider timestampAdjusterProvider,
long timestampAdjusterInitializationTimeoutMs,
@Nullable HlsMediaChunk previousChunk,
@Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey,
Expand Down Expand Up @@ -189,6 +194,7 @@ public static HlsMediaChunk createInstance(
mediaSegment.hasGapTag,
isPrimaryTimestampSource,
/* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber),
timestampAdjusterInitializationTimeoutMs,
mediaSegment.drmInitData,
previousExtractor,
id3Decoder,
Expand Down Expand Up @@ -267,6 +273,7 @@ public static boolean shouldSpliceIn(
private final boolean mediaSegmentEncrypted;
private final boolean initSegmentEncrypted;
private final PlayerId playerId;
private final long timestampAdjusterInitializationTimeoutMs;

private @MonotonicNonNull HlsMediaChunkExtractor extractor;
private @MonotonicNonNull HlsSampleStreamWrapper output;
Expand Down Expand Up @@ -302,6 +309,7 @@ private HlsMediaChunk(
boolean hasGapTag,
boolean isPrimaryTimestampSource,
TimestampAdjuster timestampAdjuster,
long timestampAdjusterInitializationTimeoutMs,
@Nullable DrmInitData drmInitData,
@Nullable HlsMediaChunkExtractor previousExtractor,
Id3Decoder id3Decoder,
Expand All @@ -328,6 +336,7 @@ private HlsMediaChunk(
this.playlistUrl = playlistUrl;
this.isPrimaryTimestampSource = isPrimaryTimestampSource;
this.timestampAdjuster = timestampAdjuster;
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
this.hasGapTag = hasGapTag;
this.extractorFactory = extractorFactory;
this.muxedCaptionFormats = muxedCaptionFormats;
Expand Down Expand Up @@ -502,9 +511,12 @@ private DefaultExtractorInput prepareExtraction(
long bytesToRead = dataSource.open(dataSpec);
if (initializeTimestampAdjuster) {
try {
timestampAdjuster.sharedInitializeOrWait(isPrimaryTimestampSource, startTimeUs);
timestampAdjuster.sharedInitializeOrWait(
isPrimaryTimestampSource, startTimeUs, timestampAdjusterInitializationTimeoutMs);
} catch (InterruptedException e) {
throw new InterruptedIOException();
} catch (TimeoutException e) {
throw new IOException(e);
}
}
DefaultExtractorInput extractorInput =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
private final boolean useSessionKeys;
private final PlayerId playerId;
private final HlsSampleStreamWrapper.Callback sampleStreamWrapperCallback;
private final long timestampAdjusterInitializationTimeoutMs;

@Nullable private MediaPeriod.Callback mediaPeriodCallback;
private int pendingPrepareCount;
Expand Down Expand Up @@ -118,6 +119,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
* @param metadataType The type of metadata to extract from the period.
* @param useSessionKeys Whether to use #EXT-X-SESSION-KEY tags.
* @param playerId The ID of the current player.
* @param timestampAdjusterInitializationTimeoutMs The timeout for the loading thread to wait for
* the timestamp adjuster to initialize, in milliseconds. A timeout of zero is interpreted as
* an infinite timeout.
*/
public HlsMediaPeriod(
HlsExtractorFactory extractorFactory,
Expand All @@ -134,7 +138,8 @@ public HlsMediaPeriod(
boolean allowChunklessPreparation,
@HlsMediaSource.MetadataType int metadataType,
boolean useSessionKeys,
PlayerId playerId) {
PlayerId playerId,
long timestampAdjusterInitializationTimeoutMs) {
this.extractorFactory = extractorFactory;
this.playlistTracker = playlistTracker;
this.dataSourceFactory = dataSourceFactory;
Expand All @@ -150,6 +155,7 @@ public HlsMediaPeriod(
this.metadataType = metadataType;
this.useSessionKeys = useSessionKeys;
this.playerId = playerId;
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
sampleStreamWrapperCallback = new SampleStreamWrapperCallback();
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
Expand Down Expand Up @@ -781,6 +787,7 @@ private HlsSampleStreamWrapper buildSampleStreamWrapper(
dataSourceFactory,
mediaTransferListener,
timestampAdjusterProvider,
timestampAdjusterInitializationTimeoutMs,
muxedCaptionFormats,
playerId,
cmcdConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public static final class Factory implements MediaSourceFactory {
private @MetadataType int metadataType;
private boolean useSessionKeys;
private long elapsedRealTimeOffsetMs;
private long timestampAdjusterInitializationTimeoutMs;

/**
* Creates a new factory for {@link HlsMediaSource}s.
Expand Down Expand Up @@ -322,6 +323,21 @@ public Factory setDrmSessionManagerProvider(
return this;
}

/**
* Sets the timeout for the loading thread to wait for the timestamp adjuster to initialize, in
* milliseconds.The default value is zero, which is interpreted as an infinite timeout.
*
* @param timestampAdjusterInitializationTimeoutMs The timeout in milliseconds. A timeout of
* zero is interpreted as an infinite timeout.
* @return This factory, for convenience.
*/
@CanIgnoreReturnValue
public Factory setTimestampAdjusterInitializationTimeoutMs(
long timestampAdjusterInitializationTimeoutMs) {
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
return this;
}

/**
* Sets the offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix
* epoch. By default, is it set to {@link C#TIME_UNSET}.
Expand Down Expand Up @@ -372,7 +388,8 @@ public HlsMediaSource createMediaSource(MediaItem mediaItem) {
elapsedRealTimeOffsetMs,
allowChunklessPreparation,
metadataType,
useSessionKeys);
useSessionKeys,
timestampAdjusterInitializationTimeoutMs);
}

@Override
Expand All @@ -394,6 +411,7 @@ public HlsMediaSource createMediaSource(MediaItem mediaItem) {
private final HlsPlaylistTracker playlistTracker;
private final long elapsedRealTimeOffsetMs;
private final MediaItem mediaItem;
private final long timestampAdjusterInitializationTimeoutMs;

private MediaItem.LiveConfiguration liveConfiguration;
@Nullable private TransferListener mediaTransferListener;
Expand All @@ -410,7 +428,8 @@ private HlsMediaSource(
long elapsedRealTimeOffsetMs,
boolean allowChunklessPreparation,
@MetadataType int metadataType,
boolean useSessionKeys) {
boolean useSessionKeys,
long timestampAdjusterInitializationTimeoutMs) {
this.localConfiguration = checkNotNull(mediaItem.localConfiguration);
this.mediaItem = mediaItem;
this.liveConfiguration = mediaItem.liveConfiguration;
Expand All @@ -425,6 +444,7 @@ private HlsMediaSource(
this.allowChunklessPreparation = allowChunklessPreparation;
this.metadataType = metadataType;
this.useSessionKeys = useSessionKeys;
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
}

@Override
Expand Down Expand Up @@ -468,7 +488,8 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star
allowChunklessPreparation,
metadataType,
useSessionKeys,
getPlayerId());
getPlayerId(),
timestampAdjusterInitializationTimeoutMs);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ private HlsChunkSource createHlsChunkSource(@Nullable CmcdConfiguration cmcdConf
new DefaultHlsDataSourceFactory(new FakeDataSource.Factory()),
/* mediaTransferListener= */ null,
new TimestampAdjusterProvider(),
/* timestampAdjusterInitializationTimeoutMs= */ 0,
/* muxedCaptionFormats= */ null,
PlayerId.UNSET,
cmcdConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() {
/* allowChunklessPreparation= */ true,
HlsMediaSource.METADATA_TYPE_ID3,
/* useSessionKeys= */ false,
PlayerId.UNSET);
PlayerId.UNSET,
/* timestampAdjusterInitializationTimeoutMs= */ 0);
};

MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
Expand Down

0 comments on commit db3e662

Please sign in to comment.