From 10c19afa355054a3a8e7c62e65bedbb6f51d28a2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 20 May 2021 13:37:36 +0100 Subject: [PATCH] Don't allow spliced-in preload chunks. Preload chunks may still need to be discarded. However, we don't currently support discarding spliced-in chunks. Thus, we need to avoid loadng a preload chunk that needs to be spliced-in. Issue: #8937 #minor-release PiperOrigin-RevId: 374851661 --- .../exoplayer2/source/hls/HlsChunkSource.java | 62 +++++++++++-------- .../exoplayer2/source/hls/HlsMediaChunk.java | 47 +++++++++++--- .../source/hls/HlsSampleStreamWrapper.java | 2 +- 3 files changed, 76 insertions(+), 35 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 66cd100a63..efc7377902 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 @@ -352,70 +352,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return; } @Nullable - HlsMediaPlaylist mediaPlaylist = + HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); - // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. - checkNotNull(mediaPlaylist); - independentSegments = mediaPlaylist.hasIndependentSegments; + // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null. + checkNotNull(playlist); + independentSegments = playlist.hasIndependentSegments; - updateLiveEdgeTimeUs(mediaPlaylist); + updateLiveEdgeTimeUs(playlist); // Select the chunk. - long startOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); Pair nextMediaSequenceAndPartIndex = getNextMediaSequenceAndPartIndex( - previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); + previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs); long chunkMediaSequence = nextMediaSequenceAndPartIndex.first; int partIndex = nextMediaSequenceAndPartIndex.second; - if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) { + if (chunkMediaSequence < playlist.mediaSequence && previous != null && switchingTrack) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. selectedTrackIndex = oldTrackIndex; selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; - mediaPlaylist = + playlist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); - // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be - // non-null. - checkNotNull(mediaPlaylist); - startOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null. + checkNotNull(playlist); + startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); // Get the next segment/part without switching tracks. Pair nextMediaSequenceAndPartIndexWithoutAdapting = getNextMediaSequenceAndPartIndex( previous, /* switchingTrack= */ false, - mediaPlaylist, + playlist, startOfPlaylistInPeriodUs, loadPositionUs); chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first; partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second; } - if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + if (chunkMediaSequence < playlist.mediaSequence) { fatalError = new BehindLiveWindowException(); return; } @Nullable SegmentBaseHolder segmentBaseHolder = - getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex); + getNextSegmentHolder(playlist, chunkMediaSequence, partIndex); if (segmentBaseHolder == null) { - if (!mediaPlaylist.hasEndTag) { + if (!playlist.hasEndTag) { // Reload the playlist in case of a live stream. out.playlistUrl = selectedPlaylistUrl; seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); expectedPlaylistUrl = selectedPlaylistUrl; return; - } else if (allowEndOfStream || mediaPlaylist.segments.isEmpty()) { + } else if (allowEndOfStream || playlist.segments.isEmpty()) { out.endOfStream = true; return; } // Use the last segment available in case of a VOD stream. segmentBaseHolder = new SegmentBaseHolder( - Iterables.getLast(mediaPlaylist.segments), - mediaPlaylist.mediaSequence + mediaPlaylist.segments.size() - 1, + Iterables.getLast(playlist.segments), + playlist.mediaSequence + playlist.segments.size() - 1, /* partIndex= */ C.INDEX_UNSET); } @@ -426,24 +423,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Check if the media segment or its initialization segment are fully encrypted. @Nullable Uri initSegmentKeyUri = - getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment); + getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment); out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex); if (out.chunk != null) { return; } @Nullable - Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase); + Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase); out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex); if (out.chunk != null) { return; } + + boolean shouldSpliceIn = + HlsMediaChunk.shouldSpliceIn( + previous, selectedPlaylistUrl, playlist, segmentBaseHolder, startOfPlaylistInPeriodUs); + if (shouldSpliceIn && segmentBaseHolder.isPreload) { + // We don't support discarding spliced-in segments [internal: b/159904763], but preload + // parts may need to be discarded if they are removed before becoming permanently published. + // Hence, don't allow this combination and instead wait with loading the next part until it + // becomes fully available (or the track selection selects another track). + return; + } + out.chunk = HlsMediaChunk.createInstance( extractorFactory, mediaDataSource, playlistFormats[selectedTrackIndex], startOfPlaylistInPeriodUs, - mediaPlaylist, + playlist, segmentBaseHolder, selectedPlaylistUrl, muxedCaptionFormats, @@ -453,7 +462,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; timestampAdjusterProvider, previous, /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), - /* initSegmentKey= */ keyCache.get(initSegmentKeyUri)); + /* initSegmentKey= */ keyCache.get(initSegmentKeyUri), + shouldSpliceIn); } @Nullable 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 3527eb1fc3..49a93ed806 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 @@ -75,6 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise. * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * otherwise. + * @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples. */ public static HlsMediaChunk createInstance( HlsExtractorFactory extractorFactory, @@ -91,7 +92,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; TimestampAdjusterProvider timestampAdjusterProvider, @Nullable HlsMediaChunk previousChunk, @Nullable byte[] mediaSegmentKey, - @Nullable byte[] initSegmentKey) { + @Nullable byte[] initSegmentKey, + boolean shouldSpliceIn) { // Media segment. HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase; DataSpec dataSpec = @@ -135,17 +137,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Nullable HlsMediaChunkExtractor previousExtractor = null; Id3Decoder id3Decoder; ParsableByteArray scratchId3Data; - boolean shouldSpliceIn; + if (previousChunk != null) { boolean isFollowingChunk = playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted; id3Decoder = previousChunk.id3Decoder; scratchId3Data = previousChunk.scratchId3Data; - boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist); - boolean canContinueWithoutSplice = - isFollowingChunk - || (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs); - shouldSpliceIn = !canContinueWithoutSplice; previousExtractor = isFollowingChunk && !previousChunk.extractorInvalidated @@ -155,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else { id3Decoder = new Id3Decoder(); scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); - shouldSpliceIn = false; } return new HlsMediaChunk( extractorFactory, @@ -186,6 +182,41 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; shouldSpliceIn); } + /** + * Returns whether samples of a new HLS media chunk should be spliced into existing samples. + * + * @param previousChunk The previous existing media chunk, or null if the new chunk is the first + * in the queue. + * @param playlistUrl The URL of the playlist from which the new chunk will be obtained. + * @param mediaPlaylist The {@link HlsMediaPlaylist} containing the new chunk. + * @param segmentBaseHolder The {@link HlsChunkSource.SegmentBaseHolder} with information about + * the new chunk. + * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds. + * @return Whether samples of the new chunk should be spliced into existing samples. + */ + public static boolean shouldSpliceIn( + @Nullable HlsMediaChunk previousChunk, + Uri playlistUrl, + HlsMediaPlaylist mediaPlaylist, + HlsChunkSource.SegmentBaseHolder segmentBaseHolder, + long startOfPlaylistInPeriodUs) { + if (previousChunk == null) { + // First chunk doesn't require splicing. + return false; + } + if (playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted) { + // Continuing with the next chunk in the same playlist after fully loading the previous chunk + // (i.e. the load wasn't cancelled or failed) is always possible. + return false; + } + // Changing playlists or continuing after a chunk cancellation/failure requires independent, + // non-overlapping segments to avoid the splice. + long segmentStartTimeInPeriodUs = + startOfPlaylistInPeriodUs + segmentBaseHolder.segmentBase.relativeStartTimeUs; + return !isIndependent(segmentBaseHolder, mediaPlaylist) + || segmentStartTimeInPeriodUs < previousChunk.endTimeUs; + } + public static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; 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 da28de4094..61afe9bcd8 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 @@ -709,6 +709,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ? lastMediaChunk.endTimeUs : max(lastSeekPositionUs, lastMediaChunk.startTimeUs); } + nextChunkHolder.clear(); chunkSource.getNextChunk( positionUs, loadPositionUs, @@ -718,7 +719,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; boolean endOfStream = nextChunkHolder.endOfStream; @Nullable Chunk loadable = nextChunkHolder.chunk; @Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; - nextChunkHolder.clear(); if (endOfStream) { pendingResetPositionUs = C.TIME_UNSET;