From 39f8c775689a38548554b784b2fdfed7b865d813 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 19 Nov 2020 14:20:55 +0000 Subject: [PATCH] Replace cancelled HLS preload parts Issue: #5011 PiperOrigin-RevId: 343277357 --- .../exoplayer2/source/hls/HlsChunkSource.java | 59 +++++++++++++++---- .../exoplayer2/source/hls/HlsMediaChunk.java | 6 ++ .../exoplayer2/source/hls/HlsMediaPeriod.java | 3 + .../source/hls/HlsSampleStreamWrapper.java | 19 +++++- 4 files changed, 75 insertions(+), 12 deletions(-) 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 653dc20a7b..314714fe95 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; import android.net.Uri; @@ -39,7 +40,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; @@ -84,7 +84,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; endOfStream = false; playlistUrl = null; } - } /** @@ -222,6 +221,44 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.isTimestampMaster = isTimestampMaster; } + /** + * Checks whether the previous media chunk is a preload chunk that has been removed in the current + * playlist. + * + * @param previous The previous media chunk. + * @return True if the previous media chunk has been removed in the current playlist. + */ + public boolean isMediaChunkRemoved(HlsMediaChunk previous) { + if (!previous.isPreload) { + return false; + } + Uri playlistUrl = playlistUrls[trackGroup.indexOf(previous.trackFormat)]; + HlsMediaPlaylist mediaPlaylist = + checkNotNull(playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false)); + int segmentIndexInPlaylist = (int) (previous.chunkIndex - mediaPlaylist.mediaSequence); + if (segmentIndexInPlaylist < 0) { + // The segment of the previous chunk is not in the current playlist anymore. + return false; + } + List partsInCurrentPlaylist = + segmentIndexInPlaylist < mediaPlaylist.segments.size() + ? mediaPlaylist.segments.get(segmentIndexInPlaylist).parts + : mediaPlaylist.trailingParts; + if (previous.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 + // which we can avoid with a small inefficiency of discarding in vain. We could allow this + // 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; + } + HlsMediaPlaylist.Part publishedPart = partsInCurrentPlaylist.get(previous.partIndex); + Uri publishedUri = Uri.parse(UriUtil.resolve(mediaPlaylist.baseUri, publishedPart.url)); + return !Util.areEqual(publishedUri, previous.dataSpec.uri); + } + /** * Returns the next chunk to load. * @@ -270,7 +307,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; trackSelection.updateSelectedTrack( playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators); int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup(); - boolean switchingTrack = oldTrackIndex != selectedTrackIndex; Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; if (!playlistTracker.isSnapshotValid(selectedPlaylistUrl)) { @@ -284,7 +320,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. - Assertions.checkNotNull(mediaPlaylist); + checkNotNull(mediaPlaylist); independentSegments = mediaPlaylist.hasIndependentSegments; updateLiveEdgeTimeUs(mediaPlaylist); @@ -306,7 +342,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be // non-null. - Assertions.checkNotNull(mediaPlaylist); + checkNotNull(mediaPlaylist); startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); // Get the next segment/part without switching tracks. @@ -366,7 +402,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (out.chunk != null) { return; } - out.chunk = HlsMediaChunk.createInstance( extractorFactory, @@ -399,7 +434,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist); if (nextPartIndex == C.INDEX_UNSET) { - return new SegmentBaseHolder(mediaSegment, nextMediaSequence, nextPartIndex); + return new SegmentBaseHolder(mediaSegment, nextMediaSequence, /* partIndex= */ C.INDEX_UNSET); } if (nextPartIndex < mediaSegment.parts.size()) { @@ -417,6 +452,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return new SegmentBaseHolder( mediaPlaylist.trailingParts.get(0), nextMediaSequence + 1, /* partIndex= */ 0); } + // End of stream. return null; } @@ -430,8 +466,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); - keyCache.put( - encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult())); + keyCache.put(encryptionKeyChunk.dataSpec.uri, checkNotNull(encryptionKeyChunk.getResult())); } } @@ -499,7 +534,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false); // Playlist snapshot is valid (checked by if() above) so playlist must be non-null. - Assertions.checkNotNull(playlist); + checkNotNull(playlist); long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); boolean switchingTrack = trackIndex != oldTrackIndex; @@ -704,6 +739,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public final HlsMediaPlaylist.SegmentBase segmentBase; public final long mediaSequence; public final int partIndex; + public final boolean isPreload; /** Creates a new instance. */ public SegmentBaseHolder( @@ -711,6 +747,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.segmentBase = segmentBase; this.mediaSequence = mediaSequence; this.partIndex = partIndex; + this.isPreload = + segmentBase instanceof HlsMediaPlaylist.Part + && ((HlsMediaPlaylist.Part) segmentBase).isPreload; } } 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 7b96e8a218..663ed10ee4 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,6 +169,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; segmentEndTimeInPeriodUs, segmentBaseHolder.mediaSequence, segmentBaseHolder.partIndex, + segmentBaseHolder.isPreload, discontinuitySequenceNumber, mediaSegment.hasGapTag, isMasterTimestampSource, @@ -204,6 +205,9 @@ 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; @@ -247,6 +251,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; long endTimeUs, long chunkMediaSequence, int partIndex, + boolean isPreload, int discontinuitySequenceNumber, boolean hasGapTag, boolean isMasterTimestampSource, @@ -267,6 +272,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; chunkMediaSequence); this.mediaSegmentEncrypted = mediaSegmentEncrypted; this.partIndex = partIndex; + this.isPreload = isPreload; this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.initDataSource = initDataSource; 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 0089f68bf4..1ef6ab3afa 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 @@ -445,6 +445,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public void onPlaylistChanged() { + for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) { + streamWrapper.onPlaylistUpdated(); + } callback.onContinueLoadingRequested(this); } 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 e6126d36ae..b8a026aa30 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 @@ -504,6 +504,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return true; } + /** Called when the playlist is updated. */ + public void onPlaylistUpdated() { + if (!loadingFinished + && loader.isLoading() + && !mediaChunks.isEmpty() + && chunkSource.isMediaChunkRemoved(Iterables.getLast(mediaChunks))) { + loader.cancelLoading(); + } + } + public void release() { if (prepared) { // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise @@ -672,8 +682,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(), nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; - Chunk loadable = nextChunkHolder.chunk; - Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; + @Nullable Chunk loadable = nextChunkHolder.chunk; + @Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; nextChunkHolder.clear(); if (endOfStream) { @@ -727,6 +737,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return; } + if (!readOnlyMediaChunks.isEmpty() + && chunkSource.isMediaChunkRemoved(Iterables.getLast(readOnlyMediaChunks))) { + discardUpstream(mediaChunks.size() - 1); + } + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); if (preferredQueueSize < mediaChunks.size()) { discardUpstream(preferredQueueSize);