Commit e3c725aa by bachinger Committed by Christos Tsilopoulos

Create chunks from parts in HlsChunkSource

Issue: #5011
PiperOrigin-RevId: 342022947
parent 2693a107
...@@ -19,7 +19,9 @@ import static java.lang.Math.max; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 previous.isLoadCompleted() ? previous.getNextChunkIndex() : previous.chunkIndex; return new Pair<>(mediaSequence, /* partIndex */ C.INDEX_UNSET);
}
// 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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 ...@@ -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 ...@@ -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 =
......
/*
* 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; ...@@ -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 { ...@@ -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);
...@@ -545,6 +547,27 @@ public class HlsMediaPlaylistParserTest { ...@@ -545,6 +547,27 @@ public class HlsMediaPlaylistParserTest {
} }
@Test @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
public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData() public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData()
throws IOException { throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
......
#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"
#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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment