Propagate segment properties when merging HLS delta updates

Issue: #5011
PiperOrigin-RevId: 350540152
This commit is contained in:
bachinger 2021-01-07 13:30:10 +00:00 committed by Ian Baker
parent fc3b91c96c
commit b05fa731d8
8 changed files with 232 additions and 114 deletions

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
/** Default implementation for {@link HlsPlaylistParserFactory}. */ /** Default implementation for {@link HlsPlaylistParserFactory}. */
@ -27,7 +28,7 @@ public final class DefaultHlsPlaylistParserFactory implements HlsPlaylistParserF
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) { HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) {
return new HlsPlaylistParser(masterPlaylist); return new HlsPlaylistParser(masterPlaylist, previousMediaPlaylist);
} }
} }

View File

@ -68,7 +68,6 @@ public final class DefaultHlsPlaylistTracker
private final List<PlaylistEventListener> listeners; private final List<PlaylistEventListener> listeners;
private final double playlistStuckTargetDurationCoefficient; private final double playlistStuckTargetDurationCoefficient;
@Nullable private ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser;
@Nullable private EventDispatcher eventDispatcher; @Nullable private EventDispatcher eventDispatcher;
@Nullable private Loader initialPlaylistLoader; @Nullable private Loader initialPlaylistLoader;
@Nullable private Handler playlistRefreshHandler; @Nullable private Handler playlistRefreshHandler;
@ -243,7 +242,6 @@ public final class DefaultHlsPlaylistTracker
masterPlaylist = (HlsMasterPlaylist) result; masterPlaylist = (HlsMasterPlaylist) result;
} }
this.masterPlaylist = masterPlaylist; this.masterPlaylist = masterPlaylist;
mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist);
primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url;
createBundles(masterPlaylist.mediaPlaylistUrls); createBundles(masterPlaylist.mediaPlaylistUrls);
LoadEventInfo loadEventInfo = LoadEventInfo loadEventInfo =
@ -671,6 +669,8 @@ public final class DefaultHlsPlaylistTracker
} }
private void loadPlaylistImmediately(Uri playlistRequestUri) { private void loadPlaylistImmediately(Uri playlistRequestUri) {
ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser =
playlistParserFactory.createPlaylistParser(masterPlaylist, playlistSnapshot);
ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable = ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable =
new ParsingLoadable<>( new ParsingLoadable<>(
mediaPlaylistDataSource, mediaPlaylistDataSource,
@ -691,10 +691,6 @@ public final class DefaultHlsPlaylistTracker
private void processLoadedPlaylist( private void processLoadedPlaylist(
HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) { HlsMediaPlaylist loadedPlaylist, LoadEventInfo loadEventInfo) {
@Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot; @Nullable HlsMediaPlaylist oldPlaylist = playlistSnapshot;
loadedPlaylist =
loadedPlaylist.skippedSegmentCount > 0
? loadedPlaylist.expandSkippedSegments(checkNotNull(playlistSnapshot))
: loadedPlaylist;
long currentTimeMs = SystemClock.elapsedRealtime(); long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs; lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
@ -759,9 +755,7 @@ public final class DefaultHlsPlaylistTracker
Uri.Builder uriBuilder = playlistUrl.buildUpon(); Uri.Builder uriBuilder = playlistUrl.buildUpon();
if (playlistSnapshot.serverControl.canBlockReload) { if (playlistSnapshot.serverControl.canBlockReload) {
long targetMediaSequence = long targetMediaSequence =
playlistSnapshot.mediaSequence playlistSnapshot.mediaSequence + playlistSnapshot.segments.size();
+ playlistSnapshot.segments.size()
+ playlistSnapshot.skippedSegmentCount;
uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(targetMediaSequence)); uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(targetMediaSequence));
if (playlistSnapshot.partTargetDurationUs != C.TIME_UNSET) { if (playlistSnapshot.partTargetDurationUs != C.TIME_UNSET) {
List<Part> trailingParts = playlistSnapshot.trailingParts; List<Part> trailingParts = playlistSnapshot.trailingParts;
@ -774,9 +768,8 @@ public final class DefaultHlsPlaylistTracker
} }
} }
if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) { if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) {
// TODO: Fix skipped segment merging before re-enabling. uriBuilder.appendQueryParameter(
// uriBuilder.appendQueryParameter( SKIP_PARAM, playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
// SKIP_PARAM, playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
} }
return uriBuilder.build(); return uriBuilder.build();
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.FilteringManifestParser;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
@ -48,8 +49,9 @@ public final class FilteringHlsPlaylistParserFactory implements HlsPlaylistParse
@Override @Override
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser( public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist) { HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) {
return new FilteringManifestParser<>( return new FilteringManifestParser<>(
hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist), streamKeys); hlsPlaylistParserFactory.createPlaylistParser(masterPlaylist, previousMediaPlaylist),
streamKeys);
} }
} }

View File

@ -15,7 +15,6 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
@ -170,6 +169,30 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.title = title; this.title = title;
this.parts = ImmutableList.copyOf(parts); this.parts = ImmutableList.copyOf(parts);
} }
public Segment copyWith(long relativeStartTimeUs, int relativeDiscontinuitySequence) {
List<Part> updatedParts = new ArrayList<>();
long relativePartStartTimeUs = relativeStartTimeUs;
for (int i = 0; i < parts.size(); i++) {
Part part = parts.get(i);
updatedParts.add(part.copyWith(relativePartStartTimeUs, relativeDiscontinuitySequence));
relativePartStartTimeUs += part.durationUs;
}
return new Segment(
url,
initializationSegment,
title,
durationUs,
relativeDiscontinuitySequence,
relativeStartTimeUs,
drmInitData,
fullSegmentEncryptionKeyUri,
encryptionIV,
byteRangeOffset,
byteRangeLength,
hasGapTag,
updatedParts);
}
} }
/** A media part. */ /** A media part. */
@ -226,6 +249,23 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.isIndependent = isIndependent; this.isIndependent = isIndependent;
this.isPreload = isPreload; this.isPreload = isPreload;
} }
public Part copyWith(long relativeStartTimeUs, int relativeDiscontinuitySequence) {
return new Part(
url,
initializationSegment,
durationUs,
relativeDiscontinuitySequence,
relativeStartTimeUs,
drmInitData,
fullSegmentEncryptionKeyUri,
encryptionIV,
byteRangeOffset,
byteRangeLength,
hasGapTag,
isIndependent,
isPreload);
}
} }
/** The base for a {@link Segment} or a {@link Part} required for playback. */ /** The base for a {@link Segment} or a {@link Part} required for playback. */
@ -405,8 +445,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* The list of segments in the playlist. * The list of segments in the playlist.
*/ */
public final List<Segment> segments; public final List<Segment> segments;
/** The number of skipped segments. */
public int skippedSegmentCount;
/** /**
* The list of parts at the end of the playlist for which the segment is not in the playlist yet. * The list of parts at the end of the playlist for which the segment is not in the playlist yet.
*/ */
@ -434,7 +472,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param hasProgramDateTime See {@link #hasProgramDateTime}. * @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param protectionSchemes See {@link #protectionSchemes}. * @param protectionSchemes See {@link #protectionSchemes}.
* @param segments See {@link #segments}. * @param segments See {@link #segments}.
* @param skippedSegmentCount See {@link #skippedSegmentCount}.
* @param trailingParts See {@link #trailingParts}. * @param trailingParts See {@link #trailingParts}.
* @param serverControl See {@link #serverControl} * @param serverControl See {@link #serverControl}
* @param renditionReports See {@link #renditionReports}. * @param renditionReports See {@link #renditionReports}.
@ -456,7 +493,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
boolean hasProgramDateTime, boolean hasProgramDateTime,
@Nullable DrmInitData protectionSchemes, @Nullable DrmInitData protectionSchemes,
List<Segment> segments, List<Segment> segments,
int skippedSegmentCount,
List<Part> trailingParts, List<Part> trailingParts,
ServerControl serverControl, ServerControl serverControl,
Map<Uri, RenditionReport> renditionReports) { Map<Uri, RenditionReport> renditionReports) {
@ -473,7 +509,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this.hasProgramDateTime = hasProgramDateTime; this.hasProgramDateTime = hasProgramDateTime;
this.protectionSchemes = protectionSchemes; this.protectionSchemes = protectionSchemes;
this.segments = ImmutableList.copyOf(segments); this.segments = ImmutableList.copyOf(segments);
this.skippedSegmentCount = skippedSegmentCount;
this.trailingParts = ImmutableList.copyOf(trailingParts); this.trailingParts = ImmutableList.copyOf(trailingParts);
this.renditionReports = ImmutableMap.copyOf(renditionReports); this.renditionReports = ImmutableMap.copyOf(renditionReports);
if (!trailingParts.isEmpty()) { if (!trailingParts.isEmpty()) {
@ -509,10 +544,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
return false; return false;
} }
// The media sequences are equal. // The media sequences are equal.
int segmentCount = segments.size() + skippedSegmentCount; int segmentCountDifference = segments.size() - other.segments.size();
int otherSegmentCount = other.segments.size() + other.skippedSegmentCount; if (segmentCountDifference != 0) {
if (segmentCount != otherSegmentCount) { return segmentCountDifference > 0;
return segmentCount > otherSegmentCount;
} }
int partCount = trailingParts.size(); int partCount = trailingParts.size();
int otherPartCount = other.trailingParts.size(); int otherPartCount = other.trailingParts.size();
@ -527,52 +561,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
return startTimeUs + durationUs; return startTimeUs + durationUs;
} }
/**
* Merges the skipped segments of the previous playlist and returns a copy with a {@link
* #skippedSegmentCount} of 0.
*
* @param previousPlaylist The previous playlist with a {@link #skippedSegmentCount} of zero.
* @return A new playlist with a complete list of segments.
*/
public HlsMediaPlaylist expandSkippedSegments(HlsMediaPlaylist previousPlaylist) {
if (skippedSegmentCount == 0) {
return this;
}
checkArgument(previousPlaylist.skippedSegmentCount == 0);
List<Segment> mergedSegments = new ArrayList<>();
long mediaSequence = this.mediaSequence;
int startIndex = (int) (mediaSequence - previousPlaylist.mediaSequence);
int endIndex = startIndex + skippedSegmentCount;
if (startIndex >= 0 && endIndex <= previousPlaylist.segments.size()) {
mergedSegments.addAll(previousPlaylist.segments.subList(startIndex, endIndex));
} else {
// Adjust the media sequence if the old playlist doesn't contain all of the skipped segments.
mediaSequence += skippedSegmentCount;
}
mergedSegments.addAll(segments);
return new HlsMediaPlaylist(
playlistType,
baseUri,
tags,
startOffsetUs,
startTimeUs,
hasDiscontinuitySequence,
discontinuitySequence,
mediaSequence,
version,
targetDurationUs,
partTargetDurationUs,
hasIndependentSegments,
hasEndTag,
hasProgramDateTime,
protectionSchemes,
mergedSegments,
/* skippedSegmentCount= */ 0,
trailingParts,
serverControl,
renditionReports);
}
/** /**
* Returns a playlist identical to this one except for the start time, the discontinuity sequence * Returns a playlist identical to this one except for the start time, the discontinuity sequence
* and {@code hasDiscontinuitySequence} values. The first two are set to the specified values, * and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
@ -600,7 +588,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
hasProgramDateTime, hasProgramDateTime,
protectionSchemes, protectionSchemes,
segments, segments,
skippedSegmentCount,
trailingParts, trailingParts,
serverControl, serverControl,
renditionReports); renditionReports);
@ -631,7 +618,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
hasProgramDateTime, hasProgramDateTime,
protectionSchemes, protectionSchemes,
segments, segments,
skippedSegmentCount,
trailingParts, trailingParts,
serverControl, serverControl,
renditionReports); renditionReports);

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
@ -211,13 +213,14 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}"); Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}");
private final HlsMasterPlaylist masterPlaylist; private final HlsMasterPlaylist masterPlaylist;
@Nullable private final HlsMediaPlaylist previousMediaPlaylist;
/** /**
* Creates an instance where media playlists are parsed without inheriting attributes from a * Creates an instance where media playlists are parsed without inheriting attributes from a
* master playlist. * master playlist.
*/ */
public HlsPlaylistParser() { public HlsPlaylistParser() {
this(HlsMasterPlaylist.EMPTY); this(HlsMasterPlaylist.EMPTY, /* previousMediaPlaylist= */ null);
} }
/** /**
@ -225,9 +228,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
* playlist. * playlist.
* *
* @param masterPlaylist The master playlist from which media playlists will inherit attributes. * @param masterPlaylist The master playlist from which media playlists will inherit attributes.
* @param previousMediaPlaylist The previous media playlist from which the new media playlist may
* inherit skipped segments.
*/ */
public HlsPlaylistParser(HlsMasterPlaylist masterPlaylist) { public HlsPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) {
this.masterPlaylist = masterPlaylist; this.masterPlaylist = masterPlaylist;
this.previousMediaPlaylist = previousMediaPlaylist;
} }
@Override @Override
@ -257,7 +264,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|| line.equals(TAG_ENDLIST)) { || line.equals(TAG_ENDLIST)) {
extraLines.add(line); extraLines.add(line);
return parseMediaPlaylist( return parseMediaPlaylist(
masterPlaylist, new LineIterator(extraLines, reader), uri.toString()); masterPlaylist,
previousMediaPlaylist,
new LineIterator(extraLines, reader),
uri.toString());
} else { } else {
extraLines.add(line); extraLines.add(line);
} }
@ -603,7 +613,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
private static HlsMediaPlaylist parseMediaPlaylist( private static HlsMediaPlaylist parseMediaPlaylist(
HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { HlsMasterPlaylist masterPlaylist,
@Nullable HlsMediaPlaylist previousMediaPlaylist,
LineIterator iterator,
String baseUri)
throws IOException {
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
long startOffsetUs = C.TIME_UNSET; long startOffsetUs = C.TIME_UNSET;
long mediaSequence = 0; long mediaSequence = 0;
@ -642,7 +656,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* holdBackUs= */ C.TIME_UNSET, /* holdBackUs= */ C.TIME_UNSET,
/* partHoldBackUs= */ C.TIME_UNSET, /* partHoldBackUs= */ C.TIME_UNSET,
/* canBlockReload= */ false); /* canBlockReload= */ false);
int skippedSegmentCount = 0;
@Nullable DrmInitData playlistProtectionSchemes = null; @Nullable DrmInitData playlistProtectionSchemes = null;
@Nullable String fullSegmentEncryptionKeyUri = null; @Nullable String fullSegmentEncryptionKeyUri = null;
@ -727,7 +740,41 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND); (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions); segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
} else if (line.startsWith(TAG_SKIP)) { } else if (line.startsWith(TAG_SKIP)) {
skippedSegmentCount = parseIntAttr(line, REGEX_SKIPPED_SEGMENTS); int skippedSegmentCount = parseIntAttr(line, REGEX_SKIPPED_SEGMENTS);
checkState(previousMediaPlaylist != null && segments.isEmpty());
int startIndex = (int) (mediaSequence - castNonNull(previousMediaPlaylist).mediaSequence);
int endIndex = startIndex + skippedSegmentCount;
if (startIndex >= 0 && endIndex <= previousMediaPlaylist.segments.size()) {
// Merge only if all skipped segments are available in the previous playlist.
for (int i = startIndex; i < endIndex; i++) {
Segment segment = previousMediaPlaylist.segments.get(i);
if (mediaSequence != previousMediaPlaylist.mediaSequence) {
// If the media sequences of the playlists are not the same, we need to recreate the
// object with the updated relative start time and the relative discontinuity
// sequence. With identical playlist media sequences these values do not change.
int newRelativeDiscontinuitySequence =
previousMediaPlaylist.discontinuitySequence
- playlistDiscontinuitySequence
+ segment.relativeDiscontinuitySequence;
segment = segment.copyWith(segmentStartTimeUs, newRelativeDiscontinuitySequence);
}
segments.add(segment);
segmentStartTimeUs += segment.durationUs;
partStartTimeUs = segmentStartTimeUs;
if (segment.byteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset = segment.byteRangeOffset + segment.byteRangeLength;
}
relativeDiscontinuitySequence = segment.relativeDiscontinuitySequence;
initializationSegment = segment.initializationSegment;
cachedDrmInitData = segment.drmInitData;
fullSegmentEncryptionKeyUri = segment.fullSegmentEncryptionKeyUri;
if (segment.encryptionIV == null
|| !segment.encryptionIV.equals(Long.toHexString(segmentMediaSequence))) {
fullSegmentEncryptionIV = segment.encryptionIV;
}
segmentMediaSequence++;
}
}
} else if (line.startsWith(TAG_KEY)) { } else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String keyFormat = String keyFormat =
@ -972,7 +1019,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* hasProgramDateTime= */ playlistStartTimeUs != 0, /* hasProgramDateTime= */ playlistStartTimeUs != 0,
playlistProtectionSchemes, playlistProtectionSchemes,
segments, segments,
skippedSegmentCount,
trailingParts, trailingParts,
serverControl, serverControl,
renditionReports); renditionReports);

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
/** Factory for {@link HlsPlaylist} parsers. */ /** Factory for {@link HlsPlaylist} parsers. */
@ -32,7 +33,10 @@ public interface HlsPlaylistParserFactory {
* {@code masterPlaylist}. * {@code masterPlaylist}.
* *
* @param masterPlaylist The master playlist that referenced any parsed media playlists. * @param masterPlaylist The master playlist that referenced any parsed media playlists.
* @param previousMediaPlaylist The previous media playlist or null if there is no previous media
* playlist.
* @return A parser for HLS playlists. * @return A parser for HLS playlists.
*/ */
ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(HlsMasterPlaylist masterPlaylist); ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
HlsMasterPlaylist masterPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist);
} }

View File

@ -24,7 +24,7 @@ import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -116,7 +116,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -128,14 +128,12 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(firstFullPlaylist.segments).hasSize(6); assertThat(firstFullPlaylist.segments).hasSize(6);
HlsMediaPlaylist secondFullPlaylist = mediaPlaylists.get(1); HlsMediaPlaylist secondFullPlaylist = mediaPlaylists.get(1);
assertThat(secondFullPlaylist.mediaSequence).isEqualTo(11); assertThat(secondFullPlaylist.mediaSequence).isEqualTo(11);
assertThat(secondFullPlaylist.skippedSegmentCount).isEqualTo(0);
assertThat(secondFullPlaylist.segments.get(0).url).isEqualTo("fileSequence11.ts"); assertThat(secondFullPlaylist.segments.get(0).url).isEqualTo("fileSequence11.ts");
assertThat(secondFullPlaylist.segments.get(5).url).isEqualTo("fileSequence16.ts"); assertThat(secondFullPlaylist.segments.get(5).url).isEqualTo("fileSequence16.ts");
assertThat(secondFullPlaylist.segments).hasSize(6); assertThat(secondFullPlaylist.segments).hasSize(6);
assertThat(secondFullPlaylist.segments).containsNoneIn(firstFullPlaylist.segments); assertThat(secondFullPlaylist.segments).containsNoneIn(firstFullPlaylist.segments);
} }
@Ignore // Test disabled because playlist delta updates are temporarily disabled.
@Test @Test
public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments() public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments()
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {
@ -150,7 +148,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -160,14 +158,14 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1); HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1);
assertThat(mergedPlaylist.mediaSequence).isEqualTo(11); assertThat(mergedPlaylist.mediaSequence).isEqualTo(11);
assertThat(mergedPlaylist.skippedSegmentCount).isEqualTo(0);
assertThat(mergedPlaylist.segments).hasSize(6); assertThat(mergedPlaylist.segments).hasSize(6);
// First 2 segments of the merged playlist need to be copied from the previous playlist. // First 2 segments of the merged playlist need to be copied from the previous playlist.
assertThat(mergedPlaylist.segments.subList(0, 2)) assertThat(mergedPlaylist.segments.get(0).url)
.containsExactlyElementsIn(initialPlaylistWithAllSegments.segments.subList(1, 3)) .isEqualTo(initialPlaylistWithAllSegments.segments.get(1).url);
.inOrder(); assertThat(mergedPlaylist.segments.get(0).relativeStartTimeUs).isEqualTo(0);
assertThat(mergedPlaylist.segments.get(2).url) assertThat(mergedPlaylist.segments.get(1).url)
.isEqualTo(initialPlaylistWithAllSegments.segments.get(3).url); .isEqualTo(initialPlaylistWithAllSegments.segments.get(2).url);
assertThat(mergedPlaylist.segments.get(1).relativeStartTimeUs).isEqualTo(4000000);
} }
@Ignore // Test disabled because playlist delta updates are temporarily disabled. @Ignore // Test disabled because playlist delta updates are temporarily disabled.
@ -185,7 +183,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -195,11 +193,9 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1); HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1);
assertThat(mergedPlaylist.mediaSequence).isEqualTo(22); assertThat(mergedPlaylist.mediaSequence).isEqualTo(22);
assertThat(mergedPlaylist.skippedSegmentCount).isEqualTo(0);
assertThat(mergedPlaylist.segments).hasSize(4); assertThat(mergedPlaylist.segments).hasSize(4);
} }
@Ignore // Test disabled because playlist delta updates are temporarily disabled.
@Test @Test
public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2() public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2()
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {
@ -214,7 +210,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -224,7 +220,6 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11); assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
} }
@Ignore // Test disabled because playlist delta updates are temporarily disabled.
@Test @Test
public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams() public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams()
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {
@ -241,7 +236,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -266,7 +261,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -292,7 +287,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -321,7 +316,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -352,7 +347,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(), new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
@ -365,7 +360,6 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2); assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2);
} }
@Ignore // Test disabled because playlist delta updates are temporarily disabled.
@Test @Test
public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest() public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest()
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {
@ -386,7 +380,7 @@ public class DefaultHlsPlaylistTrackerTest {
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
/* dataSourceFactory= */ new DefaultHttpDataSourceFactory(), /* dataSourceFactory= */ new DefaultHttpDataSource.Factory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()), Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 3); /* awaitedMediaPlaylistCount= */ 3);

View File

@ -303,24 +303,112 @@ public class HlsMediaPlaylistParserTest {
} }
@Test @Test
public void parseMediaPlaylist_withSkippedSegments_parsesNumberOfSkippedSegments() public void parseMediaPlaylist_withSkippedSegments_correctlyMergedSegments() throws IOException {
throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String previousPlaylistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:6\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
+ "#EXT-X-MEDIA-SEQUENCE:263\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence264.mp4\n"
+ "#EXT-X-DISCONTINUITY\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence265.mp4\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence266.mp4";
String playlistString = String playlistString =
"#EXTM3U\n" "#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n" + "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:6\n" + "#EXT-X-VERSION:6\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n" + "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
+ "#EXT-X-MEDIA-SEQUENCE:266\n" + "#EXT-X-MEDIA-SEQUENCE:265\n"
+ "#EXT-X-SKIP:SKIPPED-SEGMENTS=1234\n" + "#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n"
+ "#EXTINF:4.00008,\n" + "#EXTINF:4.00008,\n"
+ "fileSequence266.mp4"; + "fileSequence266.mp4"
+ "#EXTINF:4.00008,\n"
+ "fileSequence267.mp4\n";
InputStream previousInputStream =
new ByteArrayInputStream(Util.getUtf8Bytes(previousPlaylistString));
HlsMediaPlaylist previousPlaylist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, previousInputStream);
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); (HlsMediaPlaylist)
new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist)
.parse(playlistUri, inputStream);
assertThat(playlist.skippedSegmentCount).isEqualTo(1234); assertThat(playlist.segments).hasSize(3);
assertThat(playlist.segments.get(1).relativeStartTimeUs).isEqualTo(4000079);
assertThat(previousPlaylist.segments.get(0).relativeDiscontinuitySequence).isEqualTo(0);
assertThat(previousPlaylist.segments.get(1).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(previousPlaylist.segments.get(2).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(0).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(1).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(2).relativeDiscontinuitySequence).isEqualTo(1);
}
@Test
public void parseMediaPlaylist_withSkippedSegments_correctlyMergedParts() throws IOException {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String previousPlaylistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:6\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
+ "#EXT-X-MEDIA-SEQUENCE:264\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part264.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part264.2.ts\"\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence264.mp4\n"
+ "#EXT-X-DISCONTINUITY\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part265.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part265.2.ts\"\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence265.mp4\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.1.ts\"\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part266.2.ts\"\n"
+ "#EXTINF:4.00008,\n"
+ "fileSequence266.mp4\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"";
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:6\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:1234\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24.0\n"
+ "#EXT-X-MEDIA-SEQUENCE:265\n"
+ "#EXT-X-SKIP:SKIPPED-SEGMENTS=2\n"
+ "#EXT-X-PART:DURATION=2.00000,URI=\"part267.1.ts\"";
InputStream previousInputStream =
new ByteArrayInputStream(Util.getUtf8Bytes(previousPlaylistString));
HlsMediaPlaylist previousPlaylist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, previousInputStream);
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist)
new HlsPlaylistParser(HlsMasterPlaylist.EMPTY, previousPlaylist)
.parse(playlistUri, inputStream);
assertThat(playlist.segments).hasSize(2);
assertThat(playlist.segments.get(0).relativeStartTimeUs).isEqualTo(0);
assertThat(playlist.segments.get(0).parts.get(0).relativeStartTimeUs).isEqualTo(0);
assertThat(playlist.segments.get(0).parts.get(0).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(0).parts.get(1).relativeStartTimeUs).isEqualTo(2000000);
assertThat(playlist.segments.get(0).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(1).relativeStartTimeUs).isEqualTo(4000079);
assertThat(playlist.segments.get(1).parts.get(0).relativeStartTimeUs).isEqualTo(4000079);
assertThat(playlist.segments.get(1).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.segments.get(1).parts.get(1).relativeStartTimeUs).isEqualTo(6000079);
assertThat(playlist.segments.get(1).parts.get(1).relativeDiscontinuitySequence).isEqualTo(1);
assertThat(playlist.trailingParts.get(0).relativeStartTimeUs).isEqualTo(8000158);
assertThat(playlist.trailingParts.get(0).relativeDiscontinuitySequence).isEqualTo(1);
} }
@Test @Test
@ -1125,7 +1213,9 @@ public class HlsMediaPlaylistParserTest {
/* variableDefinitions= */ Collections.emptyMap(), /* variableDefinitions= */ Collections.emptyMap(),
/* sessionKeyDrmInitData= */ Collections.emptyList()); /* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlistWithInheritance = HlsMediaPlaylist playlistWithInheritance =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); (HlsMediaPlaylist)
new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null)
.parse(playlistUri, inputStream);
assertThat(playlistWithInheritance.hasIndependentSegments).isTrue(); assertThat(playlistWithInheritance.hasIndependentSegments).isTrue();
} }
@ -1187,7 +1277,9 @@ public class HlsMediaPlaylistParserTest {
variableDefinitions, variableDefinitions,
/* sessionKeyDrmInitData= */ Collections.emptyList()); /* sessionKeyDrmInitData= */ Collections.emptyList());
HlsMediaPlaylist playlist = HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser(masterPlaylist).parse(playlistUri, inputStream); (HlsMediaPlaylist)
new HlsPlaylistParser(masterPlaylist, /* previousMediaPlaylist= */ null)
.parse(playlistUri, inputStream);
for (int i = 1; i <= 4; i++) { for (int i = 1; i <= 4; i++) {
assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts"); assertThat(playlist.segments.get(i - 1).url).isEqualTo("long_path" + i + ".ts");
} }