mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Discard HLS preload chunks when an HTTP 410 or 404 occurs
This change avoids an early fatal exception for replaced hinted parts. Issue: #5011 PiperOrigin-RevId: 344828076
This commit is contained in:
parent
e508fb64f3
commit
e6046a5c07
4 changed files with 135 additions and 28 deletions
|
|
@ -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.
|
||||
*
|
||||
* <p>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 {}
|
||||
|
|
@ -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.
|
||||
*
|
||||
* <p>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<HlsMediaPlaylist.Part> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<Integer> 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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue