mirror of
https://github.com/samsonjs/media.git
synced 2026-04-22 14:05:55 +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.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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.UriUtil;
|
||||
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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
|
|
@ -242,7 +247,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
List<HlsMediaChunk> queue,
|
||||
boolean allowEndOfStream,
|
||||
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);
|
||||
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
|
||||
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
|
||||
|
|
@ -275,6 +280,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
// Retry when playlist is refreshed.
|
||||
return;
|
||||
}
|
||||
@Nullable
|
||||
HlsMediaPlaylist mediaPlaylist =
|
||||
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
|
||||
// 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.
|
||||
long startOfPlaylistInPeriodUs =
|
||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
long chunkMediaSequence =
|
||||
getChunkMediaSequence(
|
||||
Pair<Long, Integer> nextMediaSequenceAndPartIndex =
|
||||
getNextMediaSequenceAndPartIndex(
|
||||
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
|
||||
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
|
||||
int partIndex = nextMediaSequenceAndPartIndex.second;
|
||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) {
|
||||
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||
// behind the live window.
|
||||
selectedTrackIndex = oldTrackIndex;
|
||||
selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
|
||||
// We try getting the next chunk without adapting in case that's the reason for falling
|
||||
// behind the live window.
|
||||
selectedTrackIndex = oldTrackIndex;
|
||||
selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
|
||||
mediaPlaylist =
|
||||
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
|
||||
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be
|
||||
// non-null.
|
||||
Assertions.checkNotNull(mediaPlaylist);
|
||||
startOfPlaylistInPeriodUs =
|
||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
chunkMediaSequence = previous.getNextChunkIndex();
|
||||
startOfPlaylistInPeriodUs =
|
||||
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
// 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) {
|
||||
|
|
@ -309,36 +326,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
return;
|
||||
}
|
||||
|
||||
int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence);
|
||||
int availableSegmentCount = mediaPlaylist.segments.size();
|
||||
if (segmentIndexInPlaylist >= availableSegmentCount) {
|
||||
if (mediaPlaylist.hasEndTag) {
|
||||
if (allowEndOfStream || availableSegmentCount == 0) {
|
||||
out.endOfStream = true;
|
||||
return;
|
||||
}
|
||||
segmentIndexInPlaylist = availableSegmentCount - 1;
|
||||
} else /* Live */ {
|
||||
@Nullable
|
||||
SegmentBaseHolder segmentBaseHolder =
|
||||
getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex);
|
||||
if (segmentBaseHolder == null) {
|
||||
if (!mediaPlaylist.hasEndTag) {
|
||||
// Reload the playlist in case of a live stream.
|
||||
out.playlistUrl = selectedPlaylistUrl;
|
||||
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
|
||||
expectedPlaylistUrl = selectedPlaylistUrl;
|
||||
return;
|
||||
} else if (allowEndOfStream || mediaPlaylist.segments.isEmpty()) {
|
||||
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;
|
||||
expectedPlaylistUrl = null;
|
||||
|
||||
// Handle encryption.
|
||||
HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
|
||||
|
||||
// Check if the segment or its initialization segment are fully encrypted.
|
||||
Uri initSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment.initializationSegment);
|
||||
// Check if the media segment or its initialization segment are fully encrypted.
|
||||
@Nullable
|
||||
Uri initSegmentKeyUri =
|
||||
getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment);
|
||||
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
|
||||
if (out.chunk != null) {
|
||||
return;
|
||||
}
|
||||
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segment);
|
||||
@Nullable
|
||||
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase);
|
||||
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
|
||||
if (out.chunk != null) {
|
||||
return;
|
||||
|
|
@ -351,7 +374,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
playlistFormats[selectedTrackIndex],
|
||||
startOfPlaylistInPeriodUs,
|
||||
mediaPlaylist,
|
||||
segmentIndexInPlaylist,
|
||||
segmentBaseHolder,
|
||||
selectedPlaylistUrl,
|
||||
muxedCaptionFormats,
|
||||
trackSelection.getSelectionReason(),
|
||||
|
|
@ -363,6 +386,40 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
/* 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
|
||||
* source.
|
||||
|
|
@ -438,6 +495,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
chunkIterators[i] = MediaChunkIterator.EMPTY;
|
||||
continue;
|
||||
}
|
||||
@Nullable
|
||||
HlsMediaPlaylist playlist =
|
||||
playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false);
|
||||
// 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 =
|
||||
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
|
||||
boolean switchingTrack = trackIndex != oldTrackIndex;
|
||||
long chunkMediaSequence =
|
||||
getChunkMediaSequence(
|
||||
Pair<Long, Integer> chunkMediaSequenceAndPartIndex =
|
||||
getNextMediaSequenceAndPartIndex(
|
||||
previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
|
||||
if (chunkMediaSequence < playlist.mediaSequence) {
|
||||
chunkIterators[i] = MediaChunkIterator.EMPTY;
|
||||
continue;
|
||||
}
|
||||
int chunkIndex = (int) (chunkMediaSequence - playlist.mediaSequence);
|
||||
long chunkMediaSequence = chunkMediaSequenceAndPartIndex.first;
|
||||
int partIndex = chunkMediaSequenceAndPartIndex.second;
|
||||
chunkIterators[i] =
|
||||
new HlsMediaPlaylistSegmentIterator(playlist, startOfPlaylistInPeriodUs, chunkIndex);
|
||||
new HlsMediaPlaylistSegmentIterator(
|
||||
playlist.baseUri,
|
||||
startOfPlaylistInPeriodUs,
|
||||
getSegmentBaseList(playlist, chunkMediaSequence, partIndex));
|
||||
}
|
||||
return chunkIterators;
|
||||
}
|
||||
|
|
@ -495,10 +553,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
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.
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* 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,
|
||||
boolean switchingTrack,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
|
|
@ -521,17 +625,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
(previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs;
|
||||
if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) {
|
||||
// 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;
|
||||
return Util.binarySearchFloor(
|
||||
mediaPlaylist.segments,
|
||||
/* value= */ targetPositionInPlaylistUs,
|
||||
/* inclusive= */ true,
|
||||
/* stayInBounds= */ !playlistTracker.isLive() || previous == null)
|
||||
+ mediaPlaylist.mediaSequence;
|
||||
long mediaSequence =
|
||||
Util.binarySearchFloor(
|
||||
mediaPlaylist.segments,
|
||||
/* value= */ targetPositionInPlaylistUs,
|
||||
/* inclusive= */ true,
|
||||
/* 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) {
|
||||
|
|
@ -574,11 +689,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
}
|
||||
|
||||
@Nullable
|
||||
private static Uri getFullEncryptionKeyUri(HlsMediaPlaylist playlist, @Nullable Segment segment) {
|
||||
if (segment == null || segment.fullSegmentEncryptionKeyUri == null) {
|
||||
private static Uri getFullEncryptionKeyUri(
|
||||
HlsMediaPlaylist playlist, @Nullable HlsMediaPlaylist.SegmentBase segmentBase) {
|
||||
if (segmentBase == null || segmentBase.fullSegmentEncryptionKeyUri == 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.
|
||||
|
|
@ -665,48 +798,52 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
}
|
||||
|
||||
/** {@link MediaChunkIterator} wrapping a {@link HlsMediaPlaylist}. */
|
||||
private static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {
|
||||
@VisibleForTesting
|
||||
/* package */ static final class HlsMediaPlaylistSegmentIterator extends BaseMediaChunkIterator {
|
||||
|
||||
private final HlsMediaPlaylist playlist;
|
||||
private final List<HlsMediaPlaylist.SegmentBase> segmentBases;
|
||||
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
|
||||
* 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(
|
||||
HlsMediaPlaylist playlist, long startOfPlaylistInPeriodUs, int chunkIndex) {
|
||||
super(/* fromIndex= */ chunkIndex, /* toIndex= */ playlist.segments.size() - 1);
|
||||
this.playlist = playlist;
|
||||
String playlistBaseUri,
|
||||
long startOfPlaylistInPeriodUs,
|
||||
List<HlsMediaPlaylist.SegmentBase> segmentBases) {
|
||||
super(/* fromIndex= */ 0, segmentBases.size() - 1);
|
||||
this.playlistBaseUri = playlistBaseUri;
|
||||
this.startOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs;
|
||||
this.segmentBases = segmentBases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSpec getDataSpec() {
|
||||
checkInBounds();
|
||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
||||
Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url);
|
||||
return new DataSpec(chunkUri, segment.byteRangeOffset, segment.byteRangeLength);
|
||||
HlsMediaPlaylist.SegmentBase segmentBase = segmentBases.get((int) getCurrentIndex());
|
||||
Uri chunkUri = UriUtil.resolveToUri(playlistBaseUri, segmentBase.url);
|
||||
return new DataSpec(chunkUri, segmentBase.byteRangeOffset, segmentBase.byteRangeLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChunkStartTimeUs() {
|
||||
checkInBounds();
|
||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
||||
return startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
||||
return startOfPlaylistInPeriodUs
|
||||
+ segmentBases.get((int) getCurrentIndex()).relativeStartTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getChunkEndTimeUs() {
|
||||
checkInBounds();
|
||||
Segment segment = playlist.segments.get((int) getCurrentIndex());
|
||||
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs;
|
||||
return segmentStartTimeInPeriodUs + segment.durationUs;
|
||||
HlsMediaPlaylist.SegmentBase segmentBase = segmentBases.get((int) getCurrentIndex());
|
||||
long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segmentBase.relativeStartTimeUs;
|
||||
return segmentStartTimeInPeriodUs + segmentBase.durationUs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
* @param format The chunk format.
|
||||
* @param startOfPlaylistInPeriodUs The position of the playlist in the period in microseconds.
|
||||
* @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 muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption
|
||||
* information is available in the master playlist.
|
||||
|
|
@ -79,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
Format format,
|
||||
long startOfPlaylistInPeriodUs,
|
||||
HlsMediaPlaylist mediaPlaylist,
|
||||
int segmentIndexInPlaylist,
|
||||
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
|
||||
Uri playlistUrl,
|
||||
@Nullable List<Format> muxedCaptionFormats,
|
||||
int trackSelectionReason,
|
||||
|
|
@ -90,7 +90,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
@Nullable byte[] mediaSegmentKey,
|
||||
@Nullable byte[] initSegmentKey) {
|
||||
// Media segment.
|
||||
HlsMediaPlaylist.Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
|
||||
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
|
||||
DataSpec dataSpec =
|
||||
new DataSpec(
|
||||
UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
|
||||
|
|
@ -136,10 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
|
||||
id3Decoder = previousChunk.id3Decoder;
|
||||
scratchId3Data = previousChunk.scratchId3Data;
|
||||
boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist);
|
||||
boolean canContinueWithoutSplice =
|
||||
isFollowingChunk
|
||||
|| (mediaPlaylist.hasIndependentSegments
|
||||
&& segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
|
||||
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
|
||||
shouldSpliceIn = !canContinueWithoutSplice;
|
||||
previousExtractor =
|
||||
isFollowingChunk
|
||||
|
|
@ -152,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
|
||||
shouldSpliceIn = false;
|
||||
}
|
||||
|
||||
return new HlsMediaChunk(
|
||||
extractorFactory,
|
||||
mediaDataSource,
|
||||
|
|
@ -168,7 +167,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
trackSelectionData,
|
||||
segmentStartTimeInPeriodUs,
|
||||
segmentEndTimeInPeriodUs,
|
||||
/* chunkMediaSequence= */ mediaPlaylist.mediaSequence + segmentIndexInPlaylist,
|
||||
segmentBaseHolder.mediaSequence,
|
||||
segmentBaseHolder.partIndex,
|
||||
discontinuitySequenceNumber,
|
||||
mediaSegment.hasGapTag,
|
||||
isMasterTimestampSource,
|
||||
|
|
@ -201,6 +201,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
/** Whether samples for this chunk should be spliced into existing samples. */
|
||||
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 DataSpec initDataSpec;
|
||||
@Nullable private final HlsMediaChunkExtractor previousExtractor;
|
||||
|
|
@ -243,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
long startTimeUs,
|
||||
long endTimeUs,
|
||||
long chunkMediaSequence,
|
||||
int partIndex,
|
||||
int discontinuitySequenceNumber,
|
||||
boolean hasGapTag,
|
||||
boolean isMasterTimestampSource,
|
||||
|
|
@ -262,6 +266,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
endTimeUs,
|
||||
chunkMediaSequence);
|
||||
this.mediaSegmentEncrypted = mediaSegmentEncrypted;
|
||||
this.partIndex = partIndex;
|
||||
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
|
||||
this.initDataSpec = initDataSpec;
|
||||
this.initDataSource = initDataSource;
|
||||
|
|
@ -541,4 +546,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
}
|
||||
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);
|
||||
long byteRangeStart =
|
||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ 0);
|
||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */ C.LENGTH_UNSET);
|
||||
long byteRangeLength =
|
||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.TIME_UNSET);
|
||||
parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */ C.LENGTH_UNSET);
|
||||
@Nullable
|
||||
String segmentEncryptionIV =
|
||||
getSegmentEncryptionIV(
|
||||
|
|
@ -811,21 +811,24 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
|
||||
}
|
||||
}
|
||||
preloadPart =
|
||||
new Part(
|
||||
url,
|
||||
initializationSegment,
|
||||
/* durationUs= */ 0,
|
||||
relativeDiscontinuitySequence,
|
||||
partStartTimeUs,
|
||||
cachedDrmInitData,
|
||||
fullSegmentEncryptionKeyUri,
|
||||
segmentEncryptionIV,
|
||||
byteRangeStart,
|
||||
byteRangeLength,
|
||||
/* hasGapTag= */ false,
|
||||
/* isIndependent= */ false,
|
||||
/* isPreload= */ true);
|
||||
if (byteRangeStart == C.LENGTH_UNSET || byteRangeLength != C.LENGTH_UNSET) {
|
||||
// Skip preload part if it is an unbounded range request.
|
||||
preloadPart =
|
||||
new Part(
|
||||
url,
|
||||
initializationSegment,
|
||||
/* durationUs= */ 0,
|
||||
relativeDiscontinuitySequence,
|
||||
partStartTimeUs,
|
||||
cachedDrmInitData,
|
||||
fullSegmentEncryptionKeyUri,
|
||||
segmentEncryptionIV,
|
||||
byteRangeStart != C.LENGTH_UNSET ? byteRangeStart : 0,
|
||||
byteRangeLength,
|
||||
/* hasGapTag= */ false,
|
||||
/* isIndependent= */ false,
|
||||
/* isPreload= */ true);
|
||||
}
|
||||
} else if (line.startsWith(TAG_PART)) {
|
||||
@Nullable
|
||||
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.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
|
@ -362,6 +363,7 @@ public class HlsMediaPlaylistParserTest {
|
|||
assertThat(secondPart.byteRangeOffset).isEqualTo(1234);
|
||||
// Assert trailing parts.
|
||||
HlsMediaPlaylist.Part thirdPart = playlist.trailingParts.get(0);
|
||||
// Assert tailing parts.
|
||||
assertThat(thirdPart.byteRangeLength).isEqualTo(1000);
|
||||
assertThat(thirdPart.byteRangeOffset).isEqualTo(1234);
|
||||
assertThat(thirdPart.relativeStartTimeUs).isEqualTo(8_000_000);
|
||||
|
|
@ -544,6 +546,27 @@ public class HlsMediaPlaylistParserTest {
|
|||
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
|
||||
public void parseMediaPlaylist_withPreloadHintTypePartAndAesPlayReadyKey_inheritsDrmInitData()
|
||||
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