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:
parent
5fc6b2ff9d
commit
10c19afa35
@ -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
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user