mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Throw exception when TimestampAdjuster
initialization hits timeout
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: androidx/media#323 #minor-release PiperOrigin-RevId: 539108886 (cherry picked from commit db3e662bdc17945acbe835120806b6aa597dee8a)
This commit is contained in:
parent
21fb8c9942
commit
23e92805a1
@ -85,6 +85,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.):
|
||||
|
@ -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,
|
||||
@ -100,20 +105,40 @@ public final class TimestampAdjuster {
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,7 +222,7 @@ public final class TimestampAdjuster {
|
||||
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.
|
||||
|
@ -133,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* @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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
HlsDataSourceFactory dataSourceFactory,
|
||||
@Nullable TransferListener mediaTransferListener,
|
||||
TimestampAdjusterProvider timestampAdjusterProvider,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
PlayerId playerId,
|
||||
@Nullable CmcdConfiguration cmcdConfiguration) {
|
||||
@ -180,6 +185,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
trackSelection.getSelectionData(),
|
||||
isPrimaryTimestampSource,
|
||||
timestampAdjusterProvider,
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
previous,
|
||||
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
|
||||
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
|
||||
|
@ -47,6 +47,7 @@ import java.io.IOException;
|
||||
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 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
* @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 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
@Nullable Object trackSelectionData,
|
||||
boolean isPrimaryTimestampSource,
|
||||
TimestampAdjusterProvider timestampAdjusterProvider,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable HlsMediaChunk previousChunk,
|
||||
@Nullable byte[] mediaSegmentKey,
|
||||
@Nullable byte[] initSegmentKey,
|
||||
@ -189,6 +194,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
mediaSegment.hasGapTag,
|
||||
isPrimaryTimestampSource,
|
||||
/* timestampAdjuster= */ timestampAdjusterProvider.getAdjuster(discontinuitySequenceNumber),
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
mediaSegment.drmInitData,
|
||||
previousExtractor,
|
||||
id3Decoder,
|
||||
@ -267,6 +273,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
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 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
boolean hasGapTag,
|
||||
boolean isPrimaryTimestampSource,
|
||||
TimestampAdjuster timestampAdjuster,
|
||||
long timestampAdjusterInitializationTimeoutMs,
|
||||
@Nullable DrmInitData drmInitData,
|
||||
@Nullable HlsMediaChunkExtractor previousExtractor,
|
||||
Id3Decoder id3Decoder,
|
||||
@ -328,6 +336,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
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 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
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 =
|
||||
|
@ -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;
|
||||
@ -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,
|
||||
@ -134,7 +138,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
|
||||
boolean allowChunklessPreparation,
|
||||
@HlsMediaSource.MetadataType int metadataType,
|
||||
boolean useSessionKeys,
|
||||
PlayerId playerId) {
|
||||
PlayerId playerId,
|
||||
long timestampAdjusterInitializationTimeoutMs) {
|
||||
this.extractorFactory = extractorFactory;
|
||||
this.playlistTracker = playlistTracker;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
@ -150,6 +155,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
|
||||
this.metadataType = metadataType;
|
||||
this.useSessionKeys = useSessionKeys;
|
||||
this.playerId = playerId;
|
||||
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
|
||||
sampleStreamWrapperCallback = new SampleStreamWrapperCallback();
|
||||
compositeSequenceableLoader =
|
||||
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader();
|
||||
@ -781,6 +787,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsPlaylistTracker.Pla
|
||||
dataSourceFactory,
|
||||
mediaTransferListener,
|
||||
timestampAdjusterProvider,
|
||||
timestampAdjusterInitializationTimeoutMs,
|
||||
muxedCaptionFormats,
|
||||
playerId,
|
||||
cmcdConfiguration);
|
||||
|
@ -113,6 +113,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
private @MetadataType int metadataType;
|
||||
private boolean useSessionKeys;
|
||||
private long elapsedRealTimeOffsetMs;
|
||||
private long timestampAdjusterInitializationTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates a new factory for {@link HlsMediaSource}s.
|
||||
@ -322,6 +323,21 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
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}.
|
||||
@ -372,7 +388,8 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
elapsedRealTimeOffsetMs,
|
||||
allowChunklessPreparation,
|
||||
metadataType,
|
||||
useSessionKeys);
|
||||
useSessionKeys,
|
||||
timestampAdjusterInitializationTimeoutMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -394,6 +411,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
private final HlsPlaylistTracker playlistTracker;
|
||||
private final long elapsedRealTimeOffsetMs;
|
||||
private final MediaItem mediaItem;
|
||||
private final long timestampAdjusterInitializationTimeoutMs;
|
||||
|
||||
private MediaItem.LiveConfiguration liveConfiguration;
|
||||
@Nullable private TransferListener mediaTransferListener;
|
||||
@ -410,7 +428,8 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
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;
|
||||
@ -425,6 +444,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
this.allowChunklessPreparation = allowChunklessPreparation;
|
||||
this.metadataType = metadataType;
|
||||
this.useSessionKeys = useSessionKeys;
|
||||
this.timestampAdjusterInitializationTimeoutMs = timestampAdjusterInitializationTimeoutMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -468,7 +488,8 @@ public final class HlsMediaSource extends BaseMediaSource
|
||||
allowChunklessPreparation,
|
||||
metadataType,
|
||||
useSessionKeys,
|
||||
getPlayerId());
|
||||
getPlayerId(),
|
||||
timestampAdjusterInitializationTimeoutMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -321,6 +321,7 @@ public class HlsChunkSourceTest {
|
||||
new DefaultHlsDataSourceFactory(new FakeDataSource.Factory()),
|
||||
/* mediaTransferListener= */ null,
|
||||
new TimestampAdjusterProvider(),
|
||||
/* timestampAdjusterInitializationTimeoutMs= */ 0,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
PlayerId.UNSET,
|
||||
cmcdConfiguration);
|
||||
|
@ -96,7 +96,8 @@ public final class HlsMediaPeriodTest {
|
||||
/* allowChunklessPreparation= */ true,
|
||||
HlsMediaSource.METADATA_TYPE_ID3,
|
||||
/* useSessionKeys= */ false,
|
||||
PlayerId.UNSET);
|
||||
PlayerId.UNSET,
|
||||
/* timestampAdjusterInitializationTimeoutMs= */ 0);
|
||||
};
|
||||
|
||||
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
|
||||
|
Loading…
x
Reference in New Issue
Block a user