mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Create chunks from parts in HlsChunkSource
Issue: #5011 PiperOrigin-RevId: 342022947
This commit is contained in:
parent
2693a107cd
commit
e3c725aa38
7 changed files with 534 additions and 91 deletions
|
|
@ -19,7 +19,9 @@ import static java.lang.Math.max;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
|
|
@ -41,10 +43,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||||
import com.google.android.exoplayer2.util.UriUtil;
|
import com.google.android.exoplayer2.util.UriUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
|
@ -242,7 +247,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
List<HlsMediaChunk> queue,
|
List<HlsMediaChunk> queue,
|
||||||
boolean allowEndOfStream,
|
boolean allowEndOfStream,
|
||||||
HlsChunkHolder out) {
|
HlsChunkHolder out) {
|
||||||
HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);
|
@Nullable HlsMediaChunk previous = queue.isEmpty() ? null : Iterables.getLast(queue);
|
||||||
int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
|
int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
|
||||||
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
||||||
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
|
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
|
||||||
|
|
@ -275,6 +280,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
// Retry when playlist is refreshed.
|
// Retry when playlist is refreshed.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@Nullable
|
||||||
HlsMediaPlaylist mediaPlaylist =
|
HlsMediaPlaylist mediaPlaylist =
|
||||||
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 mediaPlaylist must be non-null.
|
||||||
|
|
@ -286,22 +292,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
// Select the chunk.
|
// Select the chunk.
|
||||||
long startOfPlaylistInPeriodUs =
|
long startOfPlaylistInPeriodUs =
|
||||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||||
long chunkMediaSequence =
|
Pair<Long, Integer> nextMediaSequenceAndPartIndex =
|
||||||
getChunkMediaSequence(
|
getNextMediaSequenceAndPartIndex(
|
||||||
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
|
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
|
||||||
|
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
|
||||||
|
int partIndex = nextMediaSequenceAndPartIndex.second;
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) {
|
if (chunkMediaSequence < mediaPlaylist.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 =
|
mediaPlaylist =
|
||||||
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 mediaPlaylist must be
|
||||||
// non-null.
|
// non-null.
|
||||||
Assertions.checkNotNull(mediaPlaylist);
|
Assertions.checkNotNull(mediaPlaylist);
|
||||||
startOfPlaylistInPeriodUs =
|
startOfPlaylistInPeriodUs =
|
||||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||||
chunkMediaSequence = previous.getNextChunkIndex();
|
// Get the next segment/part without switching tracks.
|
||||||
|
Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting =
|
||||||
|
getNextMediaSequenceAndPartIndex(
|
||||||
|
previous,
|
||||||
|
/* switchingTrack= */ false,
|
||||||
|
mediaPlaylist,
|
||||||
|
startOfPlaylistInPeriodUs,
|
||||||
|
loadPositionUs);
|
||||||
|
chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first;
|
||||||
|
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||||
|
|
@ -309,36 +326,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);
|
@Nullable
|
||||||
int availableSegmentCount = mediaPlaylist.segments.size();
|
SegmentBaseHolder segmentBaseHolder =
|
||||||
if (segmentIndexInPlaylist >= availableSegmentCount) {
|
getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex);
|
||||||
if (mediaPlaylist.hasEndTag) {
|
if (segmentBaseHolder == null) {
|
||||||
if (allowEndOfStream || availableSegmentCount == 0) {
|
if (!mediaPlaylist.hasEndTag) {
|
||||||
out.endOfStream = true;
|
// Reload the playlist in case of a live stream.
|
||||||
return;
|
|
||||||
}
|
|
||||||
segmentIndexInPlaylist = availableSegmentCount - 1;
|
|
||||||
} else /* Live */ {
|
|
||||||
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()) {
|
||||||
|
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,
|
||||||
|
/* partIndex= */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
// We have a valid playlist snapshot, we can discard any playlist errors at this point.
|
|
||||||
|
// We have a valid media segment, we can discard any playlist errors at this point.
|
||||||
seenExpectedPlaylistError = false;
|
seenExpectedPlaylistError = false;
|
||||||
expectedPlaylistUrl = null;
|
expectedPlaylistUrl = null;
|
||||||
|
|
||||||
// Handle encryption.
|
// Check if the media segment or its initialization segment are fully encrypted.
|
||||||
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
|
@Nullable
|
||||||
|
Uri initSegmentKeyUri =
|
||||||
// Check if the segment or its initialization segment are fully encrypted.
|
getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment);
|
||||||
Uri initSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment.initializationSegment);
|
|
||||||
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
|
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
|
||||||
if (out.chunk != null) {
|
if (out.chunk != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment);
|
@Nullable
|
||||||
|
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase);
|
||||||
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
|
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
|
||||||
if (out.chunk != null) {
|
if (out.chunk != null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -351,7 +374,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
playlistFormats[selectedTrackIndex],
|
playlistFormats[selectedTrackIndex],
|
||||||
startOfPlaylistInPeriodUs,
|
startOfPlaylistInPeriodUs,
|
||||||
mediaPlaylist,
|
mediaPlaylist,
|
||||||
segmentIndexInPlaylist,
|
segmentBaseHolder,
|
||||||
selectedPlaylistUrl,
|
selectedPlaylistUrl,
|
||||||
muxedCaptionFormats,
|
muxedCaptionFormats,
|
||||||
trackSelection.getSelectionReason(),
|
trackSelection.getSelectionReason(),
|
||||||
|
|
@ -363,6 +386,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri));
|
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static SegmentBaseHolder getNextSegmentHolder(
|
||||||
|
HlsMediaPlaylist mediaPlaylist, long nextMediaSequence, int nextPartIndex) {
|
||||||
|
int segmentIndexInPlaylist = (int) (nextMediaSequence - mediaPlaylist.mediaSequence);
|
||||||
|
if (segmentIndexInPlaylist == mediaPlaylist.segments.size()) {
|
||||||
|
int index = nextPartIndex != C.INDEX_UNSET ? nextPartIndex : 0;
|
||||||
|
return index < mediaPlaylist.trailingParts.size()
|
||||||
|
? new SegmentBaseHolder(mediaPlaylist.trailingParts.get(index), nextMediaSequence, index)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
|
||||||
|
if (nextPartIndex == C.INDEX_UNSET) {
|
||||||
|
return new SegmentBaseHolder(mediaSegment, nextMediaSequence, nextPartIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPartIndex < mediaSegment.parts.size()) {
|
||||||
|
// The requested part is available in the requested segment.
|
||||||
|
return new SegmentBaseHolder(
|
||||||
|
mediaSegment.parts.get(nextPartIndex), nextMediaSequence, nextPartIndex);
|
||||||
|
} else if (segmentIndexInPlaylist + 1 < mediaPlaylist.segments.size()) {
|
||||||
|
// The first part of the next segment is requested, but we can use the next full segment.
|
||||||
|
return new SegmentBaseHolder(
|
||||||
|
mediaPlaylist.segments.get(segmentIndexInPlaylist + 1),
|
||||||
|
nextMediaSequence + 1,
|
||||||
|
/* partIndex= */ C.INDEX_UNSET);
|
||||||
|
} else if (!mediaPlaylist.trailingParts.isEmpty()) {
|
||||||
|
// The part index is rolling over to the first trailing part.
|
||||||
|
return new SegmentBaseHolder(
|
||||||
|
mediaPlaylist.trailingParts.get(0), nextMediaSequence + 1, /* partIndex= */ 0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
|
* Called when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
|
||||||
* source.
|
* source.
|
||||||
|
|
@ -438,6 +495,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
chunkIterators[i] = MediaChunkIterator.EMPTY;
|
chunkIterators[i] = MediaChunkIterator.EMPTY;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@Nullable
|
||||||
HlsMediaPlaylist playlist =
|
HlsMediaPlaylist playlist =
|
||||||
playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false);
|
playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false);
|
||||||
// Playlist snapshot is valid (checked by if() above) so playlist must be non-null.
|
// Playlist snapshot is valid (checked by if() above) so playlist must be non-null.
|
||||||
|
|
@ -445,16 +503,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
long startOfPlaylistInPeriodUs =
|
long startOfPlaylistInPeriodUs =
|
||||||
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||||
boolean switchingTrack = trackIndex != oldTrackIndex;
|
boolean switchingTrack = trackIndex != oldTrackIndex;
|
||||||
long chunkMediaSequence =
|
Pair<Long, Integer> chunkMediaSequenceAndPartIndex =
|
||||||
getChunkMediaSequence(
|
getNextMediaSequenceAndPartIndex(
|
||||||
previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
|
previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
|
||||||
if (chunkMediaSequence < playlist.mediaSequence) {
|
long chunkMediaSequence = chunkMediaSequenceAndPartIndex.first;
|
||||||
chunkIterators[i] = MediaChunkIterator.EMPTY;
|
int partIndex = chunkMediaSequenceAndPartIndex.second;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence);
|
|
||||||
chunkIterators[i] =
|
chunkIterators[i] =
|
||||||
new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex);
|
new HlsMediaPlaylistSegmentIterator(
|
||||||
|
playlist.baseUri,
|
||||||
|
startOfPlaylistInPeriodUs,
|
||||||
|
getSegmentBaseList(playlist, chunkMediaSequence, partIndex));
|
||||||
}
|
}
|
||||||
return chunkIterators;
|
return chunkIterators;
|
||||||
}
|
}
|
||||||
|
|
@ -495,10 +553,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue);
|
return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package methods.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list with all segment bases in the playlist starting from {@code mediaSequence} and
|
||||||
|
* {@code partIndex} in the given playlist. The list may be empty if the starting point is not in
|
||||||
|
* the playlist.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
/* package */ static List<HlsMediaPlaylist.SegmentBase> getSegmentBaseList(
|
||||||
|
HlsMediaPlaylist playlist, long mediaSequence, int partIndex) {
|
||||||
|
int firstSegmentIndexInPlaylist = (int) (mediaSequence - playlist.mediaSequence);
|
||||||
|
if (firstSegmentIndexInPlaylist < 0 || playlist.segments.size() < firstSegmentIndexInPlaylist) {
|
||||||
|
// The first media sequence is not in the playlist.
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
List<HlsMediaPlaylist.SegmentBase> segmentBases = new ArrayList<>();
|
||||||
|
if (firstSegmentIndexInPlaylist < playlist.segments.size()) {
|
||||||
|
if (partIndex != C.INDEX_UNSET) {
|
||||||
|
// The iterator starts with a part that belongs to a segment.
|
||||||
|
Segment firstSegment = playlist.segments.get(firstSegmentIndexInPlaylist);
|
||||||
|
if (partIndex == 0) {
|
||||||
|
// Use the full segment instead of the first part.
|
||||||
|
segmentBases.add(firstSegment);
|
||||||
|
} else if (partIndex < firstSegment.parts.size()) {
|
||||||
|
// Add the parts from the first requested segment.
|
||||||
|
segmentBases.addAll(firstSegment.parts.subList(partIndex, firstSegment.parts.size()));
|
||||||
|
}
|
||||||
|
firstSegmentIndexInPlaylist++;
|
||||||
|
}
|
||||||
|
partIndex = 0;
|
||||||
|
// Add all remaining segments.
|
||||||
|
segmentBases.addAll(
|
||||||
|
playlist.segments.subList(firstSegmentIndexInPlaylist, playlist.segments.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlist.partTargetDurationUs != C.TIME_UNSET) {
|
||||||
|
// That's a low latency playlist.
|
||||||
|
partIndex = partIndex == C.INDEX_UNSET ? 0 : partIndex;
|
||||||
|
if (partIndex < playlist.trailingParts.size()) {
|
||||||
|
segmentBases.addAll(
|
||||||
|
playlist.trailingParts.subList(partIndex, playlist.trailingParts.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(segmentBases);
|
||||||
|
}
|
||||||
|
|
||||||
// Private methods.
|
// Private methods.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the media sequence number of the segment to load next in {@code mediaPlaylist}.
|
* Returns the media sequence number and part index to load next in the {@code mediaPlaylist}.
|
||||||
*
|
*
|
||||||
* @param previous The last (at least partially) loaded segment.
|
* @param previous The last (at least partially) loaded segment.
|
||||||
* @param switchingTrack Whether the segment to load is not preceded by a segment in the same
|
* @param switchingTrack Whether the segment to load is not preceded by a segment in the same
|
||||||
|
|
@ -507,9 +611,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
* @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period
|
* @param startOfPlaylistInPeriodUs The start of {@code mediaPlaylist} relative to the period
|
||||||
* start in microseconds.
|
* start in microseconds.
|
||||||
* @param loadPositionUs The current load position relative to the period start in microseconds.
|
* @param loadPositionUs The current load position relative to the period start in microseconds.
|
||||||
* @return The media sequence of the segment to load.
|
* @return The media sequence and part index to load.
|
||||||
*/
|
*/
|
||||||
private long getChunkMediaSequence(
|
private Pair<Long, Integer> getNextMediaSequenceAndPartIndex(
|
||||||
@Nullable HlsMediaChunk previous,
|
@Nullable HlsMediaChunk previous,
|
||||||
boolean switchingTrack,
|
boolean switchingTrack,
|
||||||
HlsMediaPlaylist mediaPlaylist,
|
HlsMediaPlaylist mediaPlaylist,
|
||||||
|
|
@ -521,17 +625,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
|
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
|
||||||
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
|
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
|
||||||
// If the playlist is too old to contain the chunk, we need to refresh it.
|
// If the playlist is too old to contain the chunk, we need to refresh it.
|
||||||
return mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
|
return new Pair<>(
|
||||||
|
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(),
|
||||||
|
/* partIndex */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
|
long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs;
|
||||||
return Util.binarySearchFloor(
|
long mediaSequence =
|
||||||
mediaPlaylist.segments,
|
Util.binarySearchFloor(
|
||||||
/* value= */ targetPositionInPlaylistUs,
|
mediaPlaylist.segments,
|
||||||
/* inclusive= */ true,
|
/* value= */ targetPositionInPlaylistUs,
|
||||||
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
|
/* inclusive= */ true,
|
||||||
+ mediaPlaylist.mediaSequence;
|
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
|
||||||
|
+ mediaPlaylist.mediaSequence;
|
||||||
|
return new Pair<>(mediaSequence, /* partIndex */ C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
return previous.isLoadCompleted() ? previous.getNextChunkIndex() : previous.chunkIndex;
|
// If loading has not completed, we return the previous chunk again.
|
||||||
|
return (previous.isLoadCompleted()
|
||||||
|
? new Pair<>(
|
||||||
|
previous.partIndex == C.INDEX_UNSET
|
||||||
|
? previous.getNextChunkIndex()
|
||||||
|
: previous.chunkIndex,
|
||||||
|
previous.partIndex == C.INDEX_UNSET ? C.INDEX_UNSET : previous.partIndex + 1)
|
||||||
|
: new Pair<>(previous.chunkIndex, previous.partIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
|
private long resolveTimeToLiveEdgeUs(long playbackPositionUs) {
|
||||||
|
|
@ -574,11 +689,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static Uri getFullEncryptionKeyUri(HlsMediaPlaylist playlist, @Nullable Segment segment) {
|
private static Uri getFullEncryptionKeyUri(
|
||||||
if (segment == null || segment.fullSegmentEncryptionKeyUri == null) {
|
HlsMediaPlaylist playlist, @Nullable HlsMediaPlaylist.SegmentBase segmentBase) {
|
||||||
|
if (segmentBase == null || segmentBase.fullSegmentEncryptionKeyUri == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return UriUtil.resolveToUri(playlist.baseUri, segment.fullSegmentEncryptionKeyUri);
|
return UriUtil.resolveToUri(playlist.baseUri, segmentBase.fullSegmentEncryptionKeyUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Package classes.
|
||||||
|
|
||||||
|
/* package */ static final class SegmentBaseHolder {
|
||||||
|
|
||||||
|
public final HlsMediaPlaylist.SegmentBase segmentBase;
|
||||||
|
public final long mediaSequence;
|
||||||
|
public final int partIndex;
|
||||||
|
|
||||||
|
/** Creates a new instance. */
|
||||||
|
public SegmentBaseHolder(
|
||||||
|
HlsMediaPlaylist.SegmentBase segmentBase, long mediaSequence, int partIndex) {
|
||||||
|
this.segmentBase = segmentBase;
|
||||||
|
this.mediaSequence = mediaSequence;
|
||||||
|
this.partIndex = partIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private classes.
|
// Private classes.
|
||||||
|
|
@ -665,48 +798,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */
|
@VisibleForTesting
|
||||||
private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {
|
/* package */ static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {
|
||||||
|
|
||||||
private final HlsMediaPlaylist playlist;
|
private final List<HlsMediaPlaylist.SegmentBase> segmentBases;
|
||||||
private final long startOfPlaylistInPeriodUs;
|
private final long startOfPlaylistInPeriodUs;
|
||||||
|
private final String playlistBaseUri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates iterator.
|
* Creates an iterator instance wrapping a list of {@link HlsMediaPlaylist.SegmentBase}.
|
||||||
*
|
*
|
||||||
* @param playlist The {@link HlsMediaPlaylist} to wrap.
|
* @param playlistBaseUri The base URI of the {@link HlsMediaPlaylist}.
|
||||||
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in
|
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in
|
||||||
* microseconds.
|
* microseconds.
|
||||||
* @param chunkIndex The index of the first available chunk in the playlist.
|
* @param segmentBases The list of {@link HlsMediaPlaylist.SegmentBase segment bases} to wrap.
|
||||||
*/
|
*/
|
||||||
public HlsMediaPlaylistSegmentIterator(
|
public HlsMediaPlaylistSegmentIterator(
|
||||||
HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) {
|
String playlistBaseUri,
|
||||||
super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1);
|
long startOfPlaylistInPeriodUs,
|
||||||
this.playlist = playlist;
|
List<HlsMediaPlaylist.SegmentBase> segmentBases) {
|
||||||
|
super(/* fromIndex= */ 0, segmentBases.size() - 1);
|
||||||
|
this.playlistBaseUri = playlistBaseUri;
|
||||||
this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs;
|
this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs;
|
||||||
|
this.segmentBases = segmentBases;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DataSpec getDataSpec() {
|
public DataSpec getDataSpec() {
|
||||||
checkInBounds();
|
checkInBounds();
|
||||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
HlsMediaPlaylist.SegmentBase segmentBase = segmentBases.get((int) getCurrentIndex());
|
||||||
Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url);
|
Uri chunkUri = UriUtil.resolveToUri(playlistBaseUri, segmentBase.url);
|
||||||
return new DataSpec(chunkUri, segment.byteRangeOffset, segment.byteRangeLength);
|
return new DataSpec(chunkUri, segmentBase.byteRangeOffset, segmentBase.byteRangeLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getChunkStartTimeUs() {
|
public long getChunkStartTimeUs() {
|
||||||
checkInBounds();
|
checkInBounds();
|
||||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
return startOfPlaylistInPeriodUs
|
||||||
return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
+ segmentBases.get((int) getCurrentIndex()).relativeStartTimeUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getChunkEndTimeUs() {
|
public long getChunkEndTimeUs() {
|
||||||
checkInBounds();
|
checkInBounds();
|
||||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
HlsMediaPlaylist.SegmentBase segmentBase = segmentBases.get((int) getCurrentIndex());
|
||||||
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segmentBase.relativeStartTimeUs;
|
||||||
return segmentStartTimeInPeriodUs + segment.durationUs;
|
return segmentStartTimeInPeriodUs + segmentBase.durationUs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
* @param format The chunk format.
|
* @param format The chunk format.
|
||||||
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
|
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
|
||||||
* @param mediaPlaylist The media playlist from which this chunk was obtained.
|
* @param mediaPlaylist The media playlist from which this chunk was obtained.
|
||||||
* @param segmentIndexInPlaylist The index of the segment in the media playlist.
|
* @param segmentBaseHolder The segment holder.
|
||||||
* @param playlistUrl The url of the playlist from which this chunk was obtained.
|
* @param playlistUrl The url of the playlist from which this chunk was obtained.
|
||||||
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
* @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
||||||
* information is available in the master playlist.
|
* information is available in the master playlist.
|
||||||
|
|
@ -79,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
Format format,
|
Format format,
|
||||||
long startOfPlaylistInPeriodUs,
|
long startOfPlaylistInPeriodUs,
|
||||||
HlsMediaPlaylist mediaPlaylist,
|
HlsMediaPlaylist mediaPlaylist,
|
||||||
int segmentIndexInPlaylist,
|
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
|
||||||
Uri playlistUrl,
|
Uri playlistUrl,
|
||||||
@Nullable List<Format> muxedCaptionFormats,
|
@Nullable List<Format> muxedCaptionFormats,
|
||||||
int trackSelectionReason,
|
int trackSelectionReason,
|
||||||
|
|
@ -90,7 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
@Nullable byte[] mediaSegmentKey,
|
@Nullable byte[] mediaSegmentKey,
|
||||||
@Nullable byte[] initSegmentKey) {
|
@Nullable byte[] initSegmentKey) {
|
||||||
// Media segment.
|
// Media segment.
|
||||||
HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
|
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
|
||||||
DataSpec dataSpec =
|
DataSpec dataSpec =
|
||||||
new DataSpec(
|
new DataSpec(
|
||||||
UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
|
UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
|
||||||
|
|
@ -136,10 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
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 =
|
boolean canContinueWithoutSplice =
|
||||||
isFollowingChunk
|
isFollowingChunk
|
||||||
|| (mediaPlaylist.hasIndependentSegments
|
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
|
||||||
&& segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
|
|
||||||
shouldSpliceIn = !canContinueWithoutSplice;
|
shouldSpliceIn = !canContinueWithoutSplice;
|
||||||
previousExtractor =
|
previousExtractor =
|
||||||
isFollowingChunk
|
isFollowingChunk
|
||||||
|
|
@ -152,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
|
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
|
||||||
shouldSpliceIn = false;
|
shouldSpliceIn = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new HlsMediaChunk(
|
return new HlsMediaChunk(
|
||||||
extractorFactory,
|
extractorFactory,
|
||||||
mediaDataSource,
|
mediaDataSource,
|
||||||
|
|
@ -168,7 +167,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
trackSelectionData,
|
trackSelectionData,
|
||||||
segmentStartTimeInPeriodUs,
|
segmentStartTimeInPeriodUs,
|
||||||
segmentEndTimeInPeriodUs,
|
segmentEndTimeInPeriodUs,
|
||||||
/* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,
|
segmentBaseHolder.mediaSequence,
|
||||||
|
segmentBaseHolder.partIndex,
|
||||||
discontinuitySequenceNumber,
|
discontinuitySequenceNumber,
|
||||||
mediaSegment.hasGapTag,
|
mediaSegment.hasGapTag,
|
||||||
isMasterTimestampSource,
|
isMasterTimestampSource,
|
||||||
|
|
@ -201,6 +201,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
/** Whether samples for this chunk should be spliced into existing samples. */
|
/** Whether samples for this chunk should be spliced into existing samples. */
|
||||||
public final boolean shouldSpliceIn;
|
public final boolean shouldSpliceIn;
|
||||||
|
|
||||||
|
/** The part index or {@link C#INDEX_UNSET} if the chunk is a full segment */
|
||||||
|
public final int partIndex;
|
||||||
|
|
||||||
@Nullable private final DataSource initDataSource;
|
@Nullable private final DataSource initDataSource;
|
||||||
@Nullable private final DataSpec initDataSpec;
|
@Nullable private final DataSpec initDataSpec;
|
||||||
@Nullable private final HlsMediaChunkExtractor previousExtractor;
|
@Nullable private final HlsMediaChunkExtractor previousExtractor;
|
||||||
|
|
@ -243,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
long startTimeUs,
|
long startTimeUs,
|
||||||
long endTimeUs,
|
long endTimeUs,
|
||||||
long chunkMediaSequence,
|
long chunkMediaSequence,
|
||||||
|
int partIndex,
|
||||||
int discontinuitySequenceNumber,
|
int discontinuitySequenceNumber,
|
||||||
boolean hasGapTag,
|
boolean hasGapTag,
|
||||||
boolean isMasterTimestampSource,
|
boolean isMasterTimestampSource,
|
||||||
|
|
@ -262,6 +266,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
endTimeUs,
|
endTimeUs,
|
||||||
chunkMediaSequence);
|
chunkMediaSequence);
|
||||||
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
|
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
|
||||||
|
this.partIndex = partIndex;
|
||||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||||
this.initDataSpec = initDataSpec;
|
this.initDataSpec = initDataSpec;
|
||||||
this.initDataSource = initDataSource;
|
this.initDataSource = initDataSource;
|
||||||
|
|
@ -541,4 +546,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
}
|
}
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isIndependent(
|
||||||
|
HlsChunkSource.SegmentBaseHolder segmentBaseHolder, HlsMediaPlaylist mediaPlaylist) {
|
||||||
|
if (segmentBaseHolder.segmentBase instanceof HlsMediaPlaylist.Part) {
|
||||||
|
return ((HlsMediaPlaylist.Part) segmentBaseHolder.segmentBase).isIndependent
|
||||||
|
|| (segmentBaseHolder.partIndex == 0 && mediaPlaylist.hasIndependentSegments);
|
||||||
|
}
|
||||||
|
return mediaPlaylist.hasIndependentSegments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -797,9 +797,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
}
|
}
|
||||||
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
|
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
|
||||||
long byteRangeStart =
|
long byteRangeStart =
|
||||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ 0);
|
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ C.LENGTH_UNSET);
|
||||||
long byteRangeLength =
|
long byteRangeLength =
|
||||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.TIME_UNSET);
|
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.LENGTH_UNSET);
|
||||||
@Nullable
|
@Nullable
|
||||||
String segmentEncryptionIV =
|
String segmentEncryptionIV =
|
||||||
getSegmentEncryptionIV(
|
getSegmentEncryptionIV(
|
||||||
|
|
@ -811,21 +811,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
||||||
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
|
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preloadPart =
|
if (byteRangeStart == C.LENGTH_UNSET || byteRangeLength != C.LENGTH_UNSET) {
|
||||||
new Part(
|
// Skip preload part if it is an unbounded range request.
|
||||||
url,
|
preloadPart =
|
||||||
initializationSegment,
|
new Part(
|
||||||
/* durationUs= */ 0,
|
url,
|
||||||
relativeDiscontinuitySequence,
|
initializationSegment,
|
||||||
partStartTimeUs,
|
/* durationUs= */ 0,
|
||||||
cachedDrmInitData,
|
relativeDiscontinuitySequence,
|
||||||
fullSegmentEncryptionKeyUri,
|
partStartTimeUs,
|
||||||
segmentEncryptionIV,
|
cachedDrmInitData,
|
||||||
byteRangeStart,
|
fullSegmentEncryptionKeyUri,
|
||||||
byteRangeLength,
|
segmentEncryptionIV,
|
||||||
/* hasGapTag= */ false,
|
byteRangeStart != C.LENGTH_UNSET ? byteRangeStart : 0,
|
||||||
/* isIndependent= */ false,
|
byteRangeLength,
|
||||||
/* isPreload= */ true);
|
/* hasGapTag= */ false,
|
||||||
|
/* isIndependent= */ false,
|
||||||
|
/* isPreload= */ true);
|
||||||
|
}
|
||||||
} else if (line.startsWith(TAG_PART)) {
|
} else if (line.startsWith(TAG_PART)) {
|
||||||
@Nullable
|
@Nullable
|
||||||
String segmentEncryptionIV =
|
String segmentEncryptionIV =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 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.source.hls;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||||
|
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link HlsChunkSource.HlsMediaPlaylistSegmentIterator}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class HlsMediaPlaylistSegmentIteratorTest {
|
||||||
|
|
||||||
|
public static final String LOW_LATENCY_SEGMENTS_AND_PARTS =
|
||||||
|
"media/m3u8/live_low_latency_segments_and_parts";
|
||||||
|
public static final String SEGMENTS_ONLY = "media/m3u8/live_low_latency_segments_only";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_withMediaSequenceBehindLiveWindow_isEmpty() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, mediaPlaylist.mediaSequence - 1, /* partIndex= */ C.INDEX_UNSET));
|
||||||
|
|
||||||
|
assertThat(hlsMediaPlaylistSegmentIterator.next()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_withMediaSequenceBeforeTrailingPartSegment_isEmpty() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist,
|
||||||
|
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size() + 1,
|
||||||
|
/* partIndex= */ C.INDEX_UNSET));
|
||||||
|
|
||||||
|
assertThat(hlsMediaPlaylistSegmentIterator.next()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void create_withPartIndexBeforeLastTrailingPartSegment_isEmpty() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist,
|
||||||
|
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(),
|
||||||
|
/* partIndex= */ 3));
|
||||||
|
|
||||||
|
assertThat(hlsMediaPlaylistSegmentIterator.next()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void next_conventionalLiveStartIteratorAtSecondSegment_correctElements() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(SEGMENTS_ONLY);
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, /* mediaSequence= */ 11, /* partIndex= */ C.INDEX_UNSET));
|
||||||
|
|
||||||
|
List<DataSpec> datasSpecs = new ArrayList<>();
|
||||||
|
while (hlsMediaPlaylistSegmentIterator.next()) {
|
||||||
|
datasSpecs.add(hlsMediaPlaylistSegmentIterator.getDataSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(datasSpecs).hasSize(5);
|
||||||
|
assertThat(datasSpecs.get(0).uri.toString()).isEqualTo("fileSequence11.ts");
|
||||||
|
assertThat(Iterables.getLast(datasSpecs).uri.toString()).isEqualTo("fileSequence15.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void next_startIteratorAtFirstSegment_correctElements() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, /* mediaSequence= */ 10, /* partIndex= */ C.INDEX_UNSET));
|
||||||
|
|
||||||
|
List<DataSpec> datasSpecs = new ArrayList<>();
|
||||||
|
while (hlsMediaPlaylistSegmentIterator.next()) {
|
||||||
|
datasSpecs.add(hlsMediaPlaylistSegmentIterator.getDataSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(datasSpecs).hasSize(9);
|
||||||
|
// The iterator starts with 6 segments.
|
||||||
|
assertThat(datasSpecs.get(0).uri.toString()).isEqualTo("fileSequence10.ts");
|
||||||
|
// Followed by trailing parts.
|
||||||
|
assertThat(datasSpecs.get(6).uri.toString()).isEqualTo("fileSequence16.0.ts");
|
||||||
|
// The preload part is the last.
|
||||||
|
assertThat(Iterables.getLast(datasSpecs).uri.toString()).isEqualTo("fileSequence16.2.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void next_startIteratorAtFirstPartInaSegment_usesFullSegment() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, /* mediaSequence= */ 14, /* partIndex= */ 0));
|
||||||
|
|
||||||
|
List<DataSpec> datasSpecs = new ArrayList<>();
|
||||||
|
while (hlsMediaPlaylistSegmentIterator.next()) {
|
||||||
|
datasSpecs.add(hlsMediaPlaylistSegmentIterator.getDataSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(datasSpecs).hasSize(5);
|
||||||
|
// The iterator starts with 6 segments.
|
||||||
|
assertThat(datasSpecs.get(0).uri.toString()).isEqualTo("fileSequence14.ts");
|
||||||
|
assertThat(datasSpecs.get(1).uri.toString()).isEqualTo("fileSequence15.ts");
|
||||||
|
// Followed by trailing parts.
|
||||||
|
assertThat(datasSpecs.get(2).uri.toString()).isEqualTo("fileSequence16.0.ts");
|
||||||
|
assertThat(datasSpecs.get(3).uri.toString()).isEqualTo("fileSequence16.1.ts");
|
||||||
|
// The preload part is the last.
|
||||||
|
assertThat(Iterables.getLast(datasSpecs).uri.toString()).isEqualTo("fileSequence16.2.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void next_startIteratorAtTrailingPart_correctElements() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, /* mediaSequence= */ 16, /* partIndex= */ 1));
|
||||||
|
|
||||||
|
List<DataSpec> datasSpecs = new ArrayList<>();
|
||||||
|
while (hlsMediaPlaylistSegmentIterator.next()) {
|
||||||
|
datasSpecs.add(hlsMediaPlaylistSegmentIterator.getDataSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(datasSpecs).hasSize(2);
|
||||||
|
// The iterator starts with 2 parts.
|
||||||
|
assertThat(datasSpecs.get(0).uri.toString()).isEqualTo("fileSequence16.1.ts");
|
||||||
|
// The preload part is the last.
|
||||||
|
assertThat(Iterables.getLast(datasSpecs).uri.toString()).isEqualTo("fileSequence16.2.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void next_startIteratorAtPartWithinSegment_correctElements() {
|
||||||
|
HlsMediaPlaylist mediaPlaylist = getHlsMediaPlaylist(LOW_LATENCY_SEGMENTS_AND_PARTS);
|
||||||
|
HlsChunkSource.HlsMediaPlaylistSegmentIterator hlsMediaPlaylistSegmentIterator =
|
||||||
|
new HlsChunkSource.HlsMediaPlaylistSegmentIterator(
|
||||||
|
mediaPlaylist.baseUri,
|
||||||
|
/* startOfPlaylistInPeriodUs= */ 0,
|
||||||
|
HlsChunkSource.getSegmentBaseList(
|
||||||
|
mediaPlaylist, /* mediaSequence= */ 14, /* partIndex= */ 1));
|
||||||
|
|
||||||
|
List<DataSpec> datasSpecs = new ArrayList<>();
|
||||||
|
while (hlsMediaPlaylistSegmentIterator.next()) {
|
||||||
|
datasSpecs.add(hlsMediaPlaylistSegmentIterator.getDataSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(datasSpecs).hasSize(7);
|
||||||
|
// The iterator starts with 11 parts.
|
||||||
|
assertThat(datasSpecs.get(0).uri.toString()).isEqualTo("fileSequence14.1.ts");
|
||||||
|
assertThat(datasSpecs.get(1).uri.toString()).isEqualTo("fileSequence14.2.ts");
|
||||||
|
assertThat(datasSpecs.get(2).uri.toString()).isEqualTo("fileSequence14.3.ts");
|
||||||
|
// Use a segment in between if possible.
|
||||||
|
assertThat(datasSpecs.get(3).uri.toString()).isEqualTo("fileSequence15.ts");
|
||||||
|
// Then parts again.
|
||||||
|
assertThat(datasSpecs.get(4).uri.toString()).isEqualTo("fileSequence16.0.ts");
|
||||||
|
assertThat(datasSpecs.get(5).uri.toString()).isEqualTo("fileSequence16.1.ts");
|
||||||
|
assertThat(datasSpecs.get(6).uri.toString()).isEqualTo("fileSequence16.2.ts");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HlsMediaPlaylist getHlsMediaPlaylist(String file) {
|
||||||
|
try {
|
||||||
|
return (HlsMediaPlaylist)
|
||||||
|
new HlsPlaylistParser()
|
||||||
|
.parse(
|
||||||
|
Uri.EMPTY,
|
||||||
|
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), file));
|
||||||
|
} catch (IOException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -362,6 +363,7 @@ public class HlsMediaPlaylistParserTest {
|
||||||
assertThat(secondPart.byteRangeOffset).isEqualTo(1234);
|
assertThat(secondPart.byteRangeOffset).isEqualTo(1234);
|
||||||
// Assert trailing parts.
|
// Assert trailing parts.
|
||||||
HlsMediaPlaylist.Part thirdPart = playlist.trailingParts.get(0);
|
HlsMediaPlaylist.Part thirdPart = playlist.trailingParts.get(0);
|
||||||
|
// Assert tailing parts.
|
||||||
assertThat(thirdPart.byteRangeLength).isEqualTo(1000);
|
assertThat(thirdPart.byteRangeLength).isEqualTo(1000);
|
||||||
assertThat(thirdPart.byteRangeOffset).isEqualTo(1234);
|
assertThat(thirdPart.byteRangeOffset).isEqualTo(1234);
|
||||||
assertThat(thirdPart.relativeStartTimeUs).isEqualTo(8_000_000);
|
assertThat(thirdPart.relativeStartTimeUs).isEqualTo(8_000_000);
|
||||||
|
|
@ -544,6 +546,27 @@ public class HlsMediaPlaylistParserTest {
|
||||||
assertThat(playlist.trailingParts.get(1).isPreload).isTrue();
|
assertThat(playlist.trailingParts.get(1).isPreload).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseMediaPlaylist_withUnboundedPreloadHintTypePart_ignoresPreloadPart()
|
||||||
|
throws IOException {
|
||||||
|
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||||
|
String playlistString =
|
||||||
|
"#EXTM3U\n"
|
||||||
|
+ "#EXT-X-TARGETDURATION:4\n"
|
||||||
|
+ "#EXT-X-VERSION:6\n"
|
||||||
|
+ "#EXT-X-MEDIA-SEQUENCE:266\n"
|
||||||
|
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"\n"
|
||||||
|
+ "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart267.2.ts,BYTERANGE-START=0\"\n";
|
||||||
|
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
|
||||||
|
|
||||||
|
HlsMediaPlaylist playlist =
|
||||||
|
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||||
|
|
||||||
|
assertThat(playlist.trailingParts).hasSize(1);
|
||||||
|
assertThat(Iterables.getLast(playlist.trailingParts).url).isEqualTo("part267.1.ts");
|
||||||
|
assertThat(Iterables.getLast(playlist.trailingParts).isPreload).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData()
|
public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData()
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
|
||||||
28
testdata/src/test/assets/media/m3u8/live_low_latency_segments_and_parts
vendored
Normal file
28
testdata/src/test/assets/media/m3u8/live_low_latency_segments_and_parts
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-TARGETDURATION:4
|
||||||
|
#EXT-X-PART-INF:PART-TARGET=1.000400
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:10
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence10.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence11.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence12.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence13.ts
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.1.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.2.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.3.ts"
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence14.ts
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.0.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.1.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.2.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence15.3.ts"
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence15.ts
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.0.ts"
|
||||||
|
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.1.ts"
|
||||||
|
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence16.2.ts"
|
||||||
16
testdata/src/test/assets/media/m3u8/live_low_latency_segments_only
vendored
Normal file
16
testdata/src/test/assets/media/m3u8/live_low_latency_segments_only
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-TARGETDURATION:4
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:10
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence10.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence11.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence12.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence13.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence14.ts
|
||||||
|
#EXTINF:4.00000,
|
||||||
|
fileSequence15.ts
|
||||||
Loading…
Reference in a new issue