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
This commit is contained in:
tonihei 2021-05-20 13:37:36 +01:00 committed by Oliver Woodman
parent 5fc6b2ff9d
commit 10c19afa35
3 changed files with 76 additions and 35 deletions

View File

@ -352,70 +352,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return; return;
} }
@Nullable @Nullable
HlsMediaPlaylist mediaPlaylist = HlsMediaPlaylist playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(mediaPlaylist); checkNotNull(playlist);
independentSegments = mediaPlaylist.hasIndependentSegments; independentSegments = playlist.hasIndependentSegments;
updateLiveEdgeTimeUs(mediaPlaylist); updateLiveEdgeTimeUs(playlist);
// Select the chunk. // Select the chunk.
long startOfPlaylistInPeriodUs = long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
Pair<Long, Integer> nextMediaSequenceAndPartIndex = Pair<Long, Integer> nextMediaSequenceAndPartIndex =
getNextMediaSequenceAndPartIndex( getNextMediaSequenceAndPartIndex(
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first; long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
int partIndex = nextMediaSequenceAndPartIndex.second; 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 // We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window. // behind the live window.
selectedTrackIndex = oldTrackIndex; selectedTrackIndex = oldTrackIndex;
selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
mediaPlaylist = playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
// non-null. checkNotNull(playlist);
checkNotNull(mediaPlaylist); startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
// Get the next segment/part without switching tracks. // Get the next segment/part without switching tracks.
Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting = Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting =
getNextMediaSequenceAndPartIndex( getNextMediaSequenceAndPartIndex(
previous, previous,
/* switchingTrack= */ false, /* switchingTrack= */ false,
mediaPlaylist, playlist,
startOfPlaylistInPeriodUs, startOfPlaylistInPeriodUs,
loadPositionUs); loadPositionUs);
chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first; chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first;
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second; partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
} }
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < playlist.mediaSequence) {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
} }
@Nullable @Nullable
SegmentBaseHolder segmentBaseHolder = SegmentBaseHolder segmentBaseHolder =
getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex); getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);
if (segmentBaseHolder == null) { if (segmentBaseHolder == null) {
if (!mediaPlaylist.hasEndTag) { if (!playlist.hasEndTag) {
// Reload the playlist in case of a live stream. // Reload the playlist in case of a live stream.
out.playlistUrl = selectedPlaylistUrl; out.playlistUrl = selectedPlaylistUrl;
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
expectedPlaylistUrl = selectedPlaylistUrl; expectedPlaylistUrl = selectedPlaylistUrl;
return; return;
} else if (allowEndOfStream || mediaPlaylist.segments.isEmpty()) { } else if (allowEndOfStream || playlist.segments.isEmpty()) {
out.endOfStream = true; out.endOfStream = true;
return; return;
} }
// Use the last segment available in case of a VOD stream. // Use the last segment available in case of a VOD stream.
segmentBaseHolder = segmentBaseHolder =
new SegmentBaseHolder( new SegmentBaseHolder(
Iterables.getLast(mediaPlaylist.segments), Iterables.getLast(playlist.segments),
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size() - 1, playlist.mediaSequence + playlist.segments.size() - 1,
/* partIndex= */ C.INDEX_UNSET); /* 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. // Check if the media segment or its initialization segment are fully encrypted.
@Nullable @Nullable
Uri initSegmentKeyUri = Uri initSegmentKeyUri =
getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment); getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex); out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) { if (out.chunk != null) {
return; return;
} }
@Nullable @Nullable
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase); Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex); out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) { if (out.chunk != null) {
return; 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 = out.chunk =
HlsMediaChunk.createInstance( HlsMediaChunk.createInstance(
extractorFactory, extractorFactory,
mediaDataSource, mediaDataSource,
playlistFormats[selectedTrackIndex], playlistFormats[selectedTrackIndex],
startOfPlaylistInPeriodUs, startOfPlaylistInPeriodUs,
mediaPlaylist, playlist,
segmentBaseHolder, segmentBaseHolder,
selectedPlaylistUrl, selectedPlaylistUrl,
muxedCaptionFormats, muxedCaptionFormats,
@ -453,7 +462,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
timestampAdjusterProvider, timestampAdjusterProvider,
previous, previous,
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri)); /* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn);
} }
@Nullable @Nullable

View File

@ -75,6 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise. * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
* otherwise. * otherwise.
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
*/ */
public static HlsMediaChunk createInstance( public static HlsMediaChunk createInstance(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
@ -91,7 +92,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
TimestampAdjusterProvider timestampAdjusterProvider, TimestampAdjusterProvider timestampAdjusterProvider,
@Nullable HlsMediaChunk previousChunk, @Nullable HlsMediaChunk previousChunk,
@Nullable byte[] mediaSegmentKey, @Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey) { @Nullable byte[] initSegmentKey,
boolean shouldSpliceIn) {
// Media segment. // Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase; HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
DataSpec dataSpec = DataSpec dataSpec =
@ -135,17 +137,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable HlsMediaChunkExtractor previousExtractor = null; @Nullable HlsMediaChunkExtractor previousExtractor = null;
Id3Decoder id3Decoder; Id3Decoder id3Decoder;
ParsableByteArray scratchId3Data; ParsableByteArray scratchId3Data;
boolean shouldSpliceIn;
if (previousChunk != null) { if (previousChunk != null) {
boolean isFollowingChunk = boolean isFollowingChunk =
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted; playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
id3Decoder = previousChunk.id3Decoder; id3Decoder = previousChunk.id3Decoder;
scratchId3Data = previousChunk.scratchId3Data; scratchId3Data = previousChunk.scratchId3Data;
boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist);
boolean canContinueWithoutSplice =
isFollowingChunk
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
shouldSpliceIn = !canContinueWithoutSplice;
previousExtractor = previousExtractor =
isFollowingChunk isFollowingChunk
&& !previousChunk.extractorInvalidated && !previousChunk.extractorInvalidated
@ -155,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} else { } else {
id3Decoder = new Id3Decoder(); id3Decoder = new Id3Decoder();
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false;
} }
return new HlsMediaChunk( return new HlsMediaChunk(
extractorFactory, extractorFactory,
@ -186,6 +182,41 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
shouldSpliceIn); 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 = public static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp"; "com.apple.streaming.transportStreamTimestamp";

View File

@ -709,6 +709,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
? lastMediaChunk.endTimeUs ? lastMediaChunk.endTimeUs
: max(lastSeekPositionUs, lastMediaChunk.startTimeUs); : max(lastSeekPositionUs, lastMediaChunk.startTimeUs);
} }
nextChunkHolder.clear();
chunkSource.getNextChunk( chunkSource.getNextChunk(
positionUs, positionUs,
loadPositionUs, loadPositionUs,
@ -718,7 +719,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
boolean endOfStream = nextChunkHolder.endOfStream; boolean endOfStream = nextChunkHolder.endOfStream;
@Nullable Chunk loadable = nextChunkHolder.chunk; @Nullable Chunk loadable = nextChunkHolder.chunk;
@Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; @Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;
nextChunkHolder.clear();
if (endOfStream) { if (endOfStream) {
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;