diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/StableApiCandidate.java b/library/core/src/main/java/com/google/android/exoplayer2/util/StableApiCandidate.java
new file mode 100644
index 0000000000..ff96e02e05
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/util/StableApiCandidate.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to mark an API as a candidate for being part of a stable subset of the API.
+ *
+ *
Note: this is experimental, and no guarantees are made about the stability of APIs even if
+ * they are marked with this annotation.
+ */
+@Retention(CLASS)
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+public @interface StableApiCandidate {}
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 314714fe95..c2254a4280 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
@@ -21,6 +21,7 @@ import static java.lang.Math.max;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Pair;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
@@ -47,6 +48,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -86,6 +89,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
+ /**
+ * Chunk publication state. One of {@link #CHUNK_PUBLICATION_STATE_PRELOAD}, {@link
+ * #CHUNK_PUBLICATION_STATE_PUBLISHED}, {@link #CHUNK_PUBLICATION_STATE_REMOVED}.
+ */
+ @IntDef({
+ CHUNK_PUBLICATION_STATE_PRELOAD,
+ CHUNK_PUBLICATION_STATE_PUBLISHED,
+ CHUNK_PUBLICATION_STATE_REMOVED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ChunkPublicationState {}
+
+ /** Indicates that the chunk is based on a preload hint. */
+ public static final int CHUNK_PUBLICATION_STATE_PRELOAD = 0;
+ /** Indicates that the chunk is definitely published. */
+ public static final int CHUNK_PUBLICATION_STATE_PUBLISHED = 1;
+ /**
+ * Indicates that the chunk has been removed from the playlist.
+ *
+ *
See RFC 8216, Section 6.2.6 also.
+ */
+ public static final int CHUNK_PUBLICATION_STATE_REMOVED = 2;
+
/**
* The maximum number of keys that the key cache can hold. This value must be 2 or greater in
* order to hold initialization segment and media segment keys simultaneously.
@@ -222,29 +248,32 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
/**
- * Checks whether the previous media chunk is a preload chunk that has been removed in the current
- * playlist.
+ * Returns the publication state of the given chunk.
*
- * @param previous The previous media chunk.
- * @return True if the previous media chunk has been removed in the current playlist.
+ * @param mediaChunk The media chunk for which to evaluate the publication state.
+ * @return Whether the media chunk is {@link #CHUNK_PUBLICATION_STATE_PRELOAD a preload chunk},
+ * has been {@link #CHUNK_PUBLICATION_STATE_REMOVED removed} or is definitely {@link
+ * #CHUNK_PUBLICATION_STATE_PUBLISHED published}.
*/
- public boolean isMediaChunkRemoved(HlsMediaChunk previous) {
- if (!previous.isPreload) {
- return false;
+ @ChunkPublicationState
+ public int getChunkPublicationState(HlsMediaChunk mediaChunk) {
+ if (mediaChunk.partIndex == C.INDEX_UNSET) {
+ // Chunks based on full segments can't be removed and are always published.
+ return CHUNK_PUBLICATION_STATE_PUBLISHED;
}
- Uri playlistUrl = playlistUrls[trackGroup.indexOf(previous.trackFormat)];
+ Uri playlistUrl = playlistUrls[trackGroup.indexOf(mediaChunk.trackFormat)];
HlsMediaPlaylist mediaPlaylist =
checkNotNull(playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false));
- int segmentIndexInPlaylist = (int) (previous.chunkIndex - mediaPlaylist.mediaSequence);
+ int segmentIndexInPlaylist = (int) (mediaChunk.chunkIndex - mediaPlaylist.mediaSequence);
if (segmentIndexInPlaylist < 0) {
- // The segment of the previous chunk is not in the current playlist anymore.
- return false;
+ // The parent segment of the previous chunk is not in the current playlist anymore.
+ return CHUNK_PUBLICATION_STATE_PUBLISHED;
}
List partsInCurrentPlaylist =
segmentIndexInPlaylist < mediaPlaylist.segments.size()
? mediaPlaylist.segments.get(segmentIndexInPlaylist).parts
: mediaPlaylist.trailingParts;
- if (previous.partIndex >= partsInCurrentPlaylist.size()) {
+ if (mediaChunk.partIndex >= partsInCurrentPlaylist.size()) {
// In case the part hinted in the previous playlist has been wrongly assigned to the then full
// but not yet terminated segment, we discard it regardless whether the URI is different or
// not. While this is theoretically possible and unspecified, it appears to be an edge case
@@ -252,11 +281,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// here but, if the chunk is not discarded, it could create unpredictable problems later,
// because the media sequence in previous.chunkIndex does not match to the actual media
// sequence in the new playlist.
- return true;
+ return CHUNK_PUBLICATION_STATE_REMOVED;
}
- HlsMediaPlaylist.Part publishedPart = partsInCurrentPlaylist.get(previous.partIndex);
- Uri publishedUri = Uri.parse(UriUtil.resolve(mediaPlaylist.baseUri, publishedPart.url));
- return !Util.areEqual(publishedUri, previous.dataSpec.uri);
+ HlsMediaPlaylist.Part newPart = partsInCurrentPlaylist.get(mediaChunk.partIndex);
+ if (newPart.isPreload) {
+ // The playlist did not change and the part in the new playlist is still a preload hint.
+ return CHUNK_PUBLICATION_STATE_PRELOAD;
+ }
+ Uri newUri = Uri.parse(UriUtil.resolve(mediaPlaylist.baseUri, newPart.url));
+ return Util.areEqual(newUri, mediaChunk.dataSpec.uri)
+ ? CHUNK_PUBLICATION_STATE_PUBLISHED
+ : CHUNK_PUBLICATION_STATE_REMOVED;
}
/**
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 e294a3f1be..643f5e5dd5 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
@@ -169,7 +169,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
segmentEndTimeInPeriodUs,
segmentBaseHolder.mediaSequence,
segmentBaseHolder.partIndex,
- segmentBaseHolder.isPreload,
+ /* isPublished= */ !segmentBaseHolder.isPreload,
discontinuitySequenceNumber,
mediaSegment.hasGapTag,
isMasterTimestampSource,
@@ -205,9 +205,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** The part index or {@link C#INDEX_UNSET} if the chunk is a full segment */
public final int partIndex;
- /** Whether this chunk is a preload chunk. */
- public final boolean isPreload;
-
@Nullable private final DataSource initDataSource;
@Nullable private final DataSpec initDataSpec;
@Nullable private final HlsMediaChunkExtractor previousExtractor;
@@ -233,6 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private boolean loadCompleted;
private ImmutableList sampleQueueFirstSampleIndices;
private boolean extractorInvalidated;
+ private boolean isPublished;
private HlsMediaChunk(
HlsExtractorFactory extractorFactory,
@@ -251,7 +249,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
long endTimeUs,
long chunkMediaSequence,
int partIndex,
- boolean isPreload,
+ boolean isPublished,
int discontinuitySequenceNumber,
boolean hasGapTag,
boolean isMasterTimestampSource,
@@ -272,7 +270,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
chunkMediaSequence);
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
this.partIndex = partIndex;
- this.isPreload = isPreload;
+ this.isPublished = isPublished;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.initDataSpec = initDataSpec;
this.initDataSource = initDataSource;
@@ -356,6 +354,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
}
+ /**
+ * Whether the chunk is a published chunk as opposed to a preload hint that may change when the
+ * playlist updates.
+ */
+ public boolean isPublished() {
+ return isPublished;
+ }
+
+ /**
+ * Sets the publish flag of the media chunk to indicate that it is not based on a part that is a
+ * preload hint in the playlist.
+ */
+ public void publish() {
+ isPublished = true;
+ }
+
// Internal methods.
@RequiresNonNull("output")
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index b8a026aa30..2255151b92 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source.hls;
+import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED;
+import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_REMOVED;
import static java.lang.Math.max;
import android.net.Uri;
@@ -54,6 +56,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataReader;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import com.google.android.exoplayer2.upstream.Loader;
@@ -506,10 +509,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** Called when the playlist is updated. */
public void onPlaylistUpdated() {
- if (!loadingFinished
- && loader.isLoading()
- && !mediaChunks.isEmpty()
- && chunkSource.isMediaChunkRemoved(Iterables.getLast(mediaChunks))) {
+ if (mediaChunks.isEmpty()) {
+ return;
+ }
+ HlsMediaChunk lastMediaChunk = Iterables.getLast(mediaChunks);
+ @HlsChunkSource.ChunkPublicationState
+ int chunkState = chunkSource.getChunkPublicationState(lastMediaChunk);
+ if (chunkState == CHUNK_PUBLICATION_STATE_PUBLISHED) {
+ lastMediaChunk.publish();
+ } else if (chunkState == CHUNK_PUBLICATION_STATE_REMOVED
+ && !loadingFinished
+ && loader.isLoading()) {
loader.cancelLoading();
}
}
@@ -738,7 +748,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
if (!readOnlyMediaChunks.isEmpty()
- && chunkSource.isMediaChunkRemoved(Iterables.getLast(readOnlyMediaChunks))) {
+ && chunkSource.getChunkPublicationState(Iterables.getLast(readOnlyMediaChunks))
+ == CHUNK_PUBLICATION_STATE_REMOVED) {
discardUpstream(mediaChunks.size() - 1);
}
@@ -820,8 +831,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
long loadDurationMs,
IOException error,
int errorCount) {
- long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable);
+ if (isMediaChunk
+ && !((HlsMediaChunk) loadable).isPublished()
+ && error instanceof HttpDataSource.InvalidResponseCodeException) {
+ int responseCode = ((HttpDataSource.InvalidResponseCodeException) error).responseCode;
+ if (responseCode == 410 || responseCode == 404) {
+ // According to RFC 8216, Section 6.2.6 a server should respond with an HTTP 404 (Not found)
+ // for requests of hinted parts that are replaced and not available anymore. We've seen test
+ // streams with HTTP 410 (Gone) also.
+ return Loader.RETRY;
+ }
+ }
+ long bytesLoaded = loadable.bytesLoaded();
boolean exclusionSucceeded = false;
LoadEventInfo loadEventInfo =
new LoadEventInfo(