diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index 4094145f978..2856fd2a273 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -15,8 +15,13 @@ */ package com.google.android.exoplayer2.util; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; + +import android.os.SystemClock; import androidx.annotation.GuardedBy; import com.google.android.exoplayer2.C; +import java.util.concurrent.TimeoutException; /** * Adjusts and offsets sample timestamps. MPEG-2 TS timestamps scaling and adjustment is supported, @@ -99,20 +104,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; + } } } } @@ -196,7 +221,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. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 36fd82d8315..d5aa6b8a790 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -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; @@ -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. */ @@ -172,6 +176,7 @@ public HlsChunkSource( HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, TimestampAdjusterProvider timestampAdjusterProvider, + long timestampAdjusterInitializationTimeoutMs, @Nullable List muxedCaptionFormats, PlayerId playerId, @Nullable CmcdConfiguration cmcdConfiguration) { @@ -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; @@ -519,6 +525,7 @@ public void getNextChunk( trackSelection.getSelectionData(), isPrimaryTimestampSource, timestampAdjusterProvider, + timestampAdjusterInitializationTimeoutMs, previous, /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), /* initSegmentKey= */ keyCache.get(initSegmentKeyUri), diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 4c3400e4111..6cd1d5b44d3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -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; @@ -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 @@ -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, @@ -189,6 +194,7 @@ public static HlsMediaChunk createInstance( mediaSegment.hasGapTag, isPrimaryTimestampSource, /* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber), + timestampAdjusterInitializationTimeoutMs, mediaSegment.drmInitData, previousExtractor, id3Decoder, @@ -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; @@ -302,6 +309,7 @@ private HlsMediaChunk( boolean hasGapTag, boolean isPrimaryTimestampSource, TimestampAdjuster timestampAdjuster, + long timestampAdjusterInitializationTimeoutMs, @Nullable DrmInitData drmInitData, @Nullable HlsMediaChunkExtractor previousExtractor, Id3Decoder id3Decoder, @@ -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; @@ -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 = diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 13f19d2a94b..6d771171eb3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -82,6 +82,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; @@ -116,6 +117,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, @@ -132,7 +136,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; @@ -148,6 +153,7 @@ public HlsMediaPeriod( this.metadataType = metadataType; this.useSessionKeys = useSessionKeys; this.playerId = playerId; + this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs; sampleStreamWrapperCallback = new SampleStreamWrapperCallback(); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); @@ -779,6 +785,7 @@ private HlsSampleStreamWrapper buildSampleStreamWrapper( dataSourceFactory, mediaTransferListener, timestampAdjusterProvider, + timestampAdjusterInitializationTimeoutMs, muxedCaptionFormats, playerId, cmcdConfiguration); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 410b1b5f828..e147356a9f7 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -111,6 +111,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. @@ -320,6 +321,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}. @@ -370,7 +386,8 @@ public HlsMediaSource createMediaSource(MediaItem mediaItem) { elapsedRealTimeOffsetMs, allowChunklessPreparation, metadataType, - useSessionKeys); + useSessionKeys, + timestampAdjusterInitializationTimeoutMs); } @Override @@ -392,6 +409,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; @@ -408,7 +426,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; @@ -423,6 +442,7 @@ private HlsMediaSource( this.allowChunklessPreparation = allowChunklessPreparation; this.metadataType = metadataType; this.useSessionKeys = useSessionKeys; + this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs; } @Override @@ -466,7 +486,8 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star allowChunklessPreparation, metadataType, useSessionKeys, - getPlayerId()); + getPlayerId(), + timestampAdjusterInitializationTimeoutMs); } @Override diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java index 1cf8776f325..998caaa52c8 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java @@ -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); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java index c80a7ea6576..9ca2e54ee20 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -96,7 +96,8 @@ public void getSteamKeys_isCompatibleWithHlsMultivariantPlaylistFilter() { /* allowChunklessPreparation= */ true, HlsMediaSource.METADATA_TYPE_ID3, /* useSessionKeys= */ false, - PlayerId.UNSET); + PlayerId.UNSET, + /* timestampAdjusterInitializationTimeoutMs= */ 0); }; MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(